feat: add CSRF protection for admin API routes

Double-submit cookie pattern: login sets bh-csrf-token cookie,
proxy.ts validates X-CSRF-Token header on POST/PUT/DELETE to /api/admin/*.
New adminFetch() helper in src/lib/csrf.ts auto-includes the header.
All admin pages migrated from fetch() to adminFetch().

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-03-17 17:53:02 +03:00
parent 3ac6a4d840
commit 6cbdba2197
12 changed files with 161 additions and 53 deletions

View File

@@ -11,6 +11,7 @@ import {
GripVertical,
Check,
} from "lucide-react";
import { adminFetch } from "@/lib/csrf";
import type { TeamMember } from "@/types/content";
type Member = TeamMember & { id: number };
@@ -29,7 +30,7 @@ export default function TeamEditorPage() {
const itemRefs = useRef<(HTMLDivElement | null)[]>([]);
useEffect(() => {
fetch("/api/admin/team")
adminFetch("/api/admin/team")
.then((r) => r.json())
.then(setMembers)
.finally(() => setLoading(false));
@@ -38,7 +39,7 @@ export default function TeamEditorPage() {
const saveOrder = useCallback(async (updated: Member[]) => {
setMembers(updated);
setSaving(true);
await fetch("/api/admin/team/reorder", {
await adminFetch("/api/admin/team/reorder", {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ ids: updated.map((m) => m.id) }),
@@ -159,7 +160,7 @@ export default function TeamEditorPage() {
async function deleteMember(id: number) {
if (!confirm("Удалить этого участника?")) return;
await fetch(`/api/admin/team/${id}`, { method: "DELETE" });
await adminFetch(`/api/admin/team/${id}`, { method: "DELETE" });
setMembers((prev) => prev.filter((m) => m.id !== id));
}