using Hangfire; using Microsoft.AspNetCore.Components.Authorization; using Microsoft.AspNetCore.Identity; using Serilog; using TrueCV.Infrastructure; using TrueCV.Infrastructure.Data; using TrueCV.Infrastructure.Identity; using TrueCV.Web; using TrueCV.Web.Components; using TrueCV.Web.Services; // Configure Serilog Log.Logger = new LoggerConfiguration() .WriteTo.Console() .CreateBootstrapLogger(); try { Log.Information("Starting TrueCV 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"); var app = builder.Build(); // Seed default admin user using (var scope = app.Services.CreateScope()) { var userManager = scope.ServiceProvider.GetRequiredService>(); var defaultEmail = "admin@truecv.local"; var defaultPassword = "TrueCV_Admin123!"; if (await userManager.FindByEmailAsync(defaultEmail) == null) { var adminUser = new ApplicationUser { UserName = defaultEmail, Email = defaultEmail, EmailConfirmed = true }; await userManager.CreateAsync(adminUser, defaultPassword); Log.Information("Created default admin user: {Email}", defaultEmail); } } // 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(); // Add Hangfire Dashboard (only in development) if (app.Environment.IsDevelopment()) { app.UseHangfireDashboard("/hangfire", new DashboardOptions { Authorization = [] // Allow anonymous access in development }); } // 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."); } }); // 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(); }