using System.Text; using System.Threading.RateLimiting; using FluentValidation; using FluentValidation.AspNetCore; using MicCheck.Api.ApiKeys; using MicCheck.Api.Audit; using MicCheck.Api.Authentication; using MicCheck.Api.Authorization; using MicCheck.Api.Data; using MicCheck.Api.Environments; using MicCheck.Api.Features; using MicCheck.Api.Identities; using MicCheck.Api.Organizations; using MicCheck.Api.Projects; using MicCheck.Api.Segments; using MicCheck.Api.Users; using MicCheck.Api.Webhooks; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.RateLimiting; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Scalar.AspNetCore; using Serilog; Log.Logger = new LoggerConfiguration() .WriteTo.Console() .CreateBootstrapLogger(); try { var builder = WebApplication.CreateBuilder(args); builder.Host.UseSerilog((context, services, config) => config.ReadFrom.Configuration(context.Configuration) .ReadFrom.Services(services) .WriteTo.Console()); builder.Services.AddOpenApi(); builder.Services.AddControllers() .AddJsonOptions(options => options.JsonSerializerOptions.Converters.Add( new System.Text.Json.Serialization.JsonStringEnumConverter())); builder.Services.AddAuthentication() .AddScheme( EnvironmentKeyAuthenticationHandler.SchemeName, _ => { }) .AddScheme( ApiKeyAuthenticationHandler.SchemeName, _ => { }) .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateIssuerSigningKey = true, ValidIssuer = builder.Configuration["Jwt:Issuer"], ValidAudience = builder.Configuration["Jwt:Audience"], IssuerSigningKey = new SymmetricSecurityKey( Encoding.UTF8.GetBytes(builder.Configuration["Jwt:SecretKey"]!)) }; }); builder.Services.AddAuthorization(options => { options.AddPolicy(AuthorizationPolicies.FlagsApiAccess, policy => policy.AddAuthenticationSchemes(EnvironmentKeyAuthenticationHandler.SchemeName) .RequireClaim("EnvironmentId")); options.AddPolicy(AuthorizationPolicies.AdminApiAccess, policy => policy.AddAuthenticationSchemes(ApiKeyAuthenticationHandler.SchemeName, JwtBearerDefaults.AuthenticationScheme) .RequireAuthenticatedUser()); options.AddPolicy(AuthorizationPolicies.OrganizationAdmin, policy => policy.AddAuthenticationSchemes(ApiKeyAuthenticationHandler.SchemeName, JwtBearerDefaults.AuthenticationScheme) .RequireClaim("OrganizationRole", "Admin")); }); builder.Services.AddHttpContextAccessor(); builder.Services.AddRateLimiter(options => options.AddFixedWindowLimiter("AdminApi", limiter => { limiter.PermitLimit = 500; limiter.Window = TimeSpan.FromMinutes(1); limiter.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; limiter.QueueLimit = 0; })); builder.Services.AddFluentValidationAutoValidation(); builder.Services.AddValidatorsFromAssemblyContaining(); builder.Services.Configure(options => { options.InvalidModelStateResponseFactory = context => { var errors = context.ModelState .Where(e => e.Value?.Errors.Count > 0) .ToDictionary( kvp => kvp.Key, kvp => kvp.Value!.Errors.Select(e => e.ErrorMessage).ToArray()); return new UnprocessableEntityObjectResult(new { errors }); }; }); builder.Services.AddMemoryCache(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped, PasswordHasher>(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); builder.Services.AddHttpClient("Webhooks", client => client.DefaultRequestHeaders.Add("User-Agent", "MicCheck-Webhook/1.0")); var connectionString = System.Environment.GetEnvironmentVariable("DATABASE_URL") is { } databaseUrl ? DatabaseUrlParser.ToNpgsqlConnectionString(databaseUrl) : builder.Configuration.GetConnectionString("DefaultConnection")!; builder.Services.AddDbContext(options => options.UseNpgsql(connectionString)); var app = builder.Build(); // Apply any pending EF Core migrations on startup (safe to run on every boot) using (var migrationScope = app.Services.CreateScope()) { var db = migrationScope.ServiceProvider.GetRequiredService(); await db.Database.MigrateAsync(); } if (app.Environment.IsDevelopment()) { app.MapOpenApi(); app.MapScalarApiReference(); app.MapGet("/", () => Results.Redirect("/scalar/v1")).ExcludeFromDescription(); using var scope = app.Services.CreateScope(); var seeder = scope.ServiceProvider.GetRequiredService(); await seeder.SeedAsync(); } app.UseSerilogRequestLogging(); app.UseRateLimiter(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.MapAuthEndpoints(); app.MapApiKeyEndpoints(); app.Run(); } catch (Exception ex) { Log.Fatal(ex, "Application terminated unexpectedly"); } finally { Log.CloseAndFlush(); }