Skip to content

fix: 53 verified bugs (auth, security/IDOR, scoring, brackets, websocket, nav, DB)#21

Open
itkujo wants to merge 26 commits into
feature/logto-integrationfrom
fix/bug-sweep
Open

fix: 53 verified bugs (auth, security/IDOR, scoring, brackets, websocket, nav, DB)#21
itkujo wants to merge 26 commits into
feature/logto-integrationfrom
fix/bug-sweep

Conversation

@itkujo

@itkujo itkujo commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator

Automated multi-agent bug review + fixes

This PR fixes 53 confirmed bugs found by an exhaustive automated review of feature/logto-integration (Go API + React/TS web app). Every finding was independently verified and every fix built/tested green before integration.

How it was produced

  • Review: 18 focused reviewers swept the backend logic, SQL/multi-tenancy, the Logto/OIDC auth delta, and the frontend. Each candidate bug was then adversarially verified by 2 independent skeptics (reachability + hostile-skeptic lenses). 59 raw findings → 53 confirmed, 2 disputed, 4 rejected.
  • Fixes: 11 file-disjoint batches, each fixed in an isolated git worktree and committed on its own branch, then merged here with zero conflicts.

Verification (integration tree)

Check Result
go build ./... ✅ clean
go vet ./... ✅ clean
Go logic tests (engine, bracket, middleware, incl. new regression tests) ✅ pass
vite build (full bundle) ✅ pass
tsc -b --noEmit ✅ clean

Highlights

🔴 Critical

  • Global chimw.Timeout(60s) was applied to the /ws mount → every WebSocket (live scoreboards, feeds, OBS overlays) force-disconnected after 60s. Decoupled the WS subscription from the request deadline.

🟠 High (26) — grouped:

  • Auth/Logto: /auth/me and OptionalJWT never resolved platform_admin elevation (admins saw role:"player", admin UI hidden; writes denied). Suspended/banned users kept working API keys.
  • Security / multi-tenant: scoring-preset writes open to any user; public registration leaked admin_notes; overlay-config write IDOR; registration mutations not division-scoped; upload trusted client file extension (stored XSS); MapView InfoWindow DOM XSS; ImageUpload still cookie-auth (401 under Logto).
  • Match integrity: undo restored the post-event score (no-op/corrupt); lost-update race (match read outside the lock); RecordMatchResult ignored matchID/idempotency → inflatable series wins.
  • Brackets/standings: double-elimination losers-bracket wiring collided slots → corrupt bracket persisted; standings recompute dropped forfeits.
  • Real-time FE: overlay WS reconnect race → zombie socket + cross-court data.
  • Broken navigation (×6): dashboard/public/search links missing the $sport route prefix or using slug where the API expects a numeric id → 404s.
  • FE correctness: league-announcement edit/delete didn't invalidate cache; bulk No-Show only marked the visible page; referee keyboard shortcuts fired real scoring behind the Game-Over modal; VenuePicker rendered "Unknown Venue".

🟡 21 medium incl. 8 DB soft-delete unique-constraint bugs — fixed with one new migration 00044_partial_unique_soft_delete.sql (partial unique indexes WHERE deleted_at IS NULL; no existing migration modified). 🟢 5 low.

Deploy notes

  • 00044 migration: builds partial unique indexes; will only fail if prod somehow has duplicate live rows on a constrained key (the old blanket constraint already forbade that). Dedupe live rows first if that ever happened.
  • sqlc: 3 generated files were hand-edited (local sqlc v1.31.0 vs repo v1.31.1); regenerate with v1.31.1 to confirm byte-for-byte parity.

