session.js 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114
  1. import { cookies } from "next/headers";
  2. import { SignJWT, jwtVerify } from "jose";
  3. export const SESSION_COOKIE_NAME = "auth_session";
  4. export const SESSION_MAX_AGE_SECONDS = 60 * 60 * 8; // 8 hours
  5. function getSessionSecretKey() {
  6. const secret = process.env.SESSION_SECRET;
  7. if (!secret) {
  8. throw new Error("SESSION_SECRET environment variable is not set");
  9. }
  10. return new TextEncoder().encode(secret);
  11. }
  12. /**
  13. * Create a signed session JWT and store it in a HTTP-only cookie.
  14. *
  15. * @param {Object} params
  16. * @param {string} params.userId - MongoDB user id as string.
  17. * @param {string} params.role - User role ("branch" | "admin" | "dev").
  18. * @param {string|null} params.branchId - Branch id or null.
  19. * @returns {Promise<string>} The signed JWT.
  20. */
  21. export async function createSession({ userId, role, branchId }) {
  22. if (!userId || !role) {
  23. throw new Error("createSession requires userId and role");
  24. }
  25. const payload = {
  26. userId,
  27. role,
  28. branchId: branchId ?? null,
  29. };
  30. const jwt = await new SignJWT(payload)
  31. .setProtectedHeader({ alg: "HS256", typ: "JWT" })
  32. .setIssuedAt()
  33. .setExpirationTime(`${SESSION_MAX_AGE_SECONDS}s`)
  34. .sign(getSessionSecretKey());
  35. const cookieStore = cookies();
  36. cookieStore.set(SESSION_COOKIE_NAME, jwt, {
  37. httpOnly: true,
  38. secure: process.env.NODE_ENV === "production",
  39. sameSite: "lax",
  40. path: "/",
  41. maxAge: SESSION_MAX_AGE_SECONDS,
  42. });
  43. return jwt;
  44. }
  45. /**
  46. * Read the current session from the HTTP-only cookie.
  47. *
  48. * @returns {Promise<{ userId: string, role: string, branchId: string | null } | null>}
  49. * The session payload, or null if no valid session exists.
  50. */
  51. export async function getSession() {
  52. const cookieStore = cookies();
  53. const cookie = cookieStore.get(SESSION_COOKIE_NAME);
  54. if (!cookie?.value) {
  55. return null;
  56. }
  57. const secretKey = getSessionSecretKey();
  58. try {
  59. const { payload } = await jwtVerify(cookie.value, secretKey);
  60. const { userId, role, branchId } = payload;
  61. if (typeof userId !== "string" || typeof role !== "string") {
  62. return null;
  63. }
  64. return {
  65. userId,
  66. role,
  67. branchId: typeof branchId === "string" ? branchId : null,
  68. };
  69. } catch (error) {
  70. // Invalid or expired token: clear the cookie for hygiene and return null
  71. const store = cookies();
  72. store.set(SESSION_COOKIE_NAME, "", {
  73. httpOnly: true,
  74. secure: process.env.NODE_ENV === "production",
  75. sameSite: "lax",
  76. path: "/",
  77. maxAge: 0,
  78. });
  79. return null;
  80. }
  81. }
  82. /**
  83. * Destroy the current session by clearing the session cookie.
  84. */
  85. export function destroySession() {
  86. const cookieStore = cookies();
  87. cookieStore.set(SESSION_COOKIE_NAME, "", {
  88. httpOnly: true,
  89. secure: process.env.NODE_ENV === "production",
  90. sameSite: "lax",
  91. path: "/",
  92. maxAge: 0,
  93. });
  94. }