122 lines
3.7 KiB
TypeScript
122 lines
3.7 KiB
TypeScript
/**
|
|
* 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<SignupResultDTO> {
|
|
// 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<string> {
|
|
// 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);
|
|
}
|
|
} |