♻️(backend) pass the participant role in the LiveKit token

The backend previously passed an abstract is_admin_or_owner boolean
flag in the LiveKit token. That kept the frontend minimalistic and
saved it from having to handle role comparisons.

As we introduce more features that need to distinguish between the
room owner and admins, refactor the token to carry the role
directly. The frontend can then derive the relevant flags from a
richer piece of information.
This commit is contained in:
lebaudantoine
2026-07-07 19:21:12 +02:00
committed by aleb_the_flash
parent f043ad6f98
commit 5a641a4366
13 changed files with 143 additions and 67 deletions
+17 -7
View File
@@ -104,21 +104,30 @@ class LobbyService:
)
@staticmethod
def can_bypass_lobby(room, user) -> bool:
def can_bypass_lobby(room, user, role) -> bool:
"""Determines if a user can bypass the waiting lobby and join a room directly.
A user can bypass the lobby if:
1. The room is public (open to everyone)
2. The room has TRUSTED access level and the user is authenticated
2. The room has RESTRICTED access level and the user has any role
Note: Room access levels can change while participants are waiting in the lobby.
This function only checks the current state and should be called each time
a participant requests entry to ensure consistent access control, even for
participants who have already begun waiting.
"""
return room.is_public or (
room.access_level == models.RoomAccessLevel.TRUSTED
and user.is_authenticated
return (
room.is_public
or (
room.access_level == models.RoomAccessLevel.TRUSTED
and user.is_authenticated
)
or (
room.access_level == models.RoomAccessLevel.RESTRICTED
and user.is_authenticated
and role is not None
)
)
def request_entry(
@@ -144,8 +153,9 @@ class LobbyService:
participant = self._get_participant(room.id, participant_id)
room_id = str(room.id)
user_role = room.get_role(request.user)
if self.can_bypass_lobby(room=room, user=request.user):
if self.can_bypass_lobby(room=room, user=request.user, role=user_role):
if participant is None:
participant = LobbyParticipant(
status=LobbyParticipantStatus.ACCEPTED,
@@ -162,8 +172,8 @@ class LobbyService:
username=username,
color=participant.color,
configuration=room.configuration,
is_admin_or_owner=False,
participant_id=participant_id,
role=user_role,
)
return participant, livekit_config
@@ -183,8 +193,8 @@ class LobbyService:
username=username,
color=participant.color,
configuration=room.configuration,
is_admin_or_owner=False,
participant_id=participant_id,
role=user_role,
)
return participant, livekit_config
+4 -4
View File
@@ -4,7 +4,7 @@ Single entry point for changing a user's role on a room, used by:
- the in-meeting endpoint (promote/demote a connected participant)
- (more to come soon)
`ResourceAccess` is the source of truth. The LiveKit `room_admin`
`ResourceAccess` is the source of truth. The LiveKit `room_role`
participant attribute is only a projection of it, synced best-effort.
"""
@@ -138,7 +138,7 @@ class RoomRoleService:
livekit_synced = self._sync_livekit_role(
room_name=room_name,
participant_identity=str(participant_identity),
is_admin=role == models.RoleChoices.ADMIN,
role=str(role),
)
return {
@@ -147,7 +147,7 @@ class RoomRoleService:
}
@staticmethod
def _sync_livekit_role(room_name: str, participant_identity: str, is_admin: bool):
def _sync_livekit_role(room_name: str, participant_identity: str, role: str):
"""Mirror the role to the participant's LiveKit attributes.
Best-effort: returns False on failure instead of raising, so callers
@@ -157,7 +157,7 @@ class RoomRoleService:
ParticipantsManagement().update(
room_name=room_name,
identity=participant_identity,
attributes={"room_admin": "true" if is_admin else "false"},
attributes={"room_role": role},
)
except ParticipantNotFoundException:
# The participant left between the presence check and the update: