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 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 11:19:10 +01:00
parent 2e66186fac
commit bbc6f2c3f4
3 changed files with 41 additions and 10 deletions

View File

@@ -1,11 +1,11 @@
--- ---
# nuzlocke-tracker-5svj # nuzlocke-tracker-5svj
title: Expose admin status to frontend via user API title: Expose admin status to frontend via user API
status: todo status: completed
type: task type: task
priority: normal priority: normal
created_at: 2026-03-21T10:06:20Z 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 parent: nuzlocke-tracker-ce4o
blocked_by: blocked_by:
- nuzlocke-tracker-dwah - 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 ## Checklist
- [ ] Add `is_admin` field to the user response schema (`/api/users/me` endpoint) - [x] 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 - [x] Update `AuthContext` to fetch `/api/users/me` after login and store `isAdmin` in context
- [ ] Expose `isAdmin` boolean from `useAuth()` hook - [x] 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] 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 ## Files to change
- `backend/src/app/schemas/user.py` or equivalent — add `is_admin` to response - `backend/src/app/schemas/user.py` or equivalent — add `is_admin` to response
- `backend/src/app/api/users.py` — ensure `/me` returns `is_admin` - `backend/src/app/api/users.py` — ensure `/me` returns `is_admin`
- `frontend/src/contexts/AuthContext.tsx` — fetch and store admin status - `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)

View File

@@ -16,6 +16,7 @@ class UserResponse(CamelModel):
id: UUID id: UUID
email: str email: str
display_name: str | None = None display_name: str | None = None
is_admin: bool = False
@router.post("/me", response_model=UserResponse) @router.post("/me", response_model=UserResponse)

View File

@@ -1,11 +1,20 @@
import { createContext, useContext, useEffect, useState, useCallback, useMemo } from 'react' import { createContext, useContext, useEffect, useState, useCallback, useMemo } from 'react'
import type { User, Session, AuthError } from '@supabase/supabase-js' import type { User, Session, AuthError } from '@supabase/supabase-js'
import { supabase } from '../lib/supabase' import { supabase } from '../lib/supabase'
import { api } from '../api/client'
interface UserProfile {
id: string
email: string
displayName: string | null
isAdmin: boolean
}
interface AuthState { interface AuthState {
user: User | null user: User | null
session: Session | null session: Session | null
loading: boolean loading: boolean
isAdmin: boolean
} }
interface AuthContextValue extends AuthState { interface AuthContextValue extends AuthState {
@@ -18,22 +27,35 @@ interface AuthContextValue extends AuthState {
const AuthContext = createContext<AuthContextValue | null>(null) const AuthContext = createContext<AuthContextValue | null>(null)
async function syncUserProfile(session: Session | null): Promise<boolean> {
if (!session) return false
try {
const profile = await api.post<UserProfile>('/users/me', {})
return profile.isAdmin
} catch {
return false
}
}
export function AuthProvider({ children }: { children: React.ReactNode }) { export function AuthProvider({ children }: { children: React.ReactNode }) {
const [state, setState] = useState<AuthState>({ const [state, setState] = useState<AuthState>({
user: null, user: null,
session: null, session: null,
loading: true, loading: true,
isAdmin: false,
}) })
useEffect(() => { useEffect(() => {
supabase.auth.getSession().then(({ data: { session } }) => { supabase.auth.getSession().then(async ({ data: { session } }) => {
setState({ user: session?.user ?? null, session, loading: false }) const isAdmin = await syncUserProfile(session)
setState({ user: session?.user ?? null, session, loading: false, isAdmin })
}) })
const { const {
data: { subscription }, data: { subscription },
} = supabase.auth.onAuthStateChange((_event, session) => { } = supabase.auth.onAuthStateChange(async (_event, session) => {
setState({ user: session?.user ?? null, session, loading: false }) const isAdmin = await syncUserProfile(session)
setState({ user: session?.user ?? null, session, loading: false, isAdmin })
}) })
return () => subscription.unsubscribe() return () => subscription.unsubscribe()