Эх сурвалжийг харах

RHL-003-feat(auth): implement session management with JWT and cookie handling

Code_Uwe 1 долоо хоног өмнө
parent
commit
34d95103ac
1 өөрчлөгдсөн 114 нэмэгдсэн , 0 устгасан
  1. 114 0
      lib/auth/session.js

+ 114 - 0
lib/auth/session.js

@@ -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,
+	});
+}