session.js 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131
  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. function normalizeMustChangePassword(value) {
  26. return value === true;
  27. }
  28. /**
  29. * Create a signed session JWT and store it in a HTTP-only cookie.
  30. */
  31. export async function createSession({
  32. userId,
  33. role,
  34. branchId,
  35. email,
  36. mustChangePassword,
  37. }) {
  38. if (!userId || !role) {
  39. throw new Error("createSession requires userId and role");
  40. }
  41. const payload = {
  42. userId,
  43. role,
  44. branchId: branchId ?? null,
  45. email: normalizeEmailOrNull(email),
  46. mustChangePassword: normalizeMustChangePassword(mustChangePassword),
  47. };
  48. const jwt = await new SignJWT(payload)
  49. .setProtectedHeader({ alg: "HS256", typ: "JWT" })
  50. .setIssuedAt()
  51. .setExpirationTime(`${SESSION_MAX_AGE_SECONDS}s`)
  52. .sign(getSessionSecretKey());
  53. const cookieStore = await cookies();
  54. cookieStore.set(SESSION_COOKIE_NAME, jwt, {
  55. httpOnly: true,
  56. secure: resolveCookieSecureFlag(),
  57. sameSite: "lax",
  58. path: "/",
  59. maxAge: SESSION_MAX_AGE_SECONDS,
  60. });
  61. return jwt;
  62. }
  63. /**
  64. * Read the current session from the HTTP-only cookie.
  65. */
  66. export async function getSession() {
  67. const cookieStore = await cookies();
  68. const cookie = cookieStore.get(SESSION_COOKIE_NAME);
  69. if (!cookie?.value) {
  70. return null;
  71. }
  72. const secretKey = getSessionSecretKey();
  73. try {
  74. const { payload } = await jwtVerify(cookie.value, secretKey);
  75. const { userId, role, branchId, email, mustChangePassword } = payload;
  76. if (typeof userId !== "string" || typeof role !== "string") {
  77. return null;
  78. }
  79. return {
  80. userId,
  81. role,
  82. branchId: typeof branchId === "string" ? branchId : null,
  83. email: typeof email === "string" ? email : null,
  84. mustChangePassword: normalizeMustChangePassword(mustChangePassword),
  85. };
  86. } catch {
  87. const store = await cookies();
  88. store.set(SESSION_COOKIE_NAME, "", {
  89. httpOnly: true,
  90. secure: resolveCookieSecureFlag(),
  91. sameSite: "lax",
  92. path: "/",
  93. maxAge: 0,
  94. });
  95. return null;
  96. }
  97. }
  98. export async function destroySession() {
  99. const cookieStore = await cookies();
  100. cookieStore.set(SESSION_COOKIE_NAME, "", {
  101. httpOnly: true,
  102. secure: resolveCookieSecureFlag(),
  103. sameSite: "lax",
  104. path: "/",
  105. maxAge: 0,
  106. });
  107. }