Follow-ups (tracked separately, some being added to this PR)

  1. Division-registration mutation routes aren't behind useAuth (org isolation middleware never runs) — the cross-division IDOR is fixed at the service layer; router-level split still needed.
  2. court.go UpdateCourt/DeleteCourt miss the same ownership check now added to overlay handlers.
  3. useMatchWebSocket.ts has the same reconnect-race fixed in the overlay hook.
  4. Scoring backend defense-in-depth (reject duplicate /point during the unconfirmed-game window from non-UI clients).
  5. API-key revocation at suspend/ban time (auth path already closes the bypass).

🤖 Generated with Claude Code

Daniel Velez and others added 26 commits June 30, 2026 10:15
…er refill, trim CORS origins

- ws/handler.go: subscribe with context.WithoutCancel so the global
  chimw.Timeout(60s) no longer forcibly closes every WebSocket after 60s;
  disconnect teardown still handled by cancelSub + read-pump done channel
- middleware/rate_limit.go: credit only whole-interval refills and advance
  lastSeen by the consumed time, preserving sub-interval remainder so a
  steadily-polling client reaches the sustained rate instead of hard-locking
- config/config.go: TrimSpace each CORS_ALLOWED_ORIGINS entry and drop empties
  so the WS origin allowlist matches CORS even with spaces after commas

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lug/email/name

Soft-deleted rows kept occupying table-level UNIQUE constraints
(teams.slug, venues.slug, leagues.slug, tournaments.slug,
seasons(league_id,slug), divisions(tournament_id,slug),
pods(division_id,name), users.email) even though the app's existence
checks all filter deleted_at IS NULL, causing 23505 unique_violation on
recreate/re-register. New migration 00044 drops each blanket UNIQUE and
recreates it as a partial unique index scoped to live rows (matching the
courts 00006 pattern), with a full goose Down to restore the originals.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…irmation windows

- RefMatchConsole: gate keyboard shortcut hook on !gameOverPrompt && !matchOverPrompt so keys don't mutate match state behind the Game/Match Over modal
- ScorekeeperMatchConsole: gate keyboard hook on scoring/confirm in-flight pending state and mirror confirm-pending in the scoreboard pending prop to close the auto-confirm race

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- useOverlayWebSocket: replace shared closedByUnmountRef with a per-run
  cancelled flag captured in the effect closure, and guard each onclose
  against the run-local sockets map (ws identity check). Reset
  state/sockets/attempts/timers maps per run. Stops torn-down runs from
  rescheduling connect() to a stale court/match, leaking zombie sockets,
  and flapping the live aggregate state on courtID/matchPublicID changes.
- applyMessage overlay_data: write by prefix predicate (setQueriesData)
  instead of a hardcoded (token=null, demo=false) key so token-gated and
  demo overlays get the in-place merge with no HTTP round-trip; drop the
  broad invalidate that defeated the zero-fetch design.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ason check

- standings: include forfeited matches in recompute (carry win/loss/points)
- standings: honor admin override_points and is_withdrawn when ranking
- standings: validate division belongs to the given season before recompute
- registration: count approved+checked_in for division capacity, not just approved
- court queue: order ListMatchesByCourt by court_queue_position so reorder takes effect

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…active users

- MeJWT (/auth/me): inject OrgRoleResolver and add the Management-API
  fallback JWTSession uses, so genuine platform_admins no longer report
  role='player' and get locked out of the admin/elevated SPA.
- OptionalJWT: add the same Path-2 resolver elevation so mixed-auth
  admin-only writes (standings recompute/override, tournament staff)
  authorize real platform_admins instead of 403ing them.
- ValidateApiKey: reject keys owned by non-active or soft-deleted users
  so a suspended/banned account can't keep authenticating via a
  pre-existing API key.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ulk no-show, render-phase setState

- useUpdateAnnouncement/useDeleteAnnouncement now invalidate ['announcements']
  so league announcement feeds refresh after edit/delete
- Bulk No-Show now marks every non-checked-in registration in the division
  (fetchAllRegistrations walks all pages) instead of only the current page
- RegistrationsForDivision pushes data via useEffect, not a render-phase useMemo

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…, date-only TZ

