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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user