Phase 8, third batch: the three root components and both layouts. All five emit byte-identical JavaScript and the production bundle is byte-identical to3d5f54b. Small batch, and almost all of it was the rename. `ScrollToTop`, `UserNotRegisteredError` and both layouts take no props at all - the layouts render through `<Outlet />` - so inference already described them. `ProtectedRoute` gains the only real interface, and it clears a standing error. Both its props have fallbacks - `fallback` defaults to a spinner and `unauthenticatedElement` falls through `??` to the login redirect - and its one call site is a bare `<ProtectedRoute />` in `App.jsx`. Typed optional, which is what the defaults already said, that call site stops being an error: tsc goes 22 -> 21 with nothing introduced. The auth boundary itself is untouched. The `authError.type === 'user_not_registered'` branch, the `<Navigate to="/admin/login" state={{ from }}>` redirect and the `isLoadingAuth || !authChecked` gate are all exactly as they were - which matters, because `AdminLayout` is one of the files `skill-check.mjs` reads as source text, and it is read through the resolver added in02a2ab0. Verified: tsc 22 -> 21, set-difference showing one removed and none added; zero errors in any of the five; all five emit byte-identical JavaScript; production bundle byte-identical; npm test 1684/1691 with the same seven failures; Owliver baseline 59/59; lint 0 errors; baseline artifacts untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HBG1wnuRfJKCstGB8Fekr8
59 lines
1.8 KiB
TypeScript
59 lines
1.8 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { Navigate, Outlet, useLocation } from 'react-router-dom';
|
|
import { useAuth } from '@/lib/AuthContext';
|
|
import UserNotRegisteredError from '@/components/UserNotRegisteredError';
|
|
|
|
const DefaultFallback = () => (
|
|
<div className="fixed inset-0 flex items-center justify-center">
|
|
<div className="w-8 h-8 border-4 border-slate-200 border-t-slate-800 rounded-full animate-spin"></div>
|
|
</div>
|
|
);
|
|
|
|
/**
|
|
* `unauthenticatedElement` defaults to the login page.
|
|
*
|
|
* It used to default to `undefined`, which renders nothing. That was invisible
|
|
* while the data client reported the seeded user as permanently signed in and
|
|
* this branch was unreachable; now that `GET /me` can genuinely answer 401, the
|
|
* default is what a signed-out visitor actually sees, and a blank screen is not
|
|
* an acceptable answer to "you are not signed in".
|
|
*
|
|
* The attempted path travels in location state so signing in returns the
|
|
* visitor to where they were going.
|
|
*/
|
|
interface ProtectedRouteProps {
|
|
fallback?: any;
|
|
unauthenticatedElement?: any;
|
|
}
|
|
|
|
export default function ProtectedRoute({ fallback = <DefaultFallback />, unauthenticatedElement }: ProtectedRouteProps) {
|
|
const location = useLocation();
|
|
const { isAuthenticated, isLoadingAuth, authChecked, authError, checkUserAuth } = useAuth();
|
|
const signedOut = unauthenticatedElement ?? (
|
|
<Navigate to="/admin/login" replace state={{ from: location.pathname }} />
|
|
);
|
|
|
|
useEffect(() => {
|
|
if (!authChecked && !isLoadingAuth) {
|
|
checkUserAuth();
|
|
}
|
|
}, [authChecked, isLoadingAuth, checkUserAuth]);
|
|
|
|
if (isLoadingAuth || !authChecked) {
|
|
return fallback;
|
|
}
|
|
|
|
if (authError) {
|
|
if (authError.type === 'user_not_registered') {
|
|
return <UserNotRegisteredError />;
|
|
}
|
|
return signedOut;
|
|
}
|
|
|
|
if (!isAuthenticated) {
|
|
return signedOut;
|
|
}
|
|
|
|
return <Outlet />;
|
|
}
|