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,
})
}