diff --git a/src/app/admin/content-ratings/page.test.tsx b/src/app/admin/content-ratings/page.test.tsx index 2419c5e..1acf896 100644 --- a/src/app/admin/content-ratings/page.test.tsx +++ b/src/app/admin/content-ratings/page.test.tsx @@ -31,6 +31,25 @@ vi.mock('next/image', () => ({ }, })) +vi.mock('@/hooks/use-auth', () => { + const mockAuth = { + user: { + did: 'did:plc:user-jay-001', + handle: 'jay.bsky.team', + displayName: 'Jay', + avatarUrl: null, + }, + isAuthenticated: true, + isLoading: false, + getAccessToken: () => 'mock-access-token', + login: vi.fn(), + logout: vi.fn(), + setSessionFromCallback: vi.fn(), + authFetch: vi.fn(), + } + return { useAuth: () => mockAuth } +}) + describe('AdminContentRatingsPage', () => { it('renders content ratings heading', () => { render() diff --git a/src/app/admin/content-ratings/page.tsx b/src/app/admin/content-ratings/page.tsx index a7fbdca..a41e425 100644 --- a/src/app/admin/content-ratings/page.tsx +++ b/src/app/admin/content-ratings/page.tsx @@ -13,6 +13,7 @@ import { ErrorAlert } from '@/components/error-alert' import { getCategories, getCommunitySettings } from '@/lib/api/client' import { cn } from '@/lib/utils' import type { CategoryTreeNode, CommunitySettings, MaturityRating } from '@/lib/api/types' +import { useAuth } from '@/hooks/use-auth' const MATURITY_DESCRIPTIONS: Record = { safe: 'Suitable for all audiences. No explicit or mature content.', @@ -52,6 +53,7 @@ function CategoryRatingRow({ category, depth }: { category: CategoryTreeNode; de } export default function AdminContentRatingsPage() { + const { getAccessToken } = useAuth() const [categories, setCategories] = useState([]) const [communitySettings, setCommunitySettings] = useState(null) const [loading, setLoading] = useState(true) @@ -60,7 +62,10 @@ export default function AdminContentRatingsPage() { const fetchData = useCallback(async () => { setError(null) try { - const [catRes, settingsRes] = await Promise.all([getCategories(), getCommunitySettings()]) + const [catRes, settingsRes] = await Promise.all([ + getCategories(), + getCommunitySettings(getAccessToken() ?? ''), + ]) setCategories(catRes.categories) setCommunitySettings(settingsRes) } catch { @@ -68,7 +73,7 @@ export default function AdminContentRatingsPage() { } finally { setLoading(false) } - }, []) + }, [getAccessToken]) useEffect(() => { void fetchData() diff --git a/src/app/admin/layout.tsx b/src/app/admin/layout.tsx new file mode 100644 index 0000000..0ceb9c8 --- /dev/null +++ b/src/app/admin/layout.tsx @@ -0,0 +1,48 @@ +/** + * Admin route layout. + * Gates all /admin/* pages on authentication + admin role. + * Shows loading skeleton while auth state initializes, + * redirects non-admin users to the homepage. + * @see specs/prd-web.md Section M11 + */ + +'use client' + +import { useEffect } from 'react' +import { useRouter } from 'next/navigation' +import { useAuth } from '@/hooks/use-auth' + +export default function AdminLayout({ children }: { children: React.ReactNode }) { + const { user, isAuthenticated, isLoading } = useAuth() + const router = useRouter() + + useEffect(() => { + if (!isLoading && !isAuthenticated) { + router.replace(`/login?returnTo=${encodeURIComponent('/admin')}`) + } + }, [isLoading, isAuthenticated, router]) + + useEffect(() => { + if (!isLoading && isAuthenticated && user?.role !== 'admin') { + router.replace('/') + } + }, [isLoading, isAuthenticated, user?.role, router]) + + if (isLoading) { + return ( +
+
+
+
+
+ Loading admin panel +
+ ) + } + + if (!isAuthenticated || user?.role !== 'admin') { + return null + } + + return <>{children} +} diff --git a/src/app/admin/settings/page.tsx b/src/app/admin/settings/page.tsx index e4473fc..a2d501e 100644 --- a/src/app/admin/settings/page.tsx +++ b/src/app/admin/settings/page.tsx @@ -35,7 +35,7 @@ export default function AdminSettingsPage() { setLoadError(null) try { const [settingsData, pdsData] = await Promise.all([ - getCommunitySettings(), + getCommunitySettings(getAccessToken() ?? ''), getPdsTrustFactors(getAccessToken() ?? ''), ]) setSettings(settingsData) diff --git a/src/lib/api/client.ts b/src/lib/api/client.ts index 53e821e..6bb3046 100644 --- a/src/lib/api/client.ts +++ b/src/lib/api/client.ts @@ -317,10 +317,13 @@ export function markNotificationsRead( // --- Community endpoints --- -export function getCommunitySettings(options?: FetchOptions): Promise { +export function getCommunitySettings( + accessToken: string, + options?: FetchOptions +): Promise { return apiFetch('/api/admin/settings', { ...options, - headers: { ...options?.headers }, + headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, }) } @@ -787,7 +790,7 @@ export function reorderOnboardingFields( ...options, method: 'PUT', headers: { ...options?.headers, Authorization: `Bearer ${accessToken}` }, - body: { fields }, + body: fields, }) }