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 IEnvelopeMailService _mailService; private readonly IAuthenticator _authenticator; private readonly IReceiverService _rcvService; private readonly IEnvelopeSmsHandler _envSmsHandler; public ReceiverAuthController( ILogger logger, IMediator mediator, IEnvelopeReceiverService envRcvService, IEnvelopeHistoryService historyService, IEnvelopeMailService mailService, IAuthenticator authenticator, IReceiverService rcvService, IEnvelopeSmsHandler envSmsHandler) { _logger = logger; _mediator = mediator; _envRcvService = envRcvService; _historyService = historyService; _mailService = mailService; _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 ── // Entspricht: if (!envelopeReceiverId.TryDecode(out var decoded)) if (!key.TryDecode(out var decoded)) return NotFound(new ReceiverAuthResponse { Status = "not_found" }); // ── ReadOnly-Links ── // Entspricht: if (decoded.GetEncodeType() == EncodeType.EnvelopeReceiverReadOnly) if (decoded.GetEncodeType() == EncodeType.EnvelopeReceiverReadOnly) { // ReadOnly-Logik: Prüfe ob abgelaufen // Wir geben erstmal show_document zurück, ReadOnly-Details kommen in Phase 6 return Ok(new ReceiverAuthResponse { Status = "show_document", ReadOnly = true }); } // ── EnvelopeReceiver laden ── // Entspricht: var er = await _mediator.ReadEnvelopeReceiverAsync(envelopeReceiverId, cancel); var er = await _mediator.ReadEnvelopeReceiverAsync(key, cancel); if (er is null) return NotFound(new ReceiverAuthResponse { Status = "not_found" }); // ── Abgelehnt? ── // Entspricht: var rejRcvrs = await _historyService.ReadRejectingReceivers(er.Envelope!.Id); 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? ── // Entspricht: if (await _historyService.IsSigned(...)) 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 ── // Entspricht: if (!er.Envelope!.UseAccessCode) 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 → Code senden (wenn noch nicht gesendet) ── // Entspricht: bool accessCodeAlreadyRequested = ... bool accessCodeAlreadyRequested = await _historyService.AccessCodeAlreadyRequested( envelopeId: er.Envelope.Id, userReference: er.Receiver.EmailAddress); if (!accessCodeAlreadyRequested) { await _historyService.RecordAsync( er.EnvelopeId, er.Receiver.EmailAddress, EnvelopeStatus.AccessCodeRequested); await _mailService.SendAccessCodeAsync(envelopeReceiverDto: er); } // ── Prüfe ob der Nutzer bereits eingeloggt ist ── // Entspricht: CreateEnvelopeLockedView → Prüfung ob User.IsInRole(ReceiverFull) 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 // Entspricht: Web.EnvelopeController.LogInEnvelope() → HandleAccessCodeAsync() // ══════════════════════════════════════════════════════════════ /// /// 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 ── // Entspricht: HandleAccessCodeAsync() → if (er_secret.AccessCode != auth.AccessCode) 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? ── // Entspricht: if (er_secret.Envelope!.TFAEnabled) if (erSecret.Envelope!.TFAEnabled) { // TotpSecretKey generieren falls noch nicht vorhanden var rcv = erSecret.Receiver; if (rcv.TotpSecretkey is null) { rcv.TotpSecretkey = _authenticator.GenerateTotpSecretKey(); await _rcvService.UpdateAsync(rcv); } // SignIn mit TFA-Rolle (eingeschränkt — nur TFA erlaubt, kein Dokument) await HttpContext.SignInEnvelopeAsync(erSecret, Role.ReceiverTFA); // SMS senden wenn vom Benutzer gewählt 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 // Entspricht: Web.EnvelopeController.LogInEnvelope() → HandleSmsAsync/HandleAuthenticatorAsync // ══════════════════════════════════════════════════════════════ /// /// 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 { // ── Prüfe ob der Nutzer TFA-berechtigt ist ── if (!User.IsInRole(Role.ReceiverTFA)) return Unauthorized(new ReceiverAuthResponse { Status = "requires_access_code", ErrorMessage = "Bitte zuerst den Zugangscode eingeben." }); // ── 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; 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." }); } // ── Code verifizieren ── bool codeValid; if (request.Type == "sms") { // Entspricht: HandleSmsAsync() codeValid = _envSmsHandler.VerifyTotp(request.Code, erSecret.Receiver.TotpSecretkey); } else { // Entspricht: HandleAuthenticatorAsync() 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." }); } // ── TFA erfolgreich → Voll-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 TFA code for key {Key}", key); return StatusCode(500, new ReceiverAuthResponse { Status = "error", ErrorMessage = "Ein unerwarteter Fehler ist aufgetreten." }); } } }