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

@@ -55,6 +55,7 @@ _KEY_REGISTRATION_EMAIL_DOMAIN = "registration_email_domain"
_KEY_CONTACT_EMAIL = "contact_email"
_KEY_MEDIA_LIMITS = "media_limits"
_KEY_CONSENT_POLICY = "consent_policy"
_KEY_DEVICE_CHECK = "device_check"
BOOTSTRAP_MANAGED_KEYS: tuple[str, ...] = (
_KEY_CHAT,
@@ -79,6 +80,7 @@ _DEFAULT_REGISTRATION_EMAIL_DOMAIN_VALUE: dict[str, Any] = {"enabled": False, "d
`update()` значение переписывается в новую форму (см. `update`)."""
_DEFAULT_CONTACT_EMAIL_VALUE: dict[str, Any] = {"enabled": False, "email": None}
_DEFAULT_MEDIA_LIMITS_VALUE: dict[str, Any] = {"publish_quality_cap": "off", "stage_max_tiles": 25}
_DEFAULT_DEVICE_CHECK_VALUE = {"enabled": False}
DEFAULT_CONSENT_POLICY_TEXT = """Это типовой шаблон для предварительной демонстрации. Текст не проходил проверку юриста и не может использоваться как окончательная редакция без такой проверки. Администратор обязан заменить плейсхолдеры в квадратных скобках и, при необходимости, весь текст — под свою организацию и юрисдикцию.
@@ -151,6 +153,7 @@ class SettingsUpdateIn(BaseModel):
stage_max_tiles: StageMaxTiles | None = None
consent_required: bool | None = None
consent_policy_text: str | None = None
device_check_enabled: bool | None = None
class BootstrapOverrides(BaseModel):
@@ -203,6 +206,7 @@ def build_bootstrap_defaults(
_KEY_CONTACT_EMAIL: dict(_DEFAULT_CONTACT_EMAIL_VALUE),
_KEY_MEDIA_LIMITS: dict(_DEFAULT_MEDIA_LIMITS_VALUE),
_KEY_CONSENT_POLICY: dict(_DEFAULT_CONSENT_POLICY_VALUE),
_KEY_DEVICE_CHECK: dict(_DEFAULT_DEVICE_CHECK_VALUE),
}
if overrides is None:
return defaults
@@ -337,6 +341,10 @@ class InstanceSettingsService:
cfg.hand_queue = HandQueueConfig(enabled=patch.hand_queue_enabled)
await self._set(_KEY_HAND_QUEUE, cfg.hand_queue.model_dump(mode="json"))
if patch.device_check_enabled is not None:
cfg.device_check_enabled = patch.device_check_enabled
await self._set(_KEY_DEVICE_CHECK, {"enabled": patch.device_check_enabled})
if patch.registration_team_choice is not None:
cfg.registration_team_choice = patch.registration_team_choice
await self._set(
@@ -605,4 +613,7 @@ def _build_config(rows: dict[str, Any]) -> InstanceConfig:
consent_policy_version=rows.get(_KEY_CONSENT_POLICY, _DEFAULT_CONSENT_POLICY_VALUE).get(
"version", 1
),
device_check_enabled=rows.get(_KEY_DEVICE_CHECK, _DEFAULT_DEVICE_CHECK_VALUE).get(
"enabled", False
),
)