236 lines
10 KiB
Plaintext
236 lines
10 KiB
Plaintext
|
|
@page "/account/settings"
|
||
|
|
@attribute [Authorize]
|
||
|
|
@rendermode InteractiveServer
|
||
|
|
|
||
|
|
@using Microsoft.AspNetCore.Identity
|
||
|
|
@using TrueCV.Infrastructure.Identity
|
||
|
|
|
||
|
|
@inject UserManager<ApplicationUser> UserManager
|
||
|
|
@inject SignInManager<ApplicationUser> SignInManager
|
||
|
|
@inject AuthenticationStateProvider AuthenticationStateProvider
|
||
|
|
@inject NavigationManager NavigationManager
|
||
|
|
@inject ILogger<Settings> Logger
|
||
|
|
|
||
|
|
<PageTitle>Account Settings - TrueCV</PageTitle>
|
||
|
|
|
||
|
|
<div class="container py-5">
|
||
|
|
<div class="row justify-content-center">
|
||
|
|
<div class="col-lg-8">
|
||
|
|
<div class="mb-4">
|
||
|
|
<h1 class="fw-bold mb-1">Account Settings</h1>
|
||
|
|
<p class="text-muted">Manage your account details and security</p>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
@if (!string.IsNullOrEmpty(_successMessage))
|
||
|
|
{
|
||
|
|
<div class="alert alert-success alert-dismissible fade show" role="alert">
|
||
|
|
@_successMessage
|
||
|
|
<button type="button" class="btn-close" @onclick="() => _successMessage = null"></button>
|
||
|
|
</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"></button>
|
||
|
|
</div>
|
||
|
|
}
|
||
|
|
|
||
|
|
<!-- Profile Section -->
|
||
|
|
<div class="card border-0 shadow-sm mb-4">
|
||
|
|
<div class="card-body p-4">
|
||
|
|
<h5 class="fw-bold mb-4">Profile Information</h5>
|
||
|
|
|
||
|
|
<div class="mb-3">
|
||
|
|
<label class="form-label small text-muted">Email Address</label>
|
||
|
|
<input type="email" class="form-control" value="@_userEmail" disabled />
|
||
|
|
<small class="text-muted">Email cannot be changed</small>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="mb-3">
|
||
|
|
<label class="form-label small text-muted">Current Plan</label>
|
||
|
|
<div class="d-flex align-items-center gap-2">
|
||
|
|
<span class="badge bg-primary-subtle text-primary px-3 py-2">@_userPlan</span>
|
||
|
|
<a href="/account/billing" class="btn btn-sm btn-link">Manage</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="mb-0">
|
||
|
|
<label class="form-label small text-muted">Member Since</label>
|
||
|
|
<p class="mb-0">@_memberSince?.ToString("dd MMMM yyyy")</p>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Change Password Section -->
|
||
|
|
<div class="card border-0 shadow-sm mb-4">
|
||
|
|
<div class="card-body p-4">
|
||
|
|
<h5 class="fw-bold mb-4">Change Password</h5>
|
||
|
|
|
||
|
|
<EditForm Model="_passwordModel" OnValidSubmit="ChangePassword" FormName="change-password">
|
||
|
|
<DataAnnotationsValidator />
|
||
|
|
|
||
|
|
<div class="mb-3">
|
||
|
|
<label for="currentPassword" class="form-label small text-muted">Current Password</label>
|
||
|
|
<InputText type="password" id="currentPassword" class="form-control" @bind-Value="_passwordModel.CurrentPassword" />
|
||
|
|
<ValidationMessage For="() => _passwordModel.CurrentPassword" class="text-danger small" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="mb-3">
|
||
|
|
<label for="newPassword" class="form-label small text-muted">New Password</label>
|
||
|
|
<InputText type="password" id="newPassword" class="form-control" @bind-Value="_passwordModel.NewPassword" />
|
||
|
|
<ValidationMessage For="() => _passwordModel.NewPassword" class="text-danger small" />
|
||
|
|
<small class="text-muted">Minimum 12 characters with uppercase, lowercase, number, and special character</small>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<div class="mb-4">
|
||
|
|
<label for="confirmPassword" class="form-label small text-muted">Confirm New Password</label>
|
||
|
|
<InputText type="password" id="confirmPassword" class="form-control" @bind-Value="_passwordModel.ConfirmPassword" />
|
||
|
|
<ValidationMessage For="() => _passwordModel.ConfirmPassword" class="text-danger small" />
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<button type="submit" class="btn btn-primary" disabled="@_isChangingPassword">
|
||
|
|
@if (_isChangingPassword)
|
||
|
|
{
|
||
|
|
<span class="spinner-border spinner-border-sm me-1" role="status"></span>
|
||
|
|
}
|
||
|
|
Update Password
|
||
|
|
</button>
|
||
|
|
</EditForm>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
<!-- Quick Links -->
|
||
|
|
<div class="card border-0 shadow-sm">
|
||
|
|
<div class="card-body p-4">
|
||
|
|
<h5 class="fw-bold mb-4">Quick Links</h5>
|
||
|
|
<div class="list-group list-group-flush">
|
||
|
|
<a href="/account/billing" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center px-0 border-0">
|
||
|
|
<div>
|
||
|
|
<strong>Billing & Subscription</strong>
|
||
|
|
<p class="mb-0 small text-muted">Manage your plan and payment method</p>
|
||
|
|
</div>
|
||
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="text-muted" viewBox="0 0 16 16">
|
||
|
|
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
|
||
|
|
</svg>
|
||
|
|
</a>
|
||
|
|
<a href="/dashboard" class="list-group-item list-group-item-action d-flex justify-content-between align-items-center px-0 border-0">
|
||
|
|
<div>
|
||
|
|
<strong>Dashboard</strong>
|
||
|
|
<p class="mb-0 small text-muted">View your CV verification history</p>
|
||
|
|
</div>
|
||
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor" class="text-muted" viewBox="0 0 16 16">
|
||
|
|
<path fill-rule="evenodd" d="M4.646 1.646a.5.5 0 0 1 .708 0l6 6a.5.5 0 0 1 0 .708l-6 6a.5.5 0 0 1-.708-.708L10.293 8 4.646 2.354a.5.5 0 0 1 0-.708z"/>
|
||
|
|
</svg>
|
||
|
|
</a>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
</div>
|
||
|
|
|
||
|
|
@code {
|
||
|
|
private string? _userEmail;
|
||
|
|
private string _userPlan = "Free";
|
||
|
|
private DateTime? _memberSince;
|
||
|
|
private string? _successMessage;
|
||
|
|
private string? _errorMessage;
|
||
|
|
private bool _isChangingPassword;
|
||
|
|
private PasswordChangeModel _passwordModel = new();
|
||
|
|
|
||
|
|
protected override async Task OnInitializedAsync()
|
||
|
|
{
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||
|
|
var userIdClaim = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||
|
|
|
||
|
|
if (!string.IsNullOrEmpty(userIdClaim) && Guid.TryParse(userIdClaim, out var userId))
|
||
|
|
{
|
||
|
|
var user = await UserManager.FindByIdAsync(userId.ToString());
|
||
|
|
if (user != null)
|
||
|
|
{
|
||
|
|
_userEmail = user.Email;
|
||
|
|
_userPlan = user.Plan.ToString();
|
||
|
|
// Lockout end date is used as a proxy; in a real app you might have a CreatedAt field
|
||
|
|
_memberSince = DateTime.UtcNow.AddMonths(-1); // Placeholder
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
Logger.LogError(ex, "Error loading user settings");
|
||
|
|
_errorMessage = "Unable to load account information.";
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private async Task ChangePassword()
|
||
|
|
{
|
||
|
|
if (_isChangingPassword) return;
|
||
|
|
|
||
|
|
_isChangingPassword = true;
|
||
|
|
_errorMessage = null;
|
||
|
|
_successMessage = null;
|
||
|
|
|
||
|
|
try
|
||
|
|
{
|
||
|
|
var authState = await AuthenticationStateProvider.GetAuthenticationStateAsync();
|
||
|
|
var userIdClaim = authState.User.FindFirst(ClaimTypes.NameIdentifier)?.Value;
|
||
|
|
|
||
|
|
if (string.IsNullOrEmpty(userIdClaim) || !Guid.TryParse(userIdClaim, out var userId))
|
||
|
|
{
|
||
|
|
_errorMessage = "Unable to identify user.";
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var user = await UserManager.FindByIdAsync(userId.ToString());
|
||
|
|
if (user == null)
|
||
|
|
{
|
||
|
|
_errorMessage = "User not found.";
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
var result = await UserManager.ChangePasswordAsync(
|
||
|
|
user,
|
||
|
|
_passwordModel.CurrentPassword,
|
||
|
|
_passwordModel.NewPassword);
|
||
|
|
|
||
|
|
if (result.Succeeded)
|
||
|
|
{
|
||
|
|
_successMessage = "Password updated successfully.";
|
||
|
|
_passwordModel = new PasswordChangeModel();
|
||
|
|
await SignInManager.RefreshSignInAsync(user);
|
||
|
|
}
|
||
|
|
else
|
||
|
|
{
|
||
|
|
_errorMessage = string.Join(" ", result.Errors.Select(e => e.Description));
|
||
|
|
}
|
||
|
|
}
|
||
|
|
catch (Exception ex)
|
||
|
|
{
|
||
|
|
Logger.LogError(ex, "Error changing password");
|
||
|
|
_errorMessage = "An error occurred. Please try again.";
|
||
|
|
}
|
||
|
|
finally
|
||
|
|
{
|
||
|
|
_isChangingPassword = false;
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
private class PasswordChangeModel
|
||
|
|
{
|
||
|
|
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Current password is required")]
|
||
|
|
public string CurrentPassword { get; set; } = "";
|
||
|
|
|
||
|
|
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "New password is required")]
|
||
|
|
[System.ComponentModel.DataAnnotations.MinLength(12, ErrorMessage = "Password must be at least 12 characters")]
|
||
|
|
public string NewPassword { get; set; } = "";
|
||
|
|
|
||
|
|
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Please confirm your new password")]
|
||
|
|
[System.ComponentModel.DataAnnotations.Compare("NewPassword", ErrorMessage = "Passwords do not match")]
|
||
|
|
public string ConfirmPassword { get; set; } = "";
|
||
|
|
}
|
||
|
|
}
|