- ImageUpload: route /api/v1/uploads through apiPostForm (Bearer + X-Sport) instead of cookie auth
- VenuePicker: fetch selected venue by id via GET /api/v1/venues/{id}
- MapView: build InfoWindow content with DOM/textContent to prevent stored DOM XSS
- formatDate: parse date-only strings as local to fix US off-by-one display

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…scoped routes with correct ids

- UserDetail: impersonation onSuccess navigates to /$sportSlug/dashboard (or / fallback) instead of non-existent /dashboard
- RecentResults/UpcomingMatches: use typed /$sport/matches/$publicId with sport param from useSport()
- MyTeams: link to /$sport/teams/$teamId using numeric team.id (not slug)
- PublicDivisionDetail: bracket + matches-tab links use /$sport/matches/$publicId with 'pickleball' fallback
- SearchModal: team/org results link by numeric id (backend resolves by id, not slug)
- TournamentDirectory: reset pagination to page 1 on status filter change

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ic pagination

- upload: derive stored file extension from validated content-type, never the
  client-supplied filename (prevents stored XSS via .html/.svg)
- scoring presets: gate Create/Update/Deactivate/Activate behind platform_admin
  or tournament_director
- registrations: stop leaking admin_notes on public read paths; scope all
  single-registration writes to the URL division and gate them by role
- overlay: add CanManageCourt court-ownership check on all config writes and
  GetConfig; verify source-profile ownership in SetSourceProfile
- public tournaments: filter to public statuses in SQL with a matching count so
  offset/limit and total are computed over the same set

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…lost-update race, series + event validation

- bracket: rebuild losers-bracket internal wiring round-by-round (no more
  flat-index collisions/orphaned matches); wire WB-final loser to grand
  finals slot 2 for the 2-team (single-WB-round) case.
- engine: count SET wins (segmented by GamesPerSet/GamesToWin) for match-over
  so SetsToWin>1 no longer ends after the first set; fire IsEndChange exactly
  once per deciding-game midpoint (no double switch at 6-6).
- match service: run the engine against the row-locked match inside
  applyEngineResult (closes the lost-update race) and snapshot the PRE-action
  state in the event row so Undo restores the prior score correctly.
- RecordEvent: validate event_type against AllEventTypes and reject
  score-mutating types (routed through the engine) instead of re-implementing
  divergent inline scoring.
- match series: RecordMatchResult now validates the child match belongs to the
  series and is completed, derives the winner from the child match, and
  recomputes wins idempotently from completed child matches.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@itkujo

itkujo commented Jul 1, 2026

Copy link
Copy Markdown
Collaborator Author

Follow-up: 3 flagged residuals folded in

Added on top of the 53 fixes, verified green (go build/vet + logic tests + tsc):

  • Registration mutations now behind auth — split the /divisions/.../registrations group (mirroring the /matches split-auth idiom): the 3 public GET reads stay public; all mutations (register / status / seed / placement / check-in / withdraw / admin-notes / bulk-no-show) are now behind useAuthRequireSportMatchesJWT runs, closing the org-isolation gap. Handler role checks unchanged.
  • Court ownershipUpdateCourt/DeleteCourt now call a new VenueService.CanManageCourt (same ownership logic as the overlay write guard: platform_admin OR court creator OR owning-venue manager) → 403 on non-owners, 404 on missing.
  • useMatchWebSocket reconnect race — replaced the shared unmount ref with per-run cancellation + a stale-socket guard (wsRef.current !== ws), mirroring the overlay-hook fix; legitimate reconnect catch-up invalidations preserved.

Reviewer note: court-ownership logic is intentionally duplicated onto VenueService (which already owns court CRUD) rather than coupling CourtHandler to OverlayService; a later pass could hoist it to one shared source of truth. Deeper residuals (#4 scoring backend defense-in-depth, #5 API-key revocation-on-suspend) remain documented for a separate pass.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant