fix(voice): административный токен RoomService привязан к комнате

LiveKit принимает право `roomAdmin` только вместе с именем комнаты: без
`video.room` RoomService отвечает 401 `permissions denied` — серверный мьют и
отключение участника не доходили до SFU, хотя API отвечал 200 (AGENT.md 7.14).

- `voice.IssueAdmin(room)` кладёт в грахт `room` и `roomAdmin` и отклоняет
  пустое имя комнаты;
- `AdminClient.call` выпускает токен под конкретную комнату каждого вызова
  (GetParticipant, MutePublishedTrack, RemoveParticipant, ListParticipants);
- тест на состав грахта и отказ при пустой комнате;
- e2e: администратор и гость работают в отдельных api-контекстах (код 2FA
  одноразовый), нажатие «Войти» в комнату не ждёт стабильности исчезающей
  кнопки.
This commit is contained in:
2026-09-20 19:54:21 +03:00
parent 912a067c9a
commit a6d23a295e
4 changed files with 108 additions and 45 deletions
+10 -8
View File
@@ -120,12 +120,17 @@ func encodeSegment(payload []byte) string {
return base64.RawURLEncoding.EncodeToString(payload)
}
// IssueAdmin подписывает административный токен RoomService: полные права на
// комнаты (модерация, удаление участников), но не на запись медиа.
func (i *TokenIssuer) IssueAdmin() (string, error) {
// IssueAdmin подписывает административный токен RoomService: права на
// модерацию одной комнаты (мьют дорожки, удаление участника), но не на запись
// медиа. LiveKit проверяет `roomAdmin` вместе с именем комнаты: без `room` в
// грахте RoomService отвечает 401 `permissions denied`.
func (i *TokenIssuer) IssueAdmin(room string) (string, error) {
if !i.Enabled() {
return "", ErrDisabled
}
if strings.TrimSpace(room) == "" {
return "", fmt.Errorf("voice.invalid_room: пустое имя комнаты")
}
now := i.now().UTC()
claims := map[string]any{
"iss": i.apiKey,
@@ -133,11 +138,8 @@ func (i *TokenIssuer) IssueAdmin() (string, error) {
"nbf": now.Add(-10 * time.Second).Unix(),
"exp": now.Add(10 * time.Minute).Unix(),
"video": map[string]any{
"roomCreate": true,
"roomList": true,
"roomAdmin": true,
"roomRecord": false,
"ingressAdmin": false,
"room": room,
"roomAdmin": true,
},
}
header, err := json.Marshal(map[string]string{"alg": "HS256", "typ": "JWT"})