harden(epay): cart hygiene, auth/IDOR gates, single-page fetch, parallel downloads
Live-path hardening from the 2026-06-04 deep-dive (11 confirmed criticals). ArchiTools-only; the legacy queue is still the sole fulfiller. Security: - requireCfAccess() — staff-only, portal accounts blocked, fail-closed in-route on download / download-zip / cf-status / orders (C4 IDOR/PII) and order / recover (C3). order also enforces a daily credit cap (ANCPI_DAILY_CREDIT_CAP, default 200) and stamps userId. - /api/ancpi/test returns 404 in production — it was a GET that spends 2 real credits, CSRF-able (C5). - drop the token-metadata debug blob from the session (QW8). Correctness / robustness: - cart hygiene (C1): build the ePay cart under an invariant — the Nth add must report N items; any excess = pre-existing junk, so we wipe + abort (never submit a cart we didn't fully build). Pre-submit failures clean up our basket rows; post-submit we never touch the cart (recover owns it). metadata-less rows are deleted from the cart. - getOrderStatus fetches the whole order in ONE page (itemsPerPage, QW4); navDir loop kept only as fallback. index-fallback matches are flagged 'review' instead of silently 'completed' with a possibly-wrong PDF (R4). - downloadDocument asserts %PDF magic bytes — a login page returned mid session no longer gets stored as a .pdf (R2). Session reuse TTL aligned under ANCPI's ~10min expiry. - recover accepts ?extractId= and pre-submit states; retry buttons in the ePay tab re-run poll+download with no new charge (QW2/QW3). Performance: - parallel document downloads (V1, concurrency 4); poll writes only on status change via updateMany (QW5); getNextFileIndex scans the cadastral prefix instead of the whole bucket — and actually works now (it was ^-anchoring the full key, so every file got index 1) (V2); download-zip streams instead of buffering the whole archive, capped at 100 (V3). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/core/storage/prisma";
|
||||
import { requireCfAccess } from "@/core/auth/cf-access";
|
||||
|
||||
export const runtime = "nodejs";
|
||||
export const dynamic = "force-dynamic";
|
||||
@@ -21,6 +22,11 @@ export const dynamic = "force-dynamic";
|
||||
*/
|
||||
export async function GET(req: Request) {
|
||||
try {
|
||||
const access = await requireCfAccess();
|
||||
if (!access.ok) {
|
||||
return NextResponse.json({ error: access.error }, { status: access.status });
|
||||
}
|
||||
|
||||
const url = new URL(req.url);
|
||||
const nrCadastralParam = url.searchParams.get("nrCadastral") || undefined;
|
||||
const status = url.searchParams.get("status") || undefined;
|
||||
@@ -92,7 +98,25 @@ export async function GET(req: Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Also check for active (in-progress) orders
|
||||
// QW6: surface terminal failure/review so the UI can flag them (a
|
||||
// cadastral whose latest record failed used to show as "none"). Only
|
||||
// applies where there's no valid extract — a fresh valid one wins.
|
||||
const attentionRecords = await prisma.cfExtract.findMany({
|
||||
where: {
|
||||
nrCadastral: { in: cadastralNumbers },
|
||||
status: { in: ["failed", "review"] },
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
select: { nrCadastral: true, status: true },
|
||||
});
|
||||
for (const rec of attentionRecords) {
|
||||
if (statusMap[rec.nrCadastral] === "none") {
|
||||
statusMap[rec.nrCadastral] = rec.status; // "failed" | "review"
|
||||
}
|
||||
}
|
||||
|
||||
// Active (in-progress) orders take priority over none/failed/review/
|
||||
// expired — an in-flight re-order should read as "processing".
|
||||
const activeRecords = await prisma.cfExtract.findMany({
|
||||
where: {
|
||||
nrCadastral: { in: cadastralNumbers },
|
||||
@@ -104,8 +128,7 @@ export async function GET(req: Request) {
|
||||
});
|
||||
|
||||
for (const rec of activeRecords) {
|
||||
// If there's an active order, mark as "processing" (takes priority over "none")
|
||||
if (statusMap[rec.nrCadastral] === "none") {
|
||||
if (statusMap[rec.nrCadastral] !== "valid") {
|
||||
statusMap[rec.nrCadastral] = "processing";
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user