using ColaFlow.Modules.Identity.Application.Services; using ColaFlow.Modules.Identity.Domain.Aggregates.Tenants; using ColaFlow.Modules.Identity.Domain.Aggregates.Users; using ColaFlow.Modules.Identity.Domain.Entities; using ColaFlow.Modules.Identity.Domain.Repositories; using ColaFlow.Modules.Identity.Domain.Services; using MediatR; using Microsoft.Extensions.Logging; namespace ColaFlow.Modules.Identity.Application.Commands.ResendVerificationEmail; /// /// Handler for resending email verification link /// Implements security best practices: /// - Email enumeration prevention (always returns true) /// - Rate limiting (1 email per minute) /// - Token rotation (invalidate old token) /// public class ResendVerificationEmailCommandHandler : IRequestHandler { private readonly IUserRepository _userRepository; private readonly IEmailVerificationTokenRepository _tokenRepository; private readonly ISecurityTokenService _tokenService; private readonly IEmailService _emailService; private readonly IEmailTemplateService _templateService; private readonly IRateLimitService _rateLimitService; private readonly ILogger _logger; public ResendVerificationEmailCommandHandler( IUserRepository userRepository, IEmailVerificationTokenRepository tokenRepository, ISecurityTokenService tokenService, IEmailService emailService, IEmailTemplateService templateService, IRateLimitService rateLimitService, ILogger logger) { _userRepository = userRepository; _tokenRepository = tokenRepository; _tokenService = tokenService; _emailService = emailService; _templateService = templateService; _rateLimitService = rateLimitService; _logger = logger; } public async Task Handle(ResendVerificationEmailCommand request, CancellationToken cancellationToken) { try { // 1. Find user by email and tenant (no enumeration - don't reveal if user exists) var email = Email.Create(request.Email); var tenantId = TenantId.Create(request.TenantId); var user = await _userRepository.GetByEmailAsync(tenantId, email, cancellationToken); if (user == null) { // Email enumeration prevention: Don't reveal user doesn't exist _logger.LogWarning("Resend verification requested for non-existent email: {Email}", request.Email); return true; // Always return success } // 2. Check if already verified (success if so) if (user.IsEmailVerified) { _logger.LogInformation("Email already verified for user {UserId}", user.Id); return true; // Already verified - success } // 3. Check rate limit (1 email per minute per address) var rateLimitKey = $"resend-verification:{request.Email}:{request.TenantId}"; var isAllowed = await _rateLimitService.IsAllowedAsync( rateLimitKey, maxAttempts: 1, window: TimeSpan.FromMinutes(1), cancellationToken); if (!isAllowed) { _logger.LogWarning( "Rate limit exceeded for resend verification: {Email}", request.Email); return true; // Still return success to prevent enumeration } // 4. Generate new verification token with SHA-256 hashing var token = _tokenService.GenerateToken(); var tokenHash = _tokenService.HashToken(token); // 5. Invalidate old tokens by creating new one (token rotation) var verificationToken = EmailVerificationToken.Create( UserId.Create(user.Id), tokenHash, DateTime.UtcNow.AddHours(24)); // 24 hours expiration await _tokenRepository.AddAsync(verificationToken, cancellationToken); // 6. Send verification email var verificationLink = $"{request.BaseUrl}/verify-email?token={token}"; var htmlBody = _templateService.RenderVerificationEmail(user.FullName.Value, verificationLink); var emailMessage = new EmailMessage( To: request.Email, Subject: "Verify your email address - ColaFlow", HtmlBody: htmlBody, PlainTextBody: $"Click the link to verify your email: {verificationLink}"); var success = await _emailService.SendEmailAsync(emailMessage, cancellationToken); if (!success) { _logger.LogWarning( "Failed to send verification email to {Email} for user {UserId}", request.Email, user.Id); } else { _logger.LogInformation( "Verification email resent to {Email} for user {UserId}", request.Email, user.Id); } // 7. Always return success (prevent email enumeration) return true; } catch (Exception ex) { _logger.LogError( ex, "Error resending verification email for {Email}", request.Email); // Return true even on error to prevent enumeration return true; } } }