using LinqToDB; using LinqToDB.Data; using Azaion.Missions.Auth; using Azaion.Missions.Database; using Azaion.Missions.Infrastructure; using Azaion.Missions.Middleware; using Azaion.Missions.Services; const string DatabaseUrlEnvVar = "DATABASE_URL"; const string DatabaseUrlConfigKey = "Database:Url"; var builder = WebApplication.CreateBuilder(args); var databaseUrl = ConfigurationResolver.ResolveRequiredOrThrow( builder.Configuration, DatabaseUrlEnvVar, DatabaseUrlConfigKey, "Database connection string"); var connectionString = databaseUrl.StartsWith("postgresql://") ? ConvertPostgresUrl(databaseUrl) : databaseUrl; builder.Services.AddScoped(_ => { var options = new DataOptions().UsePostgreSQL(connectionString); return new AppDataConnection(options); }); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddJwtAuth(builder.Configuration); var allowedOrigins = builder.Configuration.GetSection("CorsConfig:AllowedOrigins").Get() ?? Array.Empty(); var allowAnyOrigin = builder.Configuration.GetValue("CorsConfig:AllowAnyOrigin"); CorsConfigurationValidator.EnsureSafeForEnvironment(allowedOrigins, allowAnyOrigin, builder.Environment.EnvironmentName); builder.Services.AddCors(options => { options.AddDefaultPolicy(policy => { if (CorsConfigurationValidator.ShouldUsePermissivePolicy(allowedOrigins, allowAnyOrigin)) policy.AllowAnyOrigin().AllowAnyHeader().AllowAnyMethod(); else policy.WithOrigins(allowedOrigins).AllowAnyHeader().AllowAnyMethod(); }); }); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app = builder.Build(); if (CorsConfigurationValidator.ShouldWarnAboutPermissiveDefault(allowedOrigins, allowAnyOrigin)) { app.Services .GetRequiredService>() .LogWarning(CorsConfigurationValidator.PermissiveDefaultWarning, app.Environment.EnvironmentName); } using (var scope = app.Services.CreateScope()) { var db = scope.ServiceProvider.GetRequiredService(); DatabaseMigrator.Migrate(db); } app.UseMiddleware(); app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); app.UseSwagger(); app.UseSwaggerUI(); app.MapControllers(); app.MapGet("/health", () => Results.Ok(new { status = "healthy" })); // Test-only JWKS refresh hook. The Microsoft.IdentityModel ConfigurationManager // hard-pins the AutomaticRefreshInterval floor to 5 minutes (static field), so // JWKS-rotation e2e scenarios cannot rely on the proactive refresh path inside // a 15-minute CI window. RequestRefresh() itself is throttled by // RefreshInterval after the first call — two rotation tests running within // 1 second cannot both refresh through the public API. The endpoint sidesteps // the throttle by resetting `_isFirstRefreshRequest` via reflection so each // call behaves like the very first refresh request. This is a TEST-ONLY // affordance — gated on ASPNETCORE_ENVIRONMENT=Test; production never maps // the route. See Helpers/JwksRefreshHelper.cs for the test-side caller. if (app.Environment.IsEnvironment("Test")) { app.MapPost("/test/refresh-jwks", async ( Microsoft.IdentityModel.Protocols.IConfigurationManager mgr, CancellationToken cancel) => { var firstField = mgr.GetType().GetField( "_isFirstRefreshRequest", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); firstField?.SetValue(mgr, true); mgr.RequestRefresh(); var jwks = await mgr.GetConfigurationAsync(cancel).ConfigureAwait(false); return Results.Ok(new { refreshed = true, kids = jwks.GetSigningKeys().Select(k => k.KeyId).ToArray(), }); }); } app.Run(); static string ConvertPostgresUrl(string url) { var uri = new Uri(url); var userInfo = uri.UserInfo.Split(':'); var host = uri.Host; var port = uri.Port > 0 ? uri.Port : 5432; var database = uri.AbsolutePath.TrimStart('/'); return $"Host={host};Port={port};Database={database};Username={userInfo[0]};Password={userInfo.ElementAtOrDefault(1) ?? ""}"; }