Files
Crussell/.env.example
T
popertots 5cc5a7f6d2 fix: review round 7 — fresh-eyes audit fixes (6 agents) + full test suites for every backend change
Fresh-eyes review round with 6 independent agents (money-safety, concurrency,
Square wire parity, security, frontend flow, testing-gaps). Every finding was
independently verified against the code before fixing. All backend changes
now carry full test suites (10+ new tests, each verified to FAIL without its
guard). All 20 packages green, race detector clean.

Money-safety:
- Gift-card purchase refunds no longer create money: manual refunds of a
  no-booking (gift-card purchase) payment are rejected with a clear message
  in the direct handler AND never re-issued by the sweep-resume path
  (processManualPaymentGroup skips them; reconcile-then-fail, no re-issue).
- BuyGiftCard no-client-key fallback: derived deterministically under the
  advisory lock (pending-row reuse fixes lost-response double-charge;
  completed-row sequence advance preserves distinct-purchase collapse fix).
- Terminal completion is never unrecorded: activeTerminalCheckoutID now calls
  recordUntrackedTerminalPayment when a provisional (tmp-) checkout is found
  COMPLETED at Square (previously only marked the row COMPLETED — a lost poll
  left the payment invisible and unrefundable).
- Sweep: provisional tmp- checkout rows are resolved against Square first
  (COMPLETED → record; live → keep guard; NOT_FOUND/CANCELED → fail;
  ambiguous → leave pending) instead of blind-failing a possibly-live
  checkout. recordUntrackedTerminalPayment re-checks the booking status
  (FOR UPDATE) and refuses to record on a cancelled booking, inserting a
  critical_payment_log admin notification instead. Till-sale post-charge
  UPDATE now requires status='pending' (no resurrection of a clawed-back sale).

Frontend (Svelte 5):
- UserPaymentModal keeps CardSelection mounted through processing (bind:this
  ref + Square iframe survive the loyalty/tokenize awaits) — new-card
  payments work again.
- BookingFlow clears the cached nonce/verification pair on any failure (retry
  re-tokenizes fresh; idempotency key retained for dedup); 409 'already paid'
  refetches the booking and reconciles depositPaid so the confirmation gate
  opens; Back button disabled during processing.
- Synchronous double-submit guards on buyGiftCard/redeemGiftCard/submitTip.

Square wire parity (mock vs real):
- processing_fee sign unified (negated at paymentFromSquare; mock agrees).
- SimulateSourceUsed (SOURCE_USED, 400) matches real CreateCard.
- GetCardsOnFile excludes disabled cards (matches ListCards).
- ForcePaymentStatus toggle + tests prove the charge path can't be status-blind.
- CreateCheckout rejects empty device_id (env fallback SQUARE_TERMINAL_DEVICE_ID);
  completed terminal checkout's payment resolvable by id.

Security:
- 2FA attempt-map data race fixed: lastAt is atomic.Int64 (nanos) — eviction
  scan reads race-free; concurrent verify+evict tests under -race.
- Backend refuses to start on weak/placeholder JWT_SECRET_KEY (<32 chars or
  known public placeholders) with openssl rand -hex 32 guidance.
- Dockerfile no longer COPYs .env (secrets injected via compose env_file).
- SabreDAV requires DAV_ADMIN_PASSWORD (no admin/admin default); compose
  fails at config time when missing.

Testing gaps closed (each verified to FAIL without its guard):
- refunded-dedup 409 (CreateBookingPayment), keyed sweep past-retention
  blind-fail, reconcile status-switch (CANCELED/FAILED/APPROVED/PENDING/unknown
  in both by-key and by-id paths), resolveChargeSource Square-failure branches,
  structured 500 / CARD_DECLINED / cancelled-context E2E (row stays pending),
  deriveBookingPaymentIdempotencyKey >45-char truncation, webhook
  findPaymentByDisputeID fallback, clawbackOneTillSale non-gift-card branch,
  dispute.evidence / terminal.checkout dispatch.

Infra:
- local-dev-2.sh fails loudly on port-5432 squatters / docker compose failures
  (previously died silently under ERR_EXIT with hidden output).
- Test harness defaults SQUARE_TERMINAL_DEVICE_ID; money_safety_fixes_test.go
  gained the missing build tag.

Verification: go test -tags test,dev -count=1 -parallel 8 ./... (20/20 ok),
-race clean on 2FA + payments money paths, go build ./... + -tags dev, go vet
clean, svelte-check 0 errors, env-docs gate OK (36 vars), docker compose
config valid.
2026-08-22 00:34:49 +01:00

125 lines
5.2 KiB
Bash

