fix: comprehensive payment system hardening (4 review passes)
CRITICAL fixes: - C1: JWT exp claim now validated via jwtauth.VerifyToken (was Decode) - C2: OverrideAmount validated post-substitution (prevents negative money minting) - C3: Terminal gift-card payments store gift_card_id; refund credits user balance - C4: Refund dedup returns stored amount, not req.Amount (prevents admin mislead) - C5: Booking recheck uses FOR UPDATE (prevents TOCTOU with cancellation) - C6: processChargeGroup idempotency key stable (charge-only, prevents double-refund) MAJOR fixes: - M2: Gift-card refund UPDATE checks RowsAffected; 0 rows -> failed - M3: ProcessCancellationRefund returns commit error (was swallowed) - M5: Dispute webhook handling (created + state.updated + disputes table) MEDIUM fixes: - ME1: CORS restricted to FRONTEND_ORIGIN env var (was reflect-any) - ME2: anonymize_user() scrubs users.notes, bookings.notes, name_history, refresh_tokens - ME3: Webhook handlers now mutate state (payment.updated, refund.updated) Frontend fixes: - Same-key retry on 503 (ambiguous failure) wired to all 8 payment flows - CHARGE_AND_STORE intent for save-card flows (SCA compliance) - Nonce staleness check verified across all flows Additional fixes from adversarial re-review: - F1: Till-sale completed dedup echoes stored amount (C4-class) - F2: Cash/giftcard terminal path uses FOR UPDATE (C5-class) - F3: Square-success UPDATE checks RowsAffected (till sales) - F4: Dispute reason truncated to 192 chars (prevents INSERT failure) - F5: Booking-user lookup failure marks refund failed (prevents silent money loss) - F6: Saved-card/tip rechecks wrapped in transaction (C5 residual) Tests: - 15 adversarial attack tests (negative override, zero override, terminal gift card, refund dedup, TOCTOU, deleted gift card, advisory lock, overcharge, zero/negative/huge amount, raw PAN, missing auth, gift card balance, concurrent refunds) - 14 webhook state tests (dispute created/state, payment/refund updated) - 3 CORS tests, 3 GDPR tests, 1 HTTP timeout test - Full suite passes with -race (25 packages, 0 failures) 25 files changed, +1532/-275 lines
This commit is contained in:
@@ -160,6 +160,10 @@ CREATE OR REPLACE FUNCTION generate_admin_audit_log_id() RETURNS CHAR(12) AS $$
|
||||
SELECT generate_short_id('admin_audit_log');
|
||||
$$ LANGUAGE sql;
|
||||
|
||||
CREATE OR REPLACE FUNCTION generate_dispute_id() RETURNS CHAR(12) AS $$
|
||||
SELECT generate_short_id('disputes');
|
||||
$$ LANGUAGE sql;
|
||||
|
||||
CREATE OR REPLACE FUNCTION generate_verification_code() RETURNS CHAR(12) AS $$ SELECT substr(encode(gen_random_bytes(6), 'hex'), 1, 12); $$ LANGUAGE sql;
|
||||
|
||||
CREATE OR REPLACE FUNCTION generate_referral_code()
|
||||
@@ -926,6 +930,8 @@ BEGIN
|
||||
phone = '+000000000000', -- NOT NULL column: use placeholder
|
||||
date_of_birth = '1900-01-01', -- NOT NULL column: use placeholder
|
||||
profile_pic_url = NULL,
|
||||
notes = NULL, -- free-text staff notes may contain PII
|
||||
last_login_at = NULL, -- removes activity metadata (right to be forgotten)
|
||||
account_role = 'guest',
|
||||
loyalty_stamps = 0,
|
||||
referral_code = NULL,
|
||||
@@ -971,6 +977,18 @@ BEGIN
|
||||
SET notes = NULL
|
||||
WHERE requested_by = target_id;
|
||||
|
||||
-- Scrub notes on this user's bookings (free-text PII written by the user)
|
||||
UPDATE bookings
|
||||
SET notes = NULL
|
||||
WHERE user_id = target_id;
|
||||
|
||||
-- Scrub previous-name history (names are PII; leaving them would let
|
||||
-- "formerly [name]" receipts reveal the erased identity)
|
||||
UPDATE name_history
|
||||
SET previous_first_name = 'Deleted',
|
||||
previous_last_name = 'User'
|
||||
WHERE user_id = target_id;
|
||||
|
||||
-- Clear notification preferences (no contractual basis after account closure)
|
||||
DELETE FROM user_notification_preferences WHERE user_id = target_id;
|
||||
|
||||
@@ -986,6 +1004,9 @@ BEGIN
|
||||
|
||||
-- Clear login audit trail (no ongoing legal basis after account closure)
|
||||
DELETE FROM login_audit WHERE user_id = target_id;
|
||||
|
||||
-- Revoke stored refresh tokens (auth credentials of an erased identity)
|
||||
DELETE FROM refresh_tokens WHERE user_id = target_id;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
@@ -1014,6 +1035,9 @@ BEGIN
|
||||
-- Clear login audit trail before deleting the user row
|
||||
DELETE FROM login_audit WHERE user_id = target_id;
|
||||
|
||||
-- Revoke stored refresh tokens (auth credentials of an erased identity)
|
||||
DELETE FROM refresh_tokens WHERE user_id = target_id;
|
||||
|
||||
DELETE FROM users WHERE id = target_id AND account_role = 'guest';
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
@@ -2305,6 +2329,28 @@ CREATE TABLE IF NOT EXISTS square_webhook_events (
|
||||
received_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- =======================================
|
||||
-- DISPUTES TABLE
|
||||
-- Chargeback/dispute tracking surfaced from Square webhooks (dispute.created,
|
||||
-- dispute.state.updated). Each row is one Square dispute against a local
|
||||
-- payment, with the terminal resolution (open → won/lost) recorded so the
|
||||
-- admin can see chargebacks in the DB-backed 'critical_payment_log'
|
||||
-- notification centre instead of un-watched CRITICAL log lines.
|
||||
-- =======================================
|
||||
CREATE TABLE IF NOT EXISTS disputes (
|
||||
id CHAR(12) PRIMARY KEY DEFAULT generate_dispute_id(),
|
||||
square_dispute_id VARCHAR(64) UNIQUE NOT NULL,
|
||||
payment_id CHAR(12) NOT NULL REFERENCES payments(id),
|
||||
status VARCHAR(32) NOT NULL DEFAULT 'open', -- open, won, lost
|
||||
amount NUMERIC(10,2) NOT NULL,
|
||||
reason VARCHAR(192),
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE INDEX idx_disputes_payment ON disputes(payment_id);
|
||||
CREATE INDEX idx_disputes_status ON disputes(status);
|
||||
|
||||
-- =======================================
|
||||
-- CARDDAV / CALDAV TABLES (used by SabreDAV sync in Go backend)
|
||||
-- =======================================
|
||||
|
||||
Reference in New Issue
Block a user