Phase 7E User Delete UI
This commit is contained in:
parent
6dee173c5c
commit
42b5ed371c
@ -1,4 +1,6 @@
|
|||||||
using System.Text.Json;
|
using System.Text.Json;
|
||||||
|
using Microsoft.AspNetCore.Authentication;
|
||||||
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
||||||
using Microsoft.AspNetCore.Authorization;
|
using Microsoft.AspNetCore.Authorization;
|
||||||
using Microsoft.AspNetCore.Mvc;
|
using Microsoft.AspNetCore.Mvc;
|
||||||
using PlotLine.Data;
|
using PlotLine.Data;
|
||||||
@ -13,9 +15,11 @@ public sealed class ManageAccountController(
|
|||||||
ICurrentUserService currentUser,
|
ICurrentUserService currentUser,
|
||||||
IUserRepository users,
|
IUserRepository users,
|
||||||
IAuthService authService,
|
IAuthService authService,
|
||||||
|
IAccountDeletionService accountDeletion,
|
||||||
ITwoFactorService twoFactorService,
|
ITwoFactorService twoFactorService,
|
||||||
ISubscriptionService subscriptions,
|
ISubscriptionService subscriptions,
|
||||||
IStripeBillingService stripeBilling) : Controller
|
IStripeBillingService stripeBilling,
|
||||||
|
ILogger<ManageAccountController> logger) : Controller
|
||||||
{
|
{
|
||||||
[HttpGet]
|
[HttpGet]
|
||||||
public async Task<IActionResult> Index()
|
public async Task<IActionResult> Index()
|
||||||
@ -29,10 +33,63 @@ public sealed class ManageAccountController(
|
|||||||
return View(new ManageAccountViewModel
|
return View(new ManageAccountViewModel
|
||||||
{
|
{
|
||||||
User = user,
|
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<IActionResult> 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]
|
[HttpGet]
|
||||||
public async Task<IActionResult> Subscription()
|
public async Task<IActionResult> Subscription()
|
||||||
{
|
{
|
||||||
|
|||||||
@ -7,6 +7,7 @@ public sealed class ManageAccountViewModel
|
|||||||
{
|
{
|
||||||
public AppUser User { get; set; } = new();
|
public AppUser User { get; set; } = new();
|
||||||
public UserSubscriptionDetail? Subscription { get; set; }
|
public UserSubscriptionDetail? Subscription { get; set; }
|
||||||
|
public bool HasPaidBillingRisk { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
public sealed class ManageSubscriptionViewModel
|
public sealed class ManageSubscriptionViewModel
|
||||||
|
|||||||
@ -16,6 +16,10 @@
|
|||||||
{
|
{
|
||||||
<div class="alert alert-info">@authMessage</div>
|
<div class="alert alert-info">@authMessage</div>
|
||||||
}
|
}
|
||||||
|
@if (TempData["AuthError"] is string authError)
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning">@authError</div>
|
||||||
|
}
|
||||||
|
|
||||||
<section class="edit-panel">
|
<section class="edit-panel">
|
||||||
<h2>Account security</h2>
|
<h2>Account security</h2>
|
||||||
@ -96,8 +100,92 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="edit-panel border border-danger-subtle">
|
||||||
|
<h2 class="text-danger">Danger Zone</h2>
|
||||||
|
<p>Permanently close your PlotDirector account.</p>
|
||||||
|
<p class="muted">This will delete your account, all projects, uploaded files, notes and project data. This action cannot be undone.</p>
|
||||||
|
|
||||||
|
@if (Model.HasPaidBillingRisk)
|
||||||
|
{
|
||||||
|
<div class="alert alert-warning">
|
||||||
|
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.
|
||||||
|
</div>
|
||||||
|
<a class="btn btn-outline-primary" asp-action="Subscription">Manage subscription</a>
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
<button class="btn btn-danger"
|
||||||
|
type="button"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#deleteAccountModal">
|
||||||
|
Delete Account
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</section>
|
||||||
|
|
||||||
|
@if (!Model.HasPaidBillingRisk)
|
||||||
|
{
|
||||||
|
<div class="modal fade" id="deleteAccountModal" tabindex="-1" aria-labelledby="deleteAccountModalTitle" aria-hidden="true">
|
||||||
|
<div class="modal-dialog modal-dialog-centered">
|
||||||
|
<div class="modal-content plotline-confirm-modal">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2 class="modal-title fs-5" id="deleteAccountModalTitle">Permanently close your account?</h2>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Cancel"></button>
|
||||||
|
</div>
|
||||||
|
<form asp-action="HardDeleteAccount" method="post" data-confirm-skip="true" data-delete-account-form>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>This will permanently delete your account and all data stored by PlotDirector, including all projects, uploaded files, notes, settings and trial information.</p>
|
||||||
|
<p class="mb-3">This action cannot be undone.</p>
|
||||||
|
<label class="form-label" for="deleteAccountConfirmationEmail">Type your email address to continue</label>
|
||||||
|
<input class="form-control"
|
||||||
|
id="deleteAccountConfirmationEmail"
|
||||||
|
name="confirmationEmail"
|
||||||
|
autocomplete="off"
|
||||||
|
data-delete-account-confirmation
|
||||||
|
data-account-email="@Model.User.Email" />
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">Cancel</button>
|
||||||
|
<button type="submit" class="btn btn-danger" disabled data-delete-account-submit>Delete Account</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
|
||||||
@functions {
|
@functions {
|
||||||
private static string FormatLimit(int value) => value >= 999999 ? "Practical unlimited" : value.ToString("N0");
|
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";
|
private static string FormatStorage(int value) => value >= 1024 ? $"{value / 1024:N0} GB" : $"{value:N0} MB";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@section Scripts {
|
||||||
|
@if (!Model.HasPaidBillingRisk)
|
||||||
|
{
|
||||||
|
<script>
|
||||||
|
(() => {
|
||||||
|
const modalElement = document.getElementById("deleteAccountModal");
|
||||||
|
if (!modalElement) return;
|
||||||
|
|
||||||
|
const input = modalElement.querySelector("[data-delete-account-confirmation]");
|
||||||
|
const submitButton = modalElement.querySelector("[data-delete-account-submit]");
|
||||||
|
const accountEmail = (input?.dataset.accountEmail || "").trim().toLowerCase();
|
||||||
|
|
||||||
|
const updateSubmitState = () => {
|
||||||
|
if (!input || !submitButton) return;
|
||||||
|
submitButton.disabled = input.value.trim().toLowerCase() !== accountEmail;
|
||||||
|
};
|
||||||
|
|
||||||
|
modalElement.addEventListener("show.bs.modal", () => {
|
||||||
|
if (input) input.value = "";
|
||||||
|
updateSubmitState();
|
||||||
|
});
|
||||||
|
|
||||||
|
modalElement.addEventListener("shown.bs.modal", () => input?.focus());
|
||||||
|
input?.addEventListener("input", updateSubmitState);
|
||||||
|
})();
|
||||||
|
</script>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user