import "server-only"; import { eq } from "drizzle-orm"; import { db, User } from "@/lib/db"; import { redis } from "@/lib/redis"; const MEMORY_TTL_MS = 60_000; const REDIS_TTL_SEC = 60; const memory = new Map(); function redisKey(userId: number): string { return `jwtver:${userId}`; } /** * Cached `users.website_jwt_version` for Auth.js JWT validation. * Avoids a DB round-trip on every authenticated request. */ export async function getCachedJwtVersion( userId: number, ): Promise { if (!Number.isInteger(userId) || userId <= 0) return null; const now = Date.now(); const hit = memory.get(userId); if (hit && hit.expiresAt > now) return hit.version; if (redis) { try { const raw = await redis.get(redisKey(userId)); if (raw !== null && raw !== undefined) { const version = Number.parseInt(raw, 10); if (Number.isFinite(version)) { memory.set(userId, { version, expiresAt: now + MEMORY_TTL_MS }); return version; } } } catch { /* fall through to DB */ } } try { const [row] = await db .select({ websiteJwtVersion: User.websiteJwtVersion }) .from(User) .where(eq(User.id, userId)) .limit(1); if (!row) return null; const version = row.websiteJwtVersion; memory.set(userId, { version, expiresAt: now + MEMORY_TTL_MS }); if (redis) { try { await redis.setex(redisKey(userId), REDIS_TTL_SEC, String(version)); } catch { /* non-critical */ } } return version; } catch { return null; } } /** Call after bumping website_jwt_version so other instances drop sessions ASAP. */ export async function invalidateJwtVersionCache(userId: number): Promise { memory.delete(userId); if (redis) { try { await redis.del(redisKey(userId)); } catch { /* non-critical */ } } }