import crypto from "node:crypto"; import { cookies } from "next/headers"; import { redirect } from "next/navigation"; const SESSION_COOKIE = "inven_session"; const SESSION_TTL_SECONDS = 60 * 60 * 24 * 14; type SessionPayload = { userId: number; email: string; role: string; expiresAt: number; }; function getAuthSecret() { return process.env.AUTH_SECRET || "dev-insecure-auth-secret"; } function hashPassword(password: string) { const salt = crypto.randomBytes(16).toString("hex"); const hash = crypto.scryptSync(password, salt, 64).toString("hex"); return `${salt}:${hash}`; } function verifyPassword(password: string, storedHash: string) { const [salt, expectedHash] = storedHash.split(":"); if (!salt || !expectedHash) { return false; } const hash = crypto.scryptSync(password, salt, 64).toString("hex"); return crypto.timingSafeEqual(Buffer.from(hash, "hex"), Buffer.from(expectedHash, "hex")); } function sign(value: string) { return crypto.createHmac("sha256", getAuthSecret()).update(value).digest("hex"); } function encodeSession(payload: SessionPayload) { const base = Buffer.from(JSON.stringify(payload)).toString("base64url"); return `${base}.${sign(base)}`; } function decodeSession(value: string | undefined): SessionPayload | null { if (!value) { return null; } const [base, signature] = value.split("."); if (!base || !signature || sign(base) !== signature) { return null; } try { const payload = JSON.parse(Buffer.from(base, "base64url").toString("utf8")) as SessionPayload; if (payload.expiresAt < Date.now()) { return null; } return payload; } catch { return null; } } export function bootstrapAdminUser(db: { prepare: (sql: string) => { get: (...args: unknown[]) => unknown; run: (...args: unknown[]) => unknown; }; }) { const countRow = db.prepare(`SELECT COUNT(*) AS count FROM users`).get() as { count: number }; if ((countRow.count ?? 0) > 0) { return; } const email = (process.env.ADMIN_EMAIL || "").trim(); const password = process.env.ADMIN_PASSWORD || ""; if (!email || !password) { return; } db.prepare(`INSERT INTO users (email, password_hash, role) VALUES (?, ?, 'admin')`).run(email, hashPassword(password)); } export async function getSession() { const cookieStore = await cookies(); return decodeSession(cookieStore.get(SESSION_COOKIE)?.value); } export async function requireSession() { const session = await getSession(); if (!session) { redirect("/login"); } return session; } export async function createSession(user: { id: number; email: string; role: string }) { const cookieStore = await cookies(); const payload: SessionPayload = { userId: user.id, email: user.email, role: user.role, expiresAt: Date.now() + SESSION_TTL_SECONDS * 1000 }; cookieStore.set(SESSION_COOKIE, encodeSession(payload), { httpOnly: true, sameSite: "lax", secure: process.env.NODE_ENV === "production", path: "/", maxAge: SESSION_TTL_SECONDS }); } export async function destroySession() { const cookieStore = await cookies(); cookieStore.delete(SESSION_COOKIE); } export function authenticateUser( db: { prepare: (sql: string) => { get: (...args: unknown[]) => unknown; }; }, email: string, password: string ) { const user = db .prepare(`SELECT id, email, password_hash AS passwordHash, role FROM users WHERE email = ?`) .get(email) as { id: number; email: string; passwordHash: string; role: string } | undefined; if (!user || !verifyPassword(password, user.passwordHash)) { return null; } return { id: user.id, email: user.email, role: user.role }; }