popertots and Sisyphus
d03ce79c19
fix: rate limiter memory pruning in Check(), services error leakage replaced with generic messages
...
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent )
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai >
2026-08-22 00:34:51 +01:00
popertots
4e398a7a2b
fix: round-2 loop-B adversarial (503c326 baseline) — B1 webhook race, APPROVED refund semantics, notification cap single-source, 2FA cooldown/StateFor hardening, register bcrypt semaphore
...
Round 2 Loop B red-team (money/security/dup-mod adversarial) findings on the full payments overhaul:
MONEY:
- HIGH: webhook COMPLETED promotion now resolves the B1 parent row (mirrors the re-poll resolveB1ParentFailed + till-sale clawback) — the sweep no longer re-replays an expired key into stacked unauthorized charges
- HIGH: A6 deposit-with-discount clamp — chargeAmount capped to max(0, remaining-discount) for ALL discount cases; overflow guard compares against the discounted remaining
- MED-HIGH: APPROVED refunds treated as NON-terminal at the webhook (event-driven, may still fail); payments call sites aligned; FAILED can now demote an APPROVED-then-failed row
- MED: B1 refund transport-error fails the row + CRITICAL immediately (no 3-charge stacking)
- MED: till_sales capped-fail surfaces the outstanding funding (gift_card_transactions trace) for manual reversal
- MED: guest-bookings cash/gift-card terminal charges now audited (NULL target); audit reordered post-commit; cancellation refunds audited
- MED: A6 no-discount skip-path returns campaign_fully_redeemed 400 (no success-shaped no-op); skip-path writes a marker row for idempotency
SECURITY:
- HIGH: notification cap centralized in adminnotify (MaxUnacknowledgedCriticalLogs) + applied at ALL insert sites (webhooks x2, jwt refresh_token_reuse, account erasure, sweep, twofa) with suppressed-insert logging; per-issue bucket for reissue alerts
- MED-HIGH: twofa.StateFor saturated state made IMMUTABLE (LastMintAt writes are no-ops; no cross-user throttling); eviction never drops in-window count>0 records
- MED: /register now uses the shared bcrypt semaphore (authBcryptSlots, 20) — botnet CPU burn bounded
- MED: NAT collateral reduced (429-reject only at top progressive tier; lower tiers sleep)
- MED: ClearMintCooldownForUser exposed for fresh-charge success; reissue cooldown-skip raises a capped alert
- LOW: audit coverage gaps (reschedule fee forgiveness, gift-card transfer, clawback) closed
DUP/MOD:
- Frontend deposit-percent literals -> POLICY constants (10 sites); LOYALTY_DISCOUNT_RATE single-sourced; generateUUID adopted; admin PaymentModal overflow-tip confirm path added; £500 gift-card cap named
Verified: 26/26 dev + 24/24 prod (CI condition), both vet tags, frontend tests+build, env-docs 42/42.
2026-08-22 00:34:50 +01:00
popertots
3866cc5963
fix: round-2 loop-A fresh review (503c326 baseline) — B1 replay cap, A6 discount record, 2FA reissue+cooldown, notification flood, lockout saturation, VAT/refund-status consolidation
...
Round 2 Loop A fresh money/security/dup-mod review. 23 findings fixed:
MONEY:
- CRITICAL: B1 duplicate auto-refund gains an attempt cap (b1_attempts col, cap 3) —
a rejected auto-refund no longer re-replays the expired key every sweep run
(which minted a stacking unauthorized charge each time); FAILED-webhook
demotion respects the cap; never re-replay a key whose B1 refund failed
- HIGH: A6 deposit_covered_by_discount skip path now APPLIES the eligible
campaign discount rows immediately (capped) instead of skipping with no
discount recorded — no more promised-discount-not-recorded overcharge
- MEDIUM: 2FA code burned by the SAVE gate is re-issued on failed
new-card+save_card charges (re-issue guard now covers req.SaveCard)
- LOW: GetBookingPaymentSummary excludes tip rows from paidAmount (remaining
now matches the authoritative tip-excluded balance)
SECURITY:
- MEDIUM: unacknowledged CRITICAL admin-notification flood capped (global cap
on critical_payment_log + refresh_token_reuse rows)
- MEDIUM: 2FA reissue no longer bypasses the mint cooldown (Check no longer
clears LastMintAt on gate-verify; cleared on terminal charge success)
- MEDIUM: twofa.StateFor map-saturation returns a shared permanently-locked
state instead of a fresh 5-guess budget per request
- MEDIUM: ProgressiveRateLimit rejects 429 past maxProgressiveSleepDelayMs
instead of sleeping unboundedly; login bcrypt concurrency semaphore added
- LOW: loginInProgress 409->429; webhook key-set/URL-unset startup check;
email-verification per-user attempt counter
DUP/MOD:
- formatCurrency single source (frontend format.ts, 7 files consolidated);
SquareRefundStatusToLocal single source (errors.go, all sites); admin
audit-log helper dedup; SCA retry model unified (proactive on all 6
surfaces); buyDailyTotal/daily-cap mirror via backend; lock TTL from
backend; generateUUID at all card-form sites; magic numbers named
(defaultPostgresHost, epsilon, fee constants); admin CASH + gift-card
terminal charges now audited; DAV_SKIP_INIT documented in manuals
Verified: 26/26 dev + 24/24 prod (GO_TESTING=1, the CI condition), both vet
tags, frontend tests+build, env-docs 42/42.
2026-08-22 00:34:50 +01:00
popertots
fe88f2084d
fix: review-loop B — adversarial findings (sweep auto-refund, admin clamp, 2FA real challenge, opaque refresh tokens, gated client IP, GBP pence)
...
Loop B aggressive adversarial round (3 attack agents) + fix + secondary + verification:
- CRITICAL: sweep replay auto-refunds provably-created-later duplicate charges (gated on parseable CreatedAt); 22h legitimate-retry window == 22h sweep cutoff (no dead zone)
- HIGH: admin Take Payment clamps to remaining obligation (cash/giftcard/saved-card/terminal); no unintended tip from overflow; campaign credit against remaining
- HIGH: /api/services/eligible-for/{id} requires auth + owner-or-admin (DOB/age + patch-test health-data leak closed)
- HIGH: opaque refresh-token rotation (login/refresh return {token, jti, refreshToken}; refresh REQUIRES opaque token; single-use rotation; logout revokes; access token rejected at refresh)
- HIGH: saved-card charges require a REAL 2FA verification code (B6/B10) — backend gate on all 8 charge paths + shared TwoFactorCodeInput frontend component on all 7 surfaces; 2FA gate is no longer setup-flag-only
- MEDIUM: ungated CF-Connecting-IP in reserve/admin_reserve gated via exported mw.ClientIP; 2FA limiter keyed on userID alone (no header-rotation bypass); ChangePassword actually revokes JTI + refresh tokens; 2FA setup mint cooldown + persistent failed-attempt counter; campaign redemption race surfaces campaign_fully_redeemed
- Terminal saved-card VAT applied (was under-collected); age-guard reconcile failures notify; isWeakJWTSecret entropy gate; gift-card redeem per-card counter + per-user limiter; webhook signature key startup validation
- NEW internal/twofa package (single source of truth breaking the payments<->user import cycle); consolidation of duplicate 2FA hash/verify
- Frontend: refresh-token storage + rotation, TwoFactorCodeInput component, amountPaidPence in admin modal, B5/B6/B10 contract wiring; 70 frontend tests
- Tests: loop_b_fixes_test.go, internal/twofa tests, updated auth/services/profile/twofa/mw tests
All 26 backend packages pass (incl. internal/twofa); frontend 70/70 + build clean; env-docs 41/41.
2026-08-22 00:34:50 +01:00
popertots
6d82535780
fix: adversarial review round — replay-rescue double-charge, discount credit, 2FA/per-IP limits, snapshot encryption, refund reconciliation, VAT, frontend parity, tests+docs
...
Addresses the adversarial fresh-eyes audit (findings A1-A20) plus review-round fixes:
- CRITICAL A1: replay-by-key rescue cross-checks replayed CreatedAt; ccof blind-fail leaves pending with CRITICAL + notification instead of clawing back
- A2/A3/A4: till idempotency key restored to unconditional hash; tip rejected in CreateBookingPayment; campaign discount now reduces the charged amount (deposit credit)
- A5: admin notifications on blind-fail, manual-refund re-arm, cap-stranded charge-group, webhook FAILED/REJECTED refunds
- A6/A10: BuyGiftCard idempotency user-scoped; gift-card slot scan advances past failed rows
- A7/A14/A15: 2FA user+IP limiter, SNAPSHOT_ENC_KEY startup validation, accurate pepper/log-delivery docs
- A8/A9: snapshot encryption on all write+reuse sites; MPV->SPV effective voucher type (single VAT point)
- A11/A12/A13/A16: amount-aware refund reconciliation; completed-booking refund re-validation; till retry dedup; PaymentWasRefunded on SquareClient interface
- A17/A18/A19/A20: CI runs npm test; confirm_overflow_tip frontend dialog; unknown-event admin notification; mock token redaction
- M7 ConfirmOverflowTip, M9 snapshot encryption, C1 discount ordering regression test
- Frontend vitest framework (41 tests), backend coverage for fixed functions, docs corrected (2,269 tests, SUPPORT_EMAIL tokens, resolution status)
All 25 backend packages pass; frontend 41/41; build + env-docs green.
2026-08-22 00:34:50 +01:00
popertots
78e6d00dc5
fix: payments review rounds — money-safety, GDPR, security, gift-card cancel, modal stacking
...
Money-safety:
- Deterministic till idempotency fallback (Square-charging only); cash/on_the_house keep unique keys; £250 till gift-card cap; 45-char key validation
- Gift-card admin caps £250/tx + £5,000/day; user buy £500/day; BuyGiftCard allowlist unchanged
- CancelGiftCard: CCR 2013 14-day right with partial-spend refund of the unspent balance (spend verified via payments.gift_card_id); atomic vs redeem/transfer; refunds stay pending until reversal commits; admin cancel surface (AdminCancelGiftCard)
- Sweep: cancelled-booking charges failed+notified instead of silently completed; source-override replay uses live square_source_id; legacy square-less refund sweep; snapshot refresh on pending reuse
- Refund lock consolidation; recordTerminalPaymentTx shared recorder; structured Square error codes; terminal checkout CustomerID
GDPR / security:
- Notes retained as de-identified medical/safety record at erasure (single field treated as health data; rest of record wiped, no re-identification map) + comments updated per UK GDPR/Art 9/Equality Act 2010
- square_request_snapshot PII scrubbed on all erasure paths; delete_guest_user FK unlinks; verification codes + dispute reasons handled; idle/stale-guest erasure deletes Square cards/customers + CardDAV/R2
- Durable square-erasure outbox job (retry-square-erasures); 2FA dev/prod build split, pepper fail-closed, no prod code-in-log; prod 2FA delivery fail-loud without a channel
- Webhook unknown-type family split (non-money acked, money retried); untracked dispute notifications; rate-limit CF/X-Real-IP trust gating; nginx CSP nonce + api_limit
Frontend:
- Dynamic z-index stack (ui/dialog/zindex.ts) claimed in open order via data-state observer; re-claims on every reopen; removes stale !z-* overrides — nested modals (booking→user→booking) always paint newest-on-top (browser-verified 3-level + reopen)
- Mobile: iOS zoom fixes, bottom-sheet dialogs, 44px touch targets, inputmode decimal, dvh
- Gift-card buy/cancel UI, admin £250 + daily limits, cancellation/privacy/terms policy accuracy
S3:
- Connect() creates buckets before probing; in-memory fallback only on genuine unreachability; health reports degraded; stale S3_PUBLIC_URL documented (host-specific)
Tests/docs:
- 2263 test functions; all 22 backend packages green; round8/9/10 regression suites; NextEditWindowTime removes wall-clock flake; docs reconciled (notes retention, gift-card partial-use, modal T15 future work)
2026-08-22 00:34:50 +01:00
popertots
6691cd5657
fix: rate-limit per-IP keying + dev no-op, admin 2FA recovery, 2FA account UI, admin modals, tip totals
...
Rate limiting (backend):
- RateLimit/ProgressiveRateLimit now derive the per-client key from
CF-Connecting-IP, then chi's GetClientIP (the X-Real-IP value nginx sets at
main.go:323), then RemoteAddr. Previously only CF-Connecting-IP/RemoteAddr
were used, so behind the Docker nginx every client shared ONE bucket per
limiter — 10 logins/min site-wide blocked all users (the reported
'Error: Rate limit exceeded' after seeding was the login 10/min bucket
tripped by the seed's 11 logins, all keyed 127.0.0.1 in dev).
- Real implementation is now //go:build !dev || test; new
mw/ratelimit_dev.go (//go:build dev && !test) is a no-op passthrough, so
'go run -tags dev' (the dev harness) never rate-limits dev/seeding traffic,
while production and tests (-tags test,dev) keep the real limiter. The docs
(Technical Manual) already claimed dev no-op behaviour — the code now
matches. NewProgressiveRateLimiter is provided in the no-op build because
tag-free ratelimit_shared.go:104 initializes the global at package init.
Admin 2FA management (backend):
- users.two_factor_last_used_at TIMESTAMPTZ column (init-script, fresh-DB).
- AdminUserDetail now returns twoFactorEnabled/twoFactorMethod/
twoFactorLastUsedAt.
- New POST /api/admin/users/{id}/2fa/remove (admin-only): clears all 5 2FA
columns + drops the user's in-memory attempt/lockout state — an admin
recovery path when a user loses 2FA access.
- two_factor_last_used_at updated on every successful 2FA verification.
Account page (/account):
- 2FA section moved under the Notifications heading, visible to all roles;
Email/SMS toggles (Notifications styling) acting as a radio group with
'none' state; Apply button only when the selection differs from saved;
unselecting shows a payment-rules warning dialog; the dev-comment
'2FA is optional right now (REQUIRE_2FA is off)' and the 'Dev code:' debug
line are removed.
- Cards tab hidden from admin role.
Admin modals:
- User Details modal: new 'Two-Factor Authentication' section above Patch
Tests showing Enabled/Disabled, method, last-used timestamp, and a Remove
2FA button with a confirmation dialog (POST to the admin endpoint, refetch
on success).
- Booking Details modal: the customer's name now links to their User Details
modal (optional openUserModal prop threaded through admin/+page and
today/+page; other call sites unaffected).
Take Payment + /today:
- PaymentModal shows pre-tip (netTotal) and post-tip (totalWithTip) totals
with a tip-amount delta row only when a tip is selected; zero-tip flow
unchanged.
- The /today Payment button is hidden unless the booking is in_progress or
completed, matching the backend gate (was shown for confirmed/pending
bookings, producing the 'Booking must be in_progress or completed' error).
Verification: go test -tags test,dev -count=1 -parallel 8 ./... (20/20 ok
incl. new admin 2FA tests + mw tests), go build ./... and -tags dev both
compile, go vet clean, svelte-check 0 errors 0 warnings, env-docs gate OK,
docker compose config valid.
2026-08-22 00:34:49 +01:00
popertots
bbbffce20d
fix: gocritic linter issues (appendAssign, ifElseChain, regexpMust)
CI / Docker compose check (push) Successful in 51s
CI / Env docs check (push) Successful in 52s
CI / Frontend deps check (push) Successful in 54s
CI / Frontend major deps (push) Successful in 53s
CI / Secrets scan (push) Successful in 55s
CI / Nginx config check (push) Successful in 57s
CI / Go build (push) Successful in 1m0s
CI / Frontend build (push) Successful in 1m4s
CI / Knip (push) Successful in 58s
CI / Go vet (dev) (push) Successful in 1m45s
CI / Frontend a11y check (push) Successful in 2m8s
CI / Go vet (prod) (push) Successful in 2m17s
CI / go mod tidy (push) Successful in 36s
CI / Frontend QC (audit) (push) Successful in 47s
CI / Staticcheck (prod) (push) Successful in 3m15s
CI / Go vulnerabilities (push) Successful in 1m22s
CI / golangci-lint (push) Has been cancelled
CI / Staticcheck (dev) (push) Has been cancelled
CI / Security scan (dev) (push) Has been cancelled
CI / Security scan (prod) (push) Has been cancelled
CI / Tests (prod) (push) Has been cancelled
CI / Tests (dev) (push) Has been cancelled
CI / Race (prod) (push) Has been cancelled
CI / Race (dev) (push) Has been cancelled
CI / Svelte strict check (push) Has been cancelled
CI / Frontend QC (typecheck) (push) Has been cancelled
CI / Frontend QC (lint) (push) Has been cancelled
2026-07-11 18:03:05 +01:00
popertots
ce0bb43ebf
fix: auth and ratelimit middleware return JSON errors instead of text/plain
2026-07-11 16:05:56 +01:00
popertots
ac104e19c5
fix: remove rate limiter dev stub, consolidate tests under all build tags
CI / Docker compose check (push) Successful in 13s
CI / Env docs check (push) Successful in 14s
CI / Nginx config check (push) Successful in 15s
CI / Frontend major deps (push) Successful in 25s
CI / Frontend deps check (push) Successful in 26s
CI / Secrets scan (push) Successful in 35s
CI / Go build (push) Successful in 42s
CI / Frontend build (push) Successful in 45s
CI / Knip (push) Successful in 23s
CI / Frontend a11y check (push) Successful in 1m13s
CI / go mod tidy (push) Successful in 37s
CI / Go vet (prod) (push) Successful in 2m5s
CI / Go vet (dev) (push) Successful in 2m16s
CI / Frontend QC (audit) (push) Successful in 45s
CI / Go vulnerabilities (push) Successful in 1m29s
CI / Staticcheck (prod) (push) Successful in 3m3s
CI / Staticcheck (dev) (push) Successful in 3m46s
CI / Frontend QC (typecheck) (push) Successful in 2m1s
CI / golangci-lint (push) Successful in 4m19s
CI / Security scan (prod) (push) Successful in 4m23s
CI / Security scan (dev) (push) Successful in 4m35s
CI / Frontend QC (lint) (push) Successful in 1m51s
CI / Svelte strict check (push) Successful in 1m21s
CI / Tests (prod) (push) Successful in 3m39s
CI / Tests (dev) (push) Failing after 3m50s
CI / Race (prod) (push) Successful in 7m4s
CI / Race (dev) (push) Failing after 7m12s
2026-07-11 14:41:58 +01:00
popertots and Sisyphus
510828c924
chore: run go fix for Go 1.26 modernization
...
CI / Go vulnerabilities (push) Successful in 1m10s
CI / Build & Vet (push) Successful in 1m39s
CI / Frontend build (gate) (push) Successful in 1m42s
CI / Frontend QC (audit) (push) Successful in 56s
CI / Frontend QC (typecheck) (push) Successful in 1m36s
CI / Frontend QC (lint) (push) Successful in 1m51s
CI / Tests (prod) (push) Has been cancelled
CI / Tests (dev) (push) Has been cancelled
CI / Race (prod) (push) Has been cancelled
CI / Race (dev) (push) Has been cancelled
106 files: interface{}→any, strings.Split→SplitSeq, CutPrefix/Cut, strings.Builder, slices.Contains, remove redundant // +build directives, gofmt import ordering and indentation.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent )
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai >
2026-07-09 17:25:23 +01:00
popertots and Sisyphus
6064a06b7d
refactor: extract shared rate limiter types into ratelimit_shared.go
...
Move RateLimiter, ProgressiveRateLimiter, and cleanup functions to ratelimit_shared.go. Add dev and prod rate limiter tests. Remove inline cleanup goroutines.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent )
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai >
2026-07-07 00:09:43 +01:00
popertots and Sisyphus
e4b9003439
refactor(handlers): migrate remaining backend handlers to clock.Now() and transaction patterns
...
Apply clock.Now() migration, transaction wrapping, and minor refactors across admin, scheduling, today, user, auth handler, notifications, webhooks, services, portfolio, ratelimit, testutils, and main.go.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent )
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai >
2026-06-24 23:43:50 +01:00
popertots and Sisyphus
b253ab9732
feat(backend): update rate limiting middleware
...
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent )
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai >
2026-06-18 16:26:06 +01:00
popertots and Sisyphus
306f942d00
feat(mw): add dev-mode rate limiting bypass
...
Add build tag split: real rate limiter under !dev, no-op under dev build tag.
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent )
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai >
2026-05-31 18:48:53 +01:00
popertots and Sisyphus
700b7c1152
fix: extract client IP via net.SplitHostPort consistently
...
Ultraworked with Sisyphus (https://github.com/code-yeongyu/oh-my-openagent )
Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai >
2026-05-31 10:55:23 +01:00
popertots
b4d91d5dc0
Security pass
2026-02-20 12:59:10 +00:00
popertots
f9eec94f2f
Security: add rate limiting, input validation, and filter category
...
validation
Backend:
- Add rate limiting middleware (mw/ratelimit.go) - in-memory per-IP
limiter
- Apply rate limits per endpoint group:
- Public read-only: 120/min
- Registration: 10/min
- Portfolio filters: 60/min
- Authenticated users: 120/min
- Admin: none (trusted)
- Add 256 char input length validation on portfolio endpoints
- Validate filter categories exist in DB before querying
- Secure GetImage endpoint: only allow UUID or numeric timestamp (15-20
digits)
- Remove pattern-based image lookup to prevent enumeration
- Add services validation: name (100), duration (1-480), patch test
(0-168)
Frontend:
- Add maxlength=256 to portfolio tag/search inputs
- Add maxlength to registration: name (50), email (255), phone (20),
password (72)
- Add maxlength=100 to service name input
2026-02-20 12:03:05 +00:00