|
@@ -1,3 +1,4 @@
|
|
|
|
|
+// lib/auth/session.js
|
|
|
import { cookies } from "next/headers";
|
|
import { cookies } from "next/headers";
|
|
|
import { SignJWT, jwtVerify } from "jose";
|
|
import { SignJWT, jwtVerify } from "jose";
|
|
|
|
|
|
|
@@ -15,13 +16,25 @@ function getSessionSecretKey() {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
- * Create a signed session JWT and store it in a HTTP-only cookie.
|
|
|
|
|
|
|
+ * Resolve whether the session cookie should be marked as "Secure".
|
|
|
|
|
+ *
|
|
|
|
|
+ * Default:
|
|
|
|
|
+ * - Secure in production (`NODE_ENV=production`)
|
|
|
*
|
|
*
|
|
|
- * @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.
|
|
|
|
|
|
|
+ * 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 }) {
|
|
export async function createSession({ userId, role, branchId }) {
|
|
|
if (!userId || !role) {
|
|
if (!userId || !role) {
|
|
@@ -40,11 +53,11 @@ export async function createSession({ userId, role, branchId }) {
|
|
|
.setExpirationTime(`${SESSION_MAX_AGE_SECONDS}s`)
|
|
.setExpirationTime(`${SESSION_MAX_AGE_SECONDS}s`)
|
|
|
.sign(getSessionSecretKey());
|
|
.sign(getSessionSecretKey());
|
|
|
|
|
|
|
|
- const cookieStore = cookies();
|
|
|
|
|
|
|
+ const cookieStore = await cookies();
|
|
|
|
|
|
|
|
cookieStore.set(SESSION_COOKIE_NAME, jwt, {
|
|
cookieStore.set(SESSION_COOKIE_NAME, jwt, {
|
|
|
httpOnly: true,
|
|
httpOnly: true,
|
|
|
- secure: process.env.NODE_ENV === "production",
|
|
|
|
|
|
|
+ secure: resolveCookieSecureFlag(),
|
|
|
sameSite: "lax",
|
|
sameSite: "lax",
|
|
|
path: "/",
|
|
path: "/",
|
|
|
maxAge: SESSION_MAX_AGE_SECONDS,
|
|
maxAge: SESSION_MAX_AGE_SECONDS,
|
|
@@ -55,12 +68,9 @@ export async function createSession({ userId, role, branchId }) {
|
|
|
|
|
|
|
|
/**
|
|
/**
|
|
|
* Read the current session from the HTTP-only cookie.
|
|
* 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() {
|
|
export async function getSession() {
|
|
|
- const cookieStore = cookies();
|
|
|
|
|
|
|
+ const cookieStore = await cookies();
|
|
|
const cookie = cookieStore.get(SESSION_COOKIE_NAME);
|
|
const cookie = cookieStore.get(SESSION_COOKIE_NAME);
|
|
|
|
|
|
|
|
if (!cookie?.value) {
|
|
if (!cookie?.value) {
|
|
@@ -83,12 +93,12 @@ export async function getSession() {
|
|
|
role,
|
|
role,
|
|
|
branchId: typeof branchId === "string" ? branchId : null,
|
|
branchId: typeof branchId === "string" ? branchId : null,
|
|
|
};
|
|
};
|
|
|
- } catch (error) {
|
|
|
|
|
- // Invalid or expired token: clear the cookie for hygiene and return null
|
|
|
|
|
- const store = cookies();
|
|
|
|
|
|
|
+ } catch {
|
|
|
|
|
+ // Invalid or expired token: clear cookie and return null
|
|
|
|
|
+ const store = await cookies();
|
|
|
store.set(SESSION_COOKIE_NAME, "", {
|
|
store.set(SESSION_COOKIE_NAME, "", {
|
|
|
httpOnly: true,
|
|
httpOnly: true,
|
|
|
- secure: process.env.NODE_ENV === "production",
|
|
|
|
|
|
|
+ secure: resolveCookieSecureFlag(),
|
|
|
sameSite: "lax",
|
|
sameSite: "lax",
|
|
|
path: "/",
|
|
path: "/",
|
|
|
maxAge: 0,
|
|
maxAge: 0,
|
|
@@ -101,12 +111,12 @@ export async function getSession() {
|
|
|
/**
|
|
/**
|
|
|
* Destroy the current session by clearing the session cookie.
|
|
* Destroy the current session by clearing the session cookie.
|
|
|
*/
|
|
*/
|
|
|
-export function destroySession() {
|
|
|
|
|
- const cookieStore = cookies();
|
|
|
|
|
|
|
+export async function destroySession() {
|
|
|
|
|
+ const cookieStore = await cookies();
|
|
|
|
|
|
|
|
cookieStore.set(SESSION_COOKIE_NAME, "", {
|
|
cookieStore.set(SESSION_COOKIE_NAME, "", {
|
|
|
httpOnly: true,
|
|
httpOnly: true,
|
|
|
- secure: process.env.NODE_ENV === "production",
|
|
|
|
|
|
|
+ secure: resolveCookieSecureFlag(),
|
|
|
sameSite: "lax",
|
|
sameSite: "lax",
|
|
|
path: "/",
|
|
path: "/",
|
|
|
maxAge: 0,
|
|
maxAge: 0,
|