PlotDirector/PlotLine/Controllers/ManageAccountController.cs

322 lines
11 KiB
C#

using System.Text.Json;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using PlotLine.Data;
using PlotLine.Models;
using PlotLine.Services;
using PlotLine.ViewModels;
namespace PlotLine.Controllers;
[Authorize]
public sealed class ManageAccountController(
ICurrentUserService currentUser,
IUserRepository users,
IAuthService authService,
IAccountDeletionService accountDeletion,
ITwoFactorService twoFactorService,
ISubscriptionService subscriptions,
IStripeBillingService stripeBilling,
ILogger<ManageAccountController> logger) : Controller
{
[HttpGet]
public async Task<IActionResult> Index()
{
var user = await GetCurrentUserAsync();
if (user is null)
{
return RedirectToAction("Login", "Account");
}
var subscription = await subscriptions.GetCurrentSubscriptionAsync(user.UserID);
return View(new ManageAccountViewModel
{
User = user,
Subscription = subscription,
Trial = TrialVisibilityViewModel.FromSubscription(subscription, DateTime.UtcNow),
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]
public async Task<IActionResult> Subscription()
{
var user = await GetCurrentUserAsync();
if (user is null)
{
return RedirectToAction("Login", "Account");
}
var subscription = await subscriptions.GetCurrentSubscriptionAsync(user.UserID);
return View(new ManageSubscriptionViewModel
{
User = user,
CurrentSubscription = subscription,
Trial = TrialVisibilityViewModel.FromSubscription(subscription, DateTime.UtcNow),
Usage = subscription is null ? null : await subscriptions.GetUsageSummaryAsync(user.UserID),
AvailableLevels = await subscriptions.GetActiveSubscriptionLevelsAsync()
});
}
[HttpPost]
[ValidateAntiForgeryToken]
public IActionResult SubscriptionPlaceholder()
{
TempData["SubscriptionMessage"] = "Online subscription changes will be available soon.";
return RedirectToAction(nameof(Subscription));
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> SubscriptionCheckout(StripeCheckoutRequest request)
{
var user = await GetCurrentUserAsync();
if (user is null)
{
return RedirectToAction("Login", "Account");
}
var successUrl = Url.Action(nameof(SubscriptionSuccess), "ManageAccount", null, Request.Scheme) ?? string.Empty;
var cancelUrl = Url.Action(nameof(SubscriptionCancelled), "ManageAccount", null, Request.Scheme) ?? string.Empty;
var result = await stripeBilling.StartCheckoutOrChangePlanAsync(user, request, successUrl, cancelUrl);
if (!result.Succeeded)
{
TempData["SubscriptionMessage"] = result.Message;
return RedirectToAction(nameof(Subscription));
}
if (!string.IsNullOrWhiteSpace(result.RedirectUrl))
{
return Redirect(result.RedirectUrl);
}
TempData["SubscriptionMessage"] = result.Message;
return RedirectToAction(nameof(Subscription));
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> CancelSubscription()
{
var user = await GetCurrentUserAsync();
if (user is null)
{
return RedirectToAction("Login", "Account");
}
var result = await stripeBilling.CancelAtPeriodEndAsync(user);
TempData["SubscriptionMessage"] = result.Message;
return RedirectToAction(nameof(Subscription));
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ManageBilling()
{
var user = await GetCurrentUserAsync();
if (user is null)
{
return RedirectToAction("Login", "Account");
}
var returnUrl = Url.Action(nameof(Subscription), "ManageAccount", null, Request.Scheme) ?? string.Empty;
var result = await stripeBilling.CreateBillingPortalSessionAsync(user, returnUrl);
if (result.Succeeded && !string.IsNullOrWhiteSpace(result.RedirectUrl))
{
return Redirect(result.RedirectUrl);
}
TempData["SubscriptionMessage"] = result.Message;
return RedirectToAction(nameof(Subscription));
}
[HttpGet]
public IActionResult SubscriptionSuccess()
{
TempData["SubscriptionMessage"] = "Thank you. Your subscription is being confirmed. This usually only takes a few moments.";
return RedirectToAction(nameof(Subscription));
}
[HttpGet]
public IActionResult SubscriptionCancelled()
{
TempData["SubscriptionMessage"] = "Stripe Checkout was cancelled. No subscription changes were made.";
return RedirectToAction(nameof(Subscription));
}
[HttpGet]
public IActionResult ChangePassword()
{
return View(new ChangePasswordViewModel());
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ChangePassword(ChangePasswordViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var result = await authService.ChangePasswordAsync(model);
if (!result.Success)
{
ModelState.AddModelError(string.Empty, result.ErrorMessage ?? "Your password could not be changed.");
return View(model);
}
TempData["AuthMessage"] = "Your password has been changed. Please sign in again.";
return RedirectToAction("Login", "Account");
}
[HttpGet]
public async Task<IActionResult> EnableTwoFactor()
{
var user = await GetCurrentUserAsync();
if (user is null)
{
return RedirectToAction("Login", "Account");
}
var secret = twoFactorService.GenerateSecretKey();
var uri = twoFactorService.GenerateQrCodeUri(user.Email, secret);
return View(new EnableTwoFactorViewModel
{
SecretKey = secret,
QrCodeUri = uri,
QrCodeImageDataUri = twoFactorService.GenerateQrCodeDataUri(uri)
});
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> EnableTwoFactor(EnableTwoFactorViewModel model)
{
if (!ModelState.IsValid)
{
model.QrCodeUri = twoFactorService.GenerateQrCodeUri(currentUser.Email ?? string.Empty, model.SecretKey);
model.QrCodeImageDataUri = twoFactorService.GenerateQrCodeDataUri(model.QrCodeUri);
return View(model);
}
var result = await authService.EnableTwoFactorAsync(currentUser.UserId!.Value, model.SecretKey, model.Code);
if (!result.Success)
{
ModelState.AddModelError(string.Empty, result.ErrorMessage ?? "Two-factor authentication could not be enabled.");
model.QrCodeUri = twoFactorService.GenerateQrCodeUri(currentUser.Email ?? string.Empty, model.SecretKey);
model.QrCodeImageDataUri = twoFactorService.GenerateQrCodeDataUri(model.QrCodeUri);
return View(model);
}
TempData["RecoveryCodes"] = JsonSerializer.Serialize(result.RecoveryCodes);
TempData["AuthMessage"] = "Two-factor authentication has been enabled.";
return RedirectToAction(nameof(RecoveryCodes));
}
[HttpGet]
public IActionResult DisableTwoFactor()
{
return View(new DisableTwoFactorViewModel());
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DisableTwoFactor(DisableTwoFactorViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var result = await authService.DisableTwoFactorAsync(currentUser.UserId!.Value, model.CurrentPassword);
if (!result.Success)
{
ModelState.AddModelError(string.Empty, result.ErrorMessage ?? "Two-factor authentication could not be disabled.");
return View(model);
}
TempData["AuthMessage"] = "Two-factor authentication has been disabled.";
return RedirectToAction(nameof(Index));
}
[HttpGet]
public IActionResult RecoveryCodes()
{
var codes = TempData["RecoveryCodes"] is string json
? JsonSerializer.Deserialize<IReadOnlyList<string>>(json) ?? Array.Empty<string>()
: Array.Empty<string>();
return View(new RecoveryCodesViewModel { RecoveryCodes = codes });
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> RegenerateRecoveryCodes()
{
var codes = await authService.GenerateRecoveryCodesAsync(currentUser.UserId!.Value);
TempData["RecoveryCodes"] = JsonSerializer.Serialize(codes);
TempData["AuthMessage"] = "New recovery codes have been generated.";
return RedirectToAction(nameof(RecoveryCodes));
}
private async Task<PlotLine.Models.AppUser?> GetCurrentUserAsync()
{
return currentUser.UserId is int userId ? await users.GetByIdAsync(userId) : null;
}
}