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:
@@ -1,7 +1,6 @@
|
||||
@page "/account/login"
|
||||
@using TrueCV.Web.Components.Layout
|
||||
@layout MainLayout
|
||||
@rendermode InteractiveServer
|
||||
|
||||
@using Microsoft.AspNetCore.Identity
|
||||
@using TrueCV.Infrastructure.Identity
|
||||
@@ -26,50 +25,40 @@
|
||||
|
||||
@if (!string.IsNullOrEmpty(_errorMessage))
|
||||
{
|
||||
<div class="alert alert-danger alert-dismissible fade show" role="alert">
|
||||
<div class="alert alert-danger" role="alert">
|
||||
@_errorMessage
|
||||
<button type="button" class="btn-close" @onclick="() => _errorMessage = null" aria-label="Close"></button>
|
||||
</div>
|
||||
}
|
||||
|
||||
<EditForm Model="_model" OnValidSubmit="HandleLogin" FormName="login">
|
||||
<DataAnnotationsValidator />
|
||||
<form method="post" action="/account/perform-login">
|
||||
<AntiforgeryToken />
|
||||
<input type="hidden" name="returnUrl" value="@ReturnUrl" />
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="email" class="form-label">Email address</label>
|
||||
<InputText id="email" class="form-control form-control-lg" @bind-Value="_model.Email"
|
||||
placeholder="name@example.com" />
|
||||
<ValidationMessage For="() => _model.Email" class="text-danger" />
|
||||
<input id="email" name="email" type="email" class="form-control form-control-lg"
|
||||
placeholder="name@example.com" required />
|
||||
</div>
|
||||
|
||||
<div class="mb-3">
|
||||
<label for="password" class="form-label">Password</label>
|
||||
<InputText id="password" type="password" class="form-control form-control-lg"
|
||||
@bind-Value="_model.Password" placeholder="Enter your password" />
|
||||
<ValidationMessage For="() => _model.Password" class="text-danger" />
|
||||
<input id="password" name="password" type="password" class="form-control form-control-lg"
|
||||
placeholder="Enter your password" required />
|
||||
</div>
|
||||
|
||||
<div class="mb-3 form-check">
|
||||
<InputCheckbox id="rememberMe" class="form-check-input" @bind-Value="_model.RememberMe" />
|
||||
<input id="rememberMe" name="rememberMe" type="checkbox" class="form-check-input" value="true" />
|
||||
<label class="form-check-label" for="rememberMe">
|
||||
Remember me
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="d-grid">
|
||||
<button type="submit" class="btn btn-primary btn-lg" disabled="@_isLoading">
|
||||
@if (_isLoading)
|
||||
{
|
||||
<span class="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true"></span>
|
||||
<span>Signing in...</span>
|
||||
}
|
||||
else
|
||||
{
|
||||
<span>Sign In</span>
|
||||
}
|
||||
<button type="submit" class="btn btn-primary btn-lg">
|
||||
Sign In
|
||||
</button>
|
||||
</div>
|
||||
</EditForm>
|
||||
</form>
|
||||
|
||||
<hr class="my-4" />
|
||||
|
||||
@@ -86,63 +75,16 @@
|
||||
</div>
|
||||
|
||||
@code {
|
||||
private LoginModel _model = new();
|
||||
private bool _isLoading;
|
||||
private string? _errorMessage;
|
||||
|
||||
[SupplyParameterFromQuery]
|
||||
public string? ReturnUrl { get; set; }
|
||||
|
||||
private async Task HandleLogin()
|
||||
[SupplyParameterFromQuery(Name = "error")]
|
||||
public string? Error { get; set; }
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
_isLoading = true;
|
||||
_errorMessage = null;
|
||||
|
||||
try
|
||||
{
|
||||
var result = await SignInManager.PasswordSignInAsync(
|
||||
_model.Email,
|
||||
_model.Password,
|
||||
_model.RememberMe,
|
||||
lockoutOnFailure: false);
|
||||
|
||||
if (result.Succeeded)
|
||||
{
|
||||
var returnUrl = string.IsNullOrEmpty(ReturnUrl) ? "/dashboard" : ReturnUrl;
|
||||
NavigationManager.NavigateTo(returnUrl, forceLoad: true);
|
||||
}
|
||||
else if (result.IsLockedOut)
|
||||
{
|
||||
_errorMessage = "This account has been locked out. Please try again later.";
|
||||
}
|
||||
else if (result.IsNotAllowed)
|
||||
{
|
||||
_errorMessage = "This account is not allowed to sign in.";
|
||||
}
|
||||
else
|
||||
{
|
||||
_errorMessage = "Invalid email or password.";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_errorMessage = $"An error occurred: {ex.Message}";
|
||||
}
|
||||
finally
|
||||
{
|
||||
_isLoading = false;
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class LoginModel
|
||||
{
|
||||
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Email is required")]
|
||||
[System.ComponentModel.DataAnnotations.EmailAddress(ErrorMessage = "Invalid email format")]
|
||||
public string Email { get; set; } = string.Empty;
|
||||
|
||||
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Password is required")]
|
||||
public string Password { get; set; } = string.Empty;
|
||||
|
||||
public bool RememberMe { get; set; }
|
||||
_errorMessage = Error;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user