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
@@ -1,21 +1,36 @@
|
||||
import { env } from "@/env";
|
||||
import { apiJson } from "@/lib/api";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { redis } from "@/lib/redis";
|
||||
import { rcon } from "@/lib/services/rcon";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
/**
|
||||
* Ops health probe: database reachability, emulator RCON reachability, SMTP
|
||||
* (when configured), and runtime info. Returns HTTP 200 always (read the
|
||||
* `status`/`database` fields), so it's safe for uptime monitors that only care
|
||||
* about reachability.
|
||||
* Ops health probe: database reachability, Redis (when configured), emulator
|
||||
* RCON, SMTP (when configured), and runtime info. Returns HTTP 200 always
|
||||
* (read the `status`/`database` fields), so it's safe for uptime monitors that
|
||||
* only care about reachability.
|
||||
*/
|
||||
export async function GET() {
|
||||
const database = await prisma.$queryRaw`SELECT 1`
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
let redisOk: boolean | null = null;
|
||||
if (env.REDIS_URL) {
|
||||
if (!redis) {
|
||||
redisOk = false;
|
||||
} else {
|
||||
try {
|
||||
const pong = await redis.ping();
|
||||
redisOk = pong === "PONG";
|
||||
} catch {
|
||||
redisOk = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const emulator = await rcon.send("ping", null).catch(() => false);
|
||||
|
||||
let smtp = null;
|
||||
@@ -35,9 +50,11 @@ export async function GET() {
|
||||
.catch(() => false);
|
||||
}
|
||||
|
||||
const degraded = !database || redisOk === false;
|
||||
return apiJson({
|
||||
status: database ? "ok" : "degraded",
|
||||
status: degraded ? "degraded" : "ok",
|
||||
database,
|
||||
redis: redisOk,
|
||||
emulator,
|
||||
smtp,
|
||||
node: process.version,
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { apiError, apiJson, positiveBigInt } from "@/lib/api";
|
||||
import { bearerUserId } from "@/lib/api-auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { rateLimit } from "@/lib/rate-limit";
|
||||
import { createOwnedTicketReply } from "@/lib/services/ticket-replies";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -19,6 +20,10 @@ export async function POST(
|
||||
const uid = await bearerUserId(req);
|
||||
if (!uid) return apiError("Unauthorized", 401);
|
||||
|
||||
if (!(await rateLimit(`api-ticket-reply:${uid}`, 10, 60_000)).ok) {
|
||||
return apiError("Too many requests", 429);
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const ticketId = positiveBigInt(id);
|
||||
if (!ticketId) return apiError("Invalid ticket id", 422);
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
import { apiError, apiJson, positiveBigInt } from "@/lib/api";
|
||||
import { bearerUserId } from "@/lib/api-auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { rateLimit } from "@/lib/rate-limit";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
@@ -40,6 +41,10 @@ export async function POST(req: Request) {
|
||||
const uid = await bearerUserId(req);
|
||||
if (!uid) return apiError("Unauthorized", 401);
|
||||
|
||||
if (!(await rateLimit(`api-ticket:${uid}`, 5, 60_000)).ok) {
|
||||
return apiError("Too many requests", 429);
|
||||
}
|
||||
|
||||
const body = (await req.json().catch(() => ({}))) as {
|
||||
title?: unknown;
|
||||
content?: unknown;
|
||||
|
||||
Reference in new issue
Block a user