Added mail system. Added password reset and email verify. Switched to new event system. Added event based mail sending
This commit is contained in:
9
Moonlight/App/Event/Args/MailVerificationEventArgs.cs
Normal file
9
Moonlight/App/Event/Args/MailVerificationEventArgs.cs
Normal file
@@ -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; }
|
||||
}
|
||||
12
Moonlight/App/Event/Events.cs
Normal file
12
Moonlight/App/Event/Events.cs
Normal file
@@ -0,0 +1,12 @@
|
||||
using Moonlight.App.Database.Entities;
|
||||
using Moonlight.App.Event.Args;
|
||||
|
||||
namespace Moonlight.App.Event;
|
||||
|
||||
public class Events
|
||||
{
|
||||
public static EventHandler<User> OnUserRegistered;
|
||||
public static EventHandler<User> OnUserPasswordChanged;
|
||||
public static EventHandler<User> OnUserTotpSet;
|
||||
public static EventHandler<MailVerificationEventArgs> OnUserMailVerify;
|
||||
}
|
||||
37
Moonlight/App/Extensions/EventHandlerExtensions.cs
Normal file
37
Moonlight/App/Extensions/EventHandlerExtensions.cs
Normal file
@@ -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<T>(this EventHandler<T>? 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);
|
||||
}
|
||||
}
|
||||
@@ -1,140 +0,0 @@
|
||||
using System.Diagnostics;
|
||||
using Moonlight.App.Models.Abstractions;
|
||||
|
||||
namespace Moonlight.App.Helpers;
|
||||
|
||||
public class EventSystem
|
||||
{
|
||||
private readonly List<Subscriber> Subscribers = new();
|
||||
|
||||
private readonly bool Debug = false;
|
||||
private readonly bool DisableWarning = false;
|
||||
private readonly TimeSpan TookToLongTime = TimeSpan.FromSeconds(1);
|
||||
|
||||
public Task On<T>(string id, object handle, Func<T, Task> 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<Task>();
|
||||
|
||||
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<T> WaitForEvent<T>(string id, object handle, Func<T, bool>? filter = null)
|
||||
{
|
||||
var taskCompletionSource = new TaskCompletionSource<T>();
|
||||
|
||||
Func<T, Task> 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<T>(id, handle, action);
|
||||
|
||||
return taskCompletionSource.Task;
|
||||
}
|
||||
}
|
||||
64
Moonlight/App/Http/Controllers/Api/Auth/ResetController.cs
Normal file
64
Moonlight/App/Http/Controllers/Api/Auth/ResetController.cs
Normal file
@@ -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<User> UserRepository;
|
||||
private readonly IdentityService IdentityService;
|
||||
private readonly JwtService JwtService;
|
||||
|
||||
public ResetController(Repository<User> userRepository, IdentityService identityService, JwtService jwtService)
|
||||
{
|
||||
UserRepository = userRepository;
|
||||
IdentityService = identityService;
|
||||
JwtService = jwtService;
|
||||
}
|
||||
|
||||
[HttpGet]
|
||||
public async Task<ActionResult> 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("/");
|
||||
}
|
||||
}
|
||||
51
Moonlight/App/Http/Controllers/Api/Auth/VerifyController.cs
Normal file
51
Moonlight/App/Http/Controllers/Api/Auth/VerifyController.cs
Normal file
@@ -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<ActionResult> 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("/");
|
||||
}
|
||||
}
|
||||
@@ -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; } = "";
|
||||
}
|
||||
6
Moonlight/App/Models/Templates/MailVerify.cs
Normal file
6
Moonlight/App/Models/Templates/MailVerify.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Moonlight.App.Models.Templates;
|
||||
|
||||
public class MailVerify
|
||||
{
|
||||
public string Url { get; set; } = "";
|
||||
}
|
||||
6
Moonlight/App/Models/Templates/ResetPassword.cs
Normal file
6
Moonlight/App/Models/Templates/ResetPassword.cs
Normal file
@@ -0,0 +1,6 @@
|
||||
namespace Moonlight.App.Models.Templates;
|
||||
|
||||
public class ResetPassword
|
||||
{
|
||||
public string Url { get; set; } = "";
|
||||
}
|
||||
23
Moonlight/App/Services/Background/AutoMailSendService.cs
Normal file
23
Moonlight/App/Services/Background/AutoMailSendService.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
103
Moonlight/App/Services/MailService.cs
Normal file
103
Moonlight/App/Services/MailService.cs
Normal file
@@ -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<string> 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<User> users, string title, string templateName, params object[] models)
|
||||
{
|
||||
foreach (var user in users)
|
||||
await Send(user, title, templateName, models);
|
||||
}
|
||||
}
|
||||
@@ -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<User> UserRepository;
|
||||
//private readonly MailService MailService;
|
||||
private readonly JwtService JwtService;
|
||||
private readonly ConfigService ConfigService;
|
||||
private readonly MailService MailService;
|
||||
|
||||
public UserAuthService(
|
||||
Repository<User> userRepository,
|
||||
//MailService mailService,
|
||||
JwtService jwtService,
|
||||
ConfigService configService)
|
||||
ConfigService configService,
|
||||
MailService mailService)
|
||||
{
|
||||
UserRepository = userRepository;
|
||||
//MailService = mailService;
|
||||
JwtService = jwtService;
|
||||
ConfigService = configService;
|
||||
MailService = mailService;
|
||||
}
|
||||
|
||||
public async Task<User> 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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
||||
<PackageReference Include="JWT" Version="10.1.1" />
|
||||
<PackageReference Include="MailKit" Version="4.2.0" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
|
||||
@@ -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<UserService>();
|
||||
builder.Services.AddScoped<UserAuthService>();
|
||||
builder.Services.AddScoped<UserDetailsService>();
|
||||
|
||||
// Services / Background
|
||||
builder.Services.AddSingleton<AutoMailSendService>();
|
||||
|
||||
// Services
|
||||
builder.Services.AddScoped<IdentityService>();
|
||||
builder.Services.AddSingleton<ConfigService>();
|
||||
builder.Services.AddSingleton<SessionService>();
|
||||
builder.Services.AddSingleton<BucketService>();
|
||||
builder.Services.AddSingleton<MailService>();
|
||||
|
||||
builder.Services.AddRazorPages();
|
||||
builder.Services.AddServerSideBlazor();
|
||||
@@ -68,4 +73,7 @@ app.MapBlazorHub();
|
||||
app.MapFallbackToPage("/_Host");
|
||||
app.MapControllers();
|
||||
|
||||
// Auto start background services
|
||||
app.Services.GetRequiredService<AutoMailSendService>();
|
||||
|
||||
app.Run();
|
||||
65
Moonlight/Shared/Components/Auth/ChangePassword.razor
Normal file
65
Moonlight/Shared/Components/Auth/ChangePassword.razor
Normal file
@@ -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
|
||||
|
||||
<div class="w-100">
|
||||
<div class="card-body">
|
||||
<div class="text-start mb-8">
|
||||
<h1 class="text-dark mb-3 fs-3x">
|
||||
Change your password
|
||||
</h1>
|
||||
<div class="text-gray-400 fw-semibold fs-6">
|
||||
You need to change your password in order to continue
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SmartForm Model="Form" OnValidSubmit="OnValidSubmit">
|
||||
<div class="fv-row mb-7">
|
||||
<input @bind="Form.Password" type="password" placeholder="Password" class="form-control form-control-solid">
|
||||
</div>
|
||||
|
||||
<div class="fv-row mb-7">
|
||||
<input @bind="Form.RepeatedPassword" type="password" placeholder="Repeat password" class="form-control form-control-solid">
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-stack">
|
||||
<button type="submit" class="btn btn-primary me-2 flex-shrink-0">Continue</button>
|
||||
</div>
|
||||
</SmartForm>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-stack flex-wrap gap-3 fs-base fw-semibold mb-10">
|
||||
<a href="/reset-password" class="link-primary">
|
||||
<a href="/password-reset" class="link-primary">
|
||||
Forgot Password ?
|
||||
</a>
|
||||
<a href="/register" class="link-primary">
|
||||
|
||||
46
Moonlight/Shared/Components/Auth/MailVerify.razor
Normal file
46
Moonlight/Shared/Components/Auth/MailVerify.razor
Normal file
@@ -0,0 +1,46 @@
|
||||
@using Moonlight.App.Services.Users
|
||||
@using Moonlight.App.Services
|
||||
|
||||
@inject UserService UserService
|
||||
@inject IdentityService IdentityService
|
||||
|
||||
<div class="w-100">
|
||||
<div class="card-body">
|
||||
@if (HasBeenSend)
|
||||
{
|
||||
<div class="text-start mb-8">
|
||||
<h1 class="text-dark mb-3 fs-3x">
|
||||
Email verification sent
|
||||
</h1>
|
||||
<div class="text-gray-400 fw-semibold fs-6">
|
||||
You should receive an email shortly. If you see no email in your inbox, look inside your spam folder
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-start mb-8">
|
||||
<h1 class="text-dark mb-3 fs-3x">
|
||||
Verify your email address
|
||||
</h1>
|
||||
<div class="text-gray-400 fw-semibold fs-6">
|
||||
We will sent you an email to verify your account
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<WButton OnClick="Send" Text="Continue" CssClasses="btn btn-primary me-2 flex-shrink-0" />
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private bool HasBeenSend = false;
|
||||
|
||||
private async Task Send()
|
||||
{
|
||||
await UserService.Auth.SendVerification(IdentityService.CurrentUser);
|
||||
HasBeenSend = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
56
Moonlight/Shared/Components/Auth/PasswordReset.razor
Normal file
56
Moonlight/Shared/Components/Auth/PasswordReset.razor
Normal file
@@ -0,0 +1,56 @@
|
||||
@page "/password-reset"
|
||||
|
||||
@using Moonlight.App.Services.Users
|
||||
@using Moonlight.App.Models.Forms
|
||||
|
||||
@inject UserService UserService
|
||||
|
||||
<div class="w-100">
|
||||
<div class="card-body">
|
||||
@if (HasBeenSend)
|
||||
{
|
||||
<div class="text-start mb-8">
|
||||
<h1 class="text-dark mb-3 fs-3x">
|
||||
Password reset email sent
|
||||
</h1>
|
||||
<div class="text-gray-400 fw-semibold fs-6">
|
||||
You should receive the email shortly. If you see no email in your inbox, look inside your spam folder
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
else
|
||||
{
|
||||
<div class="text-start mb-8">
|
||||
<h1 class="text-dark mb-3 fs-3x">
|
||||
Reset your password
|
||||
</h1>
|
||||
<div class="text-gray-400 fw-semibold fs-6">
|
||||
We will sent you an email to reset your account password
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<SmartForm Model="Form" OnValidSubmit="OnValidSubmit">
|
||||
<div class="fv-row mb-8">
|
||||
<input @bind="Form.Email" type="text" placeholder="Email" class="form-control form-control-solid">
|
||||
</div>
|
||||
|
||||
<div class="d-flex flex-stack">
|
||||
<button type="submit" class="btn btn-primary me-2 flex-shrink-0">Continue</button>
|
||||
</div>
|
||||
</SmartForm>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@code
|
||||
{
|
||||
private bool HasBeenSend = false;
|
||||
private ResetPasswordForm Form = new();
|
||||
|
||||
private async Task OnValidSubmit()
|
||||
{
|
||||
await UserService.Auth.SendResetPassword(Form.Email);
|
||||
HasBeenSend = true;
|
||||
await InvokeAsync(StateHasChanged);
|
||||
}
|
||||
}
|
||||
@@ -88,7 +88,7 @@
|
||||
ErrorMessages.Add(displayException.Message);
|
||||
}
|
||||
else
|
||||
throw e;
|
||||
throw;
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -37,7 +37,14 @@
|
||||
<div class="menu-item px-3">
|
||||
<div class="menu-content d-flex align-items-center px-3">
|
||||
<div class="symbol symbol-50px me-5">
|
||||
<img alt="Logo" src="/metronic8/demo38/assets/media/avatars/300-2.jpg">
|
||||
@if (IdentityService.CurrentUser.Avatar == null)
|
||||
{
|
||||
<img src="/assets/img/avatar.png" alt="Avatar">
|
||||
}
|
||||
else
|
||||
{
|
||||
<img src="/api/bucket/avatars/@(IdentityService.CurrentUser.Avatar)" alt="Avatar">
|
||||
}
|
||||
</div>
|
||||
<div class="d-flex flex-column">
|
||||
<div class="fw-bold d-flex align-items-center fs-5">
|
||||
@@ -73,7 +80,7 @@
|
||||
{
|
||||
[CascadingParameter]
|
||||
public DefaultLayout Layout { get; set; }
|
||||
|
||||
|
||||
private async Task Logout()
|
||||
{
|
||||
await IdentityService.Authenticate("");
|
||||
|
||||
@@ -22,9 +22,15 @@
|
||||
{
|
||||
if (!IdentityService.Flags[UserFlag.MailVerified] && ConfigService.Get().Security.EnableEmailVerify)
|
||||
{
|
||||
<OverlayLayout>
|
||||
<MailVerify />
|
||||
</OverlayLayout>
|
||||
}
|
||||
else if (IdentityService.Flags[UserFlag.PasswordPending])
|
||||
{
|
||||
<OverlayLayout>
|
||||
<ChangePassword />
|
||||
</OverlayLayout>
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -43,6 +49,12 @@
|
||||
<Register />
|
||||
</OverlayLayout>
|
||||
}
|
||||
else if (url.LocalPath == "/password-reset")
|
||||
{
|
||||
<OverlayLayout>
|
||||
<PasswordReset />
|
||||
</OverlayLayout>
|
||||
}
|
||||
else
|
||||
{
|
||||
<OverlayLayout>
|
||||
|
||||
Reference in New Issue
Block a user