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
|
public class ResetPasswordForm
|
||||||
{
|
{
|
||||||
[Required(ErrorMessage = "You need to specify an email address")]
|
[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; } = "";
|
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.Database.Entities;
|
||||||
|
using Moonlight.App.Event;
|
||||||
using Moonlight.App.Exceptions;
|
using Moonlight.App.Exceptions;
|
||||||
|
using Moonlight.App.Extensions;
|
||||||
using Moonlight.App.Helpers;
|
using Moonlight.App.Helpers;
|
||||||
using Moonlight.App.Models.Abstractions;
|
using Moonlight.App.Models.Abstractions;
|
||||||
using Moonlight.App.Models.Enums;
|
using Moonlight.App.Models.Enums;
|
||||||
|
using Moonlight.App.Models.Templates;
|
||||||
using Moonlight.App.Repositories;
|
using Moonlight.App.Repositories;
|
||||||
using Moonlight.App.Services.Utils;
|
using Moonlight.App.Services.Utils;
|
||||||
using OtpNet;
|
using OtpNet;
|
||||||
@@ -12,20 +15,20 @@ namespace Moonlight.App.Services.Users;
|
|||||||
public class UserAuthService
|
public class UserAuthService
|
||||||
{
|
{
|
||||||
private readonly Repository<User> UserRepository;
|
private readonly Repository<User> UserRepository;
|
||||||
//private readonly MailService MailService;
|
|
||||||
private readonly JwtService JwtService;
|
private readonly JwtService JwtService;
|
||||||
private readonly ConfigService ConfigService;
|
private readonly ConfigService ConfigService;
|
||||||
|
private readonly MailService MailService;
|
||||||
|
|
||||||
public UserAuthService(
|
public UserAuthService(
|
||||||
Repository<User> userRepository,
|
Repository<User> userRepository,
|
||||||
//MailService mailService,
|
|
||||||
JwtService jwtService,
|
JwtService jwtService,
|
||||||
ConfigService configService)
|
ConfigService configService,
|
||||||
|
MailService mailService)
|
||||||
{
|
{
|
||||||
UserRepository = userRepository;
|
UserRepository = userRepository;
|
||||||
//MailService = mailService;
|
|
||||||
JwtService = jwtService;
|
JwtService = jwtService;
|
||||||
ConfigService = configService;
|
ConfigService = configService;
|
||||||
|
MailService = mailService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<User> Register(string username, string email, string password)
|
public async Task<User> Register(string username, string email, string password)
|
||||||
@@ -50,37 +53,32 @@ public class UserAuthService
|
|||||||
};
|
};
|
||||||
|
|
||||||
var result = UserRepository.Add(user);
|
var result = UserRepository.Add(user);
|
||||||
/*
|
|
||||||
await MailService.Send(
|
await Events.OnUserRegistered.InvokeAsync(result);
|
||||||
result,
|
|
||||||
"Welcome {{User.Username}}",
|
|
||||||
"register",
|
|
||||||
result
|
|
||||||
);*/
|
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task ChangePassword(User user, string newPassword)
|
public async Task ChangePassword(User user, string newPassword)
|
||||||
{
|
{
|
||||||
user.Password = HashHelper.HashToString(newPassword);
|
user.Password = HashHelper.HashToString(newPassword);
|
||||||
user.TokenValidTimestamp = DateTime.UtcNow;
|
user.TokenValidTimestamp = DateTime.UtcNow;
|
||||||
UserRepository.Update(user);
|
UserRepository.Update(user);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
await Events.OnUserPasswordChanged.InvokeAsync(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task SeedTotp(User user)
|
public Task SeedTotp(User user)
|
||||||
{
|
{
|
||||||
var key = Base32Encoding.ToString(KeyGeneration.GenerateRandomKey(20));
|
var key = Base32Encoding.ToString(KeyGeneration.GenerateRandomKey(20));
|
||||||
|
|
||||||
user.TotpKey = key;
|
user.TotpKey = key;
|
||||||
UserRepository.Update(user);
|
UserRepository.Update(user);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Task SetTotp(User user, bool state)
|
public async Task SetTotp(User user, bool state)
|
||||||
{
|
{
|
||||||
// Access to flags without identity service
|
// Access to flags without identity service
|
||||||
var flags = new FlagStorage(user.Flags);
|
var flags = new FlagStorage(user.Flags);
|
||||||
@@ -89,25 +87,22 @@ public class UserAuthService
|
|||||||
|
|
||||||
if (!state)
|
if (!state)
|
||||||
user.TotpKey = null;
|
user.TotpKey = null;
|
||||||
|
|
||||||
UserRepository.Update(user);
|
UserRepository.Update(user);
|
||||||
|
|
||||||
return Task.CompletedTask;
|
await Events.OnUserTotpSet.InvokeAsync(user);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Mails
|
// Mails
|
||||||
|
|
||||||
public async Task SendVerification(User user)
|
public async Task SendVerification(User user)
|
||||||
{
|
{
|
||||||
var jwt = await JwtService.Create(data =>
|
var jwt = await JwtService.Create(data => { data.Add("mailToVerify", user.Email); }, TimeSpan.FromMinutes(10));
|
||||||
{
|
|
||||||
data.Add("mailToVerify", user.Email);
|
|
||||||
}, TimeSpan.FromMinutes(10));
|
|
||||||
/*
|
|
||||||
await MailService.Send(user, "Verify your account", "verifyMail", user, new MailVerify()
|
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)
|
public async Task SendResetPassword(string email)
|
||||||
@@ -119,14 +114,11 @@ public class UserAuthService
|
|||||||
if (user == null)
|
if (user == null)
|
||||||
throw new DisplayException("An account with that email was not found");
|
throw new DisplayException("An account with that email was not found");
|
||||||
|
|
||||||
var jwt = await JwtService.Create(data =>
|
var jwt = await JwtService.Create(data => { data.Add("accountToReset", user.Id.ToString()); });
|
||||||
{
|
|
||||||
data.Add("accountToReset", user.Id.ToString());
|
|
||||||
});
|
|
||||||
/*
|
|
||||||
await MailService.Send(user, "Password reset for your account", "passwordReset", user, new ResetPassword()
|
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>
|
<ItemGroup>
|
||||||
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
<PackageReference Include="Ben.Demystifier" Version="0.4.1" />
|
||||||
<PackageReference Include="JWT" Version="10.1.1" />
|
<PackageReference Include="JWT" Version="10.1.1" />
|
||||||
|
<PackageReference Include="MailKit" Version="4.2.0" />
|
||||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0">
|
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="7.0.0">
|
||||||
<PrivateAssets>all</PrivateAssets>
|
<PrivateAssets>all</PrivateAssets>
|
||||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ using Moonlight.App.Helpers;
|
|||||||
using Moonlight.App.Helpers.LogMigrator;
|
using Moonlight.App.Helpers.LogMigrator;
|
||||||
using Moonlight.App.Repositories;
|
using Moonlight.App.Repositories;
|
||||||
using Moonlight.App.Services;
|
using Moonlight.App.Services;
|
||||||
|
using Moonlight.App.Services.Background;
|
||||||
using Moonlight.App.Services.Interop;
|
using Moonlight.App.Services.Interop;
|
||||||
using Moonlight.App.Services.Users;
|
using Moonlight.App.Services.Users;
|
||||||
using Moonlight.App.Services.Utils;
|
using Moonlight.App.Services.Utils;
|
||||||
@@ -40,11 +41,15 @@ builder.Services.AddScoped<UserService>();
|
|||||||
builder.Services.AddScoped<UserAuthService>();
|
builder.Services.AddScoped<UserAuthService>();
|
||||||
builder.Services.AddScoped<UserDetailsService>();
|
builder.Services.AddScoped<UserDetailsService>();
|
||||||
|
|
||||||
|
// Services / Background
|
||||||
|
builder.Services.AddSingleton<AutoMailSendService>();
|
||||||
|
|
||||||
// Services
|
// Services
|
||||||
builder.Services.AddScoped<IdentityService>();
|
builder.Services.AddScoped<IdentityService>();
|
||||||
builder.Services.AddSingleton<ConfigService>();
|
builder.Services.AddSingleton<ConfigService>();
|
||||||
builder.Services.AddSingleton<SessionService>();
|
builder.Services.AddSingleton<SessionService>();
|
||||||
builder.Services.AddSingleton<BucketService>();
|
builder.Services.AddSingleton<BucketService>();
|
||||||
|
builder.Services.AddSingleton<MailService>();
|
||||||
|
|
||||||
builder.Services.AddRazorPages();
|
builder.Services.AddRazorPages();
|
||||||
builder.Services.AddServerSideBlazor();
|
builder.Services.AddServerSideBlazor();
|
||||||
@@ -68,4 +73,7 @@ app.MapBlazorHub();
|
|||||||
app.MapFallbackToPage("/_Host");
|
app.MapFallbackToPage("/_Host");
|
||||||
app.MapControllers();
|
app.MapControllers();
|
||||||
|
|
||||||
|
// Auto start background services
|
||||||
|
app.Services.GetRequiredService<AutoMailSendService>();
|
||||||
|
|
||||||
app.Run();
|
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>
|
||||||
|
|
||||||
<div class="d-flex flex-stack flex-wrap gap-3 fs-base fw-semibold mb-10">
|
<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 ?
|
Forgot Password ?
|
||||||
</a>
|
</a>
|
||||||
<a href="/register" class="link-primary">
|
<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);
|
ErrorMessages.Add(displayException.Message);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
throw e;
|
throw;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -37,7 +37,14 @@
|
|||||||
<div class="menu-item px-3">
|
<div class="menu-item px-3">
|
||||||
<div class="menu-content d-flex align-items-center px-3">
|
<div class="menu-content d-flex align-items-center px-3">
|
||||||
<div class="symbol symbol-50px me-5">
|
<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>
|
||||||
<div class="d-flex flex-column">
|
<div class="d-flex flex-column">
|
||||||
<div class="fw-bold d-flex align-items-center fs-5">
|
<div class="fw-bold d-flex align-items-center fs-5">
|
||||||
@@ -73,7 +80,7 @@
|
|||||||
{
|
{
|
||||||
[CascadingParameter]
|
[CascadingParameter]
|
||||||
public DefaultLayout Layout { get; set; }
|
public DefaultLayout Layout { get; set; }
|
||||||
|
|
||||||
private async Task Logout()
|
private async Task Logout()
|
||||||
{
|
{
|
||||||
await IdentityService.Authenticate("");
|
await IdentityService.Authenticate("");
|
||||||
|
|||||||
@@ -22,9 +22,15 @@
|
|||||||
{
|
{
|
||||||
if (!IdentityService.Flags[UserFlag.MailVerified] && ConfigService.Get().Security.EnableEmailVerify)
|
if (!IdentityService.Flags[UserFlag.MailVerified] && ConfigService.Get().Security.EnableEmailVerify)
|
||||||
{
|
{
|
||||||
|
<OverlayLayout>
|
||||||
|
<MailVerify />
|
||||||
|
</OverlayLayout>
|
||||||
}
|
}
|
||||||
else if (IdentityService.Flags[UserFlag.PasswordPending])
|
else if (IdentityService.Flags[UserFlag.PasswordPending])
|
||||||
{
|
{
|
||||||
|
<OverlayLayout>
|
||||||
|
<ChangePassword />
|
||||||
|
</OverlayLayout>
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -43,6 +49,12 @@
|
|||||||
<Register />
|
<Register />
|
||||||
</OverlayLayout>
|
</OverlayLayout>
|
||||||
}
|
}
|
||||||
|
else if (url.LocalPath == "/password-reset")
|
||||||
|
{
|
||||||
|
<OverlayLayout>
|
||||||
|
<PasswordReset />
|
||||||
|
</OverlayLayout>
|
||||||
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
<OverlayLayout>
|
<OverlayLayout>
|
||||||
|
|||||||
Reference in New Issue
Block a user