feat(web): базовый клиент Фазы 1 — вход, серверы, комнаты, настройки
Клиент React 19 + TanStack Query + Zustand:
- вход (поле TOTP появляется на `auth.2fa_required`), регистрация с проверками
и учётом `registration_enabled`, онбординг первого входа с возможностью
пропустить, редирект `/` и страница `/status`;
- защита маршрутов: неавторизованных — на `/login`, без онбординга — на
`/onboarding`;
- оболочка `/app`: рейка серверов, сайдбар категорий и комнат, шапка сервера,
панель пользователя, модалки создания сервера и комнаты, экран «нет серверов»
со вступлением в главный сервер, заглушка комнаты до Фазы 2;
- настройки: профиль, безопасность (step-up, сессии, logout-all, 2FA с
локальным QR и кодами восстановления), внешний вид, админ-раздел инстанса;
- Gateway-клиент: HELLO/IDENTIFY/RESUME, heartbeat с ожиданием ACK,
экспоненциальное переподключение, разбор `INVALID_SESSION {reason,resumable}`;
- диспетчер событий: READY/RESUMED/USER_UPDATE/GUILD_UPDATE/GUILD_DELETE/
CHANNEL_* применяются к стору, GUILD_CREATE догружается по REST,
MEMBER_*/ROLE_* инвалидируют запросы участников, ролей и карточки сервера;
- i18n ru/en, 66 тестов Vitest, `check`/`lint`/`test`/`build` зелёные.
Серверные правки под клиент:
- `totp_enabled` в профиле (`GET /users/@me`, вход, регистрация) — клиенту
нужно знать, требовать ли код при step-up;
- `POST /auth/2fa/setup` отдаёт `otpauth_url` и `qr_png` (data-URI): QR-код
рисуется локально, внешние сервисы генерации QR не используются;
- права в ответах — имена (`VIEW_CHANNEL|SEND_MESSAGES`), клиент сверяет имена.
This commit is contained in:
@@ -21,6 +21,7 @@ export function TwoFactorSection({
|
||||
const { t } = useTranslation();
|
||||
const [secret, setSecret] = useState<string | null>(null);
|
||||
const [otpauthUrl, setOtpauthUrl] = useState('');
|
||||
const [qrPng, setQrPng] = useState('');
|
||||
const [code, setCode] = useState('');
|
||||
const [recoveryCodes, setRecoveryCodes] = useState<string[] | null>(null);
|
||||
const [qrFailed, setQrFailed] = useState(false);
|
||||
@@ -31,6 +32,7 @@ export function TwoFactorSection({
|
||||
onSuccess: (payload) => {
|
||||
setSecret(payload.secret);
|
||||
setOtpauthUrl(payload.otpauth_url);
|
||||
setQrPng(payload.qr_png ?? '');
|
||||
setRecoveryCodes(null);
|
||||
setQrFailed(false);
|
||||
},
|
||||
@@ -81,15 +83,13 @@ export function TwoFactorSection({
|
||||
<p className="text-sm text-fg-muted">{t('settings.security.twoFactorSetupHint')}</p>
|
||||
|
||||
<div className="flex flex-wrap items-start gap-4">
|
||||
{qrFailed || otpauthUrl === '' ? (
|
||||
{qrFailed || qrPng === '' ? (
|
||||
<p className="text-xs text-fg-muted">
|
||||
{t('settings.security.twoFactorQrUnavailable')}
|
||||
</p>
|
||||
) : (
|
||||
<img
|
||||
src={`https://api.qrserver.com/v1/create-qr-code/?size=160x160&data=${encodeURIComponent(
|
||||
otpauthUrl,
|
||||
)}`}
|
||||
src={qrPng}
|
||||
alt={t('settings.security.twoFactorQrAlt')}
|
||||
width={160}
|
||||
height={160}
|
||||
|
||||
Reference in New Issue
Block a user