Files
threadcount-community/app/api/ops/controls/route.ts
T
ThreadCount 1bc2de655a ThreadCount Community edition
Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
2026-09-13 08:45:19 +10:00

67 lines
3.2 KiB
TypeScript

import { NextRequest, NextResponse } from "next/server";
import { sameOriginJson } from "@/lib/csrf";
import { allow, clientIp } from "@/lib/ratelimit";
import { currentOperator } from "@/lib/ops/session";
import { ControlError, deleteFacility, PLAN_NOTE_MAX, planControl, resetDemoNow, setSwitch, type PlanAct } from "@/lib/ops/controls";
export const dynamic = "force-dynamic";
/* The console's one write endpoint. Each action is a function in lib/ops/controls.ts; this route
* checks the operator, shapes the input and turns a ControlError into a status. */
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);
if (!allow("ops-controls:" + op.id, 30, 15 * 60 * 1000)) {
return NextResponse.json({ error: "Too many changes — try again in a few minutes." }, { status: 429 });
}
let body: Record<string, unknown>;
try { body = await req.json(); } catch { return NextResponse.json({ error: "Bad request" }, { status: 400 }); }
const s = (k: string, max = 200) => String(body[k] ?? "").slice(0, max);
const facilityId = s("facilityId", 40);
const idOk = /^[a-z0-9]{20,40}$/.test(facilityId);
try {
switch (s("action", 40)) {
case "switch": {
const key = s("key", 40);
if (key !== "signupsDisabled" && key !== "demoDisabled" && key !== "plansLive") return NextResponse.json({ error: "Unknown switch" }, { status: 400 });
await setSwitch(op, key, body.value === true, ip);
return NextResponse.json({ ok: true });
}
case "demo.reset":
await resetDemoNow(op, ip);
return NextResponse.json({ ok: true });
case "plan": {
if (!idOk) return NextResponse.json({ error: "Bad request" }, { status: 400 });
const num = (k: string) => Number(body[k]);
let a: PlanAct;
switch (s("act", 20)) {
case "set": a = { act: "set", plan: s("plan", 40), planNote: s("planNote", PLAN_NOTE_MAX + 1), grandfathered: typeof body.grandfathered === "boolean" ? body.grandfathered : undefined }; break;
case "trial": a = { act: "trial", days: num("days") }; break;
case "paid": a = { act: "paid", months: num("months") }; break;
case "readonly": a = { act: "readonly", on: body.on === true }; break;
case "free": a = { act: "free" }; break;
default: return NextResponse.json({ error: "Unknown plan action" }, { status: 400 });
}
await planControl(op, facilityId, a, ip);
return NextResponse.json({ ok: true });
}
case "facility.delete": {
if (!idOk) return NextResponse.json({ error: "Bad request" }, { status: 400 });
const r = await deleteFacility(op, facilityId, s("confirm", 200), s("code", 20), ip);
return NextResponse.json({ ok: true, deleted: r.name });
}
default:
return NextResponse.json({ error: "Unknown action" }, { status: 400 });
}
} catch (e) {
if (e instanceof ControlError) return NextResponse.json({ error: e.message }, { status: e.status });
throw e;
}
}