session.js 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119
  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. function resolveCookieSecureFlag() {
  13. const raw = (process.env.SESSION_COOKIE_SECURE || "").trim().toLowerCase();
  14. if (raw === "true") return true;
  15. if (raw === "false") return false;
  16. return process.env.NODE_ENV === "production";
  17. }
  18. function normalizeEmailOrNull(value) {
  19. if (typeof value !== "string") return null;
  20. const s = value.trim();
  21. if (!s) return null;
  22. // Email is not case-sensitive; keep it normalized for UI consistency.
  23. return s.toLowerCase();
  24. }
  25. /**
  26. * Create a signed session JWT and store it in a HTTP-only cookie.
  27. */
  28. export async function createSession({ userId, role, branchId, email }) {
  29. if (!userId || !role) {
  30. throw new Error("createSession requires userId and role");
  31. }
  32. const payload = {
  33. userId,
  34. role,
  35. branchId: branchId ?? null,
  36. email: normalizeEmailOrNull(email),
  37. };
  38. const jwt = await new SignJWT(payload)
  39. .setProtectedHeader({ alg: "HS256", typ: "JWT" })
  40. .setIssuedAt()
  41. .setExpirationTime(`${SESSION_MAX_AGE_SECONDS}s`)
  42. .sign(getSessionSecretKey());
  43. const cookieStore = await cookies();
  44. cookieStore.set(SESSION_COOKIE_NAME, jwt, {
  45. httpOnly: true,
  46. secure: resolveCookieSecureFlag(),
  47. sameSite: "lax",
  48. path: "/",
  49. maxAge: SESSION_MAX_AGE_SECONDS,
  50. });
  51. return jwt;
  52. }
  53. /**
  54. * Read the current session from the HTTP-only cookie.
  55. */
  56. export async function getSession() {
  57. const cookieStore = await cookies();
  58. const cookie = cookieStore.get(SESSION_COOKIE_NAME);
  59. if (!cookie?.value) {
  60. return null;
  61. }
  62. const secretKey = getSessionSecretKey();
  63. try {
  64. const { payload } = await jwtVerify(cookie.value, secretKey);
  65. const { userId, role, branchId, email } = payload;
  66. if (typeof userId !== "string" || typeof role !== "string") {
  67. return null;
  68. }
  69. return {
  70. userId,
  71. role,
  72. branchId: typeof branchId === "string" ? branchId : null,
  73. email: typeof email === "string" ? email : null,
  74. };
  75. } catch {
  76. const store = await cookies();
  77. store.set(SESSION_COOKIE_NAME, "", {
  78. httpOnly: true,
  79. secure: resolveCookieSecureFlag(),
  80. sameSite: "lax",
  81. path: "/",
  82. maxAge: 0,
  83. });
  84. return null;
  85. }
  86. }
  87. export async function destroySession() {
  88. const cookieStore = await cookies();
  89. cookieStore.set(SESSION_COOKIE_NAME, "", {
  90. httpOnly: true,
  91. secure: resolveCookieSecureFlag(),
  92. sameSite: "lax",
  93. path: "/",
  94. maxAge: 0,
  95. });
  96. }