fix(api): validate quantity on POST /api/user-cards

Match decks handler parseInt/NaN guard so non-numeric quantities cannot
corrupt user_cards row counts from scanner add paths.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Randall Stillwell 2026-05-27 14:23:33 -05:00
parent 66717c4198
commit 8827d7b20c
3 changed files with 34 additions and 4 deletions

View file

@ -24,8 +24,8 @@ Reports posted to [PR #44](https://github.com/varutasu/tcg-vault/pull/44#issueco
| Slug | Priority | Source |
| --- | --- | --- |
| `scanner-redesign-a11y-fixes` | P1 | **In progress** — PR pending |
| `scanner-user-cards-quantity-guard` | P2 | Reviewer — parseInt/NaN guard parity with decks handler |
| `scanner-redesign-a11y-fixes` | P1 | **RESOLVED** — PR #45 |
| `scanner-user-cards-quantity-guard` | P2 | **In progress** — PR pending |
| `test-scanner-redesign-surfaces` | P2 | Reviewer — unit tests for new components + upload route |
| `document-condition-foil-destination-semantics` | P3 | Reviewer — clarify or migrate condition/foil for collection/deck rows |
| `camera-scanner-token-cleanup` | P3 | Design system — replace hardcoded hex overlay colors in CameraScanner |

View file

@ -0,0 +1,25 @@
---
name: scanner-user-cards-quantity-guard
classification: fix
success_metric: |
POST /api/user-cards rejects non-numeric and sub-1 quantity with 400, matching
the decks/[id]/cards handler contract.
depends_on:
- redesign-scanner-flow
status: open
created: 2026-05-27
---
# Convoy: scanner-user-cards-quantity-guard
P2 follow-up from `audit-redesign-scanner-flow-44` reviewer report.
## Scope
- `pages/api/user-cards.js``parseInt(quantity, 10)` + NaN / `< 1` guard on POST
## Acceptance criteria
1. `quantity: "abc"` → 400 `Quantity must be at least 1`
2. `quantity: 0` → 400
3. Valid integer ≥ 1 uses parsed value for INSERT and UPDATE increment

View file

@ -21,6 +21,11 @@ export default async function handler(req, res) {
return res.status(400).json({ error: 'Card ID is required' });
}
const parsedQuantity = parseInt(quantity, 10);
if (Number.isNaN(parsedQuantity) || parsedQuantity < 1) {
return res.status(400).json({ error: 'Quantity must be at least 1' });
}
const scanImageUrl =
typeof scanImageUrlRaw === 'string' && scanImageUrlRaw.trim().length > 0
? scanImageUrlRaw.trim()
@ -34,7 +39,7 @@ export default async function handler(req, res) {
if (existingResult.rows.length > 0) {
// Update quantity; preserve existing scan image unless a new URL is supplied
const newQuantity = existingResult.rows[0].quantity + quantity;
const newQuantity = existingResult.rows[0].quantity + parsedQuantity;
await sql`
UPDATE user_cards
SET quantity = ${newQuantity},
@ -46,7 +51,7 @@ export default async function handler(req, res) {
// Insert new record
await sql`
INSERT INTO user_cards (user_id, card_id, quantity, condition, is_foil, scan_image_url)
VALUES (${user.userId}, ${cardId}, ${quantity}, ${condition}, ${is_foil}, ${scanImageUrl})
VALUES (${user.userId}, ${cardId}, ${parsedQuantity}, ${condition}, ${is_foil}, ${scanImageUrl})
`;
}