All references to IEnvelopeMailService have been removed from ReceiverAuthController. The controller no longer sends access code emails; this responsibility is now handled by the Web project when generating the link. Updated comments clarify the new flow, and related redundant code has been cleaned up. Authentication and TFA logic remain unchanged.
406 lines
16 KiB
C#
406 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 ──
|
|
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
|
|
// ══════════════════════════════════════════════════════════════
|
|
|
|
/// <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."
|
|
});
|
|
}
|
|
}
|
|
} |