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
@@ -4,13 +4,19 @@ import { revalidatePath } from "next/cache";
|
||||
import { redirect } from "next/navigation";
|
||||
import { auth } from "@/lib/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { clientIp, rateLimit } from "@/lib/rate-limit";
|
||||
|
||||
// The reaction set the UI offers. The action rejects anything outside this list
|
||||
// so the website_article_reactions.reaction VARCHAR(50) only ever holds known
|
||||
// values. Keep this in sync with REACTIONS in src/app/news/[slug]/page.tsx.
|
||||
const ALLOWED_REACTIONS = new Set(["like", "love", "wow"]);
|
||||
|
||||
type ReactionOutcome = "updated" | "invalid" | "not_found" | "error";
|
||||
type ReactionOutcome =
|
||||
| "updated"
|
||||
| "invalid"
|
||||
| "not_found"
|
||||
| "ratelimit"
|
||||
| "error";
|
||||
|
||||
function reactionRedirect(slug: string, outcome: ReactionOutcome): never {
|
||||
const path = slug ? `/news/${encodeURIComponent(slug)}` : "/news";
|
||||
@@ -50,58 +56,63 @@ export async function toggleReaction(formData: FormData): Promise<void> {
|
||||
redirect("/login");
|
||||
}
|
||||
|
||||
const reaction = String(formData.get("reaction") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!ALLOWED_REACTIONS.has(reaction)) {
|
||||
outcome = "invalid";
|
||||
await clientIp();
|
||||
if (!(await rateLimit(`article-reaction:${userId}`, 30, 60_000)).ok) {
|
||||
outcome = "ratelimit";
|
||||
} else {
|
||||
const articleIdRaw = String(formData.get("articleId") ?? "")
|
||||
const reaction = String(formData.get("reaction") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim();
|
||||
if (!/^\d+$/.test(articleIdRaw)) {
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!ALLOWED_REACTIONS.has(reaction)) {
|
||||
outcome = "invalid";
|
||||
} else {
|
||||
const articleId = BigInt(articleIdRaw);
|
||||
|
||||
const article = await prisma.websiteArticles.findUnique({
|
||||
where: { id: articleId },
|
||||
select: { slug: true },
|
||||
});
|
||||
if (!article) {
|
||||
outcome = "not_found";
|
||||
const articleIdRaw = String(formData.get("articleId") ?? "")
|
||||
.normalize("NFC")
|
||||
.trim();
|
||||
if (!/^\d+$/.test(articleIdRaw)) {
|
||||
outcome = "invalid";
|
||||
} else {
|
||||
slug = article.slug;
|
||||
const articleId = BigInt(articleIdRaw);
|
||||
|
||||
const existing = await prisma.websiteArticleReactions.findFirst({
|
||||
where: { userId, articleId, reaction },
|
||||
select: { id: true, active: true },
|
||||
const article = await prisma.websiteArticles.findUnique({
|
||||
where: { id: articleId },
|
||||
select: { slug: true },
|
||||
});
|
||||
|
||||
if (existing?.active) {
|
||||
await prisma.websiteArticleReactions.update({
|
||||
where: { id: existing.id },
|
||||
data: { active: false },
|
||||
});
|
||||
if (!article) {
|
||||
outcome = "not_found";
|
||||
} else {
|
||||
await prisma.websiteArticleReactions.updateMany({
|
||||
where: { userId, articleId, active: true },
|
||||
data: { active: false },
|
||||
slug = article.slug;
|
||||
|
||||
const existing = await prisma.websiteArticleReactions.findFirst({
|
||||
where: { userId, articleId, reaction },
|
||||
select: { id: true, active: true },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
if (existing?.active) {
|
||||
await prisma.websiteArticleReactions.update({
|
||||
where: { id: existing.id },
|
||||
data: { active: true },
|
||||
data: { active: false },
|
||||
});
|
||||
} else {
|
||||
await prisma.websiteArticleReactions.create({
|
||||
data: { userId, articleId, reaction, active: true },
|
||||
await prisma.websiteArticleReactions.updateMany({
|
||||
where: { userId, articleId, active: true },
|
||||
data: { active: false },
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
await prisma.websiteArticleReactions.update({
|
||||
where: { id: existing.id },
|
||||
data: { active: true },
|
||||
});
|
||||
} else {
|
||||
await prisma.websiteArticleReactions.create({
|
||||
data: { userId, articleId, reaction, active: true },
|
||||
});
|
||||
}
|
||||
}
|
||||
outcome = "updated";
|
||||
}
|
||||
outcome = "updated";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user