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)