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,127 @@
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { positiveBigInt } from "@/lib/api";
|
||||
import { canAccess, getAdminContext, PERMS } from "@/lib/permissions";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { AdminHelpTicketDetail } from "./admin-help-ticket-detail";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
const STAFF_RANK_THRESHOLD = 4;
|
||||
|
||||
export default async function AdminHelpTicketDetailPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ id: string }>;
|
||||
}) {
|
||||
const { session, permissions } = await getAdminContext();
|
||||
if (!canAccess(permissions, PERMS.TICKETS_VIEW, session.user.rank)) {
|
||||
redirect("/admin");
|
||||
}
|
||||
|
||||
const { id } = await params;
|
||||
const ticketId = positiveBigInt(id);
|
||||
if (!ticketId) notFound();
|
||||
|
||||
const ticket = await prisma.websiteHelpCenterTickets.findUnique({
|
||||
where: { id: ticketId },
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
title: true,
|
||||
content: true,
|
||||
open: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!ticket) notFound();
|
||||
|
||||
const replies = await prisma.websiteHelpCenterTicketReplies.findMany({
|
||||
where: { ticketId: ticket.id },
|
||||
orderBy: { id: "asc" },
|
||||
select: {
|
||||
id: true,
|
||||
userId: true,
|
||||
content: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
const authorIds = [
|
||||
...new Set([
|
||||
...(ticket.userId != null ? [ticket.userId] : []),
|
||||
...replies.map((r) => r.userId),
|
||||
]),
|
||||
];
|
||||
|
||||
const users =
|
||||
authorIds.length > 0
|
||||
? await prisma.user.findMany({
|
||||
where: { id: { in: authorIds } },
|
||||
select: {
|
||||
id: true,
|
||||
username: true,
|
||||
rank: true,
|
||||
mail: true,
|
||||
},
|
||||
})
|
||||
: [];
|
||||
|
||||
const userById = new Map(users.map((u) => [u.id, u]));
|
||||
const openerId = ticket.userId;
|
||||
|
||||
const messages = [
|
||||
{
|
||||
key: "opening",
|
||||
userId: openerId ?? 0,
|
||||
username:
|
||||
openerId != null
|
||||
? (userById.get(openerId)?.username ?? `#${openerId}`)
|
||||
: "—",
|
||||
isStaff: false,
|
||||
content: ticket.content,
|
||||
createdAt: (ticket.createdAt ?? new Date()).toISOString(),
|
||||
},
|
||||
...replies.map((r) => {
|
||||
const user = userById.get(r.userId);
|
||||
const isStaff =
|
||||
r.userId !== openerId && (user?.rank ?? 0) >= STAFF_RANK_THRESHOLD;
|
||||
return {
|
||||
key: String(r.id),
|
||||
userId: r.userId,
|
||||
username: user?.username ?? `#${r.userId}`,
|
||||
isStaff,
|
||||
content: r.content,
|
||||
createdAt: (r.createdAt ?? new Date()).toISOString(),
|
||||
};
|
||||
}),
|
||||
];
|
||||
|
||||
const creator =
|
||||
openerId != null ? userById.get(openerId) : undefined;
|
||||
|
||||
const canEdit = canAccess(permissions, PERMS.TICKETS_EDIT, session.user.rank);
|
||||
|
||||
return (
|
||||
<AdminHelpTicketDetail
|
||||
ticket={{
|
||||
id: String(ticket.id),
|
||||
title: ticket.title,
|
||||
open: ticket.open,
|
||||
createdAt: (ticket.createdAt ?? new Date()).toISOString(),
|
||||
updatedAt: (ticket.updatedAt ?? new Date()).toISOString(),
|
||||
creator: creator
|
||||
? {
|
||||
id: creator.id,
|
||||
username: creator.username,
|
||||
rank: creator.rank,
|
||||
mail: creator.mail ?? "",
|
||||
}
|
||||
: null,
|
||||
}}
|
||||
messages={messages}
|
||||
canEdit={canEdit}
|
||||
/>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user