From f7731b0497934839f489662d3794c2363adc2da1 Mon Sep 17 00:00:00 2001 From: Julian Tabel Date: Sat, 21 Mar 2026 11:06:53 +0100 Subject: [PATCH] Fix local login flow, add new auth epic --- ...ntend-routes-with-protectedroute-and-ad.md | 28 ++++++++++++++++ ...e-admin-status-to-frontend-via-user-api.md | 27 ++++++++++++++++ ...-aware-ui-and-role-based-access-control.md | 27 ++++++++++++++++ ...wah--add-is-admin-column-to-users-table.md | 23 +++++++++++++ ...-admin-dependency-and-protect-admin-end.md | 32 +++++++++++++++++++ ...racker-h205--auth-aware-navigation-menu.md | 27 ++++++++++++++++ ...l-gotrue-container-for-dev-auth-testing.md | 4 +-- backend/src/app/models/nuzlocke_run.py | 7 +++- docker-compose.yml | 2 +- frontend/src/lib/supabase.ts | 24 ++++++++++++-- 10 files changed, 194 insertions(+), 7 deletions(-) create mode 100644 .beans/nuzlocke-tracker-2zwg--protect-frontend-routes-with-protectedroute-and-ad.md create mode 100644 .beans/nuzlocke-tracker-5svj--expose-admin-status-to-frontend-via-user-api.md create mode 100644 .beans/nuzlocke-tracker-ce4o--auth-aware-ui-and-role-based-access-control.md create mode 100644 .beans/nuzlocke-tracker-dwah--add-is-admin-column-to-users-table.md create mode 100644 .beans/nuzlocke-tracker-f4d0--add-require-admin-dependency-and-protect-admin-end.md create mode 100644 .beans/nuzlocke-tracker-h205--auth-aware-navigation-menu.md diff --git a/.beans/nuzlocke-tracker-2zwg--protect-frontend-routes-with-protectedroute-and-ad.md b/.beans/nuzlocke-tracker-2zwg--protect-frontend-routes-with-protectedroute-and-ad.md new file mode 100644 index 0000000..0c62aed --- /dev/null +++ b/.beans/nuzlocke-tracker-2zwg--protect-frontend-routes-with-protectedroute-and-ad.md @@ -0,0 +1,28 @@ +--- +# nuzlocke-tracker-2zwg +title: Protect frontend routes with ProtectedRoute and AdminRoute +status: todo +type: task +priority: normal +created_at: 2026-03-21T10:06:20Z +updated_at: 2026-03-21T10:06:24Z +parent: nuzlocke-tracker-ce4o +blocked_by: + - nuzlocke-tracker-5svj +--- + +Use the existing \`ProtectedRoute\` component (currently unused) and create an \`AdminRoute\` component to guard routes in \`App.tsx\`. + +## Checklist + +- [ ] Wrap \`/runs/new\` and \`/genlockes/new\` with \`ProtectedRoute\` (requires login) +- [ ] Create \`AdminRoute\` component that checks \`isAdmin\` from \`useAuth()\`, redirects to \`/\` with a toast/message if not admin +- [ ] Wrap all \`/admin/*\` routes with \`AdminRoute\` +- [ ] Ensure \`/runs\` and \`/runs/:runId\` remain accessible to everyone (public run viewing) +- [ ] Verify deep-linking works (e.g., visiting \`/admin/games\` while logged out redirects to login, then back to \`/admin/games\` after auth) + +## Files to change + +- \`frontend/src/App.tsx\` — wrap routes +- \`frontend/src/components/ProtectedRoute.tsx\` — already exists, verify it works +- \`frontend/src/components/AdminRoute.tsx\` — new file diff --git a/.beans/nuzlocke-tracker-5svj--expose-admin-status-to-frontend-via-user-api.md b/.beans/nuzlocke-tracker-5svj--expose-admin-status-to-frontend-via-user-api.md new file mode 100644 index 0000000..3eda794 --- /dev/null +++ b/.beans/nuzlocke-tracker-5svj--expose-admin-status-to-frontend-via-user-api.md @@ -0,0 +1,27 @@ +--- +# nuzlocke-tracker-5svj +title: Expose admin status to frontend via user API +status: todo +type: task +priority: normal +created_at: 2026-03-21T10:06:20Z +updated_at: 2026-03-21T10:06:24Z +parent: nuzlocke-tracker-ce4o +blocked_by: + - nuzlocke-tracker-dwah +--- + +The frontend needs to know if the current user is an admin so it can show/hide the Admin nav link and protect admin routes client-side. + +## Checklist + +- [ ] Add `is_admin` field to the user response schema (`/api/users/me` endpoint) +- [ ] Update `AuthContext` to fetch `/api/users/me` after login and store `isAdmin` in context +- [ ] Expose `isAdmin` boolean from `useAuth()` hook +- [ ] Handle edge case: user exists in Supabase but not yet in local DB (first login creates user row with `is_admin=false`) + +## Files to change + +- `backend/src/app/schemas/user.py` or equivalent — add `is_admin` to response +- `backend/src/app/api/users.py` — ensure `/me` returns `is_admin` +- `frontend/src/contexts/AuthContext.tsx` — fetch and store admin status diff --git a/.beans/nuzlocke-tracker-ce4o--auth-aware-ui-and-role-based-access-control.md b/.beans/nuzlocke-tracker-ce4o--auth-aware-ui-and-role-based-access-control.md new file mode 100644 index 0000000..92c5399 --- /dev/null +++ b/.beans/nuzlocke-tracker-ce4o--auth-aware-ui-and-role-based-access-control.md @@ -0,0 +1,27 @@ +--- +# nuzlocke-tracker-ce4o +title: Auth-aware UI and role-based access control +status: todo +type: epic +created_at: 2026-03-21T10:05:52Z +updated_at: 2026-03-21T10:05:52Z +--- + +The app currently shows the same navigation menu to all users regardless of auth state. Logged-out users can navigate to protected pages (e.g., /runs/new, /admin) even though the backend rejects their requests. The admin interface has no role restriction — any authenticated user can access it. + +## Goals + +1. **Auth-aware navigation**: Menu items change based on login state (logged-out users only see public browsing options) +2. **Route protection**: Protected routes redirect to login, admin routes require admin role +3. **Admin role system**: Define which users are admins via a database field, enforce on both frontend and backend +4. **Backend admin enforcement**: Admin API endpoints (games, pokemon, evolutions, bosses, routes) require admin role, not just authentication + +## Success Criteria + +- [ ] Logged-out users see only: Home, Runs (public list), Genlockes, Stats, Sign In +- [ ] Logged-out users cannot navigate to /runs/new, /genlockes/new, or /admin/* +- [ ] Logged-in non-admin users see: New Run, My Runs, Genlockes, Stats (no Admin link) +- [ ] Admin users see the full menu including Admin +- [ ] Backend admin endpoints return 403 for non-admin authenticated users +- [ ] Admin role is stored in the `users` table (`is_admin` boolean column) +- [ ] Admin status is exposed to the frontend via the user API or auth context diff --git a/.beans/nuzlocke-tracker-dwah--add-is-admin-column-to-users-table.md b/.beans/nuzlocke-tracker-dwah--add-is-admin-column-to-users-table.md new file mode 100644 index 0000000..47636c3 --- /dev/null +++ b/.beans/nuzlocke-tracker-dwah--add-is-admin-column-to-users-table.md @@ -0,0 +1,23 @@ +--- +# nuzlocke-tracker-dwah +title: Add is_admin column to users table +status: todo +type: task +created_at: 2026-03-21T10:06:19Z +updated_at: 2026-03-21T10:06:19Z +parent: nuzlocke-tracker-ce4o +--- + +Add an `is_admin` boolean column (default `false`) to the `users` table via an Alembic migration. + +## Checklist + +- [ ] Create Alembic migration adding `is_admin: Mapped[bool]` column with `server_default="false"` +- [ ] Update `User` model in `backend/src/app/models/user.py` +- [ ] Run migration and verify column exists +- [ ] Seed a test admin user (or document how to set `is_admin=true` via SQL) + +## Files to change + +- `backend/src/app/models/user.py` — add `is_admin` field +- `backend/src/app/alembic/versions/` — new migration diff --git a/.beans/nuzlocke-tracker-f4d0--add-require-admin-dependency-and-protect-admin-end.md b/.beans/nuzlocke-tracker-f4d0--add-require-admin-dependency-and-protect-admin-end.md new file mode 100644 index 0000000..56a0e8c --- /dev/null +++ b/.beans/nuzlocke-tracker-f4d0--add-require-admin-dependency-and-protect-admin-end.md @@ -0,0 +1,32 @@ +--- +# nuzlocke-tracker-f4d0 +title: Add require_admin dependency and protect admin endpoints +status: todo +type: task +priority: normal +created_at: 2026-03-21T10:06:19Z +updated_at: 2026-03-21T10:06:24Z +parent: nuzlocke-tracker-ce4o +blocked_by: + - nuzlocke-tracker-dwah +--- + +Add a `require_admin` FastAPI dependency that checks the `is_admin` column on the `users` table. Apply it to all admin-facing API endpoints (games CRUD, pokemon CRUD, evolutions CRUD, bosses CRUD, route CRUD). + +## Checklist + +- [ ] Add `require_admin` dependency in `backend/src/app/core/auth.py` that: + - Requires authentication (reuses `require_auth`) + - Looks up the user in the `users` table by `AuthUser.id` + - Returns 403 if `is_admin` is not `True` +- [ ] Apply `require_admin` to write endpoints in: `games.py`, `pokemon.py`, `evolutions.py`, `bosses.py` (all POST/PUT/PATCH/DELETE) +- [ ] Keep read endpoints (GET) accessible to all authenticated users +- [ ] Add tests for 403 response when non-admin user hits admin endpoints + +## Files to change + +- `backend/src/app/core/auth.py` — add `require_admin` +- `backend/src/app/api/games.py` — replace `require_auth` with `require_admin` on mutations +- `backend/src/app/api/pokemon.py` — same +- `backend/src/app/api/evolutions.py` — same +- `backend/src/app/api/bosses.py` — same diff --git a/.beans/nuzlocke-tracker-h205--auth-aware-navigation-menu.md b/.beans/nuzlocke-tracker-h205--auth-aware-navigation-menu.md new file mode 100644 index 0000000..6ef95d5 --- /dev/null +++ b/.beans/nuzlocke-tracker-h205--auth-aware-navigation-menu.md @@ -0,0 +1,27 @@ +--- +# nuzlocke-tracker-h205 +title: Auth-aware navigation menu +status: todo +type: task +priority: normal +created_at: 2026-03-21T10:06:20Z +updated_at: 2026-03-21T10:06:24Z +parent: nuzlocke-tracker-ce4o +blocked_by: + - nuzlocke-tracker-5svj +--- + +Update the Layout component to show different nav links based on auth state and admin role. + +## Checklist + +- [ ] Replace static \`navLinks\` array with dynamic links based on \`useAuth()\` state +- [ ] **Logged out**: Home, Runs, Genlockes, Stats (no New Run, no Admin) +- [ ] **Logged in (non-admin)**: New Run, My Runs, Genlockes, Stats +- [ ] **Logged in (admin)**: New Run, My Runs, Genlockes, Stats, Admin +- [ ] Update both desktop and mobile nav (they share the same \`navLinks\` array, so this should be automatic) +- [ ] Verify menu updates reactively on login/logout + +## Files to change + +- \`frontend/src/components/Layout.tsx\` — make \`navLinks\` dynamic based on auth state diff --git a/.beans/nuzlocke-tracker-he1n--add-local-gotrue-container-for-dev-auth-testing.md b/.beans/nuzlocke-tracker-he1n--add-local-gotrue-container-for-dev-auth-testing.md index 5e533b8..ee9ff45 100644 --- a/.beans/nuzlocke-tracker-he1n--add-local-gotrue-container-for-dev-auth-testing.md +++ b/.beans/nuzlocke-tracker-he1n--add-local-gotrue-container-for-dev-auth-testing.md @@ -1,11 +1,11 @@ --- # nuzlocke-tracker-he1n title: Add local GoTrue container for dev auth testing -status: in-progress +status: todo type: feature priority: normal created_at: 2026-03-20T20:57:04Z -updated_at: 2026-03-20T21:08:22Z +updated_at: 2026-03-20T21:13:18Z --- ## Problem diff --git a/backend/src/app/models/nuzlocke_run.py b/backend/src/app/models/nuzlocke_run.py index d523791..1ab5257 100644 --- a/backend/src/app/models/nuzlocke_run.py +++ b/backend/src/app/models/nuzlocke_run.py @@ -37,7 +37,12 @@ class NuzlockeRun(Base): String(20), index=True ) # active, completed, failed visibility: Mapped[RunVisibility] = mapped_column( - Enum(RunVisibility, name="run_visibility", create_constraint=False), + Enum( + RunVisibility, + name="run_visibility", + create_constraint=False, + values_callable=lambda e: [m.value for m in e], + ), default=RunVisibility.PUBLIC, server_default="public", ) diff --git a/docker-compose.yml b/docker-compose.yml index 7b5f773..e9b1b0e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -72,7 +72,7 @@ services: - GOTRUE_SITE_URL=http://localhost:5173 # Database - GOTRUE_DB_DRIVER=postgres - - GOTRUE_DB_DATABASE_URL=postgres://postgres:postgres@db:5432/nuzlocke?sslmode=disable + - GOTRUE_DB_DATABASE_URL=postgres://postgres:postgres@db:5432/nuzlocke?sslmode=disable&search_path=auth # JWT - must match backend's SUPABASE_JWT_SECRET - GOTRUE_JWT_SECRET=super-secret-jwt-token-with-at-least-32-characters-long - GOTRUE_JWT_AUD=authenticated diff --git a/frontend/src/lib/supabase.ts b/frontend/src/lib/supabase.ts index a0a2f45..e18c664 100644 --- a/frontend/src/lib/supabase.ts +++ b/frontend/src/lib/supabase.ts @@ -2,14 +2,32 @@ import { createClient, type SupabaseClient } from '@supabase/supabase-js' const supabaseUrl = import.meta.env['VITE_SUPABASE_URL'] ?? '' const supabaseAnonKey = import.meta.env['VITE_SUPABASE_ANON_KEY'] ?? '' +const isLocalDev = supabaseUrl.includes('localhost') + +// supabase-js hardcodes /auth/v1 as the auth path prefix, but GoTrue +// serves at the root when accessed directly (no API gateway). +// This custom fetch strips the prefix for local dev. +function localGoTrueFetch( + input: RequestInfo | URL, + init?: RequestInit, +): Promise { + const url = input instanceof Request ? input.url : String(input) + const rewritten = url.replace('/auth/v1/', '/') + if (input instanceof Request) { + return fetch(new Request(rewritten, input), init) + } + return fetch(rewritten, init) +} function createSupabaseClient(): SupabaseClient { if (!supabaseUrl || !supabaseAnonKey) { - // Return a stub client for tests/dev without Supabase configured - // Uses port 9999 to match local GoTrue container return createClient('http://localhost:9999', 'stub-key') } - return createClient(supabaseUrl, supabaseAnonKey) + return createClient(supabaseUrl, supabaseAnonKey, { + ...(isLocalDev && { + global: { fetch: localGoTrueFetch }, + }), + }) } export const supabase = createSupabaseClient()