feat: auth-aware UI and role-based access control (#67)
## Summary - Add `is_admin` column to users table with Alembic migration and a `require_admin` FastAPI dependency that protects all admin-facing write endpoints (games, pokemon, evolutions, bosses, routes CRUD) - Expose admin status to frontend via user API and update AuthContext to fetch/store `isAdmin` after login - Make navigation menu auth-aware (different links for logged-out, logged-in, and admin users) and protect frontend routes with `ProtectedRoute` and `AdminRoute` components, preserving deep-linking through redirects - Fix test reliability: `drop_all` before `create_all` to clear stale PostgreSQL enums from interrupted test runs - Fix test auth: add `admin_client` fixture and use valid UUID for mock user so tests pass with new admin-protected endpoints ## Test plan - [x] All 252 backend tests pass - [ ] Verify non-admin users cannot access admin write endpoints (games, pokemon, evolutions, bosses CRUD) - [ ] Verify admin users can access admin endpoints normally - [ ] Verify navigation shows correct links for logged-out, logged-in, and admin states - [ ] Verify `/admin/*` routes redirect non-admin users with a toast - [ ] Verify `/runs/new` and `/genlockes/new` redirect unauthenticated users to login, then back after auth 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #67 Co-authored-by: Julian Tabel <juliantabel.jt@gmail.com> Co-committed-by: Julian Tabel <juliantabel.jt@gmail.com>
This commit was merged in pull request #67.
This commit is contained in:
@@ -1,16 +1,8 @@
|
||||
import { useState } from 'react'
|
||||
import { useState, useMemo } from 'react'
|
||||
import { Link, Outlet, useLocation } from 'react-router-dom'
|
||||
import { useTheme } from '../hooks/useTheme'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
|
||||
const navLinks = [
|
||||
{ to: '/runs/new', label: 'New Run' },
|
||||
{ to: '/runs', label: 'My Runs' },
|
||||
{ to: '/genlockes', label: 'Genlockes' },
|
||||
{ to: '/stats', label: 'Stats' },
|
||||
{ to: '/admin', label: 'Admin' },
|
||||
]
|
||||
|
||||
function NavLink({
|
||||
to,
|
||||
active,
|
||||
@@ -136,9 +128,34 @@ function UserMenu({ onAction }: { onAction?: () => void }) {
|
||||
export function Layout() {
|
||||
const [menuOpen, setMenuOpen] = useState(false)
|
||||
const location = useLocation()
|
||||
const { user, isAdmin } = useAuth()
|
||||
|
||||
const navLinks = useMemo(() => {
|
||||
if (!user) {
|
||||
// Logged out: Home, Runs, Genlockes, Stats
|
||||
return [
|
||||
{ to: '/', label: 'Home' },
|
||||
{ to: '/runs', label: 'Runs' },
|
||||
{ to: '/genlockes', label: 'Genlockes' },
|
||||
{ to: '/stats', label: 'Stats' },
|
||||
]
|
||||
}
|
||||
// Logged in: New Run, My Runs, Genlockes, Stats
|
||||
const links = [
|
||||
{ to: '/runs/new', label: 'New Run' },
|
||||
{ to: '/runs', label: 'My Runs' },
|
||||
{ to: '/genlockes', label: 'Genlockes' },
|
||||
{ to: '/stats', label: 'Stats' },
|
||||
]
|
||||
// Admin gets Admin link
|
||||
if (isAdmin) {
|
||||
links.push({ to: '/admin', label: 'Admin' })
|
||||
}
|
||||
return links
|
||||
}, [user, isAdmin])
|
||||
|
||||
function isActive(to: string) {
|
||||
if (to === '/runs/new') return location.pathname === '/runs/new'
|
||||
if (to === '/' || to === '/runs/new') return location.pathname === to
|
||||
return location.pathname.startsWith(to)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user