M2-T07: build auth UI (login, session bootstrap, forced password change, logout)
- real Mantine login form -> POST /api/auth/login; 401 inline error; redirect when already authed - ProtectedRoute: loading state, preserves intended destination, gates force_password_change - ChangePasswordPage forced-change gate -> POST /api/auth/password - logout control in AppLayout nav -> POST /api/auth/logout - typed client only; vitest tests for the login flow
This commit is contained in:
@@ -1,12 +1,18 @@
|
||||
/**
|
||||
* ProtectedRoute — renders children when authenticated; redirects to /login otherwise.
|
||||
*
|
||||
* Shows nothing while the session is still loading to avoid flash-of-login.
|
||||
* T07 can replace the loading placeholder with a proper spinner/skeleton.
|
||||
* Additional gate (M2-T07):
|
||||
* - If the authenticated user has force_password_change === true, redirect to
|
||||
* /change-password instead of rendering children. This prevents access to any
|
||||
* protected page until the password is changed.
|
||||
* - Shows a loading spinner while the session is still resolving to avoid flash-of-login.
|
||||
* - On unauthenticated access, preserves the intended destination in location.state.from
|
||||
* so LoginPage can redirect back after login.
|
||||
*/
|
||||
|
||||
import type { ReactNode } from 'react'
|
||||
import { Navigate } from 'react-router-dom'
|
||||
import { Navigate, useLocation } from 'react-router-dom'
|
||||
import { Center, Loader } from '@mantine/core'
|
||||
import { useSession } from './SessionProvider'
|
||||
|
||||
interface ProtectedRouteProps {
|
||||
@@ -14,15 +20,26 @@ interface ProtectedRouteProps {
|
||||
}
|
||||
|
||||
export function ProtectedRoute({ children }: ProtectedRouteProps) {
|
||||
const { status } = useSession()
|
||||
const { status, user } = useSession()
|
||||
const location = useLocation()
|
||||
|
||||
if (status === 'loading') {
|
||||
// Render nothing while we check the session — avoids a flash to /login.
|
||||
return null
|
||||
// Render a centred spinner while we check the session — avoids a flash to /login.
|
||||
return (
|
||||
<Center mih="100vh">
|
||||
<Loader />
|
||||
</Center>
|
||||
)
|
||||
}
|
||||
|
||||
if (status === 'unauthenticated') {
|
||||
return <Navigate to="/login" replace />
|
||||
// Preserve the intended destination so LoginPage can redirect back after login.
|
||||
return <Navigate to="/login" state={{ from: location }} replace />
|
||||
}
|
||||
|
||||
// Authenticated but forced to change password — gate all protected pages.
|
||||
if (user?.force_password_change && location.pathname !== '/change-password') {
|
||||
return <Navigate to="/change-password" replace />
|
||||
}
|
||||
|
||||
return <>{children}</>
|
||||
|
||||
Reference in New Issue
Block a user