Files
RealCV/src/TrueCV.Web/Components/Pages/Account/Register.razor
peter f1ccd217d8 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>
2026-01-20 16:45:43 +01:00

164 lines
7.2 KiB
Plaintext

@page "/account/register"
@using TrueCV.Web.Components.Layout
@layout MainLayout
@rendermode InteractiveServer
@using Microsoft.AspNetCore.Identity
@using TrueCV.Infrastructure.Identity
@inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager
@inject NavigationManager NavigationManager
<PageTitle>Register - TrueCV</PageTitle>
<div class="container py-5">
<div class="row justify-content-center">
<div class="col-md-5">
<div class="card border-0 shadow">
<div class="card-body p-5">
<div class="text-center mb-4">
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="currentColor" class="bi bi-patch-check-fill text-primary mb-3" viewBox="0 0 16 16">
<path d="M10.067.87a2.89 2.89 0 0 0-4.134 0l-.622.638-.89-.011a2.89 2.89 0 0 0-2.924 2.924l.01.89-.636.622a2.89 2.89 0 0 0 0 4.134l.637.622-.011.89a2.89 2.89 0 0 0 2.924 2.924l.89-.01.622.636a2.89 2.89 0 0 0 4.134 0l.622-.637.89.011a2.89 2.89 0 0 0 2.924-2.924l-.01-.89.636-.622a2.89 2.89 0 0 0 0-4.134l-.637-.622.011-.89a2.89 2.89 0 0 0-2.924-2.924l-.89.01-.622-.636zm.287 5.984-3 3a.5.5 0 0 1-.708 0l-1.5-1.5a.5.5 0 1 1 .708-.708L7 8.793l2.646-2.647a.5.5 0 0 1 .708.708z"/>
</svg>
<h3 class="fw-bold">Create Account</h3>
<p class="text-muted">Start verifying CVs with confidence</p>
</div>
@if (!string.IsNullOrEmpty(_errorMessage))
{
<div class="alert alert-danger alert-dismissible fade show" role="alert">
@_errorMessage
<button type="button" class="btn-close" @onclick="() => _errorMessage = null" aria-label="Close"></button>
</div>
}
<EditForm Model="_model" OnValidSubmit="HandleRegister" FormName="register">
<DataAnnotationsValidator />
<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" />
</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="Create a password" />
<ValidationMessage For="() => _model.Password" class="text-danger" />
<div class="form-text">Password must be at least 12 characters with uppercase, lowercase, number, and symbol.</div>
</div>
<div class="mb-4">
<label for="confirmPassword" class="form-label">Confirm Password</label>
<InputText id="confirmPassword" type="password" class="form-control form-control-lg"
@bind-Value="_model.ConfirmPassword" placeholder="Confirm your password" />
<ValidationMessage For="() => _model.ConfirmPassword" class="text-danger" />
</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>Creating account...</span>
}
else
{
<span>Create Account</span>
}
</button>
</div>
</EditForm>
<hr class="my-4" />
<div class="text-center">
<p class="mb-0">
Already have an account?
<a href="/account/login" class="text-decoration-none fw-medium">Sign in</a>
</p>
</div>
</div>
</div>
<div class="text-center mt-4">
<small class="text-muted">
By creating an account, you agree to our
<a href="#" class="text-decoration-none">Terms of Service</a>
and
<a href="#" class="text-decoration-none">Privacy Policy</a>
</small>
</div>
</div>
</div>
</div>
@code {
private RegisterModel _model = new();
private bool _isLoading;
private string? _errorMessage;
private async Task HandleRegister()
{
_isLoading = true;
_errorMessage = null;
try
{
if (_model.Password != _model.ConfirmPassword)
{
_errorMessage = "Passwords do not match.";
_isLoading = false;
return;
}
var user = new ApplicationUser
{
UserName = _model.Email,
Email = _model.Email,
Plan = Domain.Enums.UserPlan.Free,
ChecksUsedThisMonth = 0
};
var result = await UserManager.CreateAsync(user, _model.Password);
if (result.Succeeded)
{
await SignInManager.SignInAsync(user, isPersistent: false);
NavigationManager.NavigateTo("/dashboard", forceLoad: true);
}
else
{
var errors = result.Errors.Select(e => e.Description);
_errorMessage = string.Join(" ", errors);
}
}
catch (Exception ex)
{
_errorMessage = $"An error occurred: {ex.Message}";
}
finally
{
_isLoading = false;
}
}
private sealed class RegisterModel
{
[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")]
[System.ComponentModel.DataAnnotations.MinLength(12, ErrorMessage = "Password must be at least 12 characters")]
public string Password { get; set; } = string.Empty;
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Please confirm your password")]
[System.ComponentModel.DataAnnotations.Compare(nameof(Password), ErrorMessage = "Passwords do not match")]
public string ConfirmPassword { get; set; } = string.Empty;
}
}