import type { UseCase } from '@core/shared/application/UseCase'; import type { Logger } from '@core/shared/domain/Logger'; import { Result } from '@core/shared/domain/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import { AuthRepository } from '../../domain/repositories/AuthRepository'; import { MagicLinkRepository } from '../../domain/repositories/MagicLinkRepository'; import { PasswordHashingService } from '../../domain/services/PasswordHashingService'; import { EmailAddress } from '../../domain/value-objects/EmailAddress'; import { PasswordHash } from '../../domain/value-objects/PasswordHash'; export type ResetPasswordInput = { token: string; newPassword: string; }; export type ResetPasswordResult = { message: string; }; export type ResetPasswordErrorCode = 'INVALID_TOKEN' | 'EXPIRED_TOKEN' | 'WEAK_PASSWORD' | 'REPOSITORY_ERROR'; export type ResetPasswordApplicationError = ApplicationErrorCode; /** * Application Use Case: ResetPasswordUseCase * * Handles password reset using a magic link token. * Validates token, checks expiration, and updates password. */ export class ResetPasswordUseCase implements UseCase { constructor( private readonly authRepo: AuthRepository, private readonly magicLinkRepo: MagicLinkRepository, private readonly passwordService: PasswordHashingService, private readonly logger: Logger, ) {} async execute(input: ResetPasswordInput): Promise> { try { // Validate token format if (!input.token || typeof input.token !== 'string' || input.token.length < 32) { return Result.err({ code: 'INVALID_TOKEN', details: { message: 'Invalid reset token' }, }); } // Validate password strength if (!this.isPasswordStrong(input.newPassword)) { return Result.err({ code: 'WEAK_PASSWORD', details: { message: 'Password must be at least 8 characters and contain uppercase, lowercase, and number' }, }); } // Find token const resetRequest = await this.magicLinkRepo.findByToken(input.token); if (!resetRequest) { return Result.err({ code: 'INVALID_TOKEN', details: { message: 'Invalid or expired reset token' }, }); } // Check expiration if (resetRequest.expiresAt < new Date()) { return Result.err({ code: 'EXPIRED_TOKEN', details: { message: 'Reset token has expired. Please request a new one.' }, }); } // Find user by email const emailVO = EmailAddress.create(resetRequest.email); const user = await this.authRepo.findByEmail(emailVO); if (!user) { return Result.err({ code: 'INVALID_TOKEN', details: { message: 'User no longer exists' }, }); } // Hash new password const hashedPassword = await this.passwordService.hash(input.newPassword); // Create a new user instance with updated password const UserModule = await import('../../domain/entities/User'); const passwordHash = PasswordHash.fromHash(hashedPassword); const email = user.getEmail(); const iracingCustomerId = user.getIracingCustomerId(); const primaryDriverId = user.getPrimaryDriverId(); const avatarUrl = user.getAvatarUrl(); const updatedUserInstance = UserModule.User.rehydrate({ id: user.getId().value, displayName: user.getDisplayName(), ...(email !== undefined ? { email } : {}), passwordHash: passwordHash, ...(iracingCustomerId !== undefined ? { iracingCustomerId } : {}), ...(primaryDriverId !== undefined ? { primaryDriverId } : {}), ...(avatarUrl !== undefined ? { avatarUrl } : {}), }); await this.authRepo.save(updatedUserInstance); // Mark token as used await this.magicLinkRepo.markAsUsed(input.token); this.logger.info('[ResetPasswordUseCase] Password reset successful', { userId: user.getId().value, email: resetRequest.email, }); return Result.ok({ message: 'Password reset successfully. You can now log in with your new password.', }); } catch (error) { const message = error instanceof Error && error.message ? error.message : 'Failed to execute ResetPasswordUseCase'; this.logger.error('ResetPasswordUseCase.execute failed', error instanceof Error ? error : undefined, { input, }); return Result.err({ code: 'REPOSITORY_ERROR', details: { message }, }); } } private isPasswordStrong(password: string): boolean { if (password.length < 8) return false; if (!/[a-z]/.test(password)) return false; if (!/[A-Z]/.test(password)) return false; if (!/\d/.test(password)) return false; return true; } }