slate-test-1770505673/packages/auth/src/ProtectedRoute.tsx
jordan 3bc5efe56f
Some checks failed
ci/woodpecker/push/woodpecker Pipeline failed
ci/woodpecker/manual/woodpecker Pipeline was successful
Initialize project from skeleton template
2026-02-07 23:07:54 +00:00

132 lines
3.1 KiB
TypeScript

import * as React from 'react';
import { useAuth } from './AuthProvider';
export interface ProtectedRouteProps {
children: React.ReactNode;
/** Component to render while loading */
fallback?: React.ReactNode;
/** Component to render if not authenticated */
unauthorized?: React.ReactNode;
/** Required role(s) - user must have at least one */
roles?: string[];
/** Required scope(s) - user must have at least one */
scopes?: string[];
/** Redirect path for unauthorized access (alternative to unauthorized component) */
redirectTo?: string;
/** Custom redirect function (e.g., router.push). Falls back to window.location.href. */
onRedirect?: (path: string) => void;
}
/**
* ProtectedRoute guards routes that require authentication.
*
* @example
* // Basic protection
* <Route path="/dashboard" element={
* <ProtectedRoute>
* <Dashboard />
* </ProtectedRoute>
* } />
*
* @example
* // With role requirement
* <Route path="/admin" element={
* <ProtectedRoute roles={['admin']}>
* <AdminPanel />
* </ProtectedRoute>
* } />
*
* @example
* // With custom unauthorized view
* <ProtectedRoute
* unauthorized={<AccessDenied />}
* fallback={<LoadingSpinner />}
* >
* <SecureContent />
* </ProtectedRoute>
*/
export function ProtectedRoute({
children,
fallback = <DefaultLoading />,
unauthorized = <DefaultUnauthorized />,
roles,
scopes,
redirectTo,
onRedirect,
}: ProtectedRouteProps) {
const { isLoading, isAuthenticated, user } = useAuth();
// Show loading state
if (isLoading) {
return <>{fallback}</>;
}
// Not authenticated
if (!isAuthenticated) {
if (redirectTo) {
if (onRedirect) {
onRedirect(redirectTo);
} else {
window.location.href = redirectTo;
}
return null;
}
return <>{unauthorized}</>;
}
// Check role requirements
if (roles && roles.length > 0) {
const hasRequiredRole = roles.some((role) => user?.roles?.includes(role));
if (!hasRequiredRole) {
return <>{unauthorized}</>;
}
}
// Check scope requirements
if (scopes && scopes.length > 0) {
const hasRequiredScope = scopes.some((scope) => user?.scopes?.includes(scope));
if (!hasRequiredScope) {
return <>{unauthorized}</>;
}
}
return <>{children}</>;
}
// Default loading component
function DefaultLoading() {
return (
<div
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
color: 'var(--text-muted)',
}}
>
Loading...
</div>
);
}
// Default unauthorized component
function DefaultUnauthorized() {
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
justifyContent: 'center',
minHeight: '100vh',
gap: '1rem',
color: 'var(--text-primary)',
}}
>
<h1 style={{ fontSize: '1.5rem', fontWeight: 'bold' }}>Access Denied</h1>
<p style={{ color: 'var(--text-muted)' }}>You don't have permission to view this page.</p>
</div>
);
}