Release: MFA, JWKS auth, run ownership, and dependency updates #79

Merged
TheFurya merged 36 commits from develop into main 2026-03-22 11:43:03 +01:00
Owner

Summary

  • Optional TOTP MFA for email/password accounts
  • JWT verification migrated from HS256 to JWKS for improved security
  • Run ownership enforcement on all mutation endpoints, with owner info displayed in admin pages
  • Genlocke visibility inferred from the first leg's run
  • Dependency update: @tanstack/react-query to v5.94.5

Changes

63 files changed, 2649 insertions, 283 deletions across backend and frontend.

## Summary - **Optional TOTP MFA** for email/password accounts - **JWT verification migrated from HS256 to JWKS** for improved security - **Run ownership enforcement** on all mutation endpoints, with owner info displayed in admin pages - **Genlocke visibility** inferred from the first leg's run - **Dependency update:** @tanstack/react-query to v5.94.5 ## Changes 63 files changed, 2649 insertions, 283 deletions across backend and frontend.
TheFurya added 18 commits 2026-03-22 09:28:35 +01:00
Add require_run_owner helper in auth.py that enforces ownership on
mutation endpoints. Unowned (legacy) runs are now read-only.

Applied ownership checks to:
- All 4 encounter mutation endpoints
- Both boss result mutation endpoints
- Run update/delete endpoints
- All 5 genlocke mutation endpoints (via first leg's run owner)

Also sets owner_id on run creation in genlockes.py (create_genlocke,
advance_leg) and adds 22 comprehensive ownership enforcement tests.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add useAuth and canEdit logic to RunEncounters.tsx
- Guard all mutation triggers (Log Shiny, Log Egg, End Run, Randomize All,
  HoF Edit, Boss Battle, route/team clicks, Advance to Next Leg)
- Update RunDashboard.tsx canEdit to be isOwner only (no unowned fallback)
- Add read-only banner for non-owner viewers in both pages

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
feat: show owner info in admin pages
All checks were successful
CI / backend-tests (pull_request) Successful in 29s
CI / frontend-tests (pull_request) Successful in 29s
a3f332f82b
- Add Owner column to AdminRuns.tsx and AdminGenlockes.tsx
- Add owner filter dropdown to both admin pages
- Add owner field to GenlockeListItem schema (resolved from first leg's run)
- Update frontend types for GenlockeListItem

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Genlockes now inherit visibility from their first leg's run:
- Private runs make genlockes hidden from public listings
- All genlocke read endpoints now accept optional auth
- Returns 404 for private genlockes to non-owners

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
feat: add optional TOTP MFA for email/password accounts
All checks were successful
CI / backend-tests (pull_request) Successful in 26s
CI / frontend-tests (pull_request) Successful in 28s
7a828d7215
- Add MFA enrollment UI in new Settings page with QR code and backup secret
- Add TOTP challenge step to login flow for enrolled users
- Check AAL after login and show TOTP input when aal2 required
- Add disable MFA option with TOTP re-verification
- Only show MFA options for email/password users (not OAuth)
- Add Settings link to user dropdown menu

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
feat: migrate JWT verification from HS256 shared secret to JWKS
All checks were successful
CI / backend-tests (pull_request) Successful in 28s
CI / frontend-tests (pull_request) Successful in 28s
177c02006a
Replace symmetric HS256 JWT verification with asymmetric RS256 using JWKS.
Backend now fetches and caches public keys from Supabase's JWKS endpoint
instead of using a shared secret.

- Add cryptography dependency for RS256 support
- Use PyJWKClient to fetch/cache JWKS from {SUPABASE_URL}/.well-known/jwks.json
- Remove SUPABASE_JWT_SECRET from config, docker-compose, deploy workflow, .env
- Update tests to use RS256 tokens with mocked JWKS client

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
chore(deps): update dependency @tanstack/react-query to v5.94.5
All checks were successful
CI / backend-tests (pull_request) Successful in 27s
CI / frontend-tests (pull_request) Successful in 28s
e279fc76ee
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reviewed-on: #77
Merge branch 'develop' into feature/enforce-run-ownership-on-all-mutation-endpoints
All checks were successful
CI / backend-tests (pull_request) Successful in 33s
CI / frontend-tests (pull_request) Successful in 29s
38b1156a95
Merge pull request 'Enforce run ownership and show owner info' (#74) from feature/enforce-run-ownership-on-all-mutation-endpoints into develop
All checks were successful
CI / backend-tests (push) Successful in 29s
CI / frontend-tests (push) Successful in 29s
8be9718293
Reviewed-on: #74
Merge pull request 'Add optional TOTP MFA for email/password accounts' (#76) from feature/optional-totp-mfa into develop
All checks were successful
CI / backend-tests (push) Successful in 31s
CI / frontend-tests (push) Successful in 29s
50ed370d24
Reviewed-on: #76
Replace symmetric HS256 JWT verification with asymmetric RS256 using JWKS.
Backend now fetches and caches public keys from Supabase's JWKS endpoint
instead of using a shared secret.

- Add cryptography dependency for RS256 support
- Use PyJWKClient to fetch/cache JWKS from {SUPABASE_URL}/.well-known/jwks.json
- Remove SUPABASE_JWT_SECRET from config, docker-compose, deploy workflow, .env
- Update tests to use RS256 tokens with mocked JWKS client

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Merge branch 'feature/migrate-jwt-verification-to-jwks' of https://gitea.nerdboden.de/pokemon/nuzlocke-tracker into feature/migrate-jwt-verification-to-jwks
All checks were successful
CI / backend-tests (pull_request) Successful in 31s
CI / frontend-tests (pull_request) Successful in 29s
d23e24b826
Merge pull request 'chore(deps): update dependency @tanstack/react-query to v5.94.5' (#78) from renovate/tanstack-react-query-5.x into develop
Some checks failed
CI / frontend-tests (push) Has been cancelled
CI / backend-tests (push) Has been cancelled
d541b92253
Reviewed-on: #78
Merge pull request 'Migrate JWT verification from HS256 to JWKS' (#75) from feature/migrate-jwt-verification-to-jwks into develop
All checks were successful
CI / backend-tests (push) Successful in 29s
CI / frontend-tests (push) Successful in 28s
0ec1beac8f
Reviewed-on: #75
TheFurya added 2 commits 2026-03-22 09:41:46 +01:00
fix: add HS256 fallback for JWT verification in local dev
All checks were successful
CI / backend-tests (pull_request) Successful in 29s
CI / frontend-tests (pull_request) Successful in 29s
af55cdd8a6
Local GoTrue signs JWTs with HS256, but the JWKS endpoint returns an
empty key set since there are no RSA keys. Fall back to HS256 shared
secret verification when JWKS fails, using SUPABASE_JWT_SECRET.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Reviewed-on: #80
TheFurya added 1 commit 2026-03-22 09:42:21 +01:00
TheFurya added 2 commits 2026-03-22 09:53:52 +01:00
fix: use separate except clauses for JWT verification fallback
All checks were successful
CI / backend-tests (pull_request) Successful in 29s
CI / frontend-tests (pull_request) Successful in 29s
41a18edb4f
ruff format strips parentheses from `except (A, B):`, turning it into
Python 2 comma syntax that only catches the first exception. Use
separate except clauses so PyJWKClientError is actually caught.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Merge pull request 'Fix except clause syntax in JWT verification fallback' (#81) from feature/fix-except-clause-syntax-in-jwt-verification into develop
All checks were successful
CI / backend-tests (push) Successful in 30s
CI / frontend-tests (push) Successful in 28s
94cc74c0fb
Reviewed-on: #81
TheFurya added 1 commit 2026-03-22 09:57:03 +01:00
fix: catch PyJWKSetError in JWT verification fallback
All checks were successful
CI / backend-tests (push) Successful in 29s
CI / frontend-tests (push) Successful in 28s
ac0a04e71f
PyJWKSetError is not a subclass of PyJWKClientError — they are siblings
under PyJWTError. The empty JWKS key set error was not being caught.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
TheFurya added 8 commits 2026-03-22 11:35:39 +01:00
Adds token expiry checking and automatic refresh to prevent intermittent
401 errors when the cached session token expires between interactions.

- Check token expiry (60s buffer) before each API call
- Add 401 interceptor that retries once with refreshed token
- Explicitly enable autoRefreshToken in Supabase client

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Work was already committed (3bd24fc) and merged to develop.
Crash recovery bean nuzlocke-tracker-ks9c also resolved.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Crash recovery for nuzlocke-tracker-f2hs: MFA feature was already
implemented and merged via PR #76. Verified code, tests pass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The implementation was already complete and merged - just needed
the beans marked as done after agent crash.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Original bean (nuzlocke-tracker-2fp1) was already completed.
Commit a3f332f merged via PR #74.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
chore: scrap false-positive crash bean nuzlocke-tracker-9rm8
All checks were successful
CI / backend-tests (pull_request) Successful in 30s
CI / frontend-tests (pull_request) Successful in 28s
80d5d01993
MFA feature was already completed and merged via PR #76.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Merge pull request 'fix: proactively refresh Supabase JWT before API calls' (#84) from feature/fix-intermittent-401-errors into develop
Some checks failed
CI / frontend-tests (push) Has been cancelled
CI / backend-tests (push) Has been cancelled
d1ede63256
Reviewed-on: #84
TheFurya added 4 commits 2026-03-22 11:36:04 +01:00
Add responsive 2-column layout for the encounters page:
- Desktop (lg, ≥1024px): Encounters on left, team in sticky right sidebar
- Mobile/tablet: Keep current stacked layout

The sidebar scrolls independently when team exceeds viewport height.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
feat: make level field optional in boss defeat modal
All checks were successful
CI / backend-tests (pull_request) Successful in 29s
CI / frontend-tests (pull_request) Successful in 39s
4d6e1dc5b2
Remove the level input from the boss defeat modal since the app doesn't
track levels elsewhere. Team selection is now just checkboxes without
requiring level entry.

- Remove level input UI from BossDefeatModal.tsx
- Add alembic migration to make boss_result_team.level nullable
- Update model and schemas to make level optional (defaults to null)
- Conditionally render level in boss result display

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Merge pull request 'feat: team sidebar as floating panel on desktop' (#85) from feature/team-sidebar-desktop into develop
All checks were successful
CI / backend-tests (push) Successful in 30s
CI / frontend-tests (push) Successful in 28s
79cbb06ec9
Reviewed-on: #85
TheFurya merged commit 712badb69d into main 2026-03-22 11:43:03 +01:00
Sign in to join this conversation.
No Reviewers
No Label
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: pokemon/nuzlocke-tracker#79