add reset password mail
This commit is contained in:
parent
7ba1307bac
commit
f5c8e07730
|
|
@ -49,4 +49,14 @@ public interface IEmailService
|
|||
/// <returns>true if sent successfully sent, false if not.</returns>
|
||||
Task<bool> SendTestMailAsync(string toEmail, Guid clubId,
|
||||
CancellationToken ct = default);
|
||||
|
||||
/// <summary>
|
||||
/// Sends a password reset email with the reset link.
|
||||
/// </summary>
|
||||
/// <param name="toEmail">User's email address</param>
|
||||
/// <param name="resetUrl">The full password reset URL with token</param>
|
||||
/// <param name="ct">Cancellation token</param>
|
||||
/// <returns>true if sent successfully, false otherwise.</returns>
|
||||
Task<bool> SendPasswordResetEmailAsync(string toEmail, string resetUrl,
|
||||
CancellationToken ct = default);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ public class SmtpEmailService : IEmailService
|
|||
{
|
||||
await using var context = await _contextFactory.CreateDbContextAsync(ct);
|
||||
var club = await context.Clubs.FirstOrDefaultAsync(c => c.Id == clubId, ct);
|
||||
if (club is null)
|
||||
if (club is null)
|
||||
return false;
|
||||
|
||||
var senderEmail = GetSenderEmail(club);
|
||||
|
|
@ -265,6 +265,44 @@ public class SmtpEmailService : IEmailService
|
|||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<bool> SendPasswordResetEmailAsync(string toEmail, string resetUrl, CancellationToken ct = default)
|
||||
{
|
||||
if (!_settings.Enabled)
|
||||
{
|
||||
_logger.LogInformation("[EMAIL DISABLED] Password reset - To: {Email}, URL: {Url}", toEmail, resetUrl);
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var senderEmail = _settings.DefaultSenderEmail;
|
||||
var senderName = "Koogle";
|
||||
|
||||
var subject = "Passwort zurücksetzen - Koogle";
|
||||
var body = $@"
|
||||
<html>
|
||||
<body>
|
||||
<h2>Passwort zurücksetzen</h2>
|
||||
<p>Sie haben eine Anfrage zum Zurücksetzen Ihres Passworts gestellt.</p>
|
||||
<p>Klicken Sie auf den folgenden Link, um Ihr Passwort zurückzusetzen:</p>
|
||||
<p><a href=""{resetUrl}"">{resetUrl}</a></p>
|
||||
<p>Falls Sie diese Anfrage nicht gestellt haben, können Sie diese E-Mail ignorieren.</p>
|
||||
<p>Der Link ist aus Sicherheitsgründen nur für eine begrenzte Zeit gültig.</p>
|
||||
<p>Viele Grüße,<br/>Koogle</p>
|
||||
</body>
|
||||
</html>";
|
||||
|
||||
await SendEmailAsync(senderEmail, senderName, toEmail, subject, body, ct);
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Failed to send password reset email to {Email}", toEmail);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private string GetSenderEmail(Club club)
|
||||
{
|
||||
var parts = _settings.DefaultSenderEmail.Split('@');
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Fluxor;
|
||||
using Koogle.Application.DTOs;
|
||||
using Koogle.Application.Interfaces;
|
||||
using Koogle.Domain.Interfaces;
|
||||
using Koogle.Infrastructure.Identity;
|
||||
using Koogle.Web.Store.AuthState;
|
||||
using Microsoft.AspNetCore.Components;
|
||||
|
|
@ -17,10 +18,11 @@ namespace Koogle.Web.Controllers
|
|||
/// Aus dem Grund werden Login/Logout Aktionen hier im MVC Controller mit einem "normalen" Post-Request abgewickelt.
|
||||
/// </remarks>
|
||||
[Microsoft.AspNetCore.Mvc.Route("auth")]
|
||||
public class AuthController(IDispatcher dispatcher, IUserService userService) : Controller
|
||||
public class AuthController(IDispatcher dispatcher, IUserService userService, IEmailService emailService) : Controller
|
||||
{
|
||||
private readonly IDispatcher _dispatcher = dispatcher;
|
||||
private readonly IUserService _userService = userService;
|
||||
private readonly IEmailService _emailService = emailService;
|
||||
|
||||
|
||||
[HttpPost("login")]
|
||||
|
|
@ -90,10 +92,9 @@ namespace Koogle.Web.Controllers
|
|||
|
||||
if (token != null)
|
||||
{
|
||||
// TODO: Send email with reset link
|
||||
// For now, log the token for development purposes
|
||||
var resetUrl = $"/account/reset-password?email={Uri.EscapeDataString(email)}&token={Uri.EscapeDataString(token)}";
|
||||
Console.WriteLine($"[DEV] Password reset link: {resetUrl}");
|
||||
var baseUrl = $"{Request.Scheme}://{Request.Host}";
|
||||
var resetUrl = $"{baseUrl}/account/reset-password?email={Uri.EscapeDataString(email)}&token={Uri.EscapeDataString(token)}";
|
||||
await _emailService.SendPasswordResetEmailAsync(email, resetUrl);
|
||||
}
|
||||
|
||||
return LocalRedirect("/account/forgot-password?success=true");
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
using Fluxor;
|
||||
using Koogle.Application.DTOs;
|
||||
using Koogle.Application.Interfaces;
|
||||
using Koogle.Domain.Interfaces;
|
||||
using Koogle.Web.Controllers;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Identity;
|
||||
|
|
@ -16,18 +17,23 @@ public class AuthControllerTests
|
|||
{
|
||||
private readonly Mock<IDispatcher> _dispatcherMock;
|
||||
private readonly Mock<IUserService> _userServiceMock;
|
||||
private readonly Mock<IEmailService> _emailServiceMock;
|
||||
private readonly AuthController _sut;
|
||||
|
||||
public AuthControllerTests()
|
||||
{
|
||||
_dispatcherMock = new Mock<IDispatcher>();
|
||||
_userServiceMock = new Mock<IUserService>();
|
||||
_sut = new AuthController(_dispatcherMock.Object, _userServiceMock.Object);
|
||||
_emailServiceMock = new Mock<IEmailService>();
|
||||
_sut = new AuthController(_dispatcherMock.Object, _userServiceMock.Object, _emailServiceMock.Object);
|
||||
|
||||
// Setup controller context
|
||||
// Setup controller context with request info for password reset URL building
|
||||
var httpContext = new DefaultHttpContext();
|
||||
httpContext.Request.Scheme = "https";
|
||||
httpContext.Request.Host = new HostString("localhost");
|
||||
_sut.ControllerContext = new ControllerContext
|
||||
{
|
||||
HttpContext = new DefaultHttpContext()
|
||||
HttpContext = httpContext
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -241,6 +247,8 @@ public class AuthControllerTests
|
|||
// Arrange
|
||||
_userServiceMock.Setup(s => s.RequestPasswordResetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync("reset-token");
|
||||
_emailServiceMock.Setup(s => s.SendPasswordResetEmailAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
// Act
|
||||
await _sut.ForgotPassword("test@example.com");
|
||||
|
|
@ -249,6 +257,42 @@ public class AuthControllerTests
|
|||
_userServiceMock.Verify(s => s.RequestPasswordResetAsync("test@example.com", It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ForgotPassword_SendsEmail_WhenTokenGenerated()
|
||||
{
|
||||
// Arrange
|
||||
_userServiceMock.Setup(s => s.RequestPasswordResetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync("reset-token");
|
||||
_emailServiceMock.Setup(s => s.SendPasswordResetEmailAsync(It.IsAny<string>(), It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync(true);
|
||||
|
||||
// Act
|
||||
await _sut.ForgotPassword("test@example.com");
|
||||
|
||||
// Assert
|
||||
_emailServiceMock.Verify(s => s.SendPasswordResetEmailAsync(
|
||||
"test@example.com",
|
||||
It.Is<string>(url => url.Contains("reset-password") && url.Contains("token=")),
|
||||
It.IsAny<CancellationToken>()), Times.Once);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task ForgotPassword_DoesNotSendEmail_WhenNoToken()
|
||||
{
|
||||
// Arrange - No user found, no token generated
|
||||
_userServiceMock.Setup(s => s.RequestPasswordResetAsync(It.IsAny<string>(), It.IsAny<CancellationToken>()))
|
||||
.ReturnsAsync((string?)null);
|
||||
|
||||
// Act
|
||||
await _sut.ForgotPassword("nonexistent@example.com");
|
||||
|
||||
// Assert - Email should not be sent for non-existent users
|
||||
_emailServiceMock.Verify(s => s.SendPasswordResetEmailAsync(
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<string>(),
|
||||
It.IsAny<CancellationToken>()), Times.Never);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region ResetPassword Tests
|
||||
|
|
|
|||
Loading…
Reference in New Issue