import bcrypt from 'bcrypt'; import jwt from 'jsonwebtoken'; import { ulid } from 'ulid'; import { getDb } from '../db/connection.js'; const JWT_SECRET = process.env.JWT_SECRET || 'your-super-secret-jwt-key-change-in-production'; const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET || 'your-super-secret-refresh-key-change-in-production'; const BCRYPT_ROUNDS = 12; const ACCESS_TOKEN_EXPIRES_IN = '15m'; const REFRESH_TOKEN_EXPIRES_IN = '7d'; export interface User { id: string; email: string; display_name: string; avatar_url?: string; created_at: string; updated_at: string; } export interface AuthTokens { accessToken: string; refreshToken: string; } export interface AuthResult { user: User; tokens: AuthTokens; } class AuthService { private db = getDb(); async register(email: string, password: string, displayName: string): Promise { // Check if user already exists const existingUser = this.db.prepare('SELECT id FROM users WHERE email = ?').get(email); if (existingUser) { throw new Error('EMAIL_EXISTS'); } // Hash password const passwordHash = await bcrypt.hash(password, BCRYPT_ROUNDS); // Create user const userId = ulid(); const now = new Date().toISOString(); this.db.prepare(` INSERT INTO users (id, email, password_hash, display_name, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?) `).run(userId, email, passwordHash, displayName, now, now); // Get created user const user = this.db.prepare(` SELECT id, email, display_name, avatar_url, created_at, updated_at FROM users WHERE id = ? `).get(userId) as User; // Generate tokens const tokens = this.generateTokens(user); return { user, tokens }; } async login(email: string, password: string): Promise { // Get user by email const userWithPassword = this.db.prepare(` SELECT id, email, password_hash, display_name, avatar_url, created_at, updated_at FROM users WHERE email = ? `).get(email) as any; if (!userWithPassword) { throw new Error('INVALID_CREDENTIALS'); } // Check password const passwordValid = await bcrypt.compare(password, userWithPassword.password_hash); if (!passwordValid) { throw new Error('INVALID_CREDENTIALS'); } // Remove password from user object const { password_hash, ...user } = userWithPassword; // Generate tokens const tokens = this.generateTokens(user); return { user, tokens }; } async getProfile(userId: string): Promise { const user = this.db.prepare(` SELECT id, email, display_name, avatar_url, created_at, updated_at FROM users WHERE id = ? `).get(userId) as User | undefined; return user || null; } async updateProfile(userId: string, data: { display_name?: string; avatar_url?: string }): Promise { const updates: string[] = []; const values: any[] = []; if (data.display_name !== undefined) { updates.push('display_name = ?'); values.push(data.display_name); } if (data.avatar_url !== undefined) { updates.push('avatar_url = ?'); values.push(data.avatar_url); } if (updates.length === 0) { throw new Error('NO_UPDATES_PROVIDED'); } updates.push('updated_at = ?'); values.push(new Date().toISOString()); values.push(userId); this.db.prepare(` UPDATE users SET ${updates.join(', ')} WHERE id = ? `).run(...values); const user = await this.getProfile(userId); if (!user) { throw new Error('USER_NOT_FOUND'); } return user; } async changePassword(userId: string, currentPassword: string, newPassword: string): Promise { // Get current password hash const userWithPassword = this.db.prepare(` SELECT password_hash FROM users WHERE id = ? `).get(userId) as any; if (!userWithPassword) { throw new Error('USER_NOT_FOUND'); } // Verify current password const currentPasswordValid = await bcrypt.compare(currentPassword, userWithPassword.password_hash); if (!currentPasswordValid) { throw new Error('INVALID_CURRENT_PASSWORD'); } // Hash new password const newPasswordHash = await bcrypt.hash(newPassword, BCRYPT_ROUNDS); // Update password this.db.prepare(` UPDATE users SET password_hash = ?, updated_at = ? WHERE id = ? `).run(newPasswordHash, new Date().toISOString(), userId); return true; } async refreshTokens(refreshToken: string): Promise { try { const decoded = jwt.verify(refreshToken, JWT_REFRESH_SECRET) as any; const user = await this.getProfile(decoded.sub); if (!user) { throw new Error('USER_NOT_FOUND'); } const tokens = this.generateTokens(user); return { user, tokens }; } catch (error) { throw new Error('INVALID_REFRESH_TOKEN'); } } verifyAccessToken(token: string): any { try { return jwt.verify(token, JWT_SECRET); } catch (error) { throw new Error('INVALID_ACCESS_TOKEN'); } } private generateTokens(user: User): AuthTokens { const payload = { sub: user.id, email: user.email, display_name: user.display_name, }; const accessToken = jwt.sign(payload, JWT_SECRET, { expiresIn: ACCESS_TOKEN_EXPIRES_IN }); const refreshToken = jwt.sign({ sub: user.id }, JWT_REFRESH_SECRET, { expiresIn: REFRESH_TOKEN_EXPIRES_IN }); return { accessToken, refreshToken }; } } export const authService = new AuthService();