From 42b5ed371c1cd3bd3d3be04264a6d82b349ef5cb Mon Sep 17 00:00:00 2001 From: Nick Beckley Date: Fri, 12 Jun 2026 20:28:55 +0100 Subject: [PATCH] Phase 7E User Delete UI --- .../Controllers/ManageAccountController.cs | 61 ++++++++++++- PlotLine/ViewModels/AuthViewModels.cs | 1 + PlotLine/Views/ManageAccount/Index.cshtml | 88 +++++++++++++++++++ 3 files changed, 148 insertions(+), 2 deletions(-) diff --git a/PlotLine/Controllers/ManageAccountController.cs b/PlotLine/Controllers/ManageAccountController.cs index 6828898..5c00328 100644 --- a/PlotLine/Controllers/ManageAccountController.cs +++ b/PlotLine/Controllers/ManageAccountController.cs @@ -1,4 +1,6 @@ using System.Text.Json; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using PlotLine.Data; @@ -13,9 +15,11 @@ public sealed class ManageAccountController( ICurrentUserService currentUser, IUserRepository users, IAuthService authService, + IAccountDeletionService accountDeletion, ITwoFactorService twoFactorService, ISubscriptionService subscriptions, - IStripeBillingService stripeBilling) : Controller + IStripeBillingService stripeBilling, + ILogger logger) : Controller { [HttpGet] public async Task Index() @@ -29,10 +33,63 @@ public sealed class ManageAccountController( return View(new ManageAccountViewModel { User = user, - Subscription = await subscriptions.GetCurrentSubscriptionAsync(user.UserID) + Subscription = await subscriptions.GetCurrentSubscriptionAsync(user.UserID), + HasPaidBillingRisk = await subscriptions.HasPaidBillingRiskAsync(user.UserID) }); } + [HttpPost] + [ValidateAntiForgeryToken] + public async Task HardDeleteAccount(string? confirmationEmail) + { + var userId = currentUser.UserId; + if (!userId.HasValue) + { + return RedirectToAction("Login", "Account"); + } + + var user = await users.GetByIdAsync(userId.Value); + if (user is null) + { + TempData["AuthMessage"] = "Your account could not be found."; + return RedirectToAction("Login", "Account"); + } + + if (!string.Equals(confirmationEmail?.Trim(), user.Email, StringComparison.OrdinalIgnoreCase)) + { + TempData["AuthError"] = "The email address entered did not match your account email."; + return RedirectToAction(nameof(Index)); + } + + AccountDeletionResult result; + try + { + result = await accountDeletion.HardDeleteAccountAsync(user.UserID); + } + catch (Exception ex) + { + logger.LogError(ex, "Account hard delete failed unexpectedly for user {UserID}.", user.UserID); + TempData["AuthError"] = "Your account could not be deleted. Please try again or contact support."; + return RedirectToAction(nameof(Index)); + } + + if (result.BlockedByPaidSubscription) + { + TempData["AuthError"] = "You currently have an active paid subscription. Please cancel it before deleting your account."; + return RedirectToAction(nameof(Index)); + } + + if (!result.Succeeded) + { + TempData["AuthError"] = "Your account could not be deleted. Please try again or contact support."; + return RedirectToAction(nameof(Index)); + } + + await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme); + TempData["AuthMessage"] = "Your account has been permanently deleted."; + return RedirectToAction("Login", "Account"); + } + [HttpGet] public async Task Subscription() { diff --git a/PlotLine/ViewModels/AuthViewModels.cs b/PlotLine/ViewModels/AuthViewModels.cs index 69a75c8..0ef5c16 100644 --- a/PlotLine/ViewModels/AuthViewModels.cs +++ b/PlotLine/ViewModels/AuthViewModels.cs @@ -7,6 +7,7 @@ public sealed class ManageAccountViewModel { public AppUser User { get; set; } = new(); public UserSubscriptionDetail? Subscription { get; set; } + public bool HasPaidBillingRisk { get; set; } } public sealed class ManageSubscriptionViewModel diff --git a/PlotLine/Views/ManageAccount/Index.cshtml b/PlotLine/Views/ManageAccount/Index.cshtml index a3befc6..efaafba 100644 --- a/PlotLine/Views/ManageAccount/Index.cshtml +++ b/PlotLine/Views/ManageAccount/Index.cshtml @@ -16,6 +16,10 @@ {
@authMessage
} +@if (TempData["AuthError"] is string authError) +{ +
@authError
+}

Account security

@@ -96,8 +100,92 @@
+
+

Danger Zone

+

Permanently close your PlotDirector account.

+

This will delete your account, all projects, uploaded files, notes and project data. This action cannot be undone.

+ + @if (Model.HasPaidBillingRisk) + { +
+ You currently have an active paid subscription. + Please cancel your subscription before closing your account. Once your account has returned to trial or free access, you can permanently delete it. +
+ Manage subscription + } + else + { + + } +
+ +@if (!Model.HasPaidBillingRisk) +{ + +} + @functions { private static string FormatLimit(int value) => value >= 999999 ? "Practical unlimited" : value.ToString("N0"); private static string FormatStorage(int value) => value >= 1024 ? $"{value / 1024:N0} GB" : $"{value:N0} MB"; } + +@section Scripts { + @if (!Model.HasPaidBillingRisk) + { + + } +}