From bbc6f2c3f459d87397b56c3dc1ddcd17255d775b Mon Sep 17 00:00:00 2001 From: Julian Tabel Date: Sat, 21 Mar 2026 11:19:10 +0100 Subject: [PATCH] feat: expose admin status to frontend via user API Add is_admin field to UserResponse schema and update AuthContext to fetch user profile after login, storing and exposing isAdmin boolean. Co-Authored-By: Claude Opus 4.6 --- ...e-admin-status-to-frontend-via-user-api.md | 20 +++++++++---- backend/src/app/api/users.py | 1 + frontend/src/contexts/AuthContext.tsx | 30 ++++++++++++++++--- 3 files changed, 41 insertions(+), 10 deletions(-) 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 index 3eda794..45c56ef 100644 --- 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 @@ -1,11 +1,11 @@ --- # nuzlocke-tracker-5svj title: Expose admin status to frontend via user API -status: todo +status: completed type: task priority: normal created_at: 2026-03-21T10:06:20Z -updated_at: 2026-03-21T10:06:24Z +updated_at: 2026-03-21T10:18:26Z parent: nuzlocke-tracker-ce4o blocked_by: - nuzlocke-tracker-dwah @@ -15,13 +15,21 @@ The frontend needs to know if the current user is an admin so it can show/hide t ## 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`) +- [x] Add `is_admin` field to the user response schema (`/api/users/me` endpoint) +- [x] Update `AuthContext` to fetch `/api/users/me` after login and store `isAdmin` in context +- [x] Expose `isAdmin` boolean from `useAuth()` hook +- [x] 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 + +## Summary of Changes + +Added `isAdmin` field to frontend auth system: + +- **Backend**: Added `is_admin: bool = False` to `UserResponse` schema in `backend/src/app/api/users.py` +- **Frontend**: Updated `AuthContext` to fetch `/api/users/me` after login and expose `isAdmin` boolean +- Edge case handled: `syncUserProfile` returns `false` if API call fails (new user auto-created with `is_admin=false` by backend) diff --git a/backend/src/app/api/users.py b/backend/src/app/api/users.py index bfc3d38..59a3781 100644 --- a/backend/src/app/api/users.py +++ b/backend/src/app/api/users.py @@ -16,6 +16,7 @@ class UserResponse(CamelModel): id: UUID email: str display_name: str | None = None + is_admin: bool = False @router.post("/me", response_model=UserResponse) diff --git a/frontend/src/contexts/AuthContext.tsx b/frontend/src/contexts/AuthContext.tsx index edd9db5..6d39c04 100644 --- a/frontend/src/contexts/AuthContext.tsx +++ b/frontend/src/contexts/AuthContext.tsx @@ -1,11 +1,20 @@ import { createContext, useContext, useEffect, useState, useCallback, useMemo } from 'react' import type { User, Session, AuthError } from '@supabase/supabase-js' import { supabase } from '../lib/supabase' +import { api } from '../api/client' + +interface UserProfile { + id: string + email: string + displayName: string | null + isAdmin: boolean +} interface AuthState { user: User | null session: Session | null loading: boolean + isAdmin: boolean } interface AuthContextValue extends AuthState { @@ -18,22 +27,35 @@ interface AuthContextValue extends AuthState { const AuthContext = createContext(null) +async function syncUserProfile(session: Session | null): Promise { + if (!session) return false + try { + const profile = await api.post('/users/me', {}) + return profile.isAdmin + } catch { + return false + } +} + export function AuthProvider({ children }: { children: React.ReactNode }) { const [state, setState] = useState({ user: null, session: null, loading: true, + isAdmin: false, }) useEffect(() => { - supabase.auth.getSession().then(({ data: { session } }) => { - setState({ user: session?.user ?? null, session, loading: false }) + supabase.auth.getSession().then(async ({ data: { session } }) => { + const isAdmin = await syncUserProfile(session) + setState({ user: session?.user ?? null, session, loading: false, isAdmin }) }) const { data: { subscription }, - } = supabase.auth.onAuthStateChange((_event, session) => { - setState({ user: session?.user ?? null, session, loading: false }) + } = supabase.auth.onAuthStateChange(async (_event, session) => { + const isAdmin = await syncUserProfile(session) + setState({ user: session?.user ?? null, session, loading: false, isAdmin }) }) return () => subscription.unsubscribe()