feat(auth): проверка устройств на входе — запрос доступа и превью камеры

Отключаемый модуль (instance_settings.device_check, дефолт выключен):
запрос доступа к камере/микрофону на LoginPage и в карточке "Как вас
зовут?" (JoinPage), живое зеркальное превью и кнопки вкл/выкл камеры и
микрофона там же. На JoinPage кнопки определяют, с чем гость войдёт в
конференцию (RoomPage.LiveKitRoom audio/video вместо жёстких false) —
на LoginPage только пре-авторизуют разрешение, без UI (карточка ведёт
в лобби, применить выбор некуда). Вход в комнату по умолчанию, как и
раньше, с выключенными микрофоном/камерой.

Публичный GET /api/v1/public/settings отдаёт флаг модуля обеим
страницам до аутентификации.
This commit is contained in:
2026-08-09 18:09:10 +03:00
parent ba01548088
commit 7ee68b17ee
19 changed files with 713 additions and 7 deletions

View File

@@ -0,0 +1,75 @@
/*
* VidConf — превью камеры на входе (сессия 33, `JoinPage`; LoginPage
* превью не показывает — см. `DeviceCheckCard` не используется там).
* Свой файл: компонент самодостаточный, стили ему не нужны нигде за
* пределами компонента.
*/
.device-check { margin: 0 auto var(--space-5); }
/* Окошко превью и кнопки под ним — одна композиция шириной в половину
карточки (`.flow-card`, куда компонент вложен): проценты сами масштабируются
от фактической ширины родителя без хардкода пикселей. */
.device-check-inner { width: 50%; margin: 0 auto; }
.device-check-frame {
position: relative;
width: 100%;
aspect-ratio: 4 / 3;
border-radius: var(--radius-lg);
overflow: hidden;
background: var(--color-bg-alt);
border: 1px solid var(--color-border);
}
.device-check-video {
width: 100%;
height: 100%;
object-fit: cover;
/* Своё видео принято показывать зеркально — так человек видит себя
привычно, как в зеркале, а не «как его видят другие». */
transform: scaleX(-1);
}
.device-check-placeholder {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-ink-400);
}
.device-check-placeholder .lucide { width: 26px; height: 26px; }
/* Кнопки без подписи, только пиктограммы — их совместная ширина равна
ширине окошка превью (тот же `.device-check-inner`), за края не выступают. */
.device-check-toggles {
display: flex;
gap: var(--space-2);
margin-top: var(--space-2);
}
.device-check-toggle {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 8px 0;
border-radius: var(--radius-lg);
border: 1px solid var(--color-border);
background: var(--color-bg-alt);
color: var(--color-ink-500);
cursor: pointer;
}
.device-check-toggle .lucide { width: 18px; height: 18px; }
.device-check-toggle.is-on {
background: var(--color-accent);
border-color: transparent;
color: var(--color-accent-text);
}
.device-check-toggle:disabled { opacity: 0.5; cursor: not-allowed; }
.device-check-hint {
text-align: center;
margin-top: var(--space-3);
}