/** * Signup with Email Use Case * * Creates a new user account with email and password. */ import type { IUserRepository, StoredUser } from '../../domain/repositories/IUserRepository'; import type { AuthenticatedUserDTO } from '../dto/AuthenticatedUserDTO'; import type { AuthSessionDTO } from '../dto/AuthSessionDTO'; import type { IdentitySessionPort } from '../ports/IdentitySessionPort'; export interface SignupCommandDTO { email: string; password: string; displayName: string; } export interface SignupResultDTO { session: AuthSessionDTO; isNewUser: boolean; } export class SignupWithEmailUseCase { constructor( private readonly userRepository: IUserRepository, private readonly sessionPort: IdentitySessionPort, ) {} async execute(command: SignupCommandDTO): Promise { // Validate email format const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(command.email)) { throw new Error('Invalid email format'); } // Validate password strength if (command.password.length < 8) { throw new Error('Password must be at least 8 characters'); } // Validate display name if (!command.displayName || command.displayName.trim().length < 2) { throw new Error('Display name must be at least 2 characters'); } // Check if email already exists const existingUser = await this.userRepository.findByEmail(command.email); if (existingUser) { throw new Error('An account with this email already exists'); } // Hash password (simple hash for demo - in production use bcrypt) const salt = this.generateSalt(); const passwordHash = await this.hashPassword(command.password, salt); // Create user const userId = this.generateUserId(); const newUser: StoredUser = { id: userId, email: command.email.toLowerCase().trim(), displayName: command.displayName.trim(), passwordHash, salt, createdAt: new Date(), }; await this.userRepository.create(newUser); // Create session const authenticatedUser: AuthenticatedUserDTO = { id: newUser.id, displayName: newUser.displayName, email: newUser.email, }; const session = await this.sessionPort.createSession(authenticatedUser); return { session, isNewUser: true, }; } 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); } }