344b1701dd
Uniform stock management for healthcare linen rooms. Licensed under the GNU AGPL v3.
67 lines
3.2 KiB
TypeScript
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;
|
|
}
|
|
}
|