using System.Threading.RateLimiting; using Hangfire; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.RateLimiting; using Microsoft.EntityFrameworkCore; using Serilog; using RealCV.Infrastructure; using RealCV.Infrastructure.Data; using RealCV.Infrastructure.Identity; using RealCV.Web; using RealCV.Web.Components; using RealCV.Web.Services; // Configure Serilog Log.Logger = new LoggerConfiguration() .WriteTo.Console() .CreateBootstrapLogger(); try { Log.Information("Starting RealCV web application"); var builder = WebApplication.CreateBuilder(args); // Configure Serilog from appsettings builder.Host.UseSerilog((context, services, configuration) => configuration .ReadFrom.Configuration(context.Configuration) .ReadFrom.Services(services) .Enrich.FromLogContext()); // Add services to the container. builder.Services.AddRazorComponents() .AddInteractiveServerComponents(); // Add Infrastructure services (DbContext, Hangfire, HttpClients, Services) builder.Services.AddInfrastructure(builder.Configuration); // Add Web services builder.Services.AddScoped(); // Add Identity with secure password requirements builder.Services.AddIdentity>(options => { options.Password.RequireDigit = true; options.Password.RequireLowercase = true; options.Password.RequireUppercase = true; options.Password.RequireNonAlphanumeric = true; options.Password.RequiredLength = 12; options.Password.RequiredUniqueChars = 4; options.SignIn.RequireConfirmedAccount = false; options.Lockout.DefaultLockoutTimeSpan = TimeSpan.FromMinutes(5); options.Lockout.MaxFailedAccessAttempts = 5; options.Lockout.AllowedForNewUsers = true; }) .AddEntityFrameworkStores() .AddDefaultTokenProviders(); builder.Services.ConfigureApplicationCookie(options => { options.LoginPath = "/account/login"; options.LogoutPath = "/account/logout"; options.AccessDeniedPath = "/account/login"; }); // Add Cascading Authentication State builder.Services.AddCascadingAuthenticationState(); builder.Services.AddScoped>(); // Add health checks builder.Services.AddHealthChecks() .AddDbContextCheck("database"); // Add rate limiting for login endpoint builder.Services.AddRateLimiter(options => { options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; options.AddFixedWindowLimiter("login", limiterOptions => { limiterOptions.PermitLimit = 5; limiterOptions.Window = TimeSpan.FromMinutes(1); limiterOptions.QueueProcessingOrder = QueueProcessingOrder.OldestFirst; limiterOptions.QueueLimit = 0; }); }); var app = builder.Build(); // Seed default admin user and clear company cache using (var scope = app.Services.CreateScope()) { var userManager = scope.ServiceProvider.GetRequiredService>(); var defaultEmail = builder.Configuration["DefaultAdmin:Email"]; var defaultPassword = builder.Configuration["DefaultAdmin:Password"]; if (!string.IsNullOrEmpty(defaultEmail) && !string.IsNullOrEmpty(defaultPassword)) { if (await userManager.FindByEmailAsync(defaultEmail) == null) { var adminUser = new ApplicationUser { UserName = defaultEmail, Email = defaultEmail, EmailConfirmed = true }; var result = await userManager.CreateAsync(adminUser, defaultPassword); if (result.Succeeded) { Log.Information("Created default admin user: {Email}", defaultEmail); } else { Log.Warning("Failed to create admin user: {Errors}", string.Join(", ", result.Errors.Select(e => e.Description))); } } } else { Log.Information("No default admin credentials configured - skipping admin user seeding"); } // Clear company cache on startup to ensure fresh API lookups var dbContext = scope.ServiceProvider.GetRequiredService(); var cacheCount = await dbContext.CompanyCache.CountAsync(); if (cacheCount > 0) { dbContext.CompanyCache.RemoveRange(dbContext.CompanyCache); await dbContext.SaveChangesAsync(); Log.Information("Cleared {Count} entries from company cache", cacheCount); } } // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Error", createScopeForErrors: true); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } // Only use HTTPS redirection when not running in Docker/container if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DOTNET_RUNNING_IN_CONTAINER"))) { // Running in container - skip HTTPS redirect (handled by reverse proxy) } else { app.UseHttpsRedirection(); } app.UseStaticFiles(); app.UseAntiforgery(); // Add Serilog request logging app.UseSerilogRequestLogging(); app.UseAuthentication(); app.UseAuthorization(); app.UseRateLimiter(); // Add Hangfire Dashboard (only in development, requires authentication) if (app.Environment.IsDevelopment()) { app.UseHangfireDashboard("/hangfire", new DashboardOptions { Authorization = [new HangfireAuthorizationFilter()] }); } // Login endpoint app.MapPost("/account/perform-login", async ( HttpContext context, SignInManager signInManager) => { var form = await context.Request.ReadFormAsync(); var email = form["email"].ToString(); var password = form["password"].ToString(); var rememberMe = form["rememberMe"].ToString() == "true"; var returnUrl = form["returnUrl"].ToString(); Log.Information("Login attempt for {Email}", email); // Validate returnUrl is local to prevent open redirect attacks if (string.IsNullOrEmpty(returnUrl) || !Uri.IsWellFormedUriString(returnUrl, UriKind.Relative) || returnUrl.StartsWith("//")) { returnUrl = "/dashboard"; } var result = await signInManager.PasswordSignInAsync(email, password, rememberMe, lockoutOnFailure: true); if (result.Succeeded) { Log.Information("User {Email} logged in successfully", email); return Results.LocalRedirect(returnUrl); } else if (result.IsLockedOut) { Log.Warning("User {Email} account is locked out", email); return Results.Redirect("/account/login?error=Account+locked.+Try+again+later."); } else { Log.Warning("Failed login attempt for {Email}", email); return Results.Redirect("/account/login?error=Invalid+email+or+password."); } }).RequireRateLimiting("login"); // Logout endpoint app.MapPost("/account/logout", async (SignInManager signInManager) => { await signInManager.SignOutAsync(); return Results.Redirect("/"); }).RequireAuthorization(); // Health check endpoint app.MapHealthChecks("/health"); app.MapRazorComponents() .AddInteractiveServerRenderMode(); app.Run(); } catch (Exception ex) { Log.Fatal(ex, "Application terminated unexpectedly"); } finally { Log.CloseAndFlush(); }