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); } /** * Create a signed session JWT and store it in a HTTP-only cookie. * * @param {Object} params * @param {string} params.userId - MongoDB user id as string. * @param {string} params.role - User role ("branch" | "admin" | "dev"). * @param {string|null} params.branchId - Branch id or null. * @returns {Promise} The signed JWT. */ 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 = cookies(); cookieStore.set(SESSION_COOKIE_NAME, jwt, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", path: "/", maxAge: SESSION_MAX_AGE_SECONDS, }); return jwt; } /** * Read the current session from the HTTP-only cookie. * * @returns {Promise<{ userId: string, role: string, branchId: string | null } | null>} * The session payload, or null if no valid session exists. */ export async function getSession() { const cookieStore = 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 (error) { // Invalid or expired token: clear the cookie for hygiene and return null const store = cookies(); store.set(SESSION_COOKIE_NAME, "", { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", path: "/", maxAge: 0, }); return null; } } /** * Destroy the current session by clearing the session cookie. */ export function destroySession() { const cookieStore = cookies(); cookieStore.set(SESSION_COOKIE_NAME, "", { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "lax", path: "/", maxAge: 0, }); }