session.js 2.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124
  1. // lib/auth/session.js
  2. import { cookies } from "next/headers";
  3. import { SignJWT, jwtVerify } from "jose";
  4. export const SESSION_COOKIE_NAME = "auth_session";
  5. export const SESSION_MAX_AGE_SECONDS = 60 * 60 * 8; // 8 hours
  6. function getSessionSecretKey() {
  7. const secret = process.env.SESSION_SECRET;
  8. if (!secret) {
  9. throw new Error("SESSION_SECRET environment variable is not set");
  10. }
  11. return new TextEncoder().encode(secret);
  12. }
  13. /**
  14. * Resolve whether the session cookie should be marked as "Secure".
  15. *
  16. * Default:
  17. * - Secure in production (`NODE_ENV=production`)
  18. *
  19. * Override (useful for local HTTP testing):
  20. * - SESSION_COOKIE_SECURE=false
  21. * - SESSION_COOKIE_SECURE=true
  22. */
  23. function resolveCookieSecureFlag() {
  24. const raw = (process.env.SESSION_COOKIE_SECURE || "").trim().toLowerCase();
  25. if (raw === "true") return true;
  26. if (raw === "false") return false;
  27. return process.env.NODE_ENV === "production";
  28. }
  29. /**
  30. * Create a signed session JWT and store it in a HTTP-only cookie.
  31. */
  32. export async function createSession({ userId, role, branchId }) {
  33. if (!userId || !role) {
  34. throw new Error("createSession requires userId and role");
  35. }
  36. const payload = {
  37. userId,
  38. role,
  39. branchId: branchId ?? null,
  40. };
  41. const jwt = await new SignJWT(payload)
  42. .setProtectedHeader({ alg: "HS256", typ: "JWT" })
  43. .setIssuedAt()
  44. .setExpirationTime(`${SESSION_MAX_AGE_SECONDS}s`)
  45. .sign(getSessionSecretKey());
  46. const cookieStore = await cookies();
  47. cookieStore.set(SESSION_COOKIE_NAME, jwt, {
  48. httpOnly: true,
  49. secure: resolveCookieSecureFlag(),
  50. sameSite: "lax",
  51. path: "/",
  52. maxAge: SESSION_MAX_AGE_SECONDS,
  53. });
  54. return jwt;
  55. }
  56. /**
  57. * Read the current session from the HTTP-only cookie.
  58. */
  59. export async function getSession() {
  60. const cookieStore = await cookies();
  61. const cookie = cookieStore.get(SESSION_COOKIE_NAME);
  62. if (!cookie?.value) {
  63. return null;
  64. }
  65. const secretKey = getSessionSecretKey();
  66. try {
  67. const { payload } = await jwtVerify(cookie.value, secretKey);
  68. const { userId, role, branchId } = payload;
  69. if (typeof userId !== "string" || typeof role !== "string") {
  70. return null;
  71. }
  72. return {
  73. userId,
  74. role,
  75. branchId: typeof branchId === "string" ? branchId : null,
  76. };
  77. } catch {
  78. // Invalid or expired token: clear cookie and return null
  79. const store = await cookies();
  80. store.set(SESSION_COOKIE_NAME, "", {
  81. httpOnly: true,
  82. secure: resolveCookieSecureFlag(),
  83. sameSite: "lax",
  84. path: "/",
  85. maxAge: 0,
  86. });
  87. return null;
  88. }
  89. }
  90. /**
  91. * Destroy the current session by clearing the session cookie.
  92. */
  93. export async function destroySession() {
  94. const cookieStore = await cookies();
  95. cookieStore.set(SESSION_COOKIE_NAME, "", {
  96. httpOnly: true,
  97. secure: resolveCookieSecureFlag(),
  98. sameSite: "lax",
  99. path: "/",
  100. maxAge: 0,
  101. });
  102. }