| | | 1 | | using ArturRios.Data.Relational.Core.DependencyInjection; |
| | | 2 | | using ArturRios.Heimdall.Command.Auditing; |
| | | 3 | | using ArturRios.Heimdall.Command.Handlers; |
| | | 4 | | using ArturRios.Heimdall.Command.Input; |
| | | 5 | | using ArturRios.Heimdall.Command.Output; |
| | | 6 | | using ArturRios.Heimdall.Command.Services; |
| | | 7 | | using ArturRios.Heimdall.WebApi.Email; |
| | | 8 | | using ArturRios.Messaging.Email; |
| | | 9 | | using ArturRios.Util.WebApi.Configuration; |
| | | 10 | | using ArturRios.Util.WebApi.Security.Extensions; |
| | | 11 | | using Serilog; |
| | | 12 | | |
| | | 13 | | namespace ArturRios.Heimdall.WebApi; |
| | | 14 | | |
| | | 15 | | public static class EmailRegistrationExtensions |
| | | 16 | | { |
| | | 17 | | /// <summary> |
| | | 18 | | /// Chooses how the two transactional emails — UC-06's verification token and UC-12's |
| | | 19 | | /// password reset token — are delivered. With Mailgun credentials present, both go out for |
| | | 20 | | /// real through <c>ArturRios.Messaging</c>; without them, both are logged. |
| | | 21 | | /// </summary> |
| | | 22 | | /// <remarks> |
| | | 23 | | /// The fallback is deliberate rather than a failure: a developer running the API locally, |
| | | 24 | | /// and the functional suite, both need person creation and password recovery to work without |
| | | 25 | | /// credentials and without reaching the network. Failing startup instead would make Mailgun |
| | | 26 | | /// a prerequisite for running the tests. In Production, though, the fallback logs a |
| | | 27 | | /// verification token, a password reset token, or a 2FA code in plaintext on every send — an |
| | | 28 | | /// account-takeover primitive for anyone who can read the logs — so an unconfigured Production |
| | | 29 | | /// deployment fails startup outright instead of silently degrading into that. |
| | | 30 | | /// </remarks> |
| | | 31 | | public static IServiceCollection AddEmailSenders(this IServiceCollection services, IHostEnvironment environment) |
| | | 32 | | { |
| | 973 | 33 | | var options = EmailDeliveryOptions.FromEnvironment(); |
| | | 34 | | |
| | 973 | 35 | | services.AddSingleton(options); |
| | | 36 | | |
| | 973 | 37 | | if (!options.MailgunConfigured) |
| | | 38 | | { |
| | 973 | 39 | | if (environment.IsProduction()) |
| | | 40 | | { |
| | 0 | 41 | | throw new InvalidOperationException( |
| | 0 | 42 | | $"Mailgun is not configured ({MailgunEmailService.ApiKeyVariable} / " + |
| | 0 | 43 | | $"{MailgunEmailService.DomainVariable}) in Production. Refusing to start: the " + |
| | 0 | 44 | | "fallback sender logs verification tokens, password reset tokens, and 2FA codes " + |
| | 0 | 45 | | "in plaintext, which must never happen outside local development and tests."); |
| | | 46 | | } |
| | | 47 | | |
| | 973 | 48 | | Log.Warning( |
| | 973 | 49 | | "Mailgun is not configured ({ApiKeyVariable} / {DomainVariable}); verification and " + |
| | 973 | 50 | | "password reset tokens will be logged instead of emailed", |
| | 973 | 51 | | MailgunEmailService.ApiKeyVariable, MailgunEmailService.DomainVariable); |
| | | 52 | | |
| | 973 | 53 | | services.AddScoped<IEmailVerificationSender, LoggingEmailVerificationSender>(); |
| | 973 | 54 | | services.AddScoped<IPasswordResetSender, LoggingPasswordResetSender>(); |
| | 973 | 55 | | services.AddScoped<ITwoFactorEmailSender, LoggingTwoFactorEmailSender>(); |
| | 973 | 56 | | services.AddScoped<IRestrictionLiftNotifier, LoggingRestrictionLiftNotifier>(); |
| | 973 | 57 | | services.AddScoped<IRegistrationAttemptSender, LoggingRegistrationAttemptSender>(); |
| | 973 | 58 | | services.AddScoped<IRegistrationLinkSender, LoggingRegistrationLinkSender>(); |
| | 973 | 59 | | services.AddScoped<IEmailChangeNoticeSender, LoggingEmailChangeNoticeSender>(); |
| | 973 | 60 | | services.AddScoped<IScopeInvitationNoticeSender, LoggingScopeInvitationNoticeSender>(); |
| | | 61 | | |
| | 973 | 62 | | return services; |
| | | 63 | | } |
| | | 64 | | |
| | | 65 | | // A typed client, so the Mailgun service reuses pooled connections instead of creating an |
| | | 66 | | // HttpClient per send. |
| | 0 | 67 | | services.AddHttpClient<IEmailService, MailgunEmailService>(); |
| | 0 | 68 | | services.AddScoped<IEmailVerificationSender, MailgunEmailVerificationSender>(); |
| | 0 | 69 | | services.AddScoped<IPasswordResetSender, MailgunPasswordResetSender>(); |
| | 0 | 70 | | services.AddScoped<ITwoFactorEmailSender, MailgunTwoFactorEmailSender>(); |
| | 0 | 71 | | services.AddScoped<IRestrictionLiftNotifier, MailgunRestrictionLiftNotifier>(); |
| | 0 | 72 | | services.AddScoped<IRegistrationAttemptSender, MailgunRegistrationAttemptSender>(); |
| | 0 | 73 | | services.AddScoped<IRegistrationLinkSender, MailgunRegistrationLinkSender>(); |
| | 0 | 74 | | services.AddScoped<IEmailChangeNoticeSender, MailgunEmailChangeNoticeSender>(); |
| | 0 | 75 | | services.AddScoped<IScopeInvitationNoticeSender, MailgunScopeInvitationNoticeSender>(); |
| | | 76 | | |
| | 0 | 77 | | return services; |
| | | 78 | | } |
| | | 79 | | |
| | | 80 | | /// <summary> |
| | | 81 | | /// Registers the e-mail verification and password reset services, the cooldown and invitation |
| | | 82 | | /// options, the password recovery and registration queues with their dispatchers, and the audited |
| | | 83 | | /// handlers of the administrator-initiated e-mails (UC-12, UC-47, UC-51, UC-52). |
| | | 84 | | /// </summary> |
| | | 85 | | public static IServiceCollection AddAccountEmailFlows(this IServiceCollection services) |
| | | 86 | | { |
| | 973 | 87 | | services.AddSingleton(EmailVerificationOptions.FromEnvironment()); |
| | 973 | 88 | | services.AddScoped<IEmailVerificationService, EmailVerificationService>(); |
| | 973 | 89 | | services.AddSingleton(PasswordResetOptions.FromEnvironment()); |
| | 973 | 90 | | services.AddScoped<IPasswordResetService, PasswordResetService>(); |
| | | 91 | | // UC-51/UC-52 (FR-EV-07, FR-PR-06): the per-person cooldown on administrator-initiated email, |
| | | 92 | | // read once at start-up like the token lifetimes it is compared against. |
| | 973 | 93 | | var adminEmail = AdminEmailOptions.FromEnvironment(); |
| | 973 | 94 | | ConfigurationWarnings.WarnOnFallback(AdminEmailOptions.CooldownVariable, adminEmail.RejectedCooldown, |
| | 973 | 95 | | AdminEmailOptions.DefaultCooldownSeconds); |
| | 973 | 96 | | services.AddSingleton(adminEmail); |
| | | 97 | | |
| | | 98 | | // UC-21 (FR-SC-18): how long a scope owner invitation stays acceptable, read once at start-up. |
| | 973 | 99 | | var scopeInvitation = ScopeInvitationOptions.FromEnvironment(); |
| | 973 | 100 | | ConfigurationWarnings.WarnOnFallback(ScopeInvitationOptions.LifetimeVariable, scopeInvitation.RejectedLifetime, |
| | 973 | 101 | | ScopeInvitationOptions.DefaultLifetimeSeconds); |
| | 973 | 102 | | services.AddSingleton(scopeInvitation); |
| | | 103 | | // UC-51: no validator — the only input is the route's person id, already constrained to a GUID. |
| | 973 | 104 | | services.AddAuditedCommandHandler<SendPersonVerificationEmailCommand, SendPersonVerificationEmailCommandOutput, |
| | | 105 | | // UC-52: no validator either — the route's person id is the whole of the input. |
| | 973 | 106 | | services.AddAuditedCommandHandler<SendPersonPasswordResetCommand, SendPersonPasswordResetCommandOutput, SendPers |
| | | 107 | | // UC-12 runs in two halves (AF-12a): the handler queues, the dispatcher does the work after |
| | | 108 | | // the caller has been answered, so no address takes longer to answer than another. See |
| | | 109 | | // PasswordRecoveryQueue for why this is an in-memory queue rather than a floor or an outbox. |
| | 973 | 110 | | services.AddSingleton<PasswordRecoveryQueue>(); |
| | 973 | 111 | | services.AddSingleton<IPasswordRecoveryQueue>( |
| | 989 | 112 | | provider => provider.GetRequiredService<PasswordRecoveryQueue>()); |
| | 973 | 113 | | services.AddScoped<PasswordRecoveryProcessor>(); |
| | 973 | 114 | | services.AddHostedService<PasswordRecoveryDispatcher>(); |
| | | 115 | | // UC-47 runs in the same two halves as UC-12 (AF-47b): the handler queues, the dispatcher |
| | | 116 | | // stores the pending registration or tells the holder of the attempt after the caller has been |
| | | 117 | | // answered. Following the mailed link (UC-14) then reaches the confirmer, which creates the User. |
| | 973 | 118 | | services.AddSingleton<RegistrationQueue>(); |
| | 973 | 119 | | services.AddSingleton<IRegistrationQueue>( |
| | 985 | 120 | | provider => provider.GetRequiredService<RegistrationQueue>()); |
| | 973 | 121 | | services.AddScoped<RegistrationProcessor>(); |
| | | 122 | | // #206: the cap on a scope's live pending registrations, read once at start-up. |
| | 973 | 123 | | var registration = RegistrationOptions.FromEnvironment(); |
| | | 124 | | |
| | 973 | 125 | | if (registration.RejectedMaxPendingPerScope is not null) |
| | | 126 | | { |
| | 0 | 127 | | Log.Warning( |
| | 0 | 128 | | "Ignoring unusable {Variable}={Value}; using the default of {Default}", |
| | 0 | 129 | | RegistrationOptions.MaxPendingPerScopeVariable, registration.RejectedMaxPendingPerScope, |
| | 0 | 130 | | RegistrationOptions.DefaultMaxPendingPerScope); |
| | | 131 | | } |
| | | 132 | | |
| | 973 | 133 | | services.AddSingleton(registration); |
| | 973 | 134 | | services.AddScoped<IPendingRegistrationConfirmer, PendingRegistrationConfirmer>(); |
| | 973 | 135 | | services.AddHostedService<RegistrationDispatcher>(); |
| | | 136 | | |
| | 973 | 137 | | return services; |
| | | 138 | | } |
| | | 139 | | } |