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; /// /// 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. /// [Route("api/[controller]")] [ApiController] public class ReceiverAuthController : ControllerBase { private readonly ILogger _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 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() // ══════════════════════════════════════════════════════════════ /// /// Prüft den aktuellen Status eines Umschlags für den Empfänger. /// Entscheidet ob: NotFound, Rejected, Signed, AccessCode nötig, oder direkt anzeigen. /// /// Der EnvelopeReceiver-Key aus der URL (Base64-kodiert) /// Cancellation-Token /// ReceiverAuthResponse mit dem aktuellen Status [HttpGet("{key}/status")] public async Task 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 ── var er = await _mediator.ReadEnvelopeReceiverAsync(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 // ══════════════════════════════════════════════════════════════ /// /// Prüft den eingegebenen Zugangscode. /// Bei Erfolg: SignIn oder TFA-Weiterleitung. /// Bei Fehler: Fehlermeldung zurückgeben. /// [HttpPost("{key}/access-code")] public async Task 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 // ══════════════════════════════════════════════════════════════ /// /// Prüft den TFA-Code (SMS oder Authenticator). /// Setzt voraus, dass der Nutzer bereits mit ReceiverTFA-Rolle eingeloggt ist. /// [HttpPost("{key}/tfa")] public async Task 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." }); } } }