PlotDirector/PlotLine/Controllers/AccountController.cs
2026-06-26 19:47:25 +01:00

329 lines
9.4 KiB
C#

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Authorization;
using PlotLine.Models;
using PlotLine.Services;
using PlotLine.ViewModels;
namespace PlotLine.Controllers;
public sealed class AccountController(IAuthService authService) : Controller
{
[HttpGet]
[AllowAnonymous]
public IActionResult Register(string? returnUrl = null)
{
return View(new RegisterViewModel());
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(RegisterViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
RegistrationResult result;
try
{
result = await authService.RegisterAsync(model);
}
catch
{
ModelState.AddModelError(string.Empty, "Your account could not be created because the verification email could not be queued. Please try again later.");
return View(model);
}
if (!result.Success)
{
ModelState.AddModelError(string.Empty, result.ErrorMessage ?? "Registration could not be completed.");
return View(model);
}
TempData["AuthMessage"] = "Your account has been created. Please check your email to verify your account. If it does not arrive, use the resend verification email link below.";
return RedirectToAction(nameof(Login));
}
[HttpGet]
[AllowAnonymous]
public IActionResult Login(string? returnUrl = null, bool companion = false)
{
return View(new LoginViewModel { ReturnUrl = returnUrl, IsCompanionLogin = IsCompanionLogin(returnUrl, companion) });
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Login(LoginViewModel model)
{
if (!ModelState.IsValid)
{
model.IsCompanionLogin = IsCompanionLogin(model.ReturnUrl, model.IsCompanionLogin);
return View(model);
}
var result = await authService.LoginAsync(model);
if (result.RequiresTwoFactor)
{
return RedirectToAction(nameof(TwoFactor), new { returnUrl = model.ReturnUrl });
}
if (!result.Success)
{
ModelState.AddModelError(string.Empty, result.ErrorMessage ?? "Login could not be completed.");
model.IsCompanionLogin = IsCompanionLogin(model.ReturnUrl, model.IsCompanionLogin);
return View(model);
}
if (!string.IsNullOrWhiteSpace(model.ReturnUrl) && Url.IsLocalUrl(model.ReturnUrl))
{
return LocalRedirect(model.ReturnUrl);
}
return RedirectToAction("Index", "Projects");
}
private static bool IsCompanionLogin(string? returnUrl, bool explicitCompanion)
{
if (explicitCompanion)
{
return true;
}
return !string.IsNullOrWhiteSpace(returnUrl)
&& returnUrl.StartsWith("/word-companion", StringComparison.OrdinalIgnoreCase);
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Logout()
{
await authService.LogoutAsync();
return RedirectToAction("Index", "Home");
}
[HttpGet]
[AllowAnonymous]
public IActionResult AccessDenied()
{
return View();
}
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> VerifyEmail(Guid token)
{
if (token == Guid.Empty)
{
return View(new AccountStatusViewModel
{
Success = false,
Title = "Verification link problem",
Message = "This verification link is not valid."
});
}
var result = await authService.VerifyEmailAsync(token);
if (result.Success)
{
return RedirectToAction(nameof(VerifyEmailConfirmation));
}
return View(new AccountStatusViewModel
{
Success = false,
Title = "Verification link problem",
Message = result.ErrorMessage ?? "This verification link could not be used."
});
}
[HttpGet]
[AllowAnonymous]
public IActionResult VerifyEmailConfirmation()
{
return View();
}
[HttpGet]
[AllowAnonymous]
public IActionResult ForgotPassword()
{
return View(new ForgotPasswordViewModel());
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ForgotPassword(ForgotPasswordViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
try
{
var result = await authService.ForgotPasswordAsync(model);
if (!result.Success)
{
ModelState.AddModelError(string.Empty, "We could not queue a password reset email right now. Please try again later.");
return View(model);
}
}
catch
{
ModelState.AddModelError(string.Empty, "We could not queue a password reset email right now. Please try again later.");
return View(model);
}
TempData["AuthMessage"] = "If an account exists for that email address, a password reset email will be sent shortly.";
return RedirectToAction(nameof(Login));
}
[HttpGet]
[AllowAnonymous]
public IActionResult ResetPassword(Guid token)
{
if (token == Guid.Empty)
{
return View(new ResetPasswordViewModel());
}
return View(new ResetPasswordViewModel { Token = token });
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ResetPassword(ResetPasswordViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var result = await authService.ResetPasswordAsync(model);
if (!result.Success)
{
ModelState.AddModelError(string.Empty, result.ErrorMessage ?? "Your password could not be reset.");
return View(model);
}
TempData["AuthMessage"] = "Your password has been reset. You can now log in.";
return RedirectToAction(nameof(Login));
}
[HttpGet]
[AllowAnonymous]
public IActionResult ResendVerificationEmail()
{
return View(new ResendVerificationEmailViewModel());
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> ResendVerificationEmail(ResendVerificationEmailViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var result = await authService.ResendVerificationEmailAsync(model.Email);
if (!result.Success)
{
ModelState.AddModelError(string.Empty, result.ErrorMessage ?? "We could not send the verification email right now. Please try again in a few minutes.");
return View(model);
}
TempData["AuthMessage"] = "If an unverified account exists for that email address, a verification email will be sent shortly.";
return View(new ResendVerificationEmailViewModel());
}
[HttpGet]
[AllowAnonymous]
public IActionResult TwoFactor(string? returnUrl = null)
{
if (!authService.HasPendingTwoFactorLogin())
{
return RedirectToAction(nameof(Login));
}
return View(new TwoFactorCodeViewModel
{
ReturnUrl = returnUrl ?? authService.GetPendingTwoFactorReturnUrl()
});
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> TwoFactor(TwoFactorCodeViewModel model)
{
if (!ModelState.IsValid)
{
return View(model);
}
var result = await authService.CompleteTwoFactorLoginAsync(model);
if (!result.Success)
{
ModelState.AddModelError(string.Empty, result.ErrorMessage ?? "Two-factor login could not be completed.");
return View(model);
}
if (!string.IsNullOrWhiteSpace(model.ReturnUrl) && Url.IsLocalUrl(model.ReturnUrl))
{
return LocalRedirect(model.ReturnUrl);
}
return RedirectToAction("Index", "Projects");
}
[HttpGet]
[AllowAnonymous]
public IActionResult TwoFactorRecovery(string? returnUrl = null)
{
if (!authService.HasPendingTwoFactorLogin())
{
return RedirectToAction(nameof(Login));
}
return View(new TwoFactorRecoveryViewModel
{
ReturnUrl = returnUrl ?? authService.GetPendingTwoFactorReturnUrl()
});
}
[HttpPost]
[AllowAnonymous]
[ValidateAntiForgeryToken]
public async Task<IActionResult> TwoFactorRecovery(TwoFactorRecoveryViewModel model)
{
if (!authService.HasPendingTwoFactorLogin())
{
return RedirectToAction(nameof(Login));
}
if (!ModelState.IsValid)
{
return View(model);
}
var result = await authService.CompleteTwoFactorRecoveryLoginAsync(model);
if (!result.Success)
{
ModelState.AddModelError(string.Empty, result.ErrorMessage ?? "Recovery code login could not be completed.");
return View(model);
}
if (!string.IsNullOrWhiteSpace(model.ReturnUrl) && Url.IsLocalUrl(model.ReturnUrl))
{
return LocalRedirect(model.ReturnUrl);
}
return RedirectToAction("Index", "Projects");
}
}