/** * Signup with Email Use Case * * Creates a new user account with email and password. */ import type { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository'; import type { AuthenticatedUser } from '../ports/IdentityProviderPort'; import type { IdentitySessionPort } from '../ports/IdentitySessionPort'; import { Result } from '@core/shared/application/Result'; import type { ApplicationErrorCode } from '@core/shared/errors/ApplicationErrorCode'; import type { UseCaseOutputPort, Logger } from '@core/shared/application'; export type SignupWithEmailInput = { email: string; password: string; displayName: string; }; export type SignupWithEmailResult = { sessionToken: string; userId: string; displayName: string; email: string; createdAt: Date; isNewUser: boolean; }; export type SignupWithEmailErrorCode = | 'INVALID_EMAIL_FORMAT' | 'WEAK_PASSWORD' | 'INVALID_DISPLAY_NAME' | 'EMAIL_ALREADY_EXISTS' | 'REPOSITORY_ERROR'; export type SignupWithEmailApplicationError = ApplicationErrorCode< SignupWithEmailErrorCode, { message: string } >; export class SignupWithEmailUseCase { constructor( private readonly userRepository: IUserRepository, private readonly sessionPort: IdentitySessionPort, private readonly logger: Logger, private readonly output: UseCaseOutputPort, ) {} async execute(input: SignupWithEmailInput): Promise< Result > { // Validate email format const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(input.email)) { return Result.err({ code: 'INVALID_EMAIL_FORMAT', details: { message: 'Invalid email format' }, } as SignupWithEmailApplicationError); } // Validate password strength if (input.password.length < 8) { return Result.err({ code: 'WEAK_PASSWORD', details: { message: 'Password must be at least 8 characters' }, } as SignupWithEmailApplicationError); } // Validate display name if (!input.displayName || input.displayName.trim().length < 2) { return Result.err({ code: 'INVALID_DISPLAY_NAME', details: { message: 'Display name must be at least 2 characters' }, } as SignupWithEmailApplicationError); } // Check if email already exists const existingUser = await this.userRepository.findByEmail(input.email); if (existingUser) { return Result.err({ code: 'EMAIL_ALREADY_EXISTS', details: { message: 'An account with this email already exists' }, } as SignupWithEmailApplicationError); } try { // Hash password (simple hash for demo - in production use bcrypt) const salt = this.generateSalt(); const passwordHash = await this.hashPassword(input.password, salt); // Create user const userId = this.generateUserId(); const createdAt = new Date(); const newUser: StoredUser = { id: userId, email: input.email.toLowerCase().trim(), displayName: input.displayName.trim(), passwordHash, salt, createdAt, }; await this.userRepository.create(newUser); // Create session const authenticatedUser: AuthenticatedUser = { id: newUser.id, displayName: newUser.displayName, email: newUser.email, }; const session = await this.sessionPort.createSession(authenticatedUser); const result: SignupWithEmailResult = { sessionToken: session.token, userId: session.user.id, displayName: session.user.displayName, email: session.user.email ?? newUser.email, createdAt, isNewUser: true, }; this.output.present(result); return Result.ok(undefined); } catch (error) { const message = error instanceof Error && error.message ? error.message : 'Failed to execute SignupWithEmailUseCase'; this.logger.error( 'SignupWithEmailUseCase.execute failed', error instanceof Error ? error : undefined, { input }, ); return Result.err({ code: 'REPOSITORY_ERROR', details: { message }, } as SignupWithEmailApplicationError); } } private generateSalt(): string { const array = new Uint8Array(16); if (typeof crypto !== 'undefined' && crypto.getRandomValues) { crypto.getRandomValues(array); } else { for (let i = 0; i < array.length; i++) { array[i] = Math.floor(Math.random() * 256); } } return Array.from(array, byte => byte.toString(16).padStart(2, '0')).join(''); } private async hashPassword(password: string, salt: string): Promise { // Simple hash for demo - in production, use bcrypt or argon2 const data = password + salt; if (typeof crypto !== 'undefined' && crypto.subtle) { const encoder = new TextEncoder(); const dataBuffer = encoder.encode(data); const hashBuffer = await crypto.subtle.digest('SHA-256', dataBuffer); const hashArray = Array.from(new Uint8Array(hashBuffer)); return hashArray.map(b => b.toString(16).padStart(2, '0')).join(''); } // Fallback for environments without crypto.subtle let hash = 0; for (let i = 0; i < data.length; i++) { const char = data.charCodeAt(i); hash = ((hash << 5) - hash) + char; hash = hash & hash; } return Math.abs(hash).toString(16).padStart(16, '0'); } private generateUserId(): string { if (typeof crypto !== 'undefined' && crypto.randomUUID) { return crypto.randomUUID(); } return 'user-' + Date.now().toString(36) + Math.random().toString(36).substr(2, 9); } }