Files
gridpilot.gg/packages/identity/application/use-cases/SignupWithEmailUseCase.ts
2025-12-07 18:38:03 +01:00

123 lines
3.8 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,
primaryDriverId: undefined, // Will be set during onboarding
};
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);
}
}