import { randomUUID } from 'node:crypto'; import type { PrismaClient } from '@prisma/client'; import bcrypt from 'bcrypt'; /** 30 days in milliseconds */ const SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000; export interface LoginResult { token: string; expiresAt: Date; user: { id: string; email: string; role: string }; } export class AuthenticationError extends Error { readonly statusCode = 401; constructor(message: string) { super(message); this.name = 'AuthenticationError'; } } export class AuthService { constructor(private readonly prisma: PrismaClient) {} async login(email: string, password: string): Promise { const user = await this.prisma.user.findUnique({ where: { email } }); if (user === null) { throw new AuthenticationError('Invalid email or password'); } const valid = await bcrypt.compare(password, user.passwordHash); if (!valid) { throw new AuthenticationError('Invalid email or password'); } const token = randomUUID(); const expiresAt = new Date(Date.now() + SESSION_TTL_MS); await this.prisma.session.create({ data: { token, userId: user.id, expiresAt, }, }); return { token, expiresAt, user: { id: user.id, email: user.email, role: user.role }, }; } async logout(token: string): Promise { // Delete the session by token; ignore if already deleted await this.prisma.session.deleteMany({ where: { token } }); } async findSession(token: string): Promise<{ userId: string; expiresAt: Date } | null> { const session = await this.prisma.session.findUnique({ where: { token } }); if (session === null) { return null; } return { userId: session.userId, expiresAt: session.expiresAt }; } /** * Create a session for a user by email without requiring their password. * Used for admin impersonation. */ async impersonate(email: string): Promise { const user = await this.prisma.user.findUnique({ where: { email } }); if (user === null) { throw new AuthenticationError('User not found'); } const token = randomUUID(); const expiresAt = new Date(Date.now() + SESSION_TTL_MS); await this.prisma.session.create({ data: { token, userId: user.id, expiresAt, }, }); return { token, expiresAt, user: { id: user.id, email: user.email, role: user.role }, }; } }