Add UK education verification and security fixes
Features: - Add UK institution recognition (170+ universities) - Add diploma mill detection (100+ blacklisted institutions) - Add education verification service with date plausibility checks - Add local file storage option (no Azure required) - Add default admin user seeding on startup - Enhance Serilog logging with file output Security fixes: - Fix path traversal vulnerability in LocalFileStorageService - Fix open redirect in login endpoint (use LocalRedirect) - Fix password validation message (12 chars, not 6) - Fix login to use HTTP POST endpoint (avoid Blazor cookie issues) Code improvements: - Add CancellationToken propagation to CV parser - Add shared helpers (JsonDefaults, DateHelpers, ScoreThresholds) - Add IUserContextService for user ID extraction - Parallelized company verification in ProcessCVCheckJob - Add 28 unit tests for education verification Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -32,15 +32,19 @@ try
|
||||
// Add Infrastructure services (DbContext, Hangfire, HttpClients, Services)
|
||||
builder.Services.AddInfrastructure(builder.Configuration);
|
||||
|
||||
// Add Identity
|
||||
// Add Identity with secure password requirements
|
||||
builder.Services.AddIdentity<ApplicationUser, IdentityRole<Guid>>(options =>
|
||||
{
|
||||
options.Password.RequireDigit = false;
|
||||
options.Password.RequireLowercase = false;
|
||||
options.Password.RequireUppercase = false;
|
||||
options.Password.RequireNonAlphanumeric = false;
|
||||
options.Password.RequiredLength = 6;
|
||||
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();
|
||||
@@ -62,6 +66,26 @@ try
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
// Seed default admin user
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Configure the HTTP request pipeline.
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
@@ -98,6 +122,44 @@ try
|
||||
});
|
||||
}
|
||||
|
||||
// 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) =>
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user