|
|
@@ -0,0 +1,114 @@
|
|
|
+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<string>} 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,
|
|
|
+ });
|
|
|
+}
|