import { NextRequest, NextResponse } from "next/server"; import { sameOriginJson } from "@/lib/csrf"; import { allow, clientIp } from "@/lib/ratelimit"; import { currentOperator } from "@/lib/ops/session"; import { opsDb } from "@/lib/ops/db"; import { grantReveal, REASON_MIN, REASON_MAX, REVEAL_MINUTES } from "@/lib/ops/reveal"; export const dynamic = "force-dynamic"; /* Open a thirty-minute window on one facility's coordinator contacts. The whole act — grant row, * trail row, email — is lib/ops/reveal.ts; this route only checks the operator, the facility and * the reason, and answers. The contacts are not in the response: the page reads them, through the * reveal role, on its next render. */ export async function POST(req: NextRequest) { const csrf = sameOriginJson(req); if (csrf) return NextResponse.json({ error: csrf }, { status: 403 }); const op = await currentOperator(); if (!op) return NextResponse.json({ error: "Not signed in" }, { status: 401 }); const ip = clientIp(req.headers); // Ten an hour: a reveal is a considered act, and a run of them across facilities is exactly the // pattern the limit exists to slow down. if (!allow("ops-reveal:" + op.id, 10, 60 * 60 * 1000)) { return NextResponse.json({ error: "Too many reveals in the last hour." }, { status: 429 }); } let body: { facilityId?: unknown; reason?: unknown }; try { body = await req.json(); } catch { return NextResponse.json({ error: "Bad request" }, { status: 400 }); } const facilityId = String(body.facilityId ?? "").trim(); const reason = String(body.reason ?? "").trim(); if (!/^[a-z0-9]{20,40}$/.test(facilityId)) return NextResponse.json({ error: "Bad request" }, { status: 400 }); if (reason.length < REASON_MIN) { return NextResponse.json({ error: `Give a reason — at least ${REASON_MIN} characters. It goes in the trail and in the email.` }, { status: 400 }); } if (reason.length > REASON_MAX) return NextResponse.json({ error: `Keep the reason under ${REASON_MAX} characters.` }, { status: 400 }); // The facility's name and kind come from the ordinary role; nothing here reads a contact. const f = await opsDb().facility.findUnique({ where: { id: facilityId }, select: { id: true, name: true, isDemo: true } }); if (!f) return NextResponse.json({ error: "No such facility" }, { status: 404 }); if (f.isDemo) return NextResponse.json({ error: "The demo facility has no real contacts to reveal." }, { status: 400 }); const r = await grantReveal({ operator: op, facilityId: f.id, facilityName: f.name, reason, ip }); return NextResponse.json({ ok: true, minutes: REVEAL_MINUTES, expiresAt: r.expiresAt.toISOString(), mailed: r.mailed }); }