stage 4
Build and Push Docker Image / build (push) Successful in 1m6s

This commit is contained in:
jason
2026-04-21 09:29:44 -05:00
parent 41b06f89c0
commit fc5bce4868
19 changed files with 1469 additions and 190 deletions
+18 -1
View File
@@ -10,8 +10,24 @@ import { clientIp, userAgent } from "@/lib/request";
const Body = z.object({
operatorId: z.string().min(1),
pin: z.string().regex(/^\d{4}$/, "PIN must be 4 digits"),
next: z.string().optional(),
});
/**
* Accept only same-origin redirects into the operator area. A scanned QR card
* routes the operator to /op/scan/<token>; if they aren't signed in we bounce
* them to the login page with ?next=<path>, log them in, then land them back
* on the scan page. Anything that doesn't look like a plain /op/... path is
* dropped on the floor to avoid open-redirect abuse.
*/
function safeNext(raw: string | undefined): string {
if (!raw) return "/op";
if (!raw.startsWith("/op")) return "/op";
// Reject protocol-relative ("//evil.com") or backslash-smuggled inputs.
if (raw.startsWith("//") || raw.includes("\\")) return "/op";
return raw;
}
export async function POST(req: NextRequest) {
const parsed = Body.safeParse(await req.json().catch(() => ({})));
if (!parsed.success) {
@@ -19,6 +35,7 @@ export async function POST(req: NextRequest) {
}
const { operatorId, pin } = parsed.data;
const redirect = safeNext(parsed.data.next);
if (!isValidPin(pin)) {
return NextResponse.json({ error: "Invalid PIN" }, { status: 400 });
}
@@ -76,5 +93,5 @@ export async function POST(req: NextRequest) {
await audit({ actorId: user.id, action: "login", entity: "User", entityId: user.id, ipAddress: ip });
return NextResponse.json({ ok: true, redirect: "/op" });
return NextResponse.json({ ok: true, redirect });
}