feat: add admin auth hook and protected route guard
This commit is contained in:
42
src/components/ProtectedRoute.test.tsx
Normal file
42
src/components/ProtectedRoute.test.tsx
Normal file
@@ -0,0 +1,42 @@
|
||||
import { render, screen } from '@testing-library/react'
|
||||
import { MemoryRouter, Routes, Route } from 'react-router-dom'
|
||||
import { describe, it, expect, vi } from 'vitest'
|
||||
|
||||
const useAdminAuthMock = vi.fn()
|
||||
vi.mock('../hooks/useAdminAuth', () => ({ default: () => useAdminAuthMock() }))
|
||||
|
||||
import ProtectedRoute from './ProtectedRoute'
|
||||
|
||||
function renderWithRoute() {
|
||||
return render(
|
||||
<MemoryRouter initialEntries={['/admin/products']}>
|
||||
<Routes>
|
||||
<Route path="/admin/login" element={<p>Login page</p>} />
|
||||
<Route element={<ProtectedRoute />}>
|
||||
<Route path="/admin/products" element={<p>Products page</p>} />
|
||||
</Route>
|
||||
</Routes>
|
||||
</MemoryRouter>,
|
||||
)
|
||||
}
|
||||
|
||||
describe('ProtectedRoute', () => {
|
||||
it('renders nothing while loading', () => {
|
||||
useAdminAuthMock.mockReturnValue({ session: null, loading: true })
|
||||
renderWithRoute()
|
||||
expect(screen.queryByText('Products page')).not.toBeInTheDocument()
|
||||
expect(screen.queryByText('Login page')).not.toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('redirects to /admin/login when there is no session', () => {
|
||||
useAdminAuthMock.mockReturnValue({ session: null, loading: false })
|
||||
renderWithRoute()
|
||||
expect(screen.getByText('Login page')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('renders the protected content when there is a session', () => {
|
||||
useAdminAuthMock.mockReturnValue({ session: { user: { id: 'admin' } }, loading: false })
|
||||
renderWithRoute()
|
||||
expect(screen.getByText('Products page')).toBeInTheDocument()
|
||||
})
|
||||
})
|
||||
12
src/components/ProtectedRoute.tsx
Normal file
12
src/components/ProtectedRoute.tsx
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Navigate, Outlet } from 'react-router-dom'
|
||||
import useAdminAuth from '../hooks/useAdminAuth'
|
||||
|
||||
function ProtectedRoute() {
|
||||
const { session, loading } = useAdminAuth()
|
||||
|
||||
if (loading) return null
|
||||
if (!session) return <Navigate to="/admin/login" replace />
|
||||
return <Outlet />
|
||||
}
|
||||
|
||||
export default ProtectedRoute
|
||||
59
src/hooks/useAdminAuth.test.ts
Normal file
59
src/hooks/useAdminAuth.test.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
import { renderHook, waitFor } from '@testing-library/react'
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
||||
import type { Session } from '@supabase/supabase-js'
|
||||
|
||||
const getSessionMock = vi.fn()
|
||||
const onAuthStateChangeMock = vi.fn()
|
||||
|
||||
vi.mock('../lib/supabase', () => ({
|
||||
supabase: {
|
||||
auth: {
|
||||
getSession: (...args: unknown[]) => getSessionMock(...args),
|
||||
onAuthStateChange: (...args: unknown[]) => onAuthStateChangeMock(...args),
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
const FAKE_SESSION = { access_token: 'token', user: { id: 'admin' } } as unknown as Session
|
||||
|
||||
describe('useAdminAuth', () => {
|
||||
beforeEach(() => {
|
||||
getSessionMock.mockReset()
|
||||
onAuthStateChangeMock.mockReset()
|
||||
onAuthStateChangeMock.mockReturnValue({ data: { subscription: { unsubscribe: vi.fn() } } })
|
||||
})
|
||||
|
||||
it('starts loading, then resolves with the current session', async () => {
|
||||
getSessionMock.mockResolvedValue({ data: { session: FAKE_SESSION } })
|
||||
const { default: useAdminAuth } = await import('./useAdminAuth')
|
||||
const { result } = renderHook(() => useAdminAuth())
|
||||
|
||||
expect(result.current.loading).toBe(true)
|
||||
await waitFor(() => expect(result.current.loading).toBe(false))
|
||||
expect(result.current.session).toBe(FAKE_SESSION)
|
||||
})
|
||||
|
||||
it('resolves with a null session when signed out', async () => {
|
||||
getSessionMock.mockResolvedValue({ data: { session: null } })
|
||||
const { default: useAdminAuth } = await import('./useAdminAuth')
|
||||
const { result } = renderHook(() => useAdminAuth())
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false))
|
||||
expect(result.current.session).toBeNull()
|
||||
})
|
||||
|
||||
it('updates session when the auth state changes', async () => {
|
||||
getSessionMock.mockResolvedValue({ data: { session: null } })
|
||||
let capturedCallback: ((event: string, session: Session | null) => void) | undefined
|
||||
onAuthStateChangeMock.mockImplementation((callback) => {
|
||||
capturedCallback = callback
|
||||
return { data: { subscription: { unsubscribe: vi.fn() } } }
|
||||
})
|
||||
const { default: useAdminAuth } = await import('./useAdminAuth')
|
||||
const { result } = renderHook(() => useAdminAuth())
|
||||
|
||||
await waitFor(() => expect(result.current.loading).toBe(false))
|
||||
capturedCallback?.('SIGNED_IN', FAKE_SESSION)
|
||||
await waitFor(() => expect(result.current.session).toBe(FAKE_SESSION))
|
||||
})
|
||||
})
|
||||
28
src/hooks/useAdminAuth.ts
Normal file
28
src/hooks/useAdminAuth.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { useEffect, useState } from 'react'
|
||||
import type { Session } from '@supabase/supabase-js'
|
||||
import { supabase } from '../lib/supabase'
|
||||
|
||||
export interface AdminAuthState {
|
||||
session: Session | null
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
export default function useAdminAuth(): AdminAuthState {
|
||||
const [session, setSession] = useState<Session | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
|
||||
useEffect(() => {
|
||||
supabase.auth.getSession().then(({ data }) => {
|
||||
setSession(data.session)
|
||||
setLoading(false)
|
||||
})
|
||||
|
||||
const { data: listener } = supabase.auth.onAuthStateChange((_event, newSession) => {
|
||||
setSession(newSession)
|
||||
})
|
||||
|
||||
return () => listener.subscription.unsubscribe()
|
||||
}, [])
|
||||
|
||||
return { session, loading }
|
||||
}
|
||||
Reference in New Issue
Block a user