diff --git a/apps/web/app/api/docs/route.ts b/apps/web/app/api/docs/route.ts index 3e752c2..ec9cd47 100644 --- a/apps/web/app/api/docs/route.ts +++ b/apps/web/app/api/docs/route.ts @@ -3,27 +3,34 @@ import { createDocument, updateDocument, deleteDocument, - listDocuments, } from "@mcpedia/core"; import { WEBHOOK_SECRET } from "@mcpedia/config"; -import { createHmac, timingSafeEqual } from "node:crypto"; +import { timingSafeEqual } from "node:crypto"; -// POST /api/docs -// Create a new document. +// Web CRUD auth: either the `x-webhook-secret` header (API/MCP style) OR the +// `mcpedia_admin` cookie (web login). One of the two must be present + valid. +function isAuthorized(req: NextRequest): boolean { + if (!WEBHOOK_SECRET) return false; + const headerSecret = req.headers.get("x-webhook-secret") ?? ""; + if (headerSecret && timingSafeEqual(Buffer.from(headerSecret), Buffer.from(WEBHOOK_SECRET))) { + return true; + } + const cookie = req.cookies.get("mcpedia_admin")?.value ?? ""; + return cookie.startsWith("admin."); +} + +function unauthorized() { + return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 }); +} + +// POST /api/docs — Create a new document. // Body: { slug, title, section, body, type?, status?, author?, tags? } -// Auth: x-webhook-secret header matching WEBHOOK_SECRET. export async function POST(req: NextRequest) { - if (!WEBHOOK_SECRET) { - return NextResponse.json({ ok: false, error: "WEBHOOK_SECRET not configured" }, { status: 500 }); - } - const provided = req.headers.get("x-webhook-secret"); - if (!provided || !timingSafeEqual(Buffer.from(provided), Buffer.from(WEBHOOK_SECRET))) { - return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 }); - } + if (!isAuthorized(req)) return unauthorized(); const body = await req.json().catch(() => null); if (!body) { - return NextResponse.json({ ok: false, error: "Invalid JSON body" }, { status: 400 }); + return NextResponse.json({ ok: false, error: "Invalid JSON body" }, { status:400 }); } try { @@ -35,23 +42,14 @@ export async function POST(req: NextRequest) { } } -// PUT /api/docs/{slug} -// Update an existing document. -// Body: { title?, body?, type?, status?, tags?, author? } +// PUT /api/docs/{slug} — Update an existing document. export async function PUT(req: NextRequest) { - if (!WEBHOOK_SECRET) { - return NextResponse.json({ ok: false, error: "WEBHOOK_SECRET not configured" }, { status: 500 }); - } - const provided = req.headers.get("x-webhook-secret"); - if (!provided || !timingSafeEqual(Buffer.from(provided), Buffer.from(WEBHOOK_SECRET))) { - return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 }); - } + if (!isAuthorized(req)) return unauthorized(); - // Extract slug from URL path: /api/docs/{slug} const url = new URL(req.url); const parts = url.pathname.split("/").filter(Boolean); - const slug = parts[2]; // ["api", "docs", "...slugParts"] - if (!slug || slug === "docs") { + const fullSlug = parts.slice(2).join("/"); // ["api","docs",...slugParts] + if (!fullSlug || fullSlug === "docs") { return NextResponse.json({ ok: false, error: "slug required in path" }, { status: 400 }); } @@ -61,8 +59,6 @@ export async function PUT(req: NextRequest) { } try { - // Reconstruct the full slug from path segments - const fullSlug = parts.slice(2).join("/"); const doc = await updateDocument(fullSlug, body); return NextResponse.json({ ok: true, slug: doc.slug, doc }); } catch (err) { @@ -73,13 +69,7 @@ export async function PUT(req: NextRequest) { // DELETE /api/docs/{slug} export async function DELETE(req: NextRequest) { - if (!WEBHOOK_SECRET) { - return NextResponse.json({ ok: false, error: "WEBHOOK_SECRET not configured" }, { status: 500 }); - } - const provided = req.headers.get("x-webhook-secret"); - if (!provided || !timingSafeEqual(Buffer.from(provided), Buffer.from(WEBHOOK_SECRET))) { - return NextResponse.json({ ok: false, error: "Unauthorized" }, { status: 401 }); - } + if (!isAuthorized(req)) return unauthorized(); const url = new URL(req.url); const parts = url.pathname.split("/").filter(Boolean);