Replaced _mediator.ReadEnvelopeReceiverAsync with a light query (ReadEnvelopeReceiverLightQuery) that excludes Documents and Elements, improving performance by fetching only essential data.
407 lines
16 KiB
C#
407 lines
16 KiB
C#
using EnvelopeGenerator.API.Models;
|
|
using EnvelopeGenerator.Application.Common.Extensions;
|
|
using EnvelopeGenerator.Application.Common.Interfaces.Services;
|
|
using EnvelopeGenerator.Application.EnvelopeReceivers.Queries;
|
|
using EnvelopeGenerator.Domain.Constants;
|
|
using EnvelopeGenerator.API.Extensions;
|
|
using MediatR;
|
|
using Microsoft.AspNetCore.Authentication;
|
|
using Microsoft.AspNetCore.Authentication.Cookies;
|
|
using Microsoft.AspNetCore.Mvc;
|
|
using OtpNet;
|
|
|
|
namespace EnvelopeGenerator.API.Controllers;
|
|
|
|
/// <summary>
|
|
/// REST-API für den Empfänger-Authentifizierungs-Flow.
|
|
///
|
|
/// Entspricht der Logik in EnvelopeGenerator.Web.Controllers.EnvelopeController
|
|
/// (Main + LogInEnvelope), aber gibt JSON statt Views zurück.
|
|
///
|
|
/// Der Blazor-Client (ReceiverUI) ruft diese Endpunkte auf.
|
|
///
|
|
/// FLOW:
|
|
/// 1. Client ruft GET /api/receiverauth/{key}/status → Prüft Status
|
|
/// 2. Client ruft POST /api/receiverauth/{key}/access-code → Sendet AccessCode
|
|
/// 3. Client ruft POST /api/receiverauth/{key}/tfa → Sendet TFA-Code
|
|
///
|
|
/// Nach erfolgreicher Authentifizierung wird ein Cookie gesetzt (SignInEnvelopeAsync).
|
|
/// Danach kann der Client die Dokument-Daten über die bestehenden Envelope-Endpunkte laden.
|
|
/// </summary>
|
|
[Route("api/[controller]")]
|
|
[ApiController]
|
|
public class ReceiverAuthController : ControllerBase
|
|
{
|
|
private readonly ILogger<ReceiverAuthController> _logger;
|
|
private readonly IMediator _mediator;
|
|
private readonly IEnvelopeReceiverService _envRcvService;
|
|
private readonly IEnvelopeHistoryService _historyService;
|
|
private readonly IAuthenticator _authenticator;
|
|
private readonly IReceiverService _rcvService;
|
|
private readonly IEnvelopeSmsHandler _envSmsHandler;
|
|
|
|
public ReceiverAuthController(
|
|
ILogger<ReceiverAuthController> logger,
|
|
IMediator mediator,
|
|
IEnvelopeReceiverService envRcvService,
|
|
IEnvelopeHistoryService historyService,
|
|
IAuthenticator authenticator,
|
|
IReceiverService rcvService,
|
|
IEnvelopeSmsHandler envSmsHandler)
|
|
{
|
|
_logger = logger;
|
|
_mediator = mediator;
|
|
_envRcvService = envRcvService;
|
|
_historyService = historyService;
|
|
_authenticator = authenticator;
|
|
_rcvService = rcvService;
|
|
_envSmsHandler = envSmsHandler;
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// ENDPUNKT 1: STATUS PRÜFEN
|
|
// Entspricht: Web.EnvelopeController.Main()
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
/// <summary>
|
|
/// Prüft den aktuellen Status eines Umschlags für den Empfänger.
|
|
/// Entscheidet ob: NotFound, Rejected, Signed, AccessCode nötig, oder direkt anzeigen.
|
|
/// </summary>
|
|
/// <param name="key">Der EnvelopeReceiver-Key aus der URL (Base64-kodiert)</param>
|
|
/// <param name="cancel">Cancellation-Token</param>
|
|
/// <returns>ReceiverAuthResponse mit dem aktuellen Status</returns>
|
|
[HttpGet("{key}/status")]
|
|
public async Task<IActionResult> GetStatus([FromRoute] string key, CancellationToken cancel)
|
|
{
|
|
try
|
|
{
|
|
// ── Key dekodieren ──
|
|
if (!key.TryDecode(out var decoded))
|
|
return NotFound(new ReceiverAuthResponse { Status = "not_found" });
|
|
|
|
// ── ReadOnly-Links ──
|
|
if (decoded.GetEncodeType() == EncodeType.EnvelopeReceiverReadOnly)
|
|
{
|
|
return Ok(new ReceiverAuthResponse
|
|
{
|
|
Status = "show_document",
|
|
ReadOnly = true
|
|
});
|
|
}
|
|
|
|
// ── EnvelopeReceiver laden (Light-Query: ohne Documents/Elements) ──
|
|
var er = await _mediator.Send(
|
|
new ReadEnvelopeReceiverLightQuery { Key = key }, cancel);
|
|
if (er is null)
|
|
return NotFound(new ReceiverAuthResponse { Status = "not_found" });
|
|
|
|
// ── Abgelehnt? ──
|
|
var rejRcvrs = await _historyService.ReadRejectingReceivers(er.Envelope!.Id);
|
|
if (rejRcvrs.Any())
|
|
{
|
|
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
|
return Ok(new ReceiverAuthResponse
|
|
{
|
|
Status = "rejected",
|
|
Title = er.Envelope.Title,
|
|
SenderEmail = er.Envelope.User?.Email
|
|
});
|
|
}
|
|
|
|
// ── Bereits signiert? ──
|
|
if (await _historyService.IsSigned(
|
|
envelopeId: er.Envelope.Id,
|
|
userReference: er.Receiver!.EmailAddress))
|
|
{
|
|
await HttpContext.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
|
|
return Ok(new ReceiverAuthResponse
|
|
{
|
|
Status = "already_signed",
|
|
Title = er.Envelope.Title,
|
|
SenderEmail = er.Envelope.User?.Email
|
|
});
|
|
}
|
|
|
|
// ── Kein AccessCode nötig? → Direkt SignIn ──
|
|
if (!er.Envelope.UseAccessCode)
|
|
{
|
|
(string? uuid, string? signature) = decoded.ParseEnvelopeReceiverId();
|
|
var erSecretRes = await _envRcvService.ReadWithSecretByUuidSignatureAsync(
|
|
uuid: uuid!, signature: signature!);
|
|
|
|
if (erSecretRes.IsFailed)
|
|
return NotFound(new ReceiverAuthResponse { Status = "not_found" });
|
|
|
|
await HttpContext.SignInEnvelopeAsync(erSecretRes.Data, Role.ReceiverFull);
|
|
|
|
return Ok(new ReceiverAuthResponse
|
|
{
|
|
Status = "show_document",
|
|
Title = er.Envelope.Title,
|
|
Message = er.Envelope.Message,
|
|
SenderEmail = er.Envelope.User?.Email,
|
|
ReadOnly = er.Envelope.ReadOnly
|
|
});
|
|
}
|
|
|
|
// ── AccessCode nötig ──
|
|
// HINWEIS: Die E-Mail mit dem AccessCode wird NICHT hier gesendet.
|
|
// Das passiert bereits im Web-Projekt, wenn der Link generiert wird.
|
|
// Der Blazor-Flow übernimmt erst NACH dem E-Mail-Versand.
|
|
bool accessCodeAlreadyRequested = await _historyService.AccessCodeAlreadyRequested(
|
|
envelopeId: er.Envelope.Id,
|
|
userReference: er.Receiver.EmailAddress);
|
|
|
|
if (!accessCodeAlreadyRequested)
|
|
{
|
|
// AccessCode wurde noch nie angefordert — das bedeutet der Empfänger
|
|
// kommt zum ersten Mal. Wir zeichnen es auf, aber die E-Mail
|
|
// wurde bereits vom Web-Projekt gesendet.
|
|
await _historyService.RecordAsync(
|
|
er.EnvelopeId, er.Receiver.EmailAddress, EnvelopeStatus.AccessCodeRequested);
|
|
}
|
|
|
|
// ── Prüfe ob der Nutzer bereits eingeloggt ist ──
|
|
if (User.IsInRole(Role.ReceiverFull))
|
|
{
|
|
return Ok(new ReceiverAuthResponse
|
|
{
|
|
Status = "show_document",
|
|
Title = er.Envelope.Title,
|
|
Message = er.Envelope.Message,
|
|
SenderEmail = er.Envelope.User?.Email,
|
|
ReadOnly = er.Envelope.ReadOnly
|
|
});
|
|
}
|
|
|
|
return Ok(new ReceiverAuthResponse
|
|
{
|
|
Status = "requires_access_code",
|
|
Title = er.Envelope.Title,
|
|
SenderEmail = er.Envelope.User?.Email,
|
|
TfaEnabled = er.Envelope.TFAEnabled,
|
|
HasPhoneNumber = er.HasPhoneNumber
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error checking status for key {Key}", key);
|
|
return StatusCode(500, new ReceiverAuthResponse
|
|
{
|
|
Status = "error",
|
|
ErrorMessage = "Ein unerwarteter Fehler ist aufgetreten."
|
|
});
|
|
}
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// ENDPUNKT 2: ACCESS-CODE PRÜFEN
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
/// <summary>
|
|
/// Prüft den eingegebenen Zugangscode.
|
|
/// Bei Erfolg: SignIn oder TFA-Weiterleitung.
|
|
/// Bei Fehler: Fehlermeldung zurückgeben.
|
|
/// </summary>
|
|
[HttpPost("{key}/access-code")]
|
|
public async Task<IActionResult> SubmitAccessCode(
|
|
[FromRoute] string key,
|
|
[FromBody] AccessCodeRequest request,
|
|
CancellationToken cancel)
|
|
{
|
|
try
|
|
{
|
|
// ── Key dekodieren + Daten laden ──
|
|
(string? uuid, string? signature) = key.DecodeEnvelopeReceiverId();
|
|
if (uuid is null || signature is null)
|
|
return NotFound(new ReceiverAuthResponse { Status = "not_found" });
|
|
|
|
var erSecretRes = await _envRcvService.ReadWithSecretByUuidSignatureAsync(
|
|
uuid: uuid, signature: signature);
|
|
|
|
if (erSecretRes.IsFailed)
|
|
return NotFound(new ReceiverAuthResponse { Status = "not_found" });
|
|
|
|
var erSecret = erSecretRes.Data;
|
|
|
|
// ── AccessCode prüfen ──
|
|
if (erSecret.AccessCode != request.AccessCode)
|
|
{
|
|
await _historyService.RecordAsync(
|
|
erSecret.EnvelopeId,
|
|
erSecret.Receiver!.EmailAddress,
|
|
EnvelopeStatus.AccessCodeIncorrect);
|
|
|
|
return Unauthorized(new ReceiverAuthResponse
|
|
{
|
|
Status = "requires_access_code",
|
|
Title = erSecret.Envelope!.Title,
|
|
SenderEmail = erSecret.Envelope.User?.Email,
|
|
TfaEnabled = erSecret.Envelope.TFAEnabled,
|
|
HasPhoneNumber = erSecret.HasPhoneNumber,
|
|
ErrorMessage = "Falscher Zugangscode."
|
|
});
|
|
}
|
|
|
|
// ── AccessCode korrekt ──
|
|
await _historyService.RecordAsync(
|
|
erSecret.EnvelopeId,
|
|
erSecret.Receiver!.EmailAddress,
|
|
EnvelopeStatus.AccessCodeCorrect);
|
|
|
|
// ── TFA erforderlich? ──
|
|
if (erSecret.Envelope!.TFAEnabled)
|
|
{
|
|
var rcv = erSecret.Receiver;
|
|
if (rcv.TotpSecretkey is null)
|
|
{
|
|
rcv.TotpSecretkey = _authenticator.GenerateTotpSecretKey();
|
|
await _rcvService.UpdateAsync(rcv);
|
|
}
|
|
|
|
await HttpContext.SignInEnvelopeAsync(erSecret, Role.ReceiverTFA);
|
|
|
|
if (request.PreferSms)
|
|
{
|
|
var (smsRes, expiration) = await _envSmsHandler.SendTotpAsync(erSecret);
|
|
|
|
return Ok(new ReceiverAuthResponse
|
|
{
|
|
Status = "requires_tfa",
|
|
TfaType = "sms",
|
|
TfaExpiration = expiration,
|
|
Title = erSecret.Envelope.Title,
|
|
SenderEmail = erSecret.Envelope.User?.Email,
|
|
HasPhoneNumber = erSecret.HasPhoneNumber
|
|
});
|
|
}
|
|
else
|
|
{
|
|
return Ok(new ReceiverAuthResponse
|
|
{
|
|
Status = "requires_tfa",
|
|
TfaType = "authenticator",
|
|
Title = erSecret.Envelope.Title,
|
|
SenderEmail = erSecret.Envelope.User?.Email,
|
|
HasPhoneNumber = erSecret.HasPhoneNumber
|
|
});
|
|
}
|
|
}
|
|
|
|
// ── Kein TFA → Direkt SignIn ──
|
|
await HttpContext.SignInEnvelopeAsync(erSecret, Role.ReceiverFull);
|
|
|
|
return Ok(new ReceiverAuthResponse
|
|
{
|
|
Status = "show_document",
|
|
Title = erSecret.Envelope.Title,
|
|
Message = erSecret.Envelope.Message,
|
|
SenderEmail = erSecret.Envelope.User?.Email,
|
|
ReadOnly = erSecret.Envelope.ReadOnly
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error submitting access code for key {Key}", key);
|
|
return StatusCode(500, new ReceiverAuthResponse
|
|
{
|
|
Status = "error",
|
|
ErrorMessage = "Ein unerwarteter Fehler ist aufgetreten."
|
|
});
|
|
}
|
|
}
|
|
|
|
// ══════════════════════════════════════════════════════════════
|
|
// ENDPUNKT 3: TFA-CODE PRÜFEN
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
/// <summary>
|
|
/// Prüft den TFA-Code (SMS oder Authenticator).
|
|
/// Setzt voraus, dass der Nutzer bereits mit ReceiverTFA-Rolle eingeloggt ist.
|
|
/// </summary>
|
|
[HttpPost("{key}/tfa")]
|
|
public async Task<IActionResult> SubmitTfaCode(
|
|
[FromRoute] string key,
|
|
[FromBody] TfaCodeRequest request,
|
|
CancellationToken cancel)
|
|
{
|
|
try
|
|
{
|
|
if (!User.IsInRole(Role.ReceiverTFA))
|
|
return Unauthorized(new ReceiverAuthResponse
|
|
{
|
|
Status = "requires_access_code",
|
|
ErrorMessage = "Bitte zuerst den Zugangscode eingeben."
|
|
});
|
|
|
|
(string? uuid, string? signature) = key.DecodeEnvelopeReceiverId();
|
|
if (uuid is null || signature is null)
|
|
return NotFound(new ReceiverAuthResponse { Status = "not_found" });
|
|
|
|
var erSecretRes = await _envRcvService.ReadWithSecretByUuidSignatureAsync(
|
|
uuid: uuid, signature: signature);
|
|
|
|
if (erSecretRes.IsFailed)
|
|
return NotFound(new ReceiverAuthResponse { Status = "not_found" });
|
|
|
|
var erSecret = erSecretRes.Data;
|
|
|
|
if (erSecret.Receiver!.TotpSecretkey is null)
|
|
{
|
|
_logger.LogError("TotpSecretkey is null for receiver {Signature}", signature);
|
|
return StatusCode(500, new ReceiverAuthResponse
|
|
{
|
|
Status = "error",
|
|
ErrorMessage = "TFA-Konfiguration fehlt."
|
|
});
|
|
}
|
|
|
|
bool codeValid;
|
|
|
|
if (request.Type == "sms")
|
|
{
|
|
codeValid = _envSmsHandler.VerifyTotp(request.Code, erSecret.Receiver.TotpSecretkey);
|
|
}
|
|
else
|
|
{
|
|
codeValid = _authenticator.VerifyTotp(
|
|
request.Code,
|
|
erSecret.Receiver.TotpSecretkey,
|
|
window: VerificationWindow.RfcSpecifiedNetworkDelay);
|
|
}
|
|
|
|
if (!codeValid)
|
|
{
|
|
return Unauthorized(new ReceiverAuthResponse
|
|
{
|
|
Status = "requires_tfa",
|
|
TfaType = request.Type,
|
|
Title = erSecret.Envelope!.Title,
|
|
SenderEmail = erSecret.Envelope.User?.Email,
|
|
HasPhoneNumber = erSecret.HasPhoneNumber,
|
|
ErrorMessage = "Falscher Code."
|
|
});
|
|
}
|
|
|
|
await HttpContext.SignInEnvelopeAsync(erSecret, Role.ReceiverFull);
|
|
|
|
return Ok(new ReceiverAuthResponse
|
|
{
|
|
Status = "show_document",
|
|
Title = erSecret.Envelope!.Title,
|
|
Message = erSecret.Envelope.Message,
|
|
SenderEmail = erSecret.Envelope.User?.Email,
|
|
ReadOnly = erSecret.Envelope.ReadOnly
|
|
});
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogError(ex, "Error submitting TFA code for key {Key}", key);
|
|
return StatusCode(500, new ReceiverAuthResponse
|
|
{
|
|
Status = "error",
|
|
ErrorMessage = "Ein unerwarteter Fehler ist aufgetreten."
|
|
});
|
|
}
|
|
}
|
|
} |