From 0cde0fe302b89236167e49a2ebfb648b3009f739 Mon Sep 17 00:00:00 2001 From: Marcel Baumgartner Date: Mon, 16 Oct 2023 17:13:15 +0200 Subject: [PATCH] Added mail system. Added password reset and email verify. Switched to new event system. Added event based mail sending --- .../Event/Args/MailVerificationEventArgs.cs | 9 ++ Moonlight/App/Event/Events.cs | 12 ++ .../App/Extensions/EventHandlerExtensions.cs | 37 +++++ Moonlight/App/Helpers/EventSystem.cs | 140 ------------------ .../Controllers/Api/Auth/ResetController.cs | 64 ++++++++ .../Controllers/Api/Auth/VerifyController.cs | 51 +++++++ .../App/Models/Forms/ResetPasswordForm.cs | 2 +- Moonlight/App/Models/Templates/MailVerify.cs | 6 + .../App/Models/Templates/ResetPassword.cs | 6 + .../Background/AutoMailSendService.cs | 23 +++ Moonlight/App/Services/MailService.cs | 103 +++++++++++++ .../App/Services/Users/UserAuthService.cs | 62 ++++---- Moonlight/Moonlight.csproj | 1 + Moonlight/Program.cs | 8 + .../Components/Auth/ChangePassword.razor | 65 ++++++++ Moonlight/Shared/Components/Auth/Login.razor | 2 +- .../Shared/Components/Auth/MailVerify.razor | 46 ++++++ .../Components/Auth/PasswordReset.razor | 56 +++++++ .../Shared/Components/Forms/SmartForm.razor | 2 +- .../Components/Partials/PageHeader.razor | 11 +- Moonlight/Shared/Layouts/MainLayout.razor | 12 ++ 21 files changed, 538 insertions(+), 180 deletions(-) create mode 100644 Moonlight/App/Event/Args/MailVerificationEventArgs.cs create mode 100644 Moonlight/App/Event/Events.cs create mode 100644 Moonlight/App/Extensions/EventHandlerExtensions.cs delete mode 100644 Moonlight/App/Helpers/EventSystem.cs create mode 100644 Moonlight/App/Http/Controllers/Api/Auth/ResetController.cs create mode 100644 Moonlight/App/Http/Controllers/Api/Auth/VerifyController.cs create mode 100644 Moonlight/App/Models/Templates/MailVerify.cs create mode 100644 Moonlight/App/Models/Templates/ResetPassword.cs create mode 100644 Moonlight/App/Services/Background/AutoMailSendService.cs create mode 100644 Moonlight/App/Services/MailService.cs create mode 100644 Moonlight/Shared/Components/Auth/ChangePassword.razor create mode 100644 Moonlight/Shared/Components/Auth/MailVerify.razor create mode 100644 Moonlight/Shared/Components/Auth/PasswordReset.razor diff --git a/Moonlight/App/Event/Args/MailVerificationEventArgs.cs b/Moonlight/App/Event/Args/MailVerificationEventArgs.cs new file mode 100644 index 00000000..a1f31600 --- /dev/null +++ b/Moonlight/App/Event/Args/MailVerificationEventArgs.cs @@ -0,0 +1,9 @@ +using Moonlight.App.Database.Entities; + +namespace Moonlight.App.Event.Args; + +public class MailVerificationEventArgs +{ + public User User { get; set; } + public string Jwt { get; set; } +} \ No newline at end of file diff --git a/Moonlight/App/Event/Events.cs b/Moonlight/App/Event/Events.cs new file mode 100644 index 00000000..3a7e16dd --- /dev/null +++ b/Moonlight/App/Event/Events.cs @@ -0,0 +1,12 @@ +using Moonlight.App.Database.Entities; +using Moonlight.App.Event.Args; + +namespace Moonlight.App.Event; + +public class Events +{ + public static EventHandler OnUserRegistered; + public static EventHandler OnUserPasswordChanged; + public static EventHandler OnUserTotpSet; + public static EventHandler OnUserMailVerify; +} \ No newline at end of file diff --git a/Moonlight/App/Extensions/EventHandlerExtensions.cs b/Moonlight/App/Extensions/EventHandlerExtensions.cs new file mode 100644 index 00000000..31f53931 --- /dev/null +++ b/Moonlight/App/Extensions/EventHandlerExtensions.cs @@ -0,0 +1,37 @@ +namespace Moonlight.App.Extensions; + +public static class EventHandlerExtensions +{ + public static async Task InvokeAsync(this EventHandler handler) + { + var tasks = handler + .GetInvocationList() + .Select(x => new Task(() => x.DynamicInvoke(null, null))) + .ToArray(); + + foreach (var task in tasks) + { + task.Start(); + } + + await Task.WhenAll(tasks); + } + + public static async Task InvokeAsync(this EventHandler? handler, T? data = default(T)) + { + if(handler == null) + return; + + var tasks = handler + .GetInvocationList() + .Select(x => new Task(() => x.DynamicInvoke(null, data))) + .ToArray(); + + foreach (var task in tasks) + { + task.Start(); + } + + await Task.WhenAll(tasks); + } +} \ No newline at end of file diff --git a/Moonlight/App/Helpers/EventSystem.cs b/Moonlight/App/Helpers/EventSystem.cs deleted file mode 100644 index e2eca127..00000000 --- a/Moonlight/App/Helpers/EventSystem.cs +++ /dev/null @@ -1,140 +0,0 @@ -using System.Diagnostics; -using Moonlight.App.Models.Abstractions; - -namespace Moonlight.App.Helpers; - -public class EventSystem -{ - private readonly List Subscribers = new(); - - private readonly bool Debug = false; - private readonly bool DisableWarning = false; - private readonly TimeSpan TookToLongTime = TimeSpan.FromSeconds(1); - - public Task On(string id, object handle, Func action) - { - if (Debug) - Logger.Debug($"{handle} subscribed to '{id}'"); - - lock (Subscribers) - { - if (!Subscribers.Any(x => x.Id == id && x.Handle == handle)) - { - Subscribers.Add(new() - { - Action = action, - Handle = handle, - Id = id - }); - } - } - - return Task.CompletedTask; - } - - public Task Emit(string id, object? data = null) - { - Subscriber[] subscribers; - - lock (Subscribers) - { - subscribers = Subscribers - .Where(x => x.Id == id) - .ToArray(); - } - - var tasks = new List(); - - foreach (var subscriber in subscribers) - { - tasks.Add(new Task(() => - { - var stopWatch = new Stopwatch(); - stopWatch.Start(); - - var del = (Delegate)subscriber.Action; - - try - { - ((Task)del.DynamicInvoke(data)!).Wait(); - } - catch (Exception e) - { - Logger.Warn($"Error emitting '{subscriber.Id} on {subscriber.Handle}'"); - Logger.Warn(e); - } - - stopWatch.Stop(); - - if (!DisableWarning) - { - if (stopWatch.Elapsed.TotalMilliseconds > TookToLongTime.TotalMilliseconds) - { - Logger.Warn( - $"Subscriber {subscriber.Handle} for event '{subscriber.Id}' took long to process. {stopWatch.Elapsed.TotalMilliseconds}ms"); - } - } - - if (Debug) - { - Logger.Debug( - $"Subscriber {subscriber.Handle} for event '{subscriber.Id}' took {stopWatch.Elapsed.TotalMilliseconds}ms"); - } - })); - } - - foreach (var task in tasks) - { - task.Start(); - } - - Task.Run(() => - { - Task.WaitAll(tasks.ToArray()); - - if (Debug) - Logger.Debug($"Completed all event tasks for '{id}' and removed object from storage"); - }); - - if (Debug) - Logger.Debug($"Completed event emit '{id}'"); - - return Task.CompletedTask; - } - - public Task Off(string id, object handle) - { - if (Debug) - Logger.Debug($"{handle} unsubscribed to '{id}'"); - - lock (Subscribers) - { - Subscribers.RemoveAll(x => x.Id == id && x.Handle == handle); - } - - return Task.CompletedTask; - } - - public Task WaitForEvent(string id, object handle, Func? filter = null) - { - var taskCompletionSource = new TaskCompletionSource(); - - Func action = async data => - { - if (filter == null) - { - taskCompletionSource.SetResult(data); - await Off(id, handle); - } - else if (filter.Invoke(data)) - { - taskCompletionSource.SetResult(data); - await Off(id, handle); - } - }; - - On(id, handle, action); - - return taskCompletionSource.Task; - } -} \ No newline at end of file diff --git a/Moonlight/App/Http/Controllers/Api/Auth/ResetController.cs b/Moonlight/App/Http/Controllers/Api/Auth/ResetController.cs new file mode 100644 index 00000000..482da17c --- /dev/null +++ b/Moonlight/App/Http/Controllers/Api/Auth/ResetController.cs @@ -0,0 +1,64 @@ +using Microsoft.AspNetCore.Mvc; +using Moonlight.App.Database.Entities; +using Moonlight.App.Models.Enums; +using Moonlight.App.Repositories; +using Moonlight.App.Services; +using Moonlight.App.Services.Utils; + +namespace Moonlight.App.Http.Controllers.Api.Auth; + +[ApiController] +[Route("api/auth/reset")] +public class ResetController : Controller +{ + private readonly Repository UserRepository; + private readonly IdentityService IdentityService; + private readonly JwtService JwtService; + + public ResetController(Repository userRepository, IdentityService identityService, JwtService jwtService) + { + UserRepository = userRepository; + IdentityService = identityService; + JwtService = jwtService; + } + + [HttpGet] + public async Task Get([FromQuery] string token) + { + // Validate token + + if (!await JwtService.Validate(token)) + return Redirect("/password-reset"); + + var data = await JwtService.Decode(token); + + if (!data.ContainsKey("accountToReset")) + return Redirect("/password-reset"); + + var userId = int.Parse(data["accountToReset"]); + var user = UserRepository + .Get() + .FirstOrDefault(x => x.Id == userId); + + // User may have been deleted, so we check here + + if (user == null) + return Redirect("/password-reset"); + + // In order to allow the user to get access to the change password screen + // we need to authenticate him so we can read his flags. + // That's why we are creating a session here + + var sessionToken = await IdentityService.GenerateToken(user); + + // Authenticate the current identity service instance in order to + // get access to the flags field. + await IdentityService.Authenticate(sessionToken); + IdentityService.Flags[UserFlag.PasswordPending] = true; + await IdentityService.SaveFlags(); + + // Make the user login so he can reach the change password screen + Response.Cookies.Append("token", sessionToken); + return Redirect("/"); + } +} \ No newline at end of file diff --git a/Moonlight/App/Http/Controllers/Api/Auth/VerifyController.cs b/Moonlight/App/Http/Controllers/Api/Auth/VerifyController.cs new file mode 100644 index 00000000..e2b19e28 --- /dev/null +++ b/Moonlight/App/Http/Controllers/Api/Auth/VerifyController.cs @@ -0,0 +1,51 @@ +using Microsoft.AspNetCore.Mvc; +using Moonlight.App.Helpers; +using Moonlight.App.Models.Enums; +using Moonlight.App.Services; +using Moonlight.App.Services.Utils; + +namespace Moonlight.App.Http.Controllers.Api.Auth; + +[ApiController] +[Route("api/auth/verify")] +public class VerifyController : Controller +{ + private readonly IdentityService IdentityService; + private readonly JwtService JwtService; + + public VerifyController(IdentityService identityService, JwtService jwtService) + { + IdentityService = identityService; + JwtService = jwtService; + } + + [HttpGet] + public async Task Get([FromQuery] string token) + { + await IdentityService.Authenticate(Request); + + if (!IdentityService.IsSignedIn) + return Redirect("/login"); + + if (!await JwtService.Validate(token)) + return Redirect("/login"); + + var data = await JwtService.Decode(token); + + if (!data.ContainsKey("mailToVerify")) + return Redirect("/login"); + + var mailToVerify = data["mailToVerify"]; + + if (mailToVerify != IdentityService.CurrentUser.Email) + { + Logger.Warn($"User {IdentityService.CurrentUser.Email} tried to mail verify {mailToVerify} via verify api endpoint", "security"); + return Redirect("/login"); + } + + IdentityService.Flags[UserFlag.MailVerified] = true; + await IdentityService.SaveFlags(); + + return Redirect("/"); + } +} \ No newline at end of file diff --git a/Moonlight/App/Models/Forms/ResetPasswordForm.cs b/Moonlight/App/Models/Forms/ResetPasswordForm.cs index 1519d305..657eeeae 100644 --- a/Moonlight/App/Models/Forms/ResetPasswordForm.cs +++ b/Moonlight/App/Models/Forms/ResetPasswordForm.cs @@ -5,6 +5,6 @@ namespace Moonlight.App.Models.Forms; public class ResetPasswordForm { [Required(ErrorMessage = "You need to specify an email address")] - [EmailAddress] + [EmailAddress(ErrorMessage = "You need to enter a valid email address")] public string Email { get; set; } = ""; } \ No newline at end of file diff --git a/Moonlight/App/Models/Templates/MailVerify.cs b/Moonlight/App/Models/Templates/MailVerify.cs new file mode 100644 index 00000000..92542534 --- /dev/null +++ b/Moonlight/App/Models/Templates/MailVerify.cs @@ -0,0 +1,6 @@ +namespace Moonlight.App.Models.Templates; + +public class MailVerify +{ + public string Url { get; set; } = ""; +} \ No newline at end of file diff --git a/Moonlight/App/Models/Templates/ResetPassword.cs b/Moonlight/App/Models/Templates/ResetPassword.cs new file mode 100644 index 00000000..46f0cd33 --- /dev/null +++ b/Moonlight/App/Models/Templates/ResetPassword.cs @@ -0,0 +1,6 @@ +namespace Moonlight.App.Models.Templates; + +public class ResetPassword +{ + public string Url { get; set; } = ""; +} \ No newline at end of file diff --git a/Moonlight/App/Services/Background/AutoMailSendService.cs b/Moonlight/App/Services/Background/AutoMailSendService.cs new file mode 100644 index 00000000..e159855d --- /dev/null +++ b/Moonlight/App/Services/Background/AutoMailSendService.cs @@ -0,0 +1,23 @@ +using Moonlight.App.Database.Entities; +using Moonlight.App.Event; + +namespace Moonlight.App.Services.Background; + +public class AutoMailSendService // This service is responsible for sending mails automatically +{ + private readonly MailService MailService; + private readonly ConfigService ConfigService; + + public AutoMailSendService(MailService mailService, ConfigService configService) + { + MailService = mailService; + ConfigService = configService; + + Events.OnUserRegistered += OnUserRegistered; + } + + private async void OnUserRegistered(object? _, User user) + { + await MailService.Send(user, $"Welcome {user.Username}", "welcome", user); + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/MailService.cs b/Moonlight/App/Services/MailService.cs new file mode 100644 index 00000000..f1842482 --- /dev/null +++ b/Moonlight/App/Services/MailService.cs @@ -0,0 +1,103 @@ +using MailKit.Net.Smtp; +using MimeKit; +using Moonlight.App.Database.Entities; +using Moonlight.App.Helpers; + +namespace Moonlight.App.Services; + +public class MailService +{ + private readonly ConfigService ConfigService; + private readonly string BasePath; + + public MailService(ConfigService configService) + { + ConfigService = configService; + + BasePath = PathBuilder.Dir("storage", "mail"); + Directory.CreateDirectory(BasePath); + } + + public async Task Send(User user, string title, string templateName, params object[] models) + { + var config = ConfigService.Get().MailServer; + + try + { + // Build mail message + var message = new MimeMessage(); + + message.From.Add(new MailboxAddress( + "Moonlight System", //TODO: Replace with config option + config.Email + )); + + message.To.Add(new MailboxAddress( + $"{user.Username}", + user.Email + )); + + message.Subject = Formatter.ProcessTemplating(title, models); + + var body = new BodyBuilder() + { + HtmlBody = await ParseTemplate(templateName, models) + }; + message.Body = body.ToMessageBody(); + + // The actual sending will not be done in the mail thread to prevent long loading times + Task.Run(async () => + { + using var smtpClient = new SmtpClient(); + + try + { + await smtpClient.ConnectAsync(config.Host, config.Port, config.UseSsl); + await smtpClient.AuthenticateAsync(config.Email, config.Password); + await smtpClient.SendAsync(message); + await smtpClient.DisconnectAsync(true); + } + catch (Exception e) + { + Logger.Warn("An unexpected error occured while connecting and transferring mail to mailserver"); + Logger.Warn(e); + } + }); + } + catch (FileNotFoundException) + { + // ignored as we log it anyways in the parse template function + } + catch (Exception e) + { + Logger.Warn("Unhandled error occured during sending mail:"); + Logger.Warn(e); + } + } + + private async Task ParseTemplate(string templateName, params object[] models) + { + if (!File.Exists(PathBuilder.File(BasePath, templateName + ".html"))) + { + Logger.Warn($"Mail template '{templateName}' is missing. Skipping sending mail"); + throw new FileNotFoundException(); + } + + var text = await File.ReadAllTextAsync( + PathBuilder.File(BasePath, templateName + ".html") + ); + + // For details how the templating works, check out the explanation of the ProcessTemplating in the Formatter class + text = Formatter.ProcessTemplating(text, models); + + return text; + } + + // Helpers + + public async Task Send(IEnumerable users, string title, string templateName, params object[] models) + { + foreach (var user in users) + await Send(user, title, templateName, models); + } +} \ No newline at end of file diff --git a/Moonlight/App/Services/Users/UserAuthService.cs b/Moonlight/App/Services/Users/UserAuthService.cs index 63071279..4dd055dc 100644 --- a/Moonlight/App/Services/Users/UserAuthService.cs +++ b/Moonlight/App/Services/Users/UserAuthService.cs @@ -1,8 +1,11 @@ using Moonlight.App.Database.Entities; +using Moonlight.App.Event; using Moonlight.App.Exceptions; +using Moonlight.App.Extensions; using Moonlight.App.Helpers; using Moonlight.App.Models.Abstractions; using Moonlight.App.Models.Enums; +using Moonlight.App.Models.Templates; using Moonlight.App.Repositories; using Moonlight.App.Services.Utils; using OtpNet; @@ -12,20 +15,20 @@ namespace Moonlight.App.Services.Users; public class UserAuthService { private readonly Repository UserRepository; - //private readonly MailService MailService; private readonly JwtService JwtService; private readonly ConfigService ConfigService; + private readonly MailService MailService; public UserAuthService( Repository userRepository, - //MailService mailService, JwtService jwtService, - ConfigService configService) + ConfigService configService, + MailService mailService) { UserRepository = userRepository; - //MailService = mailService; JwtService = jwtService; ConfigService = configService; + MailService = mailService; } public async Task Register(string username, string email, string password) @@ -50,37 +53,32 @@ public class UserAuthService }; var result = UserRepository.Add(user); -/* - await MailService.Send( - result, - "Welcome {{User.Username}}", - "register", - result - );*/ + + await Events.OnUserRegistered.InvokeAsync(result); return result; } - - public Task ChangePassword(User user, string newPassword) + + public async Task ChangePassword(User user, string newPassword) { user.Password = HashHelper.HashToString(newPassword); user.TokenValidTimestamp = DateTime.UtcNow; UserRepository.Update(user); - return Task.CompletedTask; + await Events.OnUserPasswordChanged.InvokeAsync(user); } public Task SeedTotp(User user) { - var key = Base32Encoding.ToString(KeyGeneration.GenerateRandomKey(20)); + var key = Base32Encoding.ToString(KeyGeneration.GenerateRandomKey(20)); user.TotpKey = key; UserRepository.Update(user); - + return Task.CompletedTask; } - public Task SetTotp(User user, bool state) + public async Task SetTotp(User user, bool state) { // Access to flags without identity service var flags = new FlagStorage(user.Flags); @@ -89,25 +87,22 @@ public class UserAuthService if (!state) user.TotpKey = null; - + UserRepository.Update(user); - - return Task.CompletedTask; + + await Events.OnUserTotpSet.InvokeAsync(user); } // Mails - + public async Task SendVerification(User user) { - var jwt = await JwtService.Create(data => - { - data.Add("mailToVerify", user.Email); - }, TimeSpan.FromMinutes(10)); -/* + var jwt = await JwtService.Create(data => { data.Add("mailToVerify", user.Email); }, TimeSpan.FromMinutes(10)); + await MailService.Send(user, "Verify your account", "verifyMail", user, new MailVerify() { - Url = ConfigService.Get().AppUrl + "/api/verify?token=" + jwt - });*/ + Url = ConfigService.Get().AppUrl + "/api/auth/verify?token=" + jwt + }); } public async Task SendResetPassword(string email) @@ -119,14 +114,11 @@ public class UserAuthService if (user == null) throw new DisplayException("An account with that email was not found"); - var jwt = await JwtService.Create(data => - { - data.Add("accountToReset", user.Id.ToString()); - }); - /* + var jwt = await JwtService.Create(data => { data.Add("accountToReset", user.Id.ToString()); }); + await MailService.Send(user, "Password reset for your account", "passwordReset", user, new ResetPassword() { - Url = ConfigService.Get().AppUrl + "/api/reset?token=" + jwt - });*/ + Url = ConfigService.Get().AppUrl + "/api/auth/reset?token=" + jwt + }); } } \ No newline at end of file diff --git a/Moonlight/Moonlight.csproj b/Moonlight/Moonlight.csproj index 53c2c6eb..1c5fe55b 100644 --- a/Moonlight/Moonlight.csproj +++ b/Moonlight/Moonlight.csproj @@ -24,6 +24,7 @@ + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/Moonlight/Program.cs b/Moonlight/Program.cs index 92c182d3..252122c5 100644 --- a/Moonlight/Program.cs +++ b/Moonlight/Program.cs @@ -4,6 +4,7 @@ using Moonlight.App.Helpers; using Moonlight.App.Helpers.LogMigrator; using Moonlight.App.Repositories; using Moonlight.App.Services; +using Moonlight.App.Services.Background; using Moonlight.App.Services.Interop; using Moonlight.App.Services.Users; using Moonlight.App.Services.Utils; @@ -40,11 +41,15 @@ builder.Services.AddScoped(); builder.Services.AddScoped(); builder.Services.AddScoped(); +// Services / Background +builder.Services.AddSingleton(); + // Services builder.Services.AddScoped(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddRazorPages(); builder.Services.AddServerSideBlazor(); @@ -68,4 +73,7 @@ app.MapBlazorHub(); app.MapFallbackToPage("/_Host"); app.MapControllers(); +// Auto start background services +app.Services.GetRequiredService(); + app.Run(); \ No newline at end of file diff --git a/Moonlight/Shared/Components/Auth/ChangePassword.razor b/Moonlight/Shared/Components/Auth/ChangePassword.razor new file mode 100644 index 00000000..0c97afb7 --- /dev/null +++ b/Moonlight/Shared/Components/Auth/ChangePassword.razor @@ -0,0 +1,65 @@ +@using Moonlight.App.Services +@using Moonlight.App.Services.Users +@using Moonlight.App.Models.Forms +@using Moonlight.App.Models.Enums + +@inject IdentityService IdentityService +@inject UserService UserService + +
+
+
+

+ Change your password +

+
+ You need to change your password in order to continue +
+
+ + +
+ +
+ +
+ +
+ +
+ +
+
+
+
+ +@code +{ + private UpdateAccountPasswordForm Form = new(); + + private async Task OnValidSubmit() + { + if (Form.Password != Form.RepeatedPassword) + throw new DisplayException("The password do not match"); + + // Because of UserService.Auth.ChangePassword may logout the user before we can reset the flag + // we reset the flag before changing the password and if any error occurs we simple set it again + + try + { + IdentityService.Flags[UserFlag.PasswordPending] = false; + await IdentityService.SaveFlags(); + + await UserService.Auth.ChangePassword(IdentityService.CurrentUser, Form.Password); + } + catch (Exception) + { + IdentityService.Flags[UserFlag.PasswordPending] = true; + await IdentityService.SaveFlags(); + + throw; + } + + await IdentityService.Authenticate(); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/Auth/Login.razor b/Moonlight/Shared/Components/Auth/Login.razor index 508c3657..8b1043ec 100644 --- a/Moonlight/Shared/Components/Auth/Login.razor +++ b/Moonlight/Shared/Components/Auth/Login.razor @@ -29,7 +29,7 @@
- + Forgot Password ? diff --git a/Moonlight/Shared/Components/Auth/MailVerify.razor b/Moonlight/Shared/Components/Auth/MailVerify.razor new file mode 100644 index 00000000..15bbe1a3 --- /dev/null +++ b/Moonlight/Shared/Components/Auth/MailVerify.razor @@ -0,0 +1,46 @@ +@using Moonlight.App.Services.Users +@using Moonlight.App.Services + +@inject UserService UserService +@inject IdentityService IdentityService + +
+
+ @if (HasBeenSend) + { +
+

+ Email verification sent +

+
+ You should receive an email shortly. If you see no email in your inbox, look inside your spam folder +
+
+ } + else + { +
+

+ Verify your email address +

+
+ We will sent you an email to verify your account +
+
+ + + } +
+
+ +@code +{ + private bool HasBeenSend = false; + + private async Task Send() + { + await UserService.Auth.SendVerification(IdentityService.CurrentUser); + HasBeenSend = true; + await InvokeAsync(StateHasChanged); + } +} diff --git a/Moonlight/Shared/Components/Auth/PasswordReset.razor b/Moonlight/Shared/Components/Auth/PasswordReset.razor new file mode 100644 index 00000000..f80632b4 --- /dev/null +++ b/Moonlight/Shared/Components/Auth/PasswordReset.razor @@ -0,0 +1,56 @@ +@page "/password-reset" + +@using Moonlight.App.Services.Users +@using Moonlight.App.Models.Forms + +@inject UserService UserService + +
+
+ @if (HasBeenSend) + { +
+

+ Password reset email sent +

+
+ You should receive the email shortly. If you see no email in your inbox, look inside your spam folder +
+
+ } + else + { +
+

+ Reset your password +

+
+ We will sent you an email to reset your account password +
+
+ + +
+ +
+ +
+ +
+
+ } +
+
+ +@code +{ + private bool HasBeenSend = false; + private ResetPasswordForm Form = new(); + + private async Task OnValidSubmit() + { + await UserService.Auth.SendResetPassword(Form.Email); + HasBeenSend = true; + await InvokeAsync(StateHasChanged); + } +} \ No newline at end of file diff --git a/Moonlight/Shared/Components/Forms/SmartForm.razor b/Moonlight/Shared/Components/Forms/SmartForm.razor index 1cb2ab69..cb52711c 100644 --- a/Moonlight/Shared/Components/Forms/SmartForm.razor +++ b/Moonlight/Shared/Components/Forms/SmartForm.razor @@ -88,7 +88,7 @@ ErrorMessages.Add(displayException.Message); } else - throw e; + throw; } }); diff --git a/Moonlight/Shared/Components/Partials/PageHeader.razor b/Moonlight/Shared/Components/Partials/PageHeader.razor index 8572ec48..32f18c37 100644 --- a/Moonlight/Shared/Components/Partials/PageHeader.razor +++ b/Moonlight/Shared/Components/Partials/PageHeader.razor @@ -37,7 +37,14 @@