using System.Text; using System.Threading.RateLimiting; using FluentValidation; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.AspNetCore.Diagnostics.HealthChecks; using Microsoft.AspNetCore.RateLimiting; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.IdentityModel.Tokens; using Minio; using Prometheus; using Serilog; using StackExchange.Redis; try { var builder = WebApplication.CreateBuilder(args); builder.Host.UseSerilog((ctx, cfg) => cfg.ReadFrom.Configuration(ctx.Configuration)); // Configuration builder.Services.Configure(builder.Configuration.GetSection(JwtOptions.Section)); builder.Services.Configure(builder.Configuration.GetSection(MinioOptions.Section)); // Database builder.Services.AddDbContext(options => options.UseNpgsql(builder.Configuration.GetConnectionString("DefaultConnection"))); // Redis builder.Services.AddSingleton(sp => { var config = ConfigurationOptions.Parse( sp.GetRequiredService()["Redis:ConnectionString"]!); config.AbortOnConnectFail = false; return ConnectionMultiplexer.Connect(config); }); // MinIO var minioOptions = builder.Configuration.GetSection(MinioOptions.Section).Get()!; builder.Services.AddSingleton(new MinioClient() .WithEndpoint(minioOptions.Endpoint) .WithCredentials(minioOptions.AccessKey, minioOptions.SecretKey) .WithSSL(minioOptions.UseSsl) .Build()); // Site Configuration builder.Services.Configure( builder.Configuration.GetSection(SiteConfigOptions.Section)); builder.Services.Configure( builder.Configuration.GetSection(PromotionRetryOptions.Section)); builder.Services.Configure(builder.Configuration.GetSection(FhirOptions.Section)); // JWT Authentication var jwtOptions = builder.Configuration.GetSection(JwtOptions.Section).Get()!; if (string.IsNullOrEmpty(jwtOptions.Secret) || Encoding.UTF8.GetByteCount(jwtOptions.Secret) < 32) throw new InvalidOperationException( "JWT Secret must be at least 256 bits (32 bytes). " + "Configure a strong secret via Jwt:Secret in appsettings or the Jwt__Secret environment variable."); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = true, ValidateAudience = true, ValidateLifetime = true, ValidateIssuerSigningKey = true, ValidIssuer = jwtOptions.Issuer, ValidAudience = jwtOptions.Audience, IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtOptions.Secret)) }; }); builder.Services.AddAuthorization(); // CORS builder.Services.AddCors(options => { options.AddPolicy("VigilCare", policy => { policy.WithOrigins(builder.Configuration.GetSection("Cors:AllowedOrigins").Get()!) .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); }); }); // Rate limiting (disabled in Testing — integration tests issue many auth requests) var isTesting = builder.Environment.IsEnvironment("Testing"); if (!isTesting) { builder.Services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; options.AddFixedWindowLimiter("auth", opt => { opt.Window = TimeSpan.FromMinutes(5); opt.PermitLimit = 10; opt.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; opt.QueueLimit = 0; }); }); } // Services 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.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddHostedService(); builder.Services.AddHostedService(); // Health checks builder.Services.AddHealthChecks() .AddNpgSql( builder.Configuration.GetConnectionString("DefaultConnection")!, name: "postgresql", tags: new[] { "ready", "startup" }) .AddRedis( builder.Configuration["Redis:ConnectionString"]!, name: "redis", tags: new[] { "ready" }) .AddCheck( "minio", tags: new[] { "ready" }); builder.Services.AddValidatorsFromAssemblyContaining(); builder.Services.AddScoped(); builder.Services.AddControllers(options => { options.OutputFormatters.Insert(0, new FhirJsonOutputFormatter()); options.Filters.AddService(); }); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddVigilCareRecordsSwagger(); var app = builder.Build(); app.UseMiddleware(); app.UseMiddleware(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(options => { options.SwaggerEndpoint("/swagger/v1/swagger.json", "VigilCare Records API v1"); options.DocumentTitle = "VigilCare Records API"; }); } app.UseHttpMetrics(); app.UseCors("VigilCare"); if (!app.Environment.IsEnvironment("Testing")) app.UseRateLimiter(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); app.MapMetrics(); app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = _ => false, ResultStatusCodes = { [HealthStatus.Healthy] = StatusCodes.Status200OK, [HealthStatus.Degraded] = StatusCodes.Status200OK, [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable } }); app.MapHealthChecks("/health/ready", new HealthCheckOptions { Predicate = check => check.Tags.Contains("ready"), ResultStatusCodes = { [HealthStatus.Healthy] = StatusCodes.Status200OK, [HealthStatus.Degraded] = StatusCodes.Status200OK, [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable } }); app.MapHealthChecks("/health/startup", new HealthCheckOptions { Predicate = check => check.Tags.Contains("startup"), ResultStatusCodes = { [HealthStatus.Healthy] = StatusCodes.Status200OK, [HealthStatus.Degraded] = StatusCodes.Status200OK, [HealthStatus.Unhealthy] = StatusCodes.Status503ServiceUnavailable } }); if (!app.Environment.IsEnvironment("Testing")) { using var scope = app.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); await db.Database.MigrateAsync(); await DataSeeder.SeedAsync(db); } app.Run(); } catch (System.Exception) { throw; } public partial class Program { }