auth
This commit is contained in:
143
core/identity/application/use-cases/ResetPasswordUseCase.ts
Normal file
143
core/identity/application/use-cases/ResetPasswordUseCase.ts
Normal file
@@ -0,0 +1,143 @@
|
||||
import { IAuthRepository } from '../../domain/repositories/IAuthRepository';
|
||||
import { IMagicLinkRepository } from '../../domain/repositories/IMagicLinkRepository';
|
||||
import { IPasswordHashingService } from '../../domain/services/PasswordHashingService';
|
||||
import { EmailAddress } from '../../domain/value-objects/EmailAddress';
|
||||
import { PasswordHash } from '../../domain/value-objects/PasswordHash';
|
||||
import { Result } from '@core/shared/application/Result';
|
||||
import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode';
|
||||
import type { UseCaseOutputPort, Logger, UseCase } from '@core/shared/application';
|
||||
|
||||
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<ResetPasswordErrorCode, { message: string }>;
|
||||
|
||||
/**
|
||||
* Application Use Case: ResetPasswordUseCase
|
||||
*
|
||||
* Handles password reset using a magic link token.
|
||||
* Validates token, checks expiration, and updates password.
|
||||
*/
|
||||
export class ResetPasswordUseCase implements UseCase<ResetPasswordInput, void, ResetPasswordErrorCode> {
|
||||
constructor(
|
||||
private readonly authRepo: IAuthRepository,
|
||||
private readonly magicLinkRepo: IMagicLinkRepository,
|
||||
private readonly passwordService: IPasswordHashingService,
|
||||
private readonly logger: Logger,
|
||||
private readonly output: UseCaseOutputPort<ResetPasswordResult>,
|
||||
) {}
|
||||
|
||||
async execute(input: ResetPasswordInput): Promise<Result<void, ResetPasswordApplicationError>> {
|
||||
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,
|
||||
});
|
||||
|
||||
this.output.present({
|
||||
message: 'Password reset successfully. You can now log in with your new password.',
|
||||
});
|
||||
|
||||
return Result.ok(undefined);
|
||||
} 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user