feat: jwt cache, redis health, help-ticket admin, and write rate limits
Deploy / release (push) Skipped
Deploy / deploy (push) Successful in 1m33s

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:
SimoandCursor committed 2026-07-21 21:58:48 +02:00
1 parent 3bb96eb6f3
commit 968ca15c27
23 files changed
+1344 -205

No files matched your search

+51
View File
@@ -0,0 +1,51 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
const mockFindUnique = vi.hoisted(() => vi.fn());
const mockRedisGet = vi.hoisted(() => vi.fn());
const mockRedisSetex = vi.hoisted(() => vi.fn());
const mockRedisDel = vi.hoisted(() => vi.fn());
vi.mock("@/lib/prisma", () => ({
prisma: {
user: { findUnique: mockFindUnique },
},
}));
vi.mock("@/lib/redis", () => ({
redis: {
get: mockRedisGet,
setex: mockRedisSetex,
del: mockRedisDel,
},
}));
describe("jwt-version-cache", () => {
beforeEach(() => {
vi.resetModules();
vi.clearAllMocks();
mockRedisGet.mockResolvedValue(null);
mockRedisSetex.mockResolvedValue("OK");
mockRedisDel.mockResolvedValue(1);
});
it("returns DB version and caches it", async () => {
mockFindUnique.mockResolvedValue({ websiteJwtVersion: 3 });
const { getCachedJwtVersion } = await import("./jwt-version-cache");
await expect(getCachedJwtVersion(42)).resolves.toBe(3);
expect(mockFindUnique).toHaveBeenCalledTimes(1);
await expect(getCachedJwtVersion(42)).resolves.toBe(3);
expect(mockFindUnique).toHaveBeenCalledTimes(1);
});
it("invalidates memory and redis entries", async () => {
mockFindUnique.mockResolvedValue({ websiteJwtVersion: 1 });
const { getCachedJwtVersion, invalidateJwtVersionCache } = await import(
"./jwt-version-cache"
);
await getCachedJwtVersion(7);
await invalidateJwtVersionCache(7);
expect(mockRedisDel).toHaveBeenCalled();
mockFindUnique.mockResolvedValue({ websiteJwtVersion: 2 });
await expect(getCachedJwtVersion(7)).resolves.toBe(2);
});
});
+73
View File
@@ -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 */
}
}
}