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:
@@ -5,6 +5,7 @@ import { SectionEditor } from "../_components/SectionEditor";
|
||||
import { InputField, TextareaField } from "../_components/FormField";
|
||||
import { ArrayEditor } from "../_components/ArrayEditor";
|
||||
import { Plus, X, Upload, Loader2, ImageIcon, AlertCircle, Check, ChevronDown, ChevronUp, Instagram, Send, Trash2, Pencil } from "lucide-react";
|
||||
import { adminFetch } from "@/lib/csrf";
|
||||
import type { MasterClassItem, MasterClassSlot } from "@/types/content";
|
||||
|
||||
function PriceField({ label, value, onChange, placeholder }: { label: string; value: string; onChange: (v: string) => void; placeholder?: string }) {
|
||||
@@ -335,7 +336,7 @@ function ImageUploadField({
|
||||
formData.append("file", file);
|
||||
formData.append("folder", "master-classes");
|
||||
try {
|
||||
const res = await fetch("/api/admin/upload", {
|
||||
const res = await adminFetch("/api/admin/upload", {
|
||||
method: "POST",
|
||||
body: formData,
|
||||
});
|
||||
@@ -506,7 +507,7 @@ function RegistrationRow({
|
||||
instagram: `@${ig.trim()}`,
|
||||
telegram: tg.trim() ? `@${tg.trim()}` : undefined,
|
||||
};
|
||||
const res = await fetch("/api/admin/mc-registrations", {
|
||||
const res = await adminFetch("/api/admin/mc-registrations", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
@@ -647,7 +648,7 @@ function RegistrationsList({ title }: { title: string }) {
|
||||
|
||||
useEffect(() => {
|
||||
if (!title) return;
|
||||
fetch(`/api/admin/mc-registrations?title=${encodeURIComponent(title)}`)
|
||||
adminFetch(`/api/admin/mc-registrations?title=${encodeURIComponent(title)}`)
|
||||
.then((r) => r.json())
|
||||
.then((data: McRegistration[]) => {
|
||||
setCount(data.length);
|
||||
@@ -659,7 +660,7 @@ function RegistrationsList({ title }: { title: string }) {
|
||||
function toggle() {
|
||||
if (!open && regs.length === 0 && count !== 0) {
|
||||
setLoading(true);
|
||||
fetch(`/api/admin/mc-registrations?title=${encodeURIComponent(title)}`)
|
||||
adminFetch(`/api/admin/mc-registrations?title=${encodeURIComponent(title)}`)
|
||||
.then((r) => r.json())
|
||||
.then((data: McRegistration[]) => {
|
||||
setRegs(data);
|
||||
@@ -680,7 +681,7 @@ function RegistrationsList({ title }: { title: string }) {
|
||||
instagram: `@${newIg.trim()}`,
|
||||
telegram: newTg.trim() ? `@${newTg.trim()}` : undefined,
|
||||
};
|
||||
const res = await fetch("/api/admin/mc-registrations", {
|
||||
const res = await adminFetch("/api/admin/mc-registrations", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(body),
|
||||
@@ -705,7 +706,7 @@ function RegistrationsList({ title }: { title: string }) {
|
||||
}
|
||||
|
||||
async function handleDelete(id: number) {
|
||||
await fetch(`/api/admin/mc-registrations?id=${id}`, { method: "DELETE" });
|
||||
await adminFetch(`/api/admin/mc-registrations?id=${id}`, { method: "DELETE" });
|
||||
setRegs((prev) => prev.filter((r) => r.id !== id));
|
||||
setCount((prev) => (prev !== null ? prev - 1 : null));
|
||||
}
|
||||
@@ -823,7 +824,7 @@ export default function MasterClassesEditorPage() {
|
||||
|
||||
useEffect(() => {
|
||||
// Fetch trainers from team
|
||||
fetch("/api/admin/team")
|
||||
adminFetch("/api/admin/team")
|
||||
.then((r) => r.json())
|
||||
.then((members: { name: string }[]) => {
|
||||
setTrainers(members.map((m) => m.name));
|
||||
@@ -831,7 +832,7 @@ export default function MasterClassesEditorPage() {
|
||||
.catch(() => {});
|
||||
|
||||
// Fetch styles from classes section
|
||||
fetch("/api/admin/sections/classes")
|
||||
adminFetch("/api/admin/sections/classes")
|
||||
.then((r) => r.json())
|
||||
.then((data: { items: { name: string }[] }) => {
|
||||
setStyles(data.items.map((c) => c.name));
|
||||
@@ -839,7 +840,7 @@ export default function MasterClassesEditorPage() {
|
||||
.catch(() => {});
|
||||
|
||||
// Fetch locations from schedule section
|
||||
fetch("/api/admin/sections/schedule")
|
||||
adminFetch("/api/admin/sections/schedule")
|
||||
.then((r) => r.json())
|
||||
.then((data: { locations: { name: string; address: string }[] }) => {
|
||||
setLocations(data.locations);
|
||||
|
||||
Reference in New Issue
Block a user