Implement password recovery feature with reset token and email notifications
All checks were successful
Build And Push Dev Docker Image / docker (push) Successful in 1m28s

This commit is contained in:
troogs
2026-04-18 13:36:21 +02:00
parent c5c24d44c9
commit ad6f28023e
11 changed files with 593 additions and 3 deletions

View File

@@ -50,6 +50,11 @@ namespace FoodsharingSiegen.Server.Auth
/// </summary>
private User? _user;
/// <summary>
/// The mail service
/// </summary>
private readonly IMailService _mailService;
#endregion
#region Setup/Teardown
@@ -60,14 +65,17 @@ namespace FoodsharingSiegen.Server.Auth
/// <param name="context">The context</param>
/// <param name="localStorageService">The local storage service</param>
/// <param name="authenticationStateProvider">The authentication state provider</param>
/// <param name="mailService">The mail service</param>
public AuthService(
FsContext context,
LocalStorageService localStorageService,
AuthenticationStateProvider authenticationStateProvider)
AuthenticationStateProvider authenticationStateProvider,
IMailService mailService)
{
Context = context;
_localStorageService = localStorageService;
_authenticationStateProvider = authenticationStateProvider;
_mailService = mailService;
}
#endregion
@@ -186,5 +194,52 @@ namespace FoodsharingSiegen.Server.Auth
}
#endregion
#region Password Recovery
public async Task InitiatePasswordReset(string email, string baseUri)
{
if (string.IsNullOrWhiteSpace(email)) return;
var user = await Context.Users!.FirstOrDefaultAsync(x => x.Mail.ToLower() == email.ToLower());
if (user == null) return; // Do not leak existence
var resetToken = Guid.NewGuid().ToString("N");
user.ResetToken = resetToken;
user.ResetTokenExpiry = DateTime.UtcNow.AddMinutes(30);
await Context.SaveChangesAsync();
var resetLink = $"{baseUri.TrimEnd('/')}/reset-password/{resetToken}";
var mailBody = $"Hallo {user.Name},<br><br>Um dein Passwort zurückzusetzen, klicke bitte auf den folgenden Link (dieser ist 30 Minuten gültig):<br><a href='{resetLink}'>{resetLink}</a><br><br>Viele Grüße<br>Dein Foodsharing Team";
await _mailService.SendEmailAsync(user.Mail, "Passwort zurücksetzen", mailBody);
}
public async Task<OperationResult> ResetPassword(string token, string newPassword)
{
if (string.IsNullOrWhiteSpace(token)) return new OperationResult(new Exception("Ungültiges Token."));
if (string.IsNullOrWhiteSpace(newPassword)) return new OperationResult(new Exception("Passwort darf nicht leer sein."));
var user = await Context.Users!.FirstOrDefaultAsync(x => x.ResetToken == token && x.ResetTokenExpiry > DateTime.UtcNow);
if (user == null) return new OperationResult(new Exception("Token ist ungültig oder abgelaufen."));
user.Password = newPassword;
user.ResetToken = null;
user.ResetTokenExpiry = null;
await Context.SaveChangesAsync();
return new OperationResult();
}
public async Task<bool> IsResetTokenValid(string token)
{
if (string.IsNullOrWhiteSpace(token)) return false;
var user = await Context.Users!.FirstOrDefaultAsync(x => x.ResetToken == token && x.ResetTokenExpiry > DateTime.UtcNow);
return user != null;
}
#endregion
}
}