Files
vidconf/backend/services/conference_access.py
Max Ronzhin 42bfb88a22 feat(room): роль организатора в метаданных LiveKit-токена
Предварительная работа для очереди рук и принудительного мьюта (B1/B2):
build_join кладёт is_organizer:true в метаданные токена организатора
(создатель мгновенной конференции и владелец при обычном входе). Метаданные
токена — только подсказка для UI (см. предупреждение в докстринге
build_join), любое серверное действие организатора обязано перепроверяться
по conference.owner_id в БД — так и сделано в mute_participant (B2).

На фронте — общий парсер метаданных участника (lib/participantMetadata.ts,
переиспользован в RoomParticipantTile вместо локальной копии) и хук
useIsOrganizer (читает подсказку для локального участника через
useLocalParticipant — вызывается только внутри LiveKitRoom).
2026-08-01 22:06:10 +03:00

91 lines
4.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Проверка права входа в конференцию (статус/пароль) и генерация ответа join.
Заменяет `services/room_access.py`: комнаты больше не конкурируют за
время («правило часа» удалено вместе с бронированием, ADR-001, п.5), но
пароль закрытой конференции по-прежнему проверяется здесь же — для обычного
пользователя и гостя одинаково.
"""
import json
from core.config import get_settings
from core.security import verify_password
from models.conference import Conference
from schemas.conferences import JoinOut
from services.livekit_tokens import create_room_access_token
class ConferenceEndedError(Exception):
"""Конференция завершена (терминальный статус) — повторный вход невозможен."""
class PasswordRequiredError(Exception):
"""Конференция закрыта паролем; пароль не передан."""
class InvalidPasswordError(Exception):
"""Указанный пароль не совпадает с паролем закрытой конференции."""
def ensure_joinable(conference: Conference, *, password: str | None) -> None:
"""Проверить, что в конференцию можно войти прямо сейчас.
Бросает `ConferenceEndedError` для терминального статуса `ended`
(история и саммари остаются, но повторный вход невозможен — ADR-001,
п.2), либо `PasswordRequiredError`/`InvalidPasswordError` для закрытой
паролем конференции. Ничего не бросает для открытой конференции в
статусе `scheduled`/`active`.
"""
if conference.status == "ended":
raise ConferenceEndedError
if not conference.is_closed:
return
if conference.password_hash is None or password is None:
raise PasswordRequiredError
if not verify_password(password, conference.password_hash):
raise InvalidPasswordError
def build_join(
conference: Conference,
*,
identity: str,
name: str,
chat_enabled: bool,
avatar_url: str | None = None,
is_organizer: bool = False,
) -> JoinOut:
"""Построить ответ join: LiveKit access-токен для входа в комнату конференции.
Имя LiveKit-комнаты всегда равно `conference.slug` (ADR-001, п.4).
`chat_enabled` — снятый вызывающей стороной тоггл `instance_settings`:
читается здесь параметром, а не заново из БД, чтобы не плодить
отдельный запрос настроек на каждый join. `avatar_url`/`is_organizer`
прокидываются в метаданные токена как JSON `{"avatar_url": ..., "is_organizer": true}`
— поля добавляются, только если заданы (гость без аватара и не-организатор
получают токен вовсе без метаданных, как и раньше).
⚠️ `is_organizer` в метаданных — только подсказка для UI клиента (показать/
скрыть кнопки организатора). Метаданным токена доверять для АВТОРИЗАЦИИ
нельзя — участник технически может их подделать на своей стороне. Любое
серверное действие организатора (например, принудительный мьют) обязано
заново проверяться по `conference.owner_id` в БД, а не по этому полю.
"""
settings = get_settings()
metadata_payload: dict[str, object] = {}
if avatar_url:
metadata_payload["avatar_url"] = avatar_url
if is_organizer:
metadata_payload["is_organizer"] = True
metadata = json.dumps(metadata_payload) if metadata_payload else None
token = create_room_access_token(
room_name=conference.slug, identity=identity, name=name, metadata=metadata
)
return JoinOut(
livekit_url=settings.livekit_public_url,
token=token,
room_name=conference.slug,
conference_id=conference.id,
chat_enabled=chat_enabled,
)