fix: SCA review round + gitea pipeline green — GDPR audit scrub, backend test gaps, frontend SCA/Square-API, docs parity
7 review agents (pipeline run, self-review, codebase-context, frontend-placement, backend testing-gaps, Square-API, docs-parity) audited the SCA-primary work. ALL findings fixed, including every pre-existing red CI job: GDPR (HIGH): - anonymize_user() now scrubs admin_audit_log.target_user_id (mirrors delete_guest_user) so 2fa_fallback_charge rows (customer id + card_last4 PII) no longer survive registered-user account deletion; gdpr test added BACKEND TEST GAPS (all 10): - delivery-unavailable 503 branch: prod-tag predicate test + dev-variant marker - twoFactorFallbackEnabled alias/case/default matrix tests + exported wrapper - insertTwoFAFallbackAudit details-JSON shape + audit-row assertions for all 6 gate sites (booking/tip/gift-card/payment-method/terminal/till, both actors) - CreateTerminalPayment.VerificationToken: passthrough, too-long 400, 2FA-skip, token-less fallback + SCA-required (new terminal_sca_test.go) - isVerificationRequiredError at all 5 charge sites (402 + code:verification_required) - customer_initiated handler-level assertions (MIT false admin / CIT true customer) - Mock: ApprovePendingVerification, ChallengeResult auto/deny, _deny token suffix, parseVerifyToken unit tests FRONTEND SCA + Square-API (CRITICAL): - tokenizeSavedCardWithVerification reads result.token (the verified token) not result.verificationResult (deprecated verifyBuyer shape — saved-card SCA could never succeed in production before); parseTokenizeVerificationResult pure fn extracted + pinned in square.test.ts; 'verified' with no token proceeds tokenless - HIGH: saved-card idempotency key regenerated after a definitive 402 (fresh token under the same key = IDEMPOTENCY_KEY_REUSED dead-loop); kept on 503/cancelled - challenge-cancelled copy no longer promises a 2FA fallback the UI doesn't show; 'waiting for approval in your banking app' state on CIT surfaces - sca-unavailable demotion resets per attempt; card selection disabled mid-challenge; genuine saved-card declines no longer relabeled 'requires verification'; modal-close guard during processing; retry affordance standardized PIPELINE (every red job now green): - prod-tag build break fixed (shared square stub + test_helpers_test.go, prod-safe) - govulncheck: x/image 0.45.0 bumped (x/text resolved); go mod tidy clean - race: TestDeleteAccount_InvalidatesSquareCustomerCache made deterministic - DAV_ADMIN_PASSWORD placeholder in .env.example (compose config passes) - frontend: prettier 28 files, eslint, a11y 38 errors, knip (currentZIndex), deps in-range, audit vulns (nanoid/postcss) — all fixed; 67 vitest cases DOCS PARITY (6 DRIFTs + 5 GAPs): payments doc Ch4/Ch14/Appendix A, Technical Manual 2FA + counter-reset + payment sections, README test counts + SNAPSHOT_ENC_KEY, Feature Catalog, .env.example REQUIRE_2FA — SCA-primary/2FA-backup posture verified against code everywhere Verified: 26/26 dev + 24/24 prod packages, both vet tags, golangci-lint/staticcheck/ gosec 0 on both tags, gitleaks clean, 2,464 backend + 67 frontend tests.
This commit is contained in:
@@ -74,12 +74,18 @@ docker compose up --build -d
|
||||
|
||||
`SQUARE_WEBHOOK_NOTIFICATION_URL` and `SQUARE_WEBHOOK_SIGNATURE_KEY` in `.env` must exactly match the webhook subscription configured in the Square Dashboard. An unset URL defaults to `http://localhost:8080/webhooks/square`, which is fail-closed (503 without the signing key, 403 on missing/bad signature). If you don't need webhooks, leave both empty — the handler still rejects cleanly.
|
||||
|
||||
### Request snapshot encryption (`SNAPSHOT_ENC_KEY`)
|
||||
|
||||
The backend replays a byte-identical request to Square when it rescues a stale pending payment, so every charge's exact request body is stored on the pending row. Those snapshots contain the buyer's email and saved-card (`ccof:`) tokens — personal data — so in non-mock (sandbox/production) deployments they are encrypted at rest with AES-256-GCM under `SNAPSHOT_ENC_KEY` (a base64-encoded 32-byte key; generate with `openssl rand -base64 32`). If the key is unset or invalid, the backend logs a one-time CRITICAL warning at startup and falls back to plaintext storage — a warning, not a refusal, because money-safety first: losing a replayable snapshot would strand pending rows forever. The dev mock stores snapshots in plaintext (no real data).
|
||||
|
||||
### Two-factor authentication (2FA)
|
||||
|
||||
`REQUIRE_2FA` gates the **2FA backup** for saved-card online payments. Square **PSD2 SCA** (buyer verification via `tokenizeWithVerification`) is now the **primary authorisation** for saved-card charges — a customer-initiated stored-credential charge is a PSR 2017-regulated transaction, and Square's verification token both satisfies SCA and shifts chargeback liability to the card scheme. The homegrown 2FA gate fires **only when SCA is unavailable** (e.g. a customer's bank does not support the in-app approval flow) and is **fail-closed**: enforcement is ON by default for any `SQUARE_ENVIRONMENT` except an explicit `mock`/`dev`/`development`/`test` value — empty or unknown values are treated as production-enforced. Disable it with `REQUIRE_2FA=false` or an explicit mock env. The intended 2FA delivery channel is email/SMS (the method chosen at setup), **not yet wired**; until it lands, the 6-digit code is delivered via the server log (`[2FA]` prefix; the operator relays it) — but only in production builds when `TWO_FACTOR_ALLOW_LOG_DELIVERY=true` is set (an explicit, insecure opt-in); without it, production code issuance fails closed (503) and no user can complete 2FA setup, which then 403s every enforced saved-card payment on the fallback path. Every admin mint-or-reuse of a fallback code is recorded in the admin audit log, and every admin saved-card charge writes its own audit row.
|
||||
|
||||
`TWO_FACTOR_PEPPER` (backend `.env`) is a server-side pepper for HMAC-hashing 2FA codes: with it set, codes are hashed as HMAC-SHA256 keyed by the pepper; if unset, dev/test builds fall back to the legacy unsalted SHA-256 digest with a one-time warning. It is **required in production builds** — code issuance fails closed when it is unset, because an unsalted digest in the 1M code space would be offline-brute-forceable from a log/DB leak (mirroring the fail-fast `JWT_SECRET_KEY` check). Generate a strong random value with `openssl rand -base64 32`.
|
||||
|
||||
`TWO_FACTOR_FALLBACK` (backend `.env`, defaults to `true`) is the SCA-primary/2FA-backup posture switch. While it is on, a saved-card charge that carries no Square verification token (SCA unavailable — e.g. the customer's bank cannot run the in-app approval flow) may fall back to the homegrown 2FA code gate. Set `TWO_FACTOR_FALLBACK=false` for a security-first **SCA-only posture**: a token-less saved-card charge is then rejected 402 `verification_required` — the frontend surfaces the SCA challenge, and if the bank cannot complete it the charge fails — and cannot proceed via 2FA at all. Every charge actually authorised by the fallback writes an audited `2fa_fallback_charge` admin-audit-log row (SCA not performed, card last four, charge reference).
|
||||
|
||||
### Local dev (tmux)
|
||||
|
||||
```bash
|
||||
@@ -97,7 +103,7 @@ Default logins (password: `password`):
|
||||
```bash
|
||||
cd backend && go build -o bin/backend ./main.go
|
||||
cd frontend && npm ci && npm run build
|
||||
cd backend && go test -tags "test,dev" -count=1 -parallel 8 ./... # 2,333 tests compiled under the test,dev tags, as of 14 Aug 2026 (~2min)
|
||||
cd backend && go test -tags "test,dev" -count=1 -parallel 8 ./... # 2,440 backend test functions under the test,dev tags + 61 frontend vitest cases, as of 15 Aug 2026 (~2min)
|
||||
cd backend && go test -tags "test,dev" -count=1 -race -timeout 480s ./... # race detector (all packages, ~4min)
|
||||
# NOTE: -count=N>1 is unreliable for handlers/payments and handlers/webhooks —
|
||||
# those suites share package-global state (Square mock ledger, in-memory webhook
|
||||
|
||||
Reference in New Issue
Block a user