SCA is now the PRIMARY authorisation for saved-card (ccof) charges (PSR 2017 /
chargeback liability shift); the homegrown 2FA becomes a BACKUP used only when
SCA is unavailable (e.g. a bank without in-app approval), with a strict audit
trail. The 'approve in your banking app' UX comes from Square buyer
verification. Email/SMS remains the intended 2FA delivery channel; the [2FA]
stdout-log relay (TWO_FACTOR_ALLOW_LOG_DELIVERY=true) is the explicit-insecure
pre-email/SMS stopgap.
BACKEND:
- CreateTerminalPaymentRequest gains VerificationToken (forwarded to Square in
the admin saved-card branch; validated like the other charge handlers)
- Structured SCA-required error surfacing: isVerificationRequiredError +
writeVerificationRequiredResponse (HTTP 402 with {code:'verification_required'})
at all 5 charge error sites — the frontend keys on it to trigger the challenge
- requireTwoFactorForCardAccess reworked: SCA token present => 2FA skipped
(SCA primary); no token => 2FA fallback requires delivery channel + consume +
insertTwoFAFallbackAudit (admin_audit_log reason 2fa_fallback_charge,
{sca_performed:false,...}); TWO_FACTOR_FALLBACK env flag (default true) gates
the fallback; false => SCA-only posture
- MIT vs CIT: admin till saved-card + admin booking saved-card charges now flag
customer_initiated=false (merchant-initiated, no SCA, no liability shift);
customer-initiated online flows keep true
FRONTEND:
- square_card_id threaded through SavedCard/SelectableCard + admin lists
- isVerificationRequiredSignal + shouldFallbackTo2FA helpers (402 + code / text
fallback); VERIFICATION_REQUIRED_MESSAGE
- tokenizeSavedCardWithVerification (Square SDK tokenize(details, squareCardId))
with verified/challenge-cancelled/sca-unavailable/sca-failed outcomes
- Per-surface SCA retry with the SAME idempotency key + fresh verification_token
(booking/tip/till/gift-card/admin); 'waiting for approval in your banking
app' state on admin surfaces; 2FA backup-only UX in the shared composable
MOCK PARITY:
- SimulateSavedCardVerificationRequired toggle (default off) + grandfathering
- Challenge state (ApprovePendingVerification/DenyPendingVerification,
ChallengeResult config, token-encoded _ok|_deny outcome)
- One-time-use verify_mock_ token ledger + amount/source binding
- MockCardForm saved-card verification simulation + mock Approve button
- Tests: saved-card SCA gate, one-time-use, denied, amount-mismatch,
grandfathered; frontend helper tests
DOCS: payments-doc SCA appendix, Technical Manual 2FA section, README,
Overview, Feature Catalog updated to SCA-primary + 2FA-backup; env-var
documented (42/42).
26/26 backend packages; 95/95 frontend tests + build; env-docs 42/42.
173 lines
8.4 KiB
Bash
173 lines
8.4 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
|
|
# Public URL the BROWSER fetches images from. This is HOST-SPECIFIC: it must be
|
|
# the machine's current LAN IP (DHCP changes it). Run `hostname -I` to check.
|
|
# A stale IP makes images fail to load even though the objects exist in Rustfs.
|
|
S3_PUBLIC_URL=http://192.168.0.45: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 — merchant-level authorization gate on saved-card online payments (NOT
|
|
# PSD2 SCA; Square buyer verification is the SCA mechanism and is wired for
|
|
# new-card charges). Kept as an additional fraud control until Square buyer
|
|
# verification is wired for saved-card charges. 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_FALLBACK: whether the homegrown 2FA code gate may be used as a
|
|
# BACKUP authorisation for saved-card charges when Square SCA (3-D Secure /
|
|
# buyer verification) is unavailable — e.g. a customer's bank does not support
|
|
# in-app approval. Defaults true. Square 3DS2 SCA is the PRIMARY authorisation;
|
|
# when a saved-card charge carries a Square verification_token the 2FA gate is
|
|
# skipped entirely. Set TWO_FACTOR_FALLBACK=false for a security-first posture
|
|
# in which a saved-card charge without SCA cannot proceed via 2FA (the frontend
|
|
# surfaces the SCA challenge; if the bank can't complete it, the charge fails).
|
|
# 2FA fallback success always writes an admin_audit_log 2fa_fallback_charge row.
|
|
TWO_FACTOR_FALLBACK=true
|
|
# TWO_FACTOR_PEPPER — server-side pepper for HMAC-hashing 2FA codes. REQUIRED
|
|
# in production builds: code issuance FAILS CLOSED when it is unset (an
|
|
# unsalted SHA-256 digest in the 1M code space would be offline-brute-forceable
|
|
# from a log/DB leak), mirroring JWT_SECRET_KEY's fail-fast stance. Optional
|
|
# only in dev/test builds, where an unset pepper falls back to the legacy
|
|
# digest with a one-time warning. Generate with:
|
|
# openssl rand -base64 32
|
|
TWO_FACTOR_PEPPER=
|
|
# TWO_FACTOR_ALLOW_LOG_DELIVERY — defaults false. Production 2FA code issuance
|
|
# FAILS CLOSED without a delivery channel: there is no email/SMS transport yet,
|
|
# so the ONLY production channel is the operator's explicit opt-in to the
|
|
# insecure server-log delivery ([2FA] prefix — anyone with backend log access
|
|
# can defeat the gate on saved-card charges). MUST be set to true to deliver
|
|
# 2FA codes via the server log in production until email/SMS lands. Dev/test
|
|
# builds always deliver via the log and never consult this flag.
|
|
TWO_FACTOR_ALLOW_LOG_DELIVERY=false
|
|
# SNAPSHOT_ENC_KEY — base64-encoded 32-byte AES-256 key for encrypting stored
|
|
# square_request_snapshot rows (buyer PII: email + ccof card tokens) at rest in
|
|
# non-mock (production/sandbox) deployments. If unset/invalid, snapshots fall
|
|
# back to PLAINTEXT with a one-time CRITICAL log warning (money-safety first:
|
|
# the replayable snapshot must not be lost). Generate with:
|
|
# openssl rand -base64 32
|
|
SNAPSHOT_ENC_KEY=
|
|
# 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
|
|
|
|
# TRUST_PROXY_HEADERS — defaults false. Set to true ONLY when a trusted proxy
|
|
# (nginx and/or the Cloudflare edge) sits between clients and this backend and
|
|
# overwrites X-Real-IP / CF-Connecting-IP with the real client IP. When true,
|
|
# the per-IP rate limiter keys requests on those proxy-set headers and main.go
|
|
# registers chi's ClientIPFromHeader("X-Real-IP") middleware. MUST be true
|
|
# behind nginx/Cloudflare, or every request keyed by IP collapses onto the
|
|
# proxy's IP — one client exhausting the limit throttles everyone, and per-IP
|
|
# limiter protection is effectively bypassed. MUST stay false when the backend
|
|
# is origin-exposed: a client talking directly to the backend could otherwise
|
|
# rotate X-Real-IP/CF-Connecting-IP to bypass per-IP rate limiting.
|
|
TRUST_PROXY_HEADERS=false
|
|
|
|
# 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
|