import bcrypt from 'bcrypt'; import crypto from 'crypto'; import { prisma } from './db.js'; import { withBypassRLS } from './rls.js'; const SALT_ROUNDS = 12; const SESSION_DURATION_MS = 30 * 24 * 60 * 60 * 1000; // 30 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 async function createSession(userId: string): Promise<{ id: string; expiresAt: Date }> { const expiresAt = new Date(Date.now() + SESSION_DURATION_MS); const session = await prisma.session.create({ data: { userId, expiresAt } }); return { id: session.id, expiresAt }; } export async function validateSession(sessionId: string) { // Uses withBypassRLS because this joins to the users table (RLS-protected) // and runs before tenant context is established in hooks.server.ts. return withBypassRLS(async (tx) => { const session = await tx.session.findUnique({ where: { id: sessionId }, include: { user: { select: { id: true, email: true, name: true, globalRole: true, tenantRole: true, tenantId: true, avatarUrl: true } } } }); if (!session) return null; if (session.expiresAt < new Date()) { await tx.session.delete({ where: { id: sessionId } }); return null; } return session; }); } export async function deleteSession(sessionId: string) { await prisma.session.delete({ where: { id: sessionId } }).catch(() => {}); } export function sessionCookieName(): string { return 'session'; } export function sessionCookieOptions(expiresAt: Date) { return { path: '/', httpOnly: true, sameSite: 'lax' as const, secure: !import.meta.env.DEV, expires: expiresAt }; } // ─── API Keys ──────────────────────────────────────────────── export function hashApiKey(key: string): string { return crypto.createHash('sha256').update(key).digest('hex'); } export function generateApiKey(): { key: string; prefix: string } { const key = `pnc_${crypto.randomBytes(32).toString('hex')}`; const prefix = key.slice(0, 12); return { key, prefix }; } export async function validateApiKey(key: string) { const keyHash = hashApiKey(key); const apiKey = await prisma.apiKey.findUnique({ where: { keyHash }, include: { user: { select: { id: true, email: true, name: true, globalRole: true, tenantRole: true, tenantId: true, avatarUrl: true } } } }); if (!apiKey) return null; // Update last used timestamp (fire and forget) prisma.apiKey.update({ where: { id: apiKey.id }, data: { lastUsedAt: new Date() } }).catch(() => {}); return apiKey; }