using System.Text; using AccountTracking.Api.Data; using AccountTracking.Api.Services; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.EntityFrameworkCore; using Microsoft.IdentityModel.Tokens; using Polly; var builder = WebApplication.CreateBuilder(args); // ── Configuration ────────────────────────────────────────────────────────── var connectionString = builder.Configuration["DB_CONNECTION"]; var jwtSecret = builder.Configuration["JWT_SECRET"] ?? throw new InvalidOperationException("JWT_SECRET is required"); var appUsername = builder.Configuration["APP_USERNAME"] ?? throw new InvalidOperationException("APP_USERNAME is required"); var appPassword = builder.Configuration["APP_PASSWORD"] ?? throw new InvalidOperationException("APP_PASSWORD is required"); var allowedOrigin = builder.Configuration["ALLOWED_ORIGIN"] ?? "http://localhost:3000"; // ── Database ──────────────────────────────────────────────────────────────── if (!string.IsNullOrEmpty(connectionString)) { builder.Services.AddDbContext(opts => opts.UseMySql(connectionString, ServerVersion.AutoDetect(connectionString))); } else { // NSwag build-time spec generation: no real DB available builder.Services.AddDbContext(opts => opts.UseInMemoryDatabase("nswag_gen")); } // ── Services ──────────────────────────────────────────────────────────────── builder.Services.AddScoped(); builder.Services.AddScoped(); // ── Auth ──────────────────────────────────────────────────────────────────── var keyBytes = Convert.FromBase64String(jwtSecret); builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(opts => { opts.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, IssuerSigningKey = new SymmetricSecurityKey(keyBytes), ValidateIssuer = false, ValidateAudience = false, ClockSkew = TimeSpan.Zero }; }); builder.Services.AddAuthorization(); // Store credentials for use by AuthController builder.Services.AddSingleton(new AppCredentials(appUsername, appPassword, keyBytes)); // ── CORS ──────────────────────────────────────────────────────────────────── builder.Services.AddCors(opts => { opts.AddDefaultPolicy(policy => policy.WithOrigins(allowedOrigin, "http://localhost:3000") .AllowAnyHeader() .AllowAnyMethod()); }); // ── Controllers & OpenAPI ─────────────────────────────────────────────────── builder.Services.AddControllers(); builder.Services.AddOpenApiDocument(config => { config.Title = "AccountTracking API"; config.Version = "v1"; config.AddSecurity("Bearer", new NSwag.OpenApiSecurityScheme { Type = NSwag.OpenApiSecuritySchemeType.Http, Scheme = "bearer", BearerFormat = "JWT" }); }); // ── Kestrel: limit upload to 10 MB ───────────────────────────────────────── builder.WebHost.ConfigureKestrel(opts => opts.Limits.MaxRequestBodySize = 10 * 1024 * 1024); var app = builder.Build(); // ── Migrate DB (skip when running without a real DB) ─────────────────────── if (!string.IsNullOrEmpty(connectionString)) { var retryPolicy = Policy .Handle() .WaitAndRetry( 5, attempt => TimeSpan.FromSeconds(Math.Pow(2, attempt - 1)), (ex, delay, attempt, _) => Console.WriteLine($"DB migration attempt {attempt} failed: {ex.Message}. Retrying in {delay.TotalSeconds}s...")); using var scope = app.Services.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); retryPolicy.Execute(() => db.Database.Migrate()); } // ── Middleware pipeline ───────────────────────────────────────────────────── app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); app.UseOpenApi(); app.UseSwaggerUi(); app.MapControllers(); app.Run(); // Accessible by tests public partial class Program { } // Credential container registered in DI public record AppCredentials(string Username, string PasswordHash, byte[] JwtKeyBytes);