From da33c62d620d01a6b15c5578e2f32408e432bbc3 Mon Sep 17 00:00:00 2001 From: Julian Tabel Date: Sat, 21 Mar 2026 11:19:16 +0100 Subject: [PATCH] feat: protect frontend routes with ProtectedRoute and AdminRoute - Wrap /runs/new and /genlockes/new with ProtectedRoute (requires login) - Create AdminRoute component that checks isAdmin, redirects non-admins with a toast notification - Wrap all /admin/* routes with AdminRoute - Deep-linking preserved: unauthenticated users redirect to login, then back to the original protected route after auth Co-Authored-By: Claude Opus 4.6 --- ...ntend-routes-with-protectedroute-and-ad.md | 24 +++++++++---- ...-aware-ui-and-role-based-access-control.md | 8 ++--- frontend/src/App.tsx | 8 ++--- frontend/src/components/AdminRoute.tsx | 35 +++++++++++++++++++ frontend/src/components/index.ts | 1 + 5 files changed, 61 insertions(+), 15 deletions(-) create mode 100644 frontend/src/components/AdminRoute.tsx 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 index 0c62aed..19f8d38 100644 --- 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 @@ -1,11 +1,11 @@ --- # nuzlocke-tracker-2zwg title: Protect frontend routes with ProtectedRoute and AdminRoute -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:19:22Z parent: nuzlocke-tracker-ce4o blocked_by: - nuzlocke-tracker-5svj @@ -15,14 +15,24 @@ Use the existing \`ProtectedRoute\` component (currently unused) and create an \ ## 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) +- [x] Wrap \`/runs/new\` and \`/genlockes/new\` with \`ProtectedRoute\` (requires login) +- [x] Create \`AdminRoute\` component that checks \`isAdmin\` from \`useAuth()\`, redirects to \`/\` with a toast/message if not admin +- [x] Wrap all \`/admin/*\` routes with \`AdminRoute\` +- [x] Ensure \`/runs\` and \`/runs/:runId\` remain accessible to everyone (public run viewing) +- [x] 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 + +## Summary of Changes + +Implemented frontend route protection: + +- **ProtectedRoute**: Wraps `/runs/new` and `/genlockes/new` - redirects unauthenticated users to `/login` with return location preserved +- **AdminRoute**: New component that checks `isAdmin` from `useAuth()`, redirects non-admins to `/` with a toast notification +- **Admin routes**: Wrapped `AdminLayout` with `AdminRoute` to protect all `/admin/*` routes +- **Public routes**: `/runs` and `/runs/:runId` remain accessible to everyone +- **Deep-linking**: Location state preserved so users return to original route after login 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 index 2b3ea8f..fc8ea4e 100644 --- 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 @@ -5,7 +5,7 @@ status: completed type: epic priority: normal created_at: 2026-03-21T10:05:52Z -updated_at: 2026-03-21T10:08:39Z +updated_at: 2026-03-21T10:18:47Z --- 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. @@ -20,9 +20,9 @@ The app currently shows the same navigation menu to all users regardless of auth ## 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/* +- [x] 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 +- [x] 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 +- [x] Admin status is exposed to the frontend via the user API or auth context diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 7212b17..d96dbd4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,5 +1,5 @@ import { Routes, Route, Navigate } from 'react-router-dom' -import { Layout } from './components' +import { Layout, ProtectedRoute, AdminRoute } from './components' import { AdminLayout } from './components/admin' import { AuthCallback, @@ -35,18 +35,18 @@ function App() { } /> } /> } /> - } /> + } /> } /> } /> } /> - } /> + } /> } /> } /> } /> - }> + }> } /> } /> } /> diff --git a/frontend/src/components/AdminRoute.tsx b/frontend/src/components/AdminRoute.tsx new file mode 100644 index 0000000..25174e8 --- /dev/null +++ b/frontend/src/components/AdminRoute.tsx @@ -0,0 +1,35 @@ +import { useEffect, useRef } from 'react' +import { Navigate, useLocation } from 'react-router-dom' +import { toast } from 'sonner' +import { useAuth } from '../contexts/AuthContext' + +export function AdminRoute({ children }: { children: React.ReactNode }) { + const { user, loading, isAdmin } = useAuth() + const location = useLocation() + const toastShownRef = useRef(false) + + useEffect(() => { + if (!loading && user && !isAdmin && !toastShownRef.current) { + toastShownRef.current = true + toast.error('Admin access required') + } + }, [loading, user, isAdmin]) + + if (loading) { + return ( +
+
+
+ ) + } + + if (!user) { + return + } + + if (!isAdmin) { + return + } + + return <>{children} +} diff --git a/frontend/src/components/index.ts b/frontend/src/components/index.ts index 8f7f012..788d735 100644 --- a/frontend/src/components/index.ts +++ b/frontend/src/components/index.ts @@ -1,3 +1,4 @@ +export { AdminRoute } from './AdminRoute' export { CustomRulesDisplay } from './CustomRulesDisplay' export { ProtectedRoute } from './ProtectedRoute' export { EggEncounterModal } from './EggEncounterModal'