Files
glchat/web/src/components/AuthGuard.tsx
T
grendervill bf0d130ee8 feat(cli): включение 2FA инстанс-админа командой totp-setup
2FA обязательна для инстанс-администраторов, но до её включения вход закрыт —
получался замкнутый круг. Добавлены команды обслуживания:

- `glchat totp-setup --email <admin>`: создаёт секрет, подтверждает его кодом,
  печатает секрет, otpauth-ссылку и 8 резервных кодов (каждый одноразовый);
- `glchat totp-reset --email <admin>`: удаляет секрет при потере устройства;
- код `auth.2fa_enrollment_required` с подсказкой, какую команду выполнить;
- установщик: флаг `--admin-2fa` для автоматического включения (по умолчанию
  печатает подсказку, чтобы секреты не оседали в логах установки).

Проверено сквозным прогоном локально: bootstrap → 403 на входе без 2FA →
totp-setup → вход с TOTP-кодом → профиль, серверы, комнаты, роли, участники,
аудит, создание сервера админом, 429 на шестой попытке входа, секретов в логах
нет.
2026-09-19 21:53:35 +03:00

49 lines
1.6 KiB
TypeScript

import { Navigate, Outlet, useLocation } from 'react-router';
import { ErrorNotice, LoadingNotice } from '@/components/ui/ErrorNotice';
import { useCurrentUser } from '@/lib/hooks';
import { isUnauthorized } from '@/lib/http';
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 }} />;
}