diff --git a/.gitignore b/.gitignore index c2a3f28..e66163d 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ riderModule.iml /*.sln.DotSettings.user */appsettings.Secret.yml -*/tempkey.jwk \ No newline at end of file +*/tempkey.jwk +/.vs/* diff --git a/SS14.Auth.Shared/Emails/DummyEmailSender.cs b/SS14.Auth.Shared/Emails/DummyEmailSender.cs index ff5855a..6d09df7 100644 --- a/SS14.Auth.Shared/Emails/DummyEmailSender.cs +++ b/SS14.Auth.Shared/Emails/DummyEmailSender.cs @@ -6,14 +6,14 @@ namespace SS14.Auth.Shared.Emails; public class DummyEmailSender : IRawEmailSender { - public Task SendEmailAsync(string email, string subject, string htmlMessage) + public ValueTask SendEmailAsync(string email, string subject, string htmlMessage) { Console.WriteLine("Would send email to {0}:\n" + "Subject: {1}\n" + "Body: {2}", email, subject, htmlMessage); File.WriteAllText("Email.html", htmlMessage); - - return Task.CompletedTask; + + return ValueTask.FromResult(EmailSendingResult.Success()); } -} \ No newline at end of file +} diff --git a/SS14.Auth.Shared/Emails/EmailHelper.cs b/SS14.Auth.Shared/Emails/EmailHelper.cs new file mode 100644 index 0000000..c766d40 --- /dev/null +++ b/SS14.Auth.Shared/Emails/EmailHelper.cs @@ -0,0 +1,29 @@ +using System.Text.Encodings.Web; +using System.Threading.Tasks; + +namespace SS14.Auth.Shared.Emails; + +public class EmailHelper(IEmailSender sender) : IEmailHelper +{ + /// + public async ValueTask SendConfirmEmail(string address, string confirmLink) + { + return await sender.SendEmailAsync( + address, + "Confirm your Space Station 14 account", + $"Please confirm your account by clicking here." + + $"\n

If the above link is not working, try this one {HtmlEncoder.Default.Encode(confirmLink)}

"); + } + + /// + public async ValueTask SendResetEmail(string address, string callbackUrl) + { + return await sender.SendEmailAsync( + address, + "Reset Password", + "A password reset has been requested for your account.
" + + $"If you did indeed request this, click here to reset your password.
" + + "If you did not request this, simply ignore this email." + + $"\n

If the above link is not working, try this one {HtmlEncoder.Default.Encode(callbackUrl)} +/// Decorator for , which rate limits email sending on per-user basis. +/// This is to prevent abuse of the email system, such as spamming password reset emails or confirmation emails. +/// This is important due to us having global limits on amount of emails we can send. +/// +public class EmailHelperRateLimitingDecorator(IEmailHelper inner, IMemoryCache cache, IOptions limitOptions, ILogger logger) : IEmailHelper +{ + /// + public async ValueTask SendConfirmEmail(string address, string confirmLink) + { + var pipeline = GetOrCreatePipeline(address, "confirm"); + try + { + return await pipeline.ExecuteAsync(t => inner.SendConfirmEmail(address, confirmLink)); + } + catch (RateLimiterRejectedException) + { + return EmailSendingResult.Failure("Your rate limit for confirmation emails was reached, you can try again later."); + } + } + + /// + public async ValueTask SendResetEmail(string address, string callbackUrl) + { + var pipeline = GetOrCreatePipeline(address, "reset"); + try + { + return await pipeline.ExecuteAsync(t => inner.SendResetEmail(address, callbackUrl)); + } + catch (RateLimiterRejectedException) + { + return EmailSendingResult.Failure("Your rate limit for password reset emails was reached, you can try again later."); + } + } + + private ResiliencePipeline GetOrCreatePipeline(string address, string purpose) + { + var cacheKey = $"email_pipeline_{address}"; + + return cache.GetOrCreate(cacheKey, entry => + { + var options = limitOptions.Value; + + entry.SlidingExpiration = options.RefreshWindowPerUser.Add(TimeSpan.FromMinutes(5)); + + var rateLimiter = new SlidingWindowRateLimiter( + new SlidingWindowRateLimiterOptions + { + PermitLimit = options.MaxEmailsInWindowPerUser, + Window = options.RefreshWindowPerUser, + SegmentsPerWindow = 4, + }); + return new ResiliencePipelineBuilder() + .AddRateLimiter( + new RateLimiterStrategyOptions + { + RateLimiter = _ => rateLimiter.AcquireAsync(), + OnRejected = _ => + { + logger.LogWarning("Rate limit rejected for user: {Address}, purpose: {Purpose}.", address, purpose); + return ValueTask.CompletedTask; + } + } + ).Build(); + })!; + } +} diff --git a/SS14.Auth.Shared/Emails/EmailSender.cs b/SS14.Auth.Shared/Emails/EmailSender.cs index 7a8c5eb..79cff58 100644 --- a/SS14.Auth.Shared/Emails/EmailSender.cs +++ b/SS14.Auth.Shared/Emails/EmailSender.cs @@ -1,35 +1,60 @@ -using System.Threading.Tasks; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using SS14.Auth.Shared.MutexDb; +using Polly; +using Polly.RateLimiting; +using System.Threading.RateLimiting; +using System.Threading.Tasks; namespace SS14.Auth.Shared.Emails; public sealed class EmailSender : IEmailSender { private readonly IRawEmailSender _rawEmailSender; - private readonly MutexDatabase _mutex; - private readonly IOptions _limits; + private readonly LimitOptions _limits; private readonly ILogger _logger; + private readonly ResiliencePipeline _pipeline; - public EmailSender(IRawEmailSender rawEmailSender, MutexDatabase mutex, IOptions limits, ILogger logger) + public EmailSender(IRawEmailSender rawEmailSender, IOptions limits, ILogger logger) { _rawEmailSender = rawEmailSender; - _mutex = mutex; - _limits = limits; + _limits = limits.Value; _logger = logger; + + var rateLimiter = new SlidingWindowRateLimiter( + new SlidingWindowRateLimiterOptions + { + PermitLimit = _limits.GlobalMaxEmail, + Window = _limits.GlobalEmailWindow, + SegmentsPerWindow = 4 + }); + _pipeline = new ResiliencePipelineBuilder() + .AddRateLimiter( + new RateLimiterStrategyOptions + { + RateLimiter = _ => rateLimiter.AcquireAsync(), + OnRejected = _ => + { + _logger.LogCritical( + "HIT EMAIL LIMIT!!! Current settings are window: {window}, max per window:{maxEmail}", + _limits.GlobalEmailWindow, + _limits.GlobalMaxEmail); + return ValueTask.CompletedTask; + }, + } + ) + .Build(); } - public async Task SendEmailAsync(string email, string subject, string htmlMessage) + public async ValueTask SendEmailAsync(string email, string subject, string htmlMessage) { - var count = _mutex.IncCount("Email"); - - if (count >= _limits.Value.MaxEmailsPerHour) + try + { + await _pipeline.ExecuteAsync(context => _rawEmailSender.SendEmailAsync(email, subject, htmlMessage)); + return EmailSendingResult.Success(); + } + catch (RateLimiterRejectedException) { - _logger.LogCritical("HIT EMAIL LIMIT, no more emails will be sent for the rest of the hour!!!"); - return; + return EmailSendingResult.Failure("Rate limit for emails was reached, please try again later."); } - - await _rawEmailSender.SendEmailAsync(email, subject, htmlMessage); } -} \ No newline at end of file +} diff --git a/SS14.Auth.Shared/Emails/EmailSendingResult.cs b/SS14.Auth.Shared/Emails/EmailSendingResult.cs new file mode 100644 index 0000000..9782938 --- /dev/null +++ b/SS14.Auth.Shared/Emails/EmailSendingResult.cs @@ -0,0 +1,31 @@ +using System.Diagnostics.CodeAnalysis; +using JetBrains.Annotations; + +namespace SS14.Auth.Shared.Emails; + +///

+/// Wrapper for results of email sending. Can contain error message, success-wrapper is resued. +/// +public class EmailSendingResult +{ + private static readonly EmailSendingResult SuccessSingleton = new(null); + + private EmailSendingResult([CanBeNull] string error) + { + Error = error; + } + + public bool HasError => Error != null; + + [MemberNotNullWhen(true, nameof(HasError))] public string Error { get; } + + public static EmailSendingResult Success() + { + return SuccessSingleton; + } + + public static EmailSendingResult Failure(string error) + { + return new EmailSendingResult(error); + } +} diff --git a/SS14.Auth.Shared/Emails/IEmailHelper.cs b/SS14.Auth.Shared/Emails/IEmailHelper.cs new file mode 100644 index 0000000..9db7cbd --- /dev/null +++ b/SS14.Auth.Shared/Emails/IEmailHelper.cs @@ -0,0 +1,23 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Authentication; +using SS14.Auth.Shared.Data; + +namespace SS14.Auth.Shared.Emails; + +/// +/// Helper class for sending default (pre-defined) emails, such as confirmation and password reset emails. +/// +public interface IEmailHelper +{ + /// Sends email for account confirmation. + ValueTask SendConfirmEmail(string address, string confirmLink); + + /// Sends email for password reset. + ValueTask SendResetEmail(string address, string callbackUrl); + + /// Creates new user object. + static SpaceUser CreateNewUser(string userName, string email, ISystemClock systemClock) + { + return new SpaceUser { UserName = userName, Email = email, CreatedTime = systemClock.UtcNow }; + } +} diff --git a/SS14.Auth.Shared/Emails/IEmailSender.cs b/SS14.Auth.Shared/Emails/IEmailSender.cs index 207ee2b..3800df3 100644 --- a/SS14.Auth.Shared/Emails/IEmailSender.cs +++ b/SS14.Auth.Shared/Emails/IEmailSender.cs @@ -5,5 +5,5 @@ namespace SS14.Auth.Shared.Emails; // AspNet's built-in one is marked as "for internal use" so... public interface IEmailSender { - public Task SendEmailAsync(string email, string subject, string htmlMessage); -} \ No newline at end of file + ValueTask SendEmailAsync(string email, string subject, string htmlMessage); +} diff --git a/SS14.Auth.Shared/Emails/SmtpEmailSender.cs b/SS14.Auth.Shared/Emails/SmtpEmailSender.cs index e8a5a23..d2ef1d0 100644 --- a/SS14.Auth.Shared/Emails/SmtpEmailSender.cs +++ b/SS14.Auth.Shared/Emails/SmtpEmailSender.cs @@ -14,7 +14,7 @@ public SmtpEmailSender(IOptions options) _options = options.Value; } - public async Task SendEmailAsync(string email, string subject, string htmlMessage) + public async ValueTask SendEmailAsync(string email, string subject, string htmlMessage) { using var client = new SmtpClient(); await client.ConnectAsync(_options.Host, _options.Port); @@ -32,5 +32,6 @@ public async Task SendEmailAsync(string email, string subject, string htmlMessag }; await client.SendAsync(msg); + return EmailSendingResult.Success(); } -} \ No newline at end of file +} diff --git a/SS14.Auth.Shared/LimitOptions.cs b/SS14.Auth.Shared/LimitOptions.cs index f39924c..b57ef1a 100644 --- a/SS14.Auth.Shared/LimitOptions.cs +++ b/SS14.Auth.Shared/LimitOptions.cs @@ -1,6 +1,30 @@ -namespace SS14.Auth.Shared; +using System; +namespace SS14.Auth.Shared; + +/// +/// Options for rate limiting email sending. +/// public sealed class LimitOptions { - public int MaxEmailsPerHour { get; set; } = 5000; -} \ No newline at end of file + /// + /// Time window for refreshing per-user email sending limits. + /// + public TimeSpan RefreshWindowPerUser { get; set; } = new(0, 0, 10); + + /// + /// Maximum amount of emails that can be sent to a single user in the time window. + /// + public int MaxEmailsInWindowPerUser { get; set; } = 10; + + + /// + /// Time window for refreshing global email sending limits. + /// + public TimeSpan GlobalEmailWindow { get; set; } = new(0, 1, 0); + + /// + /// Maximum amount of emails that can be sent globally in the time window. + /// + public int GlobalMaxEmail { get; set; } = 5000; +} diff --git a/SS14.Auth.Shared/ModelShared.cs b/SS14.Auth.Shared/ModelShared.cs deleted file mode 100644 index 155367e..0000000 --- a/SS14.Auth.Shared/ModelShared.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Text.Encodings.Web; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Authentication; -using SS14.Auth.Shared.Data; -using SS14.Auth.Shared.Emails; - -namespace SS14.Auth.Shared; - -public static class ModelShared -{ - public static async Task SendConfirmEmail(IEmailSender sender, string address, string confirmLink) - { - await sender.SendEmailAsync(address, "Confirm your Space Station 14 account", - $"Please confirm your account by clicking here." + - $"\n

If the above link is not working, try this one {HtmlEncoder.Default.Encode(confirmLink)}

"); - } - - public static async Task SendResetEmail(IEmailSender emailSender, string email, string callbackUrl) - { - await emailSender.SendEmailAsync( - email, "Reset Password", - "A password reset has been requested for your account.
" + - $"If you did indeed request this, click here to reset your password.
" + - "If you did not request this, simply ignore this email." + - $"\n

If the above link is not working, try this one {HtmlEncoder.Default.Encode(callbackUrl)} + diff --git a/SS14.Auth.Shared/StartupHelpers.cs b/SS14.Auth.Shared/StartupHelpers.cs index b8594bc..9c9c779 100644 --- a/SS14.Auth.Shared/StartupHelpers.cs +++ b/SS14.Auth.Shared/StartupHelpers.cs @@ -4,10 +4,13 @@ using Microsoft.AspNetCore.DataProtection; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Caching.Memory; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Internal; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Options; using Serilog; using Serilog.Sinks.Loki; using Serilog.Sinks.Loki.Labels; @@ -65,7 +68,16 @@ public static void AddShared(IServiceCollection services, IConfiguration config) .AddEntityFrameworkStores() .AddDefaultTokenProviders(); - services.AddTransient(); + services.AddSingleton(); + services.AddTransient(); + services.AddTransient( + sp => new EmailHelperRateLimitingDecorator( + sp.GetRequiredService(), + sp.GetRequiredService(), + sp.GetRequiredService>(), + sp.GetRequiredService>()) + ); + services.AddMemoryCache(); if (string.IsNullOrEmpty(config.GetValue("Email:Host"))) { // Dummy emails. @@ -123,4 +135,4 @@ private sealed class LokiConfigurationData public string Username { get; set; } public string Password { get; set; } } -} \ No newline at end of file +} diff --git a/SS14.Auth/Controllers/AuthApiController.cs b/SS14.Auth/Controllers/AuthApiController.cs index f079d39..04f5037 100644 --- a/SS14.Auth/Controllers/AuthApiController.cs +++ b/SS14.Auth/Controllers/AuthApiController.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Internal; -using SS14.Auth.Shared; using SS14.Auth.Shared.Data; using SS14.Auth.Shared.Emails; using SS14.Auth.Shared.Sessions; @@ -25,7 +24,7 @@ namespace SS14.Auth.Controllers; public class AuthApiController : ControllerBase { private readonly SessionManager _sessionManager; - private readonly IEmailSender _emailSender; + private readonly IEmailHelper _emailSender; private readonly ISystemClock _systemClock; private readonly IConfiguration _cfg; @@ -35,7 +34,7 @@ public class AuthApiController : ControllerBase private string WebBaseUrl => _cfg.GetValue("WebBaseUrl") ?? ""; public AuthApiController(SpaceUserManager userManager, SignInManager signInManager, - SessionManager sessionManager, IEmailSender emailSender, ISystemClock systemClock, IConfiguration cfg) + SessionManager sessionManager, IEmailHelper emailSender, ISystemClock systemClock, IConfiguration cfg) { _userManager = userManager; _signInManager = signInManager; @@ -180,7 +179,11 @@ public async Task ResetPassword(ResetPasswordRequest request) code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); var callbackUrl = $"{WebBaseUrl}Identity/Account/ResetPassword?code={code}&launcher=true"; - await ModelShared.SendResetEmail(_emailSender, email, callbackUrl); + var result = await _emailSender.SendResetEmail(email, callbackUrl); + if (result.HasError) + { + return StatusCode(429, result.Error); + } return Ok(); } @@ -317,4 +320,4 @@ public enum RegisterResponseStatus { Registered, RegisteredNeedConfirmation -} \ No newline at end of file +} diff --git a/SS14.Web/Areas/Admin/Pages/Users/CreateReserved.cshtml.cs b/SS14.Web/Areas/Admin/Pages/Users/CreateReserved.cshtml.cs index 37e35cc..8ea6a31 100644 --- a/SS14.Web/Areas/Admin/Pages/Users/CreateReserved.cshtml.cs +++ b/SS14.Web/Areas/Admin/Pages/Users/CreateReserved.cshtml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Buffers.Text; using System.ComponentModel.DataAnnotations; using System.Security.Cryptography; @@ -6,8 +6,8 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; -using SS14.Auth.Shared; using SS14.Auth.Shared.Data; +using SS14.Auth.Shared.Emails; namespace SS14.Web.Areas.Admin.Pages.Users; @@ -47,7 +47,7 @@ public async Task OnPostAsync() var password = Convert.ToHexString(RandomNumberGenerator.GetBytes(32)); - var user = ModelShared.CreateNewUser(userName, $"reserved+{userName}@playss14.com", _systemClock); + var user = IEmailHelper.CreateNewUser(userName, $"reserved+{userName}@playss14.com", _systemClock); user.AdminLocked = true; user.AdminNotes = "Account reserved via admin panel. If unlocking, change email and password!"; user.EmailConfirmed = true; diff --git a/SS14.Web/Areas/Admin/Pages/Users/ViewUser.cshtml.cs b/SS14.Web/Areas/Admin/Pages/Users/ViewUser.cshtml.cs index e567821..a806a1a 100644 --- a/SS14.Web/Areas/Admin/Pages/Users/ViewUser.cshtml.cs +++ b/SS14.Web/Areas/Admin/Pages/Users/ViewUser.cshtml.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.ComponentModel.DataAnnotations; using System.Net.Mime; using System.Text; @@ -18,7 +18,7 @@ namespace SS14.Web.Areas.Admin.Pages.Users; public class ViewUser : PageModel { private readonly SpaceUserManager _userManager; - private readonly IEmailSender _emailSender; + private readonly IEmailHelper _emailSender; private readonly SessionManager _sessionManager; private readonly PatreonDataManager _patreonDataManager; private readonly ApplicationDbContext _dbContext; @@ -58,7 +58,7 @@ public class InputModel public ViewUser( SpaceUserManager userManager, - IEmailSender emailSender, + IEmailHelper emailSender, SessionManager sessionManager, PatreonDataManager patreonDataManager, ApplicationDbContext dbContext, @@ -197,8 +197,10 @@ public async Task OnPostResendConfirmationAsync(Guid id) try { - await ModelShared.SendConfirmEmail(_emailSender, SpaceUser.Email, confirmLink); - StatusMessage = "Email sent"; + var result = await _emailSender.SendConfirmEmail(SpaceUser.Email, confirmLink); + StatusMessage = result.HasError + ? "Error: " + result.Error + : "Email sent"; } catch (Exception e) { diff --git a/SS14.Web/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs b/SS14.Web/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs index 2244b31..cb9800d 100644 --- a/SS14.Web/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs +++ b/SS14.Web/Areas/Identity/Pages/Account/ExternalLogin.cshtml.cs @@ -1,7 +1,6 @@ using System.ComponentModel.DataAnnotations; using System.Security.Claims; using System.Text; -using System.Text.Encodings.Web; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; @@ -9,7 +8,6 @@ using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; -using SS14.Auth.Shared; using SS14.Auth.Shared.Data; using SS14.Auth.Shared.Emails; @@ -20,14 +18,14 @@ public class ExternalLoginModel : PageModel { private readonly SignInManager _signInManager; private readonly UserManager _userManager; - private readonly IEmailSender _emailSender; + private readonly IEmailHelper _emailSender; private readonly ILogger _logger; public ExternalLoginModel( SignInManager signInManager, UserManager userManager, ILogger logger, - IEmailSender emailSender) + IEmailHelper emailSender) { _signInManager = signInManager; _userManager = userManager; @@ -139,7 +137,12 @@ public async Task OnPostConfirmationAsync(string returnUrl = null values: new { area = "Identity", userId = userId, code = code }, protocol: Request.Scheme); - await ModelShared.SendConfirmEmail(_emailSender, Input.Email, callbackUrl); + var confirmSendResult = await _emailSender.SendConfirmEmail(Input.Email, callbackUrl); + if (confirmSendResult.HasError) + { + ModelState.AddModelError(string.Empty, confirmSendResult.Error); + return Page(); + } // If account confirmation is required, we need to show the link if we don't have a real email sender if (_userManager.Options.SignIn.RequireConfirmedAccount) diff --git a/SS14.Web/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs b/SS14.Web/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs index 4b1984a..d3d6830 100644 --- a/SS14.Web/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs +++ b/SS14.Web/Areas/Identity/Pages/Account/ForgotPassword.cshtml.cs @@ -1,14 +1,13 @@ -using System.ComponentModel.DataAnnotations; -using System.Text; -using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.WebUtilities; -using SS14.Auth.Shared; using SS14.Auth.Shared.Data; using SS14.Auth.Shared.Emails; +using System.ComponentModel.DataAnnotations; +using System.Text; +using System.Threading.Tasks; namespace SS14.Web.Areas.Identity.Pages.Account; @@ -16,9 +15,9 @@ namespace SS14.Web.Areas.Identity.Pages.Account; public class ForgotPasswordModel : PageModel { private readonly UserManager _userManager; - private readonly IEmailSender _emailSender; + private readonly IEmailHelper _emailSender; - public ForgotPasswordModel(UserManager userManager, IEmailSender emailSender) + public ForgotPasswordModel(UserManager userManager, IEmailHelper emailSender) { _userManager = userManager; _emailSender = emailSender; @@ -55,11 +54,16 @@ public async Task OnPostAsync() values: new { area = "Identity", code }, protocol: Request.Scheme); - await ModelShared.SendResetEmail(_emailSender, Input.Email, callbackUrl); + var result = await _emailSender.SendResetEmail(Input.Email, callbackUrl); + if (result.HasError) + { + ModelState.AddModelError(string.Empty, result.Error); + return Page(); + } return RedirectToPage("./ForgotPasswordConfirmation"); } return Page(); } -} \ No newline at end of file +} diff --git a/SS14.Web/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs b/SS14.Web/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs index 6ebc87d..6d36450 100644 --- a/SS14.Web/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs +++ b/SS14.Web/Areas/Identity/Pages/Account/Manage/Email.cshtml.cs @@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.WebUtilities; -using SS14.Auth.Shared; using SS14.Auth.Shared.Data; using SS14.Auth.Shared.Emails; @@ -16,13 +15,13 @@ public partial class EmailModel : PageModel { private readonly UserManager _userManager; private readonly SignInManager _signInManager; - private readonly IEmailSender _emailSender; + private readonly IEmailHelper _emailSender; private readonly AccountLogManager _logManager; public EmailModel( UserManager userManager, SignInManager signInManager, - IEmailSender emailSender, + IEmailHelper emailSender, AccountLogManager logManager) { _userManager = userManager; @@ -101,7 +100,12 @@ public async Task OnPostChangeEmailAsync() pageHandler: null, values: new { userId = userId, email = Input.NewEmail, code = code }, protocol: Request.Scheme); - await ModelShared.SendConfirmEmail(_emailSender, Input.NewEmail, callbackUrl); + var result = await _emailSender.SendConfirmEmail(Input.NewEmail, callbackUrl); + if (result.HasError) + { + ModelState.AddModelError(string.Empty, result.Error); + return Page(); + } await _logManager.LogAndSave(user, new AccountLogEmailChangeRequested(email, Input.NewEmail)); StatusMessage = "Confirmation link to change email sent. Please check your email."; @@ -135,7 +139,12 @@ public async Task OnPostSendVerificationEmailAsync() pageHandler: null, values: new { area = "Identity", userId = userId, code = code }, protocol: Request.Scheme); - await ModelShared.SendConfirmEmail(_emailSender, email, callbackUrl); + var result = await _emailSender.SendConfirmEmail(email, callbackUrl); + if (result.HasError) + { + ModelState.AddModelError(string.Empty, result.Error); + return Page(); + } StatusMessage = "Verification email sent. Please check your email."; return RedirectToPage(); diff --git a/SS14.Web/Areas/Identity/Pages/Account/Register.cshtml.cs b/SS14.Web/Areas/Identity/Pages/Account/Register.cshtml.cs index bfe5e2f..6ffe169 100644 --- a/SS14.Web/Areas/Identity/Pages/Account/Register.cshtml.cs +++ b/SS14.Web/Areas/Identity/Pages/Account/Register.cshtml.cs @@ -10,7 +10,6 @@ using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; -using SS14.Auth.Shared; using SS14.Auth.Shared.Data; using SS14.Auth.Shared.Emails; using SS14.Web.HCaptcha; @@ -24,7 +23,7 @@ public class RegisterModel : PageModel private readonly SignInManager _signInManager; private readonly UserManager _userManager; private readonly ILogger _logger; - private readonly IEmailSender _emailSender; + private readonly IEmailHelper _emailSender; private readonly ISystemClock _systemClock; private readonly HCaptchaService _hCaptcha; @@ -32,7 +31,7 @@ public RegisterModel( UserManager userManager, SignInManager signInManager, ILogger logger, - IEmailSender emailSender, + IEmailHelper emailSender, ISystemClock systemClock, HCaptchaService hCaptcha) { @@ -101,7 +100,7 @@ public async Task OnPostAsync(string returnUrl = null) if (!await _hCaptcha.ValidateHCaptcha(HCaptchaResponse, ModelState)) return Page(); - var user = ModelShared.CreateNewUser(userName, email, _systemClock); + var user = IEmailHelper.CreateNewUser(userName, email, _systemClock); var result = await _userManager.CreateAsync(user, Input.Password); if (result.Succeeded) { @@ -109,7 +108,17 @@ public async Task OnPostAsync(string returnUrl = null) var confirmLink = await GenerateEmailConfirmLink(user, returnUrl); - await ModelShared.SendConfirmEmail(_emailSender, email, confirmLink); + var sendConfirmResult = await _emailSender.SendConfirmEmail(email, confirmLink); + if (sendConfirmResult.HasError) + { + ModelState.AddModelError( + string.Empty, + "Account was created successfully, but error occurred " + + $"while sending confirmation email (Error: {sendConfirmResult.Error}). " + + "Please retry sending confirmation email using `Resend email confirmation` option on log in page." + ); + return Page(); + } if (_userManager.Options.SignIn.RequireConfirmedAccount) { @@ -150,4 +159,4 @@ public async Task GenerateEmailConfirmLink( protocol: Request.Scheme); return callbackUrl; } -} \ No newline at end of file +} diff --git a/SS14.Web/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs b/SS14.Web/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs index 37c201a..b3573aa 100644 --- a/SS14.Web/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs +++ b/SS14.Web/Areas/Identity/Pages/Account/ResendEmailConfirmation.cshtml.cs @@ -6,7 +6,6 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.WebUtilities; -using SS14.Auth.Shared; using SS14.Auth.Shared.Data; using SS14.Auth.Shared.Emails; using SS14.Web.HCaptcha; @@ -17,12 +16,12 @@ namespace SS14.Web.Areas.Identity.Pages.Account; public class ResendEmailConfirmationModel : PageModel { private readonly UserManager _userManager; - private readonly IEmailSender _emailSender; + private readonly IEmailHelper _emailSender; private readonly HCaptchaService _hCaptcha; public ResendEmailConfirmationModel( UserManager userManager, - IEmailSender emailSender, + IEmailHelper emailSender, HCaptchaService hCaptcha) { _userManager = userManager; @@ -75,9 +74,14 @@ public async Task OnPostAsync() values: new { userId = userId, code = code }, protocol: Request.Scheme); - await ModelShared.SendConfirmEmail(_emailSender, email, confirmLink); + var result = await _emailSender.SendConfirmEmail(email, confirmLink); + if (result.HasError) + { + ModelState.AddModelError(string.Empty, result.Error); + return Page(); + } ModelState.AddModelError(string.Empty, "Verification email sent. Please check your email."); return Page(); } -} \ No newline at end of file +}