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 на шестой попытке входа, секретов в логах
нет.
This commit is contained in:
2026-09-19 21:53:35 +03:00
parent c059cd9f51
commit bf0d130ee8
23 changed files with 1115 additions and 269 deletions
+1 -15
View File
@@ -2,6 +2,7 @@ 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 {
/**
@@ -45,18 +46,3 @@ export function AuthGuard({ allowIncompleteOnboarding = false }: AuthGuardProps)
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);
}