diff --git a/backend/src/auth/AuthService.ts b/backend/src/auth/AuthService.ts new file mode 100644 index 0000000..1b590ec --- /dev/null +++ b/backend/src/auth/AuthService.ts @@ -0,0 +1,201 @@ +import argon2 from 'argon2'; +import jwt from '@fastify/jwt'; +import { FastifyInstance } from 'fastify'; + +interface User { + id: string; + email: string; + displayName: string; + passwordHash: string; +} + +interface Session { + id: string; + userId: string; + expiresAt: Date; +} + +interface PasswordReset { + id: string; + userId: string; + tokenHash: string; + expiresAt: Date; +} + +class AuthService { + private users: Map = new Map(); + private sessions: Map = new Map(); + private passwordResets: Map = new Map(); + private fastify: FastifyInstance; + + constructor(fastify: FastifyInstance) { + this.fastify = fastify; + } + + async register(email: string, password: string, displayName: string): Promise> { + // Check if user already exists + for (const user of this.users.values()) { + if (user.email === email) { + throw new Error('User already exists'); + } + } + + // Hash password + const passwordHash = await argon2.hash(password); + + // Create user + const userId = this.generateId(); + const user: User = { + id: userId, + email, + displayName, + passwordHash + }; + + this.users.set(userId, user); + + // Return user without password hash + const { passwordHash: _, ...userWithoutHash } = user; + return userWithoutHash; + } + + async login(email: string, password: string): Promise { + // Find user + let user: User | undefined; + for (const u of this.users.values()) { + if (u.email === email) { + user = u; + break; + } + } + + if (!user) { + throw new Error('Invalid credentials'); + } + + // Verify password + const isValid = await argon2.verify(user.passwordHash, password); + if (!isValid) { + throw new Error('Invalid credentials'); + } + + // Create session + const sessionId = this.generateId(); + const session: Session = { + id: sessionId, + userId: user.id, + expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000) // 24 hours + }; + + this.sessions.set(sessionId, session); + + // Generate JWT token + const token = this.fastify.jwt.sign({ + userId: user.id, + sessionId, + expiresAt: session.expiresAt + }); + + return token; + } + + async logout(token: string): Promise { + try { + const decoded = this.fastify.jwt.verify(token); + const sessionId = decoded.sessionId; + + if (this.sessions.has(sessionId)) { + this.sessions.delete(sessionId); + } else { + throw new Error('Invalid session'); + } + } catch (error) { + throw new Error('Invalid token'); + } + } + + async getMe(userId: string): Promise> { + const user = this.users.get(userId); + if (!user) { + throw new Error('User not found'); + } + + const { passwordHash: _, ...userWithoutHash } = user; + return userWithoutHash; + } + + async requestPasswordReset(email: string): Promise { + // Find user + let user: User | undefined; + for (const u of this.users.values()) { + if (u.email === email) { + user = u; + break; + } + } + + if (!user) { + // Don't reveal if user exists or not + return; + } + + // Generate reset token + const resetToken = this.generateId(); + const tokenHash = await argon2.hash(resetToken); + + // Store reset token + const reset: PasswordReset = { + id: this.generateId(), + userId: user.id, + tokenHash, + expiresAt: new Date(Date.now() + 60 * 60 * 1000) // 1 hour + }; + + this.passwordResets.set(reset.id, reset); + + // In a real implementation, you would send an email here + // For now, we'll just log that we would send an email + console.log(`Password reset token for ${email}: ${resetToken}`); + } + + async confirmPasswordReset(token: string, newPassword: string): Promise { + // Find reset token + let reset: PasswordReset | undefined; + let resetId: string | undefined; + + for (const [id, r] of this.passwordResets.entries()) { + const isValid = await argon2.verify(r.tokenHash, token); + if (isValid && r.expiresAt > new Date()) { + reset = r; + resetId = id; + break; + } + } + + if (!reset || !resetId) { + throw new Error('Invalid or expired reset token'); + } + + // Get user + const user = this.users.get(reset.userId); + if (!user) { + throw new Error('User not found'); + } + + // Hash new password + const newPasswordHash = await argon2.hash(newPassword); + + // Update user password + user.passwordHash = newPasswordHash; + this.users.set(user.id, user); + + // Delete reset token + this.passwordResets.delete(resetId); + } + + private generateId(): string { + return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15); + } +} + +export default AuthService;