import { getIronSession, type IronSession, type SessionOptions } from 'iron-session' import { NextRequest, NextResponse } from 'next/server' import { cookies } from 'next/headers' import crypto from 'crypto' export interface SessionData { userId: string username: string role: 'admin' | 'user' } export function getSessionOptions(): SessionOptions { const secret = process.env.SESSION_SECRET if (!secret) throw new Error('SESSION_SECRET is not set') return { password: secret, cookieName: 'ml_session', ttl: 60 * 60 * 24 * 30, // 30 days cookieOptions: { secure: process.env.COOKIE_SECURE === 'true', httpOnly: true, sameSite: 'lax', }, } } // For use in server components and layouts (Node.js runtime, read-only) export async function getServerSession(): Promise> { const cookieStore = await cookies() return getIronSession(cookieStore, getSessionOptions()) } // For use in API routes where the session must be written (login/logout) // The caller is responsible for returning the response so Set-Cookie headers are sent export async function getWritableSession( req: NextRequest, res: NextResponse ): Promise> { return getIronSession(req, res, getSessionOptions()) } // Password hashing using Node.js built-in crypto (Node.js runtime only) export async function hashPassword(password: string): Promise { const salt = crypto.randomBytes(16) const derivedKey = await new Promise((resolve, reject) => { crypto.scrypt(password, salt, 64, { N: 16384, r: 8, p: 1 }, (err, key) => { if (err) reject(err) else resolve(key) }) }) return `scrypt:${salt.toString('hex')}:${derivedKey.toString('hex')}` } export async function verifyPassword(password: string, hash: string): Promise { const parts = hash.split(':') if (parts.length !== 3 || parts[0] !== 'scrypt') return false const salt = Buffer.from(parts[1], 'hex') const storedKey = Buffer.from(parts[2], 'hex') const derivedKey = await new Promise((resolve, reject) => { crypto.scrypt(password, salt, 64, { N: 16384, r: 8, p: 1 }, (err, key) => { if (err) reject(err) else resolve(key) }) }) if (derivedKey.length !== storedKey.length) return false return crypto.timingSafeEqual(derivedKey, storedKey) } // Auth guard result type type AuthSuccess = { session: IronSession; accessLevel?: 'admin' | 'write' | 'read' } type AuthResult = AuthSuccess | NextResponse // Read-only session from an API route request (throwaway response) async function getReadonlySession(req: NextRequest): Promise> { const res = new NextResponse() return getIronSession(req, res, getSessionOptions()) } export async function requireAuth(req: NextRequest): Promise { const session = await getReadonlySession(req) if (!session.userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } return { session } } export async function requireAdmin(req: NextRequest): Promise { const session = await getReadonlySession(req) if (!session.userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } if (session.role !== 'admin') { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } return { session } } export async function requireLibraryAccess(req: NextRequest, libraryId: string): Promise { const session = await getReadonlySession(req) if (!session.userId) { return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) } if (session.role === 'admin') return { session, accessLevel: 'admin' } // Lazy import to avoid pulling DB into edge contexts const { getLibraryAccessLevel } = await import('./users') const accessLevel = getLibraryAccessLevel(session.userId, libraryId) if (!accessLevel) { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } return { session, accessLevel } } export async function requireLibraryWriteAccess(req: NextRequest, libraryId: string): Promise { const result = await requireLibraryAccess(req, libraryId) if (result instanceof NextResponse) return result if (result.accessLevel === 'read') { return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) } return result }