import bcrypt from 'bcryptjs'; import jwt from 'jsonwebtoken'; import { prisma } from '../prisma.js'; import { DEFAULTS } from '$lib/utils/constants.js'; import type { JwtPayload, TokenPair } from '$lib/types/auth.js'; const SALT_ROUNDS = 12; function getJwtSecret(): string { const secret = process.env.JWT_SECRET; if (!secret) { throw new Error('JWT_SECRET environment variable is not set'); } return secret; } function getJwtExpiry(): string { return process.env.JWT_EXPIRY || DEFAULTS.JWT_EXPIRY; } function getRefreshTokenExpiryDays(): number { const envValue = process.env.REFRESH_TOKEN_EXPIRY; if (envValue) { const days = parseInt(envValue.replace('d', ''), 10); if (!isNaN(days)) return days; } return DEFAULTS.REFRESH_TOKEN_EXPIRY_DAYS; } export async function hashPassword(password: string): Promise { return bcrypt.hash(password, SALT_ROUNDS); } export async function verifyPassword(password: string, hash: string): Promise { return bcrypt.compare(password, hash); } export function signAccessToken(payload: JwtPayload): string { return jwt.sign(payload, getJwtSecret(), { expiresIn: getJwtExpiry() as string & jwt.SignOptions['expiresIn'] }); } export function verifyAccessToken(token: string): JwtPayload { try { const decoded = jwt.verify(token, getJwtSecret()) as JwtPayload & jwt.JwtPayload; return { userId: decoded.userId, email: decoded.email, role: decoded.role }; } catch { throw new Error('Invalid or expired access token'); } } export function generateRefreshToken(): string { const bytes = new Uint8Array(48); crypto.getRandomValues(bytes); return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join(''); } export function getRefreshTokenExpiry(): Date { const days = getRefreshTokenExpiryDays(); const expiry = new Date(); expiry.setDate(expiry.getDate() + days); return expiry; } export async function saveRefreshToken(userId: string, refreshToken: string): Promise { const hashedToken = await bcrypt.hash(refreshToken, SALT_ROUNDS); await prisma.user.update({ where: { id: userId }, data: { refreshToken: hashedToken, refreshTokenExpiresAt: getRefreshTokenExpiry() } }); } export async function validateRefreshToken( userId: string, refreshToken: string ): Promise { const user = await prisma.user.findUnique({ where: { id: userId }, select: { refreshToken: true, refreshTokenExpiresAt: true } }); if (!user?.refreshToken || !user.refreshTokenExpiresAt) { return false; } if (new Date() > user.refreshTokenExpiresAt) { return false; } return bcrypt.compare(refreshToken, user.refreshToken); } export async function revokeRefreshToken(userId: string): Promise { await prisma.user.update({ where: { id: userId }, data: { refreshToken: null, refreshTokenExpiresAt: null } }); } export async function rotateTokens(userId: string, email: string, role: string): Promise { const accessToken = signAccessToken({ userId, email, role }); const refreshToken = generateRefreshToken(); await saveRefreshToken(userId, refreshToken); return { accessToken, refreshToken }; }