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:
Claude VM
2026-06-04 23:59:44 +03:00
parent f7f7c59d17
commit f49fdb1da0
13 changed files with 602 additions and 124 deletions
@@ -114,6 +114,12 @@ function statusBadge(status: string, expiresAt: string | null): StatusStyle {
className:
"bg-rose-100 text-rose-700 border-rose-200 dark:bg-rose-950/40 dark:text-rose-400 dark:border-rose-800",
};
case "review":
return {
label: "De verificat",
className:
"bg-amber-100 text-amber-800 border-amber-200 dark:bg-amber-950/40 dark:text-amber-400 dark:border-amber-800",
};
case "cancelled":
return {
label: "Anulat",
@@ -318,6 +324,26 @@ export function EpayTab() {
/* errors surfaced inline via downstream polling later */
};
/* -- Retry download (QW3) — re-runs poll+download for an already-paid
* order, no new charge. For rows that failed at the download/poll
* stage (the order exists at ANCPI but we never stored the PDF). -- */
const [retryingId, setRetryingId] = useState<string | null>(null);
const handleRetryDownload = async (order: CfExtractRecord) => {
setRetryingId(order.id);
try {
const res = await fetch(
`/api/ancpi/recover?extractId=${encodeURIComponent(order.id)}`,
);
// 409 → the row has no orderId yet (never reached ANCPI); nothing to
// recover by row. Other errors surface on the next refresh.
await res.json().catch(() => ({}));
void fetchOrders(true);
void fetchEpayStatus();
} finally {
setRetryingId(null);
}
};
/* -- Download all valid as ZIP ----------------------------------- */
const handleDownloadAll = async () => {
const validOrders = filteredOrders.filter(
@@ -812,6 +838,69 @@ export function EpayTab() {
</Tooltip>
</TooltipProvider>
)}
{/* Review row — PDF exists but match was ambiguous;
let the operator download + verify (QW3/R4). */}
{order.status === "review" && order.minioPath && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-xs text-amber-700 dark:text-amber-400"
asChild
>
<a
href={cfDownloadUrl(order)}
target="_blank"
rel="noopener noreferrer"
>
<Download className="h-3 w-3 mr-1" />
Verifica
</a>
</Button>
</TooltipTrigger>
<TooltipContent>
Descarca PDF pentru verificare manuala
(potrivire ambigua)
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
{/* Failed-with-order row — retry poll+download, no
new charge (QW3). */}
{(order.status === "failed" ||
order.status === "review") && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 px-2 text-xs"
disabled={
!epayStatus.connected ||
retryingId === order.id
}
onClick={() =>
void handleRetryDownload(order)
}
>
{retryingId === order.id ? (
<Loader2 className="h-3 w-3 mr-1 animate-spin" />
) : (
<RefreshCw className="h-3 w-3 mr-1" />
)}
Reincearca
</Button>
</TooltipTrigger>
<TooltipContent>
Reia descarcarea (fara cost nou) daca comanda
exista la ANCPI
</TooltipContent>
</Tooltip>
</TooltipProvider>
)}
</div>
</td>
</tr>