Refactor. Add data layer

This commit is contained in:
rossmill76 committed 2026-03-06 11:05:01 +00:00
1 parent 3b7e4e6ec5
commit b6b794f0b4
82 files changed
+2930 -2811

No files matched your search

+21 -177
View File
@@ -1,7 +1,7 @@
@page "/settings"
@attribute [Authorize]
@using BudgetApp.Data.Entities
@using Microsoft.AspNetCore.Identity
@using Microsoft.EntityFrameworkCore
@inject UserManager<ApplicationUser> UserManager
@inject SignInManager<ApplicationUser> SignInManager
@inject AuthenticationStateProvider AuthStateProvider
@@ -24,30 +24,30 @@
<h5 class="mb-0">Change Password</h5>
</div>
<div class="card-body">
@if (passwordSuccess)
@if (_passwordSuccess)
{
<div class="alert alert-success">Password changed successfully.</div>
}
@if (!string.IsNullOrEmpty(passwordError))
@if (!string.IsNullOrEmpty(_passwordError))
{
<div class="alert alert-danger">@passwordError</div>
<div class="alert alert-danger">@_passwordError</div>
}
<EditForm Model="passwordForm" OnValidSubmit="ChangePassword" FormName="changePassword">
<EditForm Model="_passwordForm" OnValidSubmit="ChangePassword" FormName="changePassword">
<ToastValidator />
<div class="row g-3">
<div class="col-md-4">
<label class="form-label">Current Password</label>
<InputText @bind-Value="passwordForm.CurrentPassword" type="password" class="form-control" />
<InputText @bind-Value="_passwordForm.CurrentPassword" type="password" class="form-control" />
</div>
<div class="col-md-4">
<label class="form-label">New Password</label>
<InputText @bind-Value="passwordForm.NewPassword" type="password" class="form-control" />
<InputText @bind-Value="_passwordForm.NewPassword" type="password" class="form-control" />
</div>
<div class="col-md-4">
<label class="form-label">Confirm New Password</label>
<InputText @bind-Value="passwordForm.ConfirmNewPassword" type="password" class="form-control" />
<InputText @bind-Value="_passwordForm.ConfirmNewPassword" type="password" class="form-control" />
</div>
<div class="col-12">
<button type="submit" class="btn btn-primary">Update Password</button>
@@ -67,19 +67,19 @@
This will permanently delete all your accounts, transactions and categories, giving you a clean slate. Your user account and password will be kept.
</p>
@if (resetSuccess)
@if (_resetSuccess)
{
<div class="alert alert-success">All data has been reset successfully.</div>
}
@if (!string.IsNullOrEmpty(resetError))
@if (!string.IsNullOrEmpty(_resetError))
{
<div class="alert alert-danger">@resetError</div>
<div class="alert alert-danger">@_resetError</div>
}
@if (!confirmingReset)
@if (!_confirmingReset)
{
<button class="btn btn-outline-warning" @onclick="() => confirmingReset = true">
<button class="btn btn-outline-warning" @onclick="() => _confirmingReset = true">
Reset All Data
</button>
}
@@ -89,8 +89,8 @@
<strong>Are you sure?</strong> This cannot be undone. Type <code>I UNDERSTAND</code> and enter your password to confirm.
</div>
<div class="mb-3" style="max-width: 400px;">
<input type="text" class="form-control mb-2" placeholder="Type I UNDERSTAND to confirm" @bind="resetConfirmation" />
<input type="password" class="form-control mb-2" placeholder="Enter your password" @bind="resetPassword" />
<input type="text" class="form-control mb-2" placeholder="Type I UNDERSTAND to confirm" @bind="_resetConfirmation" />
<input type="password" class="form-control mb-2" placeholder="Enter your password" @bind="_resetPassword" />
<div class="d-flex gap-2">
<button class="btn btn-warning" @onclick="ResetData">Confirm Reset</button>
<button class="btn btn-secondary" @onclick="CancelReset">Cancel</button>
@@ -110,14 +110,14 @@
This will permanently delete your account and all associated data (accounts, transactions, and categories). This action cannot be undone.
</p>
@if (!string.IsNullOrEmpty(deleteError))
@if (!string.IsNullOrEmpty(_deleteError))
{
<div class="alert alert-danger">@deleteError</div>
<div class="alert alert-danger">@_deleteError</div>
}
@if (!confirmingDelete)
@if (!_confirmingDelete)
{
<button class="btn btn-outline-danger" @onclick="() => confirmingDelete = true">
<button class="btn btn-outline-danger" @onclick="() => _confirmingDelete = true">
Delete My Account
</button>
}
@@ -127,166 +127,10 @@
<strong>Are you sure?</strong> Type your password to confirm.
</div>
<div class="input-group mb-3" style="max-width: 400px;">
<input type="password" class="form-control" placeholder="Enter your password" @bind="deletePassword" />
<input type="password" class="form-control" placeholder="Enter your password" @bind="_deletePassword" />
<button class="btn btn-danger" @onclick="DeleteAccount">Confirm Delete</button>
<button class="btn btn-secondary" @onclick="CancelDelete">Cancel</button>
</div>
}
</div>
</div>
@code {
private static readonly string appVersion =
typeof(Program).Assembly.GetName().Version?.ToString(3) ?? "0.0.0";
private ChangePasswordForm passwordForm = new();
private bool passwordSuccess;
private string? passwordError;
private bool confirmingDelete;
private string deletePassword = string.Empty;
private string? deleteError;
private bool confirmingReset;
private string resetConfirmation = string.Empty;
private string resetPassword = string.Empty;
private string? resetError;
private bool resetSuccess;
private async Task ChangePassword()
{
passwordSuccess = false;
passwordError = null;
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var userIdClaim = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
if (userIdClaim is null) return;
var user = await UserManager.FindByIdAsync(userIdClaim);
if (user is null) return;
var result = await UserManager.ChangePasswordAsync(user, passwordForm.CurrentPassword, passwordForm.NewPassword);
if (result.Succeeded)
{
passwordSuccess = true;
passwordForm = new ChangePasswordForm();
}
else
{
passwordError = string.Join(" ", result.Errors.Select(e => e.Description));
}
}
private async Task DeleteAccount()
{
deleteError = null;
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var userIdClaim = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
if (userIdClaim is null) return;
var user = await UserManager.FindByIdAsync(userIdClaim);
if (user is null) return;
var passwordValid = await UserManager.CheckPasswordAsync(user, deletePassword);
if (!passwordValid)
{
deleteError = "Incorrect password.";
return;
}
// Delete all user data explicitly in correct dependency order
await DbContext.Transactions
.Where(t => t.Account.UserId == userIdClaim)
.ExecuteDeleteAsync();
await DbContext.Accounts
.Where(a => a.UserId == userIdClaim)
.ExecuteDeleteAsync();
await DbContext.Categories
.Where(c => c.UserId == userIdClaim)
.ExecuteDeleteAsync();
// Delete identity user
await UserManager.DeleteAsync(user);
await SignInManager.SignOutAsync();
Navigation.NavigateTo("/login", forceLoad: true);
}
private void CancelDelete()
{
confirmingDelete = false;
deletePassword = string.Empty;
deleteError = null;
}
private async Task ResetData()
{
resetError = null;
resetSuccess = false;
var authState = await AuthStateProvider.GetAuthenticationStateAsync();
var userIdClaim = authState.User.FindFirst(System.Security.Claims.ClaimTypes.NameIdentifier)?.Value;
if (userIdClaim is null) return;
var user = await UserManager.FindByIdAsync(userIdClaim);
if (user is null) return;
if (!string.Equals(resetConfirmation?.Trim(), "I UNDERSTAND", StringComparison.Ordinal))
{
resetError = "Please type 'I UNDERSTAND' exactly to confirm.";
return;
}
var passwordValid = await UserManager.CheckPasswordAsync(user, resetPassword);
if (!passwordValid)
{
resetError = "Incorrect password.";
return;
}
await DbContext.Transactions
.Where(t => t.Account.UserId == userIdClaim)
.ExecuteDeleteAsync();
await DbContext.Accounts
.Where(a => a.UserId == userIdClaim)
.ExecuteDeleteAsync();
await DbContext.Categories
.Where(c => c.UserId == userIdClaim)
.ExecuteDeleteAsync();
confirmingReset = false;
resetConfirmation = string.Empty;
resetPassword = string.Empty;
resetSuccess = true;
AppState.NotifyAccountsChanged();
}
private void CancelReset()
{
confirmingReset = false;
resetConfirmation = string.Empty;
resetPassword = string.Empty;
resetError = null;
}
private sealed class ChangePasswordForm
{
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Current password is required")]
public string CurrentPassword { get; set; } = string.Empty;
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "New password is required")]
[System.ComponentModel.DataAnnotations.MinLength(8, ErrorMessage = "Password must be at least 8 characters")]
public string NewPassword { get; set; } = string.Empty;
[System.ComponentModel.DataAnnotations.Required(ErrorMessage = "Please confirm your new password")]
[System.ComponentModel.DataAnnotations.Compare(nameof(NewPassword), ErrorMessage = "Passwords do not match")]
public string ConfirmNewPassword { get; set; } = string.Empty;
}
}
</div>