63 lines
2.2 KiB
TypeScript
63 lines
2.2 KiB
TypeScript
|
|
import { Navigate, Outlet, useLocation } from 'react-router';
|
||
|
|
|
||
|
|
import { ErrorNotice, LoadingNotice } from '@/components/ui/ErrorNotice';
|
||
|
|
import { useCurrentUser } from '@/lib/hooks';
|
||
|
|
|
||
|
|
interface AuthGuardProps {
|
||
|
|
/**
|
||
|
|
* Если `true`, пользователей с незавершённым онбордингом пускаем внутрь
|
||
|
|
* (нужно для /onboarding и /settings).
|
||
|
|
*/
|
||
|
|
allowIncompleteOnboarding?: boolean;
|
||
|
|
}
|
||
|
|
|
||
|
|
/**
|
||
|
|
* Защита маршрутов: проверяет GET /users/@me.
|
||
|
|
* Неавторизованных отправляет на /login, не прошедших онбординг — на /onboarding.
|
||
|
|
*/
|
||
|
|
export function AuthGuard({ allowIncompleteOnboarding = false }: AuthGuardProps) {
|
||
|
|
const location = useLocation();
|
||
|
|
const currentUser = useCurrentUser();
|
||
|
|
|
||
|
|
if (currentUser.isPending) {
|
||
|
|
return (
|
||
|
|
<div className="flex min-h-full items-center justify-center p-6">
|
||
|
|
<LoadingNotice />
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
if (currentUser.isError) {
|
||
|
|
if (isUnauthorized(currentUser.error)) {
|
||
|
|
return <Navigate to="/login" replace state={{ from: location.pathname }} />;
|
||
|
|
}
|
||
|
|
return (
|
||
|
|
<div className="mx-auto flex min-h-full w-full max-w-md flex-col justify-center p-6">
|
||
|
|
<ErrorNotice error={currentUser.error} onRetry={() => void currentUser.refetch()} />
|
||
|
|
</div>
|
||
|
|
);
|
||
|
|
}
|
||
|
|
|
||
|
|
const user = currentUser.data;
|
||
|
|
if (!allowIncompleteOnboarding && !user.onboarding_completed) {
|
||
|
|
return <Navigate to="/onboarding" replace />;
|
||
|
|
}
|
||
|
|
|
||
|
|
return <Outlet context={{ user }} />;
|
||
|
|
}
|
||
|
|
|
||
|
|
/** Коды, которые означают именно отсутствие сессии. */
|
||
|
|
const unauthorizedCodes = new Set(['auth.unauthorized', 'auth.session_invalid', 'auth.required']);
|
||
|
|
|
||
|
|
/** 401/403 или один из «сессионных» кодов; остальное — сеть или ошибка сервера. */
|
||
|
|
export function isUnauthorized(error: unknown): boolean {
|
||
|
|
if (typeof error !== 'object' || error === null) {
|
||
|
|
return false;
|
||
|
|
}
|
||
|
|
const candidate = error as { status?: unknown; code?: unknown };
|
||
|
|
if (candidate.status === 401) {
|
||
|
|
return true;
|
||
|
|
}
|
||
|
|
return typeof candidate.code === 'string' && unauthorizedCodes.has(candidate.code);
|
||
|
|
}
|