Files
RealCV/src/TrueCV.Web/Program.cs

201 lines
6.7 KiB
C#
Raw Normal View History

using Hangfire;
using Microsoft.AspNetCore.Components.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
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<PdfReportService>();
// Add Identity with secure password requirements
builder.Services.AddIdentity<ApplicationUser, IdentityRole<Guid>>(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<ApplicationDbContext>()
.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<AuthenticationStateProvider, RevalidatingIdentityAuthenticationStateProvider<ApplicationUser>>();
// Add health checks
builder.Services.AddHealthChecks()
.AddDbContextCheck<ApplicationDbContext>("database");
var app = builder.Build();
// Seed default admin user and clear company cache
using (var scope = app.Services.CreateScope())
{
var userManager = scope.ServiceProvider.GetRequiredService<UserManager<ApplicationUser>>();
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);
}
// Clear company cache on startup to ensure fresh API lookups
var dbContext = scope.ServiceProvider.GetRequiredService<ApplicationDbContext>();
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();
// 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<ApplicationUser> 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<ApplicationUser> signInManager) =>
{
await signInManager.SignOutAsync();
return Results.Redirect("/");
}).RequireAuthorization();
// Health check endpoint
app.MapHealthChecks("/health");
app.MapRazorComponents<App>()
.AddInteractiveServerRenderMode();
app.Run();
}
catch (Exception ex)
{
Log.Fatal(ex, "Application terminated unexpectedly");
}
finally
{
Log.CloseAndFlush();
}