# Database Credentials for all services
# These are used by the 'postgres' service to initialize the database
# These are used by 'backend' (Go) and 'sabredav' (PHP)
POSTGRES_USER=myuser
POSTGRES_PASSWORD=mypassword
POSTGRES_DB=mydb
# and to connect to the 'postgres' service on the Docker network
POSTGRES_HOST=postgres
POSTGRES_PORT=5432
# JWT_SECRET_KEY — REQUIRED, FAIL-CLOSED. The backend refuses to start with an
# empty, weak (<32 chars), or known-placeholder value, because a shared/public
# signing key lets anyone forge an admin JWT. Generate a strong random key:
# openssl rand -hex 32
JWT_SECRET_KEY=
# S3/R2 Configuration (for image storage)
# Dev: Uses local Rustfs container (see compose.yml)
# Prod: Use Cloudflare R2 credentials
S3_ENDPOINT=http://localhost:9000
S3_PUBLIC_URL=http://192.168.1.135:9000
S3_ACCESS_KEY=rustfsadmin
S3_SECRET_KEY=rustfsadmin
S3_BUCKET=crussell
S3_PROFILE_PICS_BUCKET=crussell-profile-pics
AWS_REGION=eu-west-2
# Set DAV_SKIP_INIT=1 to skip CardDAV server initialization (e.g., in CI/test environments).
DAV_SKIP_INIT=1
# Prod only: Cloudflare R2 (overrides S3_* vars in non-dev builds).
# Local dev uses the S3_* vars above (from .env). Not needed for local builds.
R2_ENDPOINT=
# Required for production object storage — the prod S3 client (backend/internal/s3/s3.go,
# via getEnv) reads all four below; not needed for local dev builds.
R2_ACCESS_KEY=
R2_SECRET_KEY=
R2_BUCKET=crussell
R2_PUBLIC_URL=
# Square Payment Gateway
SQUARE_ACCESS_TOKEN=
SQUARE_LOCATION_ID=
SQUARE_TERMINAL_DEVICE_ID=
SQUARE_ENVIRONMENT=mock
# SQUARE_ALLOW_REAL_API — dev-build safety valve. In a `//go:build dev` build the
# backend HARD-FAILS (refuses to construct the client) when SQUARE_ENVIRONMENT
# is 'production', because a leftover/typo'd production env + real key in a dev
# shell would create real charges. Set SQUARE_ALLOW_REAL_API=1 ONLY to
# deliberately route a dev build to the real production API. Never set in a
# deployed production build.
SQUARE_ALLOW_REAL_API=
# 2FA (PSD2 SCA stand-in) for online card payments. Enforcement is FAIL-CLOSED:
# ON unless REQUIRE_2FA explicitly disables it (false/0/off/no, case-insensitive)
# OR SQUARE_ENVIRONMENT explicitly equals one of mock/dev/development/test.
# Empty or unknown SQUARE_ENVIRONMENT values are treated as production-enforced
# (a mistyped env var can never silently disarm the gate; the backend logs a
# startup warning in that case). Set REQUIRE_2FA=false only in controlled
# environments.
# Code delivery: there is NO email/SMS transport yet. The verification code is
# delivered via the server log (a [2FA]-prefixed line). In enforced/production
# environments an operator must relay the logged code to the user out-of-band;
# the API never returns the code while enforcement is ON.
REQUIRE_2FA=true
# TWO_FACTOR_PEPPER — server-side pepper for HMAC-hashing 2FA codes (optional
# pre-launch; if unset, codes are hashed without pepper and a warning is logged)
TWO_FACTOR_PEPPER=
# Webhook config MUST exactly match the Square Dashboard webhook subscription
# (URL + signature key). If SQUARE_WEBHOOK_NOTIFICATION_URL is left unset it
# defaults to http://localhost:8080/webhooks/square, which is fail-closed (503
# without the signing key, 403 on missing/bad signature). Leave both empty if
# you do not use webhooks.
SQUARE_WEBHOOK_SIGNATURE_KEY=
SQUARE_WEBHOOK_NOTIFICATION_URL=
# Frontend (public — safe for the browser). Square Web Payments SDK:
# VITE_SQUARE_APPLICATION_ID — client-side application ID (sandbox IDs start with "sandbox-")
# VITE_SQUARE_LOCATION_ID — Square location ID
# VITE_SQUARE_ENVIRONMENT — 'mock' | 'sandbox' | 'production'. Local dev: 'mock' renders the
# frontend's built-in mock card form (tokens only; pairs with
# SQUARE_ENVIRONMENT=mock above). NEVER set 'mock' in production.
VITE_SQUARE_APPLICATION_ID=
VITE_SQUARE_LOCATION_ID=
VITE_SQUARE_ENVIRONMENT=mock
# Test Database (separate from main DB)
# Used by testutils/testdb for running tests without corrupting dev data
TEST_DB_HOST=localhost
TEST_DB_DSN=
# Dev/CI mode — set to "true" to enable mock services
# Disables zxcvbn password checks, skips artificial Square mock delays,
# skips DAV sync, and relaxes production guardrails
GO_TESTING=
# CardDAV (SabreDAV) — profile photo sync
DAV_BASE_URL=http://localhost:8080
# DAV_ADMIN_PASSWORD — REQUIRED, FAIL-CLOSED. sabredav/server.php refuses to
# start when unset or set to a known weak/default value ('admin' etc.) — this
# server exposes customer PII vCards, so no public default credential is ever
# acceptable. Generate a strong random value:
# openssl rand -hex 32
DAV_ADMIN_PASSWORD=
# Logging
# Set to "true" to disable ANSI color escape sequences in log output
NO_COLOR=
# Frontend
VITE_BACKEND_URL=http://localhost:8080
# Backend CORS allowlist — comma-separated list of allowed frontend origins
# (read by the backend CORS middleware, see backend/main.go). Falls back to
# http://localhost:5173 when unset.
FRONTEND_ORIGIN=http://localhost:5173
# Local S3 (Rustfs) — requires GO_TESTING=1 or dev build tag
# These are dev-only overrides used by the dev S3 implementation
RUSTFS_ENDPOINT=http://rustfs:9000
RUSTFS_ACCESS_KEY=rustfsadmin
RUSTFS_SECRET_KEY=rustfsadmin
RUSTFS_BUCKET=crussell