// lib/auth/session.js import { cookies } from "next/headers"; import { SignJWT, jwtVerify } from "jose"; export const SESSION_COOKIE_NAME = "auth_session"; export const SESSION_MAX_AGE_SECONDS = 60 * 60 * 8; // 8 hours function getSessionSecretKey() { const secret = process.env.SESSION_SECRET; if (!secret) { throw new Error("SESSION_SECRET environment variable is not set"); } return new TextEncoder().encode(secret); } /** * Resolve whether the session cookie should be marked as "Secure". * * Default: * - Secure in production (`NODE_ENV=production`) * * Override (useful for local HTTP testing): * - SESSION_COOKIE_SECURE=false * - SESSION_COOKIE_SECURE=true */ function resolveCookieSecureFlag() { const raw = (process.env.SESSION_COOKIE_SECURE || "").trim().toLowerCase(); if (raw === "true") return true; if (raw === "false") return false; return process.env.NODE_ENV === "production"; } /** * Create a signed session JWT and store it in a HTTP-only cookie. */ export async function createSession({ userId, role, branchId }) { if (!userId || !role) { throw new Error("createSession requires userId and role"); } const payload = { userId, role, branchId: branchId ?? null, }; const jwt = await new SignJWT(payload) .setProtectedHeader({ alg: "HS256", typ: "JWT" }) .setIssuedAt() .setExpirationTime(`${SESSION_MAX_AGE_SECONDS}s`) .sign(getSessionSecretKey()); const cookieStore = await cookies(); cookieStore.set(SESSION_COOKIE_NAME, jwt, { httpOnly: true, secure: resolveCookieSecureFlag(), sameSite: "lax", path: "/", maxAge: SESSION_MAX_AGE_SECONDS, }); return jwt; } /** * Read the current session from the HTTP-only cookie. */ export async function getSession() { const cookieStore = await cookies(); const cookie = cookieStore.get(SESSION_COOKIE_NAME); if (!cookie?.value) { return null; } const secretKey = getSessionSecretKey(); try { const { payload } = await jwtVerify(cookie.value, secretKey); const { userId, role, branchId } = payload; if (typeof userId !== "string" || typeof role !== "string") { return null; } return { userId, role, branchId: typeof branchId === "string" ? branchId : null, }; } catch { // Invalid or expired token: clear cookie and return null const store = await cookies(); store.set(SESSION_COOKIE_NAME, "", { httpOnly: true, secure: resolveCookieSecureFlag(), sameSite: "lax", path: "/", maxAge: 0, }); return null; } } /** * Destroy the current session by clearing the session cookie. */ export async function destroySession() { const cookieStore = await cookies(); cookieStore.set(SESSION_COOKIE_NAME, "", { httpOnly: true, secure: resolveCookieSecureFlag(), sameSite: "lax", path: "/", maxAge: 0, }); }