Files
EnvelopeGenerator/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/AuthController.cs
TekH 156988403a Add envelope receiver access code request feature
Introduced a feature to allow envelope receivers to request an
access code via email on the login page.

- Added `OnInitializedAsync` in `LoginReceiverPage.razor` to
  trigger access code requests using `AuthService`.
- Implemented `RequestEnvelopeReceiverAccessCodeAsync` in
  `AuthService.cs` to handle API calls for access code requests.
- Added a new endpoint in `AuthController` to process access
  code requests, including validation, history recording, and
  email dispatching.
- Registered `IEnvelopeMailService` and `AddDispatcher` in
  `Program.cs` to support email dispatch and database operations.
- Refactored `AuthController` constructor for readability and
  added necessary `using` directives.
- Improved logging and error handling for better traceability
  and robustness.

These changes ensure a seamless and secure process for envelope
receivers to request access codes, enhancing the user experience.
2026-09-28 13:35:32 +02:00

175 lines
7.4 KiB
C#

using DigitalData.Auth.Claims;
using EnvelopeGenerator.Application.Common.Interfaces.Services;
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
using EnvelopeGenerator.Server.Controllers.Interfaces;
using EnvelopeGenerator.Server.Models;
using EnvelopeGenerator.Domain.Constants;
using MediatR;
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.Authentication.Cookies;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;
namespace EnvelopeGenerator.Server.Controllers;
/// <summary>
/// Controller verantwortlich für die Benutzer-Authentifizierung, einschließlich Anmelden, Abmelden und Überprüfung des Authentifizierungsstatus.
/// </summary>
[Route("api/[controller]")]
[ApiController]
public partial class AuthController(
IOptions<AuthTokenKeys> authTokenKeyOptions,
IAuthorizationService authService) : ControllerBase, IAuthController
{
private readonly AuthTokenKeys authTokenKeys = authTokenKeyOptions.Value;
/// <summary>
///
/// </summary>
public IAuthorizationService AuthService { get; } = authService;
/// <summary>
/// Entfernt das Authentifizierungs-Cookie des Benutzers (AuthCookie)
/// </summary>
/// <returns>
/// Gibt eine HTTP 200 oder 401.
/// </returns>
/// <remarks>
/// Sample request:
///
/// POST /api/auth/logout
///
/// </remarks>
/// <response code="200">Erfolgreich gelöscht, wenn der Benutzer ein berechtigtes Cookie hat.</response>
/// <response code="401">Wenn es kein zugelassenes Cookie gibt, wird „nicht zugelassen“ zurückgegeben.</response>
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
[HttpPost("logout")]
public IActionResult Logout()
{
Response.Cookies.Delete(authTokenKeys.Cookie);
return Ok();
}
/// <summary>
/// Prüft, ob der Benutzer ein autorisiertes Token hat.
/// </summary>
/// <returns>Wenn ein autorisiertes Token vorhanden ist HTTP 200 asynchron 401</returns>
/// <remarks>
/// Sample request:
///
/// GET /api/auth
///
/// </remarks>
/// <response code="200">Wenn es einen autorisierten Cookie gibt.</response>
/// <response code="401">Wenn kein Cookie vorhanden ist oder nicht autorisierte.</response>
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[HttpGet("check")]
[Authorize(AuthenticationSchemes = AuthScheme.Sender)]
public IActionResult Check(string? role = null)
=> role is not null && !User.IsInRole(role)
? Unauthorized()
: Ok();
/// <summary>
/// Checks whether the caller holds a valid per-envelope receiver token for the given envelope key.
/// The request must carry a cookie named <c>AuthTokenSignFLOWReceiver.{envelopeKey}</c>.
/// </summary>
/// <param name="envelopeKey">The unique envelope key extracted from the route.</param>
/// <response code="200">Valid per-envelope token found.</response>
/// <response code="401">Token is missing, expired or invalid.</response>
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(void), StatusCodes.Status401Unauthorized)]
[Authorize(Policy = AuthPolicy.Receiver)]
[HttpGet("check/envelope/{envelopeKey}")]
public IActionResult CheckEnvelopeReceiver([FromRoute] string envelopeKey) => Ok();
/// <summary>
/// Sends the receiver access code e-mail on the first envelope login-page visit.
/// The operation is idempotent: if the code was already requested, it returns 200 without re-sending.
/// </summary>
/// <param name="key">Envelope receiver key.</param>
/// <response code="200">Access code is already available or requested successfully.</response>
/// <response code="404">Envelope receiver key was not found.</response>
/// <response code="500">Access code could not be persisted or e-mail could not be sent.</response>
[AllowAnonymous]
[HttpPost("envelope-receiver/{key}/request-access-code")]
public async Task<IActionResult> RequestEnvelopeReceiverAccessCode(
[FromRoute] string key,
[FromServices] IMediator mediator,
[FromServices] IEnvelopeHistoryService historyService,
[FromServices] IEnvelopeMailService mailService,
[FromServices] ILogger<AuthController> logger,
CancellationToken cancel)
{
var envelopeReceiver = (await mediator.Send(new ReadEnvelopeReceiverQuery { Key = key }, cancel)).SingleOrDefault();
if (envelopeReceiver is null || envelopeReceiver.Envelope is null || envelopeReceiver.Receiver is null)
return NotFound();
if (envelopeReceiver.Envelope.UseAccessCode != true)
return Ok();
var userReference = envelopeReceiver.Receiver.EmailAddress;
if (await historyService.IsSigned(envelopeReceiver.EnvelopeId, userReference))
return Ok();
var accessCodeAlreadyRequested = await historyService.AccessCodeAlreadyRequested(envelopeReceiver.EnvelopeId, userReference);
if (accessCodeAlreadyRequested)
return Ok();
var historyResult = await historyService.RecordAsync(envelopeReceiver.EnvelopeId, userReference, EnvelopeStatus.AccessCodeRequested);
if (historyResult.IsFailed)
{
logger.LogWarning(
"Failed to record AccessCodeRequested history for envelope {EnvelopeId} and receiver {ReceiverEmail}.",
envelopeReceiver.EnvelopeId,
userReference);
return StatusCode(StatusCodes.Status500InternalServerError);
}
var mailResult = await mailService.SendAccessCodeAsync(envelopeReceiver);
if (mailResult.IsFailed)
{
logger.LogWarning(
"Failed to send access code mail for envelope {EnvelopeId} and receiver {ReceiverEmail}.",
envelopeReceiver.EnvelopeId,
userReference);
return StatusCode(StatusCodes.Status500InternalServerError);
}
return Ok();
}
/// <summary>
/// Removes the per-envelope receiver cookie for the given envelope key.
/// </summary>
/// <param name="envelopeKey">The unique envelope key whose cookie should be deleted.</param>
/// <response code="200">Cookie successfully deleted.</response>
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[HttpPost("logout/envelope/{envelopeKey}")]
public IActionResult LogoutEnvelopeReceiver([FromRoute] string envelopeKey)
{
var cookieName = CookieNames.GetEnvelopeReceiverCookieName(authTokenKeys.Cookie, envelopeKey);
Response.Cookies.Delete(cookieName);
return Ok();
}
/// <summary>
/// Removes all per-envelope receiver cookies from the current request.
/// </summary>
/// <response code="200">All envelope receiver cookies successfully deleted.</response>
[ProducesResponseType(typeof(void), StatusCodes.Status200OK)]
[HttpPost("logout/envelope")]
public IActionResult LogoutAllEnvelopeReceivers()
{
foreach (var cookieName in Request.Cookies.Keys.Where(k => CookieNames.IsEnvelopeReceiverCookie(k, authTokenKeys.Cookie)))
Response.Cookies.Delete(cookieName);
return Ok();
}
}