using System.Text; using System.Web; using Hutopy.Infrastructure.Configuration; using Hutopy.Infrastructure.Emailer.Contracts; using Hutopy.Modules.Identity.Data; using Microsoft.Extensions.Options; namespace Hutopy.Modules.Identity.Handlers; [PublicAPI] public record ForgotPasswordRequest( string Email); [PublicAPI] public class ForgotPasswordHandler( UserManager userManager, IEmailSender emailSender, IOptionsSnapshot options) : Endpoint { public override void Configure() { AllowAnonymous(); Post("/api/users/forgot-password"); Options(o => o.WithTags("Users")); } public override async Task HandleAsync( ForgotPasswordRequest request, CancellationToken ct) { // Find user by email var user = await userManager.FindByEmailAsync(request.Email); // Always return OK even if user not found to prevent email enumeration if (user is null) { await SendOkAsync(ct); return; } // Generate password reset token var token = await userManager.GeneratePasswordResetTokenAsync(user); // URL encode the token as it may contain characters that are not URL safe var encodedToken = HttpUtility.UrlEncode(token); // Build reset link var resetLink = $"{options.Value.FrontendBaseUrl}/reset-password?email={HttpUtility.UrlEncode(request.Email)}&token={encodedToken}"; // TODO: Write a better email template var subject = "Reset Your Password"; var message = new StringBuilder() .AppendLine("

Reset Your Password

") .AppendLine("

Please click the link below to reset your password:

") .AppendLine($"

Reset Password

") .AppendLine("

If you did not request a password reset, please ignore this email.

") .ToString(); // Send email await emailSender.SendEmailAsync(request.Email, subject, message); await SendOkAsync(ct); } }