CI (SIAX Cloud) / security (push) Successful in 22s
Security Scan (SIAX Cloud) / security (push) Successful in 20s
CI (SIAX Cloud) / quality (push) Successful in 39s
Security Scan (SIAX Cloud) / security (pull_request) Successful in 24s
CI (SIAX Cloud) / security (pull_request) Successful in 25s
CI (SIAX Cloud) / quality (pull_request) Successful in 40s
A live compliance audit against 0pt.app confirmed both endpoints returned
HTTP 200 with zero authentication -- the full order queue and financial
pipeline aggregates (deposit/pipeline totals) were readable by anyone.
Root cause: middleware.ts's matcher only covers /api/0pt/:path*, by design
("does NOT touch ... the unrelated payment/order APIs"), so nothing upstream
of these two handlers ever verified the caller.
Fix: both GET handlers now call the same fail-closed Zitadel verifier
/api/0pt/* relies on (resolveAuthContext, src/auth/context.ts) directly, via
a new thin wrapper (src/auth/require-route-auth.ts) that maps a rejected
decision to the same 401/403 JSON shape middleware.ts already returns. The
wrapper re-verifies the bearer token itself rather than trusting any
x-0pt-verified-* header on the request, since middleware never runs in front
of these routes to overwrite a client-supplied one.
POST /api/orders (anonymous draft/quote creation before the deposit-payment
step) is intentionally left as-is -- only the read paths that leaked data are
gated. Tracked as part of docs/compliance/BACKLOG.md P0-3.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
36 lines
1.7 KiB
TypeScript
36 lines
1.7 KiB
TypeScript
import { NextRequest, NextResponse } from "next/server";
|
|
import { createOrder, listOrders, type OrderConfig } from "@/lib/orders-store";
|
|
import { requireRouteAuth } from "@/src/auth/require-route-auth";
|
|
|
|
// POST /api/orders (docs/04) — create a draft order from a configurator config.
|
|
// Intentionally anonymous: a prospect drafts/estimates a job before signing in
|
|
// to pay the deposit (see docs/01, the configurator flow). Only reading back
|
|
// the order queue (GET, below) is a data-exposure risk.
|
|
export async function POST(req: NextRequest) {
|
|
let body: { flow?: string; config?: OrderConfig };
|
|
try {
|
|
body = await req.json();
|
|
} catch {
|
|
return NextResponse.json({ error: { code: "bad_json", message: "Invalid JSON" } }, { status: 400 });
|
|
}
|
|
if (!body || typeof body !== "object") {
|
|
return NextResponse.json({ error: { code: "bad_json", message: "Invalid JSON body" } }, { status: 400 });
|
|
}
|
|
const flow = body.flow || "custom";
|
|
const config = body.config || {};
|
|
const order = await createOrder(flow, config);
|
|
return NextResponse.json({ order_id: order.order_id, status: order.status, deposit_ore: order.deposit_ore, total_ore: order.total_ore }, { status: 201 });
|
|
}
|
|
|
|
// GET /api/orders — list all orders (queue + financial aggregates). This is
|
|
// outside middleware.ts's matcher (/api/0pt/:path* only), so auth is enforced
|
|
// here directly via the same fail-closed Zitadel verifier — see
|
|
// src/auth/require-route-auth.ts for why the request is re-verified rather
|
|
// than trusting a forwarded header.
|
|
export async function GET(req: NextRequest) {
|
|
const auth = await requireRouteAuth(req);
|
|
if (auth.response) return auth.response;
|
|
const orders = await listOrders();
|
|
return NextResponse.json(orders);
|
|
}
|