ThreadCount Community edition
Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
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 });
|
||||
}
|
||||
Reference in New Issue
Block a user