diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginReceiverPage.razor b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginReceiverPage.razor index 962858a4..e224b406 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginReceiverPage.razor +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Pages/LoginReceiverPage.razor @@ -144,6 +144,14 @@ bool IsLoading; EnvelopeLoginResult? LoginResult; + protected override async Task OnInitializedAsync() + { + if (string.IsNullOrWhiteSpace(EnvelopeKey)) + return; + + _ = await AuthService.RequestEnvelopeReceiverAccessCodeAsync(EnvelopeKey); + } + async Task OnKeyDownAsync(Microsoft.AspNetCore.Components.Web.KeyboardEventArgs e) { if (e.Key == "Enter") diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AuthService.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AuthService.cs index bb8658a6..30a2e35d 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AuthService.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server.Client/Services/AuthService.cs @@ -33,6 +33,20 @@ public class AuthService(IHttpClientFactory httpClientFactory) return response.StatusCode == HttpStatusCode.OK; } + /// + /// Triggers access code e-mail request flow for the receiver login page. + /// Calls POST /api/Auth/envelope-receiver/{key}/request-access-code. + /// + public async Task RequestEnvelopeReceiverAccessCodeAsync(string envelopeKey, CancellationToken cancel = default) + { + using var http = CreateDefaultClient(); + var response = await http.PostAsync( + $"/api/Auth/envelope-receiver/{Uri.EscapeDataString(envelopeKey)}/request-access-code", + null, + cancel); + return response.IsSuccessStatusCode; + } + /// /// Submits the access code for the given envelope key. /// Calls POST /api/Auth/envelope-receiver/{key} with multipart/form-data. diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/AuthController.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/AuthController.cs index 314f4d90..2fcc6140 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/AuthController.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Controllers/AuthController.cs @@ -1,7 +1,10 @@ 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; @@ -15,7 +18,9 @@ namespace EnvelopeGenerator.Server.Controllers; /// [Route("api/[controller]")] [ApiController] -public partial class AuthController(IOptions authTokenKeyOptions, IAuthorizationService authService) : ControllerBase, IAuthController +public partial class AuthController( + IOptions authTokenKeyOptions, + IAuthorizationService authService) : ControllerBase, IAuthController { private readonly AuthTokenKeys authTokenKeys = authTokenKeyOptions.Value; @@ -82,6 +87,64 @@ public partial class AuthController(IOptions authTokenKeyOptions, [HttpGet("check/envelope/{envelopeKey}")] public IActionResult CheckEnvelopeReceiver([FromRoute] string envelopeKey) => Ok(); + /// + /// 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. + /// + /// Envelope receiver key. + /// Access code is already available or requested successfully. + /// Envelope receiver key was not found. + /// Access code could not be persisted or e-mail could not be sent. + [AllowAnonymous] + [HttpPost("envelope-receiver/{key}/request-access-code")] + public async Task RequestEnvelopeReceiverAccessCode( + [FromRoute] string key, + [FromServices] IMediator mediator, + [FromServices] IEnvelopeHistoryService historyService, + [FromServices] IEnvelopeMailService mailService, + [FromServices] ILogger 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(); + } + /// /// Removes the per-envelope receiver cookie for the given envelope key. /// @@ -108,4 +171,4 @@ public partial class AuthController(IOptions authTokenKeyOptions, Response.Cookies.Delete(cookieName); return Ok(); } -} \ No newline at end of file +} diff --git a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs index 635970cd..c9ab4fad 100644 --- a/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs +++ b/EnvelopeGenerator.Server/EnvelopeGenerator.Server/Program.cs @@ -25,6 +25,7 @@ using NLog.Web; using NLog; using DigitalData.Auth.Claims; using EnvelopeGenerator.Server; +using DigitalData.EmailProfilerDispatcher; var logger = LogManager.Setup().LoadConfigurationFromAppSettings().GetCurrentClassLogger(); logger.Info("EnvelopeGenerator.Server logging initialized!"); @@ -323,6 +324,8 @@ try .AddEnvelopeGeneratorServices(config); #pragma warning restore CS0618 + builder.Services.AddDispatcher(); + // User Preferences (theme, dark mode, grid layouts — stored in TBDD_CACHE) // Registration handled by Infrastructure DependencyInjection (AddEnvelopeGeneratorInfrastructureServices) @@ -346,6 +349,7 @@ try builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); + builder.Services.AddScoped(); builder.Services.AddSingleton(); // EnvelopeService with HttpClient factory (for SSR scenarios)