feat: jwt cache, redis health, help-ticket admin, and write rate limits
Cut Auth.js DB load with cached jwtVersion checks, surface Redis in /api/health and deploy warnings, add admin help-center ticket reply UI, rate-limit API tickets/reactions/referral claims, and revoke PATs on sign-out-everywhere. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
1 parent
3bb96eb6f3
commit
968ca15c27
23 files changed
+1344
-205
No files matched your search
@@ -0,0 +1,73 @@
|
||||
import "server-only";
|
||||
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { redis } from "@/lib/redis";
|
||||
|
||||
const MEMORY_TTL_MS = 60_000;
|
||||
const REDIS_TTL_SEC = 60;
|
||||
const memory = new Map<number, { version: number; expiresAt: number }>();
|
||||
|
||||
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<number | null> {
|
||||
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 prisma.user.findUnique({
|
||||
where: { id: userId },
|
||||
select: { websiteJwtVersion: true },
|
||||
});
|
||||
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<void> {
|
||||
memory.delete(userId);
|
||||
if (redis) {
|
||||
try {
|
||||
await redis.del(redisKey(userId));
|
||||
} catch {
|
||||
/* non-critical */
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user