fix(admin): rate-limit /api/admin/backups/run to 3/min per admin

pg-boss does not deduplicate by payload, so each click on the manual
"Backup now" button enqueues a fresh data-backup job. Without a
rate-limit, an impatient admin (or compromised admin session via XSS
or token theft) can stack dozens of identical fan-out-per-user jobs
on the queue.

Three runs per minute per admin is plenty for ad-hoc snapshots — the
weekly cron in `reminder-worker` already carries the regular cadence.

Closes H3 from .planning/phase-6-code-review-findings.md (also addresses
M-1 from .planning/phase-6-security-findings.md).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-09 17:42:20 +02:00
1 parent b3f282adab
commit 8e17f227e4
1 file changed
+15 -1
+15 -1
View File
@@ -12,15 +12,29 @@ import { apiHandler, requireAdmin, HttpError } from "@/lib/api-handler";
import { apiSuccess } from "@/lib/api-response";
import { annotate } from "@/lib/logging/context";
import { getGlobalBoss } from "@/lib/jobs/boss-instance";
import { checkRateLimit } from "@/lib/rate-limit";
export const dynamic = "force-dynamic";
const DATA_BACKUP_QUEUE = "data-backup";
export const POST = apiHandler(async () => {
await requireAdmin();
const { user: admin } = await requireAdmin();
annotate({ action: { name: "admin.backups.run" } });
// pg-boss does not dedupe by payload; without a rate-limit, an
// impatient admin (or compromised admin session) can stack dozens of
// identical jobs on the queue. Three runs/min per admin is plenty
// for ad-hoc snapshots — the weekly cron carries the regular cadence.
const rl = await checkRateLimit(
`admin-backups-run:${admin.id}`,
3,
60 * 1000,
);
if (!rl.allowed) {
throw new HttpError(429, "Too many backup runs");
}
const boss = getGlobalBoss();
if (!boss) {
// 503 instead of 500 — the request is well-formed; the worker just