Files
luna-recipes/backend/src/services/auth.service.ts

200 lines
5.5 KiB
TypeScript

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<AuthResult> {
// 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<AuthResult> {
// 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<User | null> {
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<User> {
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<boolean> {
// 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<AuthResult> {
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();