Add access control rework
This commit is contained in:
@@ -22,11 +22,7 @@ import {
|
||||
lucidePlus,
|
||||
lucideVolumeX
|
||||
} from '@ng-icons/lucide';
|
||||
import {
|
||||
selectOnlineUsers,
|
||||
selectCurrentUser,
|
||||
selectIsCurrentUserAdmin
|
||||
} from '../../../store/users/users.selectors';
|
||||
import { selectOnlineUsers, selectCurrentUser } from '../../../store/users/users.selectors';
|
||||
import {
|
||||
selectCurrentRoom,
|
||||
selectActiveChannelId,
|
||||
@@ -44,6 +40,12 @@ import { VoiceSessionFacade, VoiceWorkspaceService } from '../../../domains/voic
|
||||
import { VoicePlaybackService } from '../../../domains/voice-connection/application/voice-playback.service';
|
||||
import { VoiceControlsComponent } from '../../../domains/voice-session/feature/voice-controls/voice-controls.component';
|
||||
import { isChannelNameTaken, normalizeChannelName } from '../../../store/rooms/room-channels.rules';
|
||||
import {
|
||||
canManageMember,
|
||||
resolveRoomPermission,
|
||||
setRoleAssignmentsForMember,
|
||||
SYSTEM_ROLE_IDS
|
||||
} from '../../../domains/access-control';
|
||||
import {
|
||||
ContextMenuComponent,
|
||||
UserAvatarComponent,
|
||||
@@ -108,7 +110,6 @@ export class RoomsSidePanelComponent {
|
||||
onlineUsers = this.store.selectSignal(selectOnlineUsers);
|
||||
currentUser = this.store.selectSignal(selectCurrentUser);
|
||||
currentRoom = this.store.selectSignal(selectCurrentRoom);
|
||||
isAdmin = this.store.selectSignal(selectIsCurrentUserAdmin);
|
||||
activeChannelId = this.store.selectSignal(selectActiveChannelId);
|
||||
textChannels = this.store.selectSignal(selectTextChannels);
|
||||
voiceChannels = this.store.selectSignal(selectVoiceChannels);
|
||||
@@ -202,9 +203,9 @@ export class RoomsSidePanelComponent {
|
||||
private isCurrentUserIdentity(entity: { id?: string; oderId?: string }): boolean {
|
||||
const current = this.currentUser();
|
||||
|
||||
return !!current && (
|
||||
(typeof entity.id === 'string' && entity.id === current.id)
|
||||
|| (typeof entity.oderId === 'string' && entity.oderId === current.oderId)
|
||||
return (
|
||||
!!current &&
|
||||
((typeof entity.id === 'string' && entity.id === current.id) || (typeof entity.oderId === 'string' && entity.oderId === current.oderId))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -215,18 +216,7 @@ export class RoomsSidePanelComponent {
|
||||
if (!room || !user)
|
||||
return false;
|
||||
|
||||
if (room.hostId === user.id)
|
||||
return true;
|
||||
|
||||
const perms = room.permissions || {};
|
||||
|
||||
if (user.role === 'admin' && perms.adminsManageRooms)
|
||||
return true;
|
||||
|
||||
if (user.role === 'moderator' && perms.moderatorsManageRooms)
|
||||
return true;
|
||||
|
||||
return false;
|
||||
return resolveRoomPermission(room, user, 'manageChannels');
|
||||
}
|
||||
|
||||
selectTextChannel(channelId: string) {
|
||||
@@ -317,11 +307,7 @@ export class RoomsSidePanelComponent {
|
||||
return;
|
||||
}
|
||||
|
||||
this.notifications.setChannelMuted(
|
||||
roomId,
|
||||
channel.id,
|
||||
!this.notifications.isChannelMuted(roomId, channel.id)
|
||||
);
|
||||
this.notifications.setChannelMuted(roomId, channel.id, !this.notifications.isChannelMuted(roomId, channel.id));
|
||||
}
|
||||
|
||||
isContextChannelMuted(): boolean {
|
||||
@@ -410,9 +396,7 @@ export class RoomsSidePanelComponent {
|
||||
}
|
||||
|
||||
const channels = this.currentRoom()?.channels ?? [];
|
||||
const channelType = excludeChannelId
|
||||
? channels.find((channel) => channel.id === excludeChannelId)?.type
|
||||
: this.createChannelType();
|
||||
const channelType = excludeChannelId ? channels.find((channel) => channel.id === excludeChannelId)?.type : this.createChannelType();
|
||||
|
||||
if (!channelType) {
|
||||
return null;
|
||||
@@ -428,7 +412,7 @@ export class RoomsSidePanelComponent {
|
||||
openUserContextMenu(evt: MouseEvent, user: User) {
|
||||
evt.preventDefault();
|
||||
|
||||
if (!this.isAdmin())
|
||||
if (!this.canManageContextUser(user))
|
||||
return;
|
||||
|
||||
this.contextMenuUser.set(user);
|
||||
@@ -457,19 +441,22 @@ export class RoomsSidePanelComponent {
|
||||
|
||||
changeUserRole(role: 'admin' | 'moderator' | 'member') {
|
||||
const user = this.contextMenuUser();
|
||||
const roomId = this.currentRoom()?.id;
|
||||
const room = this.currentRoom();
|
||||
|
||||
this.closeUserMenu();
|
||||
|
||||
if (user) {
|
||||
this.store.dispatch(UsersActions.updateUserRole({ userId: user.id, role }));
|
||||
this.realtime.broadcastMessage({
|
||||
type: 'role-change',
|
||||
roomId,
|
||||
targetUserId: user.id,
|
||||
role
|
||||
});
|
||||
}
|
||||
if (!user || !room)
|
||||
return;
|
||||
|
||||
const roleIds = role === 'admin' ? [SYSTEM_ROLE_IDS.admin] : role === 'moderator' ? [SYSTEM_ROLE_IDS.moderator] : [];
|
||||
const roleAssignments = setRoleAssignmentsForMember(room.roleAssignments, user, roleIds);
|
||||
|
||||
this.store.dispatch(
|
||||
RoomsActions.updateRoomAccessControl({
|
||||
roomId: room.id,
|
||||
changes: { roleAssignments }
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
kickUserAction() {
|
||||
@@ -482,52 +469,69 @@ export class RoomsSidePanelComponent {
|
||||
}
|
||||
}
|
||||
|
||||
private openExistingVoiceWorkspace(room: Room | null, current: User | null, roomId: string): boolean {
|
||||
if (!room || !current?.voiceState?.isConnected || current.voiceState.roomId !== roomId || current.voiceState.serverId !== room.id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this.voiceWorkspace.open(null, { connectRemoteShares: true });
|
||||
return true;
|
||||
}
|
||||
|
||||
private canJoinRequestedVoiceRoom(room: Room, current: User | null, roomId: string): boolean {
|
||||
return !current || resolveRoomPermission(room, current, 'joinVoice', roomId);
|
||||
}
|
||||
|
||||
private prepareCrossServerVoiceJoin(room: Room, current: User | null): boolean {
|
||||
if (!current?.voiceState?.isConnected || current.voiceState.serverId === room.id) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (this.voiceConnection.isVoiceConnected()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (current.id) {
|
||||
this.store.dispatch(
|
||||
UsersActions.updateVoiceState({
|
||||
userId: current.id,
|
||||
voiceState: {
|
||||
isConnected: false,
|
||||
isMuted: false,
|
||||
isDeafened: false,
|
||||
roomId: undefined,
|
||||
serverId: undefined
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private enableVoiceForJoin(room: Room, current: User | null, roomId: string): Promise<void> {
|
||||
const isSwitchingChannels = !!current?.voiceState?.isConnected && current.voiceState.serverId === room.id && current.voiceState.roomId !== roomId;
|
||||
|
||||
return isSwitchingChannels ? Promise.resolve() : this.voiceConnection.enableVoice().then(() => undefined);
|
||||
}
|
||||
|
||||
joinVoice(roomId: string) {
|
||||
const room = this.currentRoom();
|
||||
const current = this.currentUser();
|
||||
|
||||
if (
|
||||
room
|
||||
&& current?.voiceState?.isConnected
|
||||
&& current.voiceState.roomId === roomId
|
||||
&& current.voiceState.serverId === room.id
|
||||
) {
|
||||
this.voiceWorkspace.open(null, { connectRemoteShares: true });
|
||||
if (this.openExistingVoiceWorkspace(room, current ?? null, roomId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (room && room.permissions && room.permissions.allowVoice === false) {
|
||||
if (!room || !this.canJoinRequestedVoiceRoom(room, current ?? null, roomId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!room)
|
||||
if (!this.prepareCrossServerVoiceJoin(room, current ?? null)) {
|
||||
return;
|
||||
|
||||
if (current?.voiceState?.isConnected && current.voiceState.serverId !== room?.id) {
|
||||
if (!this.voiceConnection.isVoiceConnected()) {
|
||||
if (current.id) {
|
||||
this.store.dispatch(
|
||||
UsersActions.updateVoiceState({
|
||||
userId: current.id,
|
||||
voiceState: {
|
||||
isConnected: false,
|
||||
isMuted: false,
|
||||
isDeafened: false,
|
||||
roomId: undefined,
|
||||
serverId: undefined
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const isSwitchingChannels = current?.voiceState?.isConnected && current.voiceState.serverId === room?.id && current.voiceState.roomId !== roomId;
|
||||
const enableVoicePromise = isSwitchingChannels ? Promise.resolve() : this.voiceConnection.enableVoice();
|
||||
|
||||
enableVoicePromise
|
||||
this.enableVoiceForJoin(room, current ?? null, roomId)
|
||||
.then(() => this.onVoiceJoinSucceeded(roomId, room, current ?? null))
|
||||
.catch(() => undefined);
|
||||
}
|
||||
@@ -668,9 +672,7 @@ export class RoomsSidePanelComponent {
|
||||
}
|
||||
|
||||
viewStream(userId: string) {
|
||||
const focusTarget = this.isUserSharing(userId)
|
||||
? `screen:${userId}`
|
||||
: `camera:${userId}`;
|
||||
const focusTarget = this.isUserSharing(userId) ? `screen:${userId}` : `camera:${userId}`;
|
||||
|
||||
this.voiceWorkspace.focusStream(focusTarget, { connectRemoteShares: true });
|
||||
}
|
||||
@@ -768,10 +770,12 @@ export class RoomsSidePanelComponent {
|
||||
serverId: room.id
|
||||
};
|
||||
|
||||
this.store.dispatch(UsersActions.updateVoiceState({
|
||||
userId: targetUser.id,
|
||||
voiceState: movedVoiceState
|
||||
}));
|
||||
this.store.dispatch(
|
||||
UsersActions.updateVoiceState({
|
||||
userId: targetUser.id,
|
||||
voiceState: movedVoiceState
|
||||
})
|
||||
);
|
||||
|
||||
this.realtime.broadcastMessage({
|
||||
type: 'voice-channel-move',
|
||||
@@ -809,8 +813,7 @@ export class RoomsSidePanelComponent {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.getPeerKeysForUser(user, userId)
|
||||
.some((peerKey) => this.hasActiveVideoStream(this.voiceConnection.getRemoteCameraStream(peerKey)));
|
||||
return this.getPeerKeysForUser(user, userId).some((peerKey) => this.hasActiveVideoStream(this.voiceConnection.getRemoteCameraStream(peerKey)));
|
||||
}
|
||||
|
||||
isUserSharing(userId: string): boolean {
|
||||
@@ -834,9 +837,10 @@ export class RoomsSidePanelComponent {
|
||||
return false;
|
||||
}
|
||||
|
||||
const stream = this.getPeerKeysForUser(user, userId)
|
||||
.map((peerKey) => this.screenShare.getRemoteScreenShareStream(peerKey))
|
||||
.find((candidate) => this.hasActiveVideoStream(candidate)) || null;
|
||||
const stream =
|
||||
this.getPeerKeysForUser(user, userId)
|
||||
.map((peerKey) => this.screenShare.getRemoteScreenShareStream(peerKey))
|
||||
.find((candidate) => this.hasActiveVideoStream(candidate)) || null;
|
||||
|
||||
return this.hasActiveVideoStream(stream);
|
||||
}
|
||||
@@ -856,16 +860,10 @@ export class RoomsSidePanelComponent {
|
||||
(user) => !!user.voiceState?.isConnected && user.voiceState?.roomId === roomId && user.voiceState?.serverId === room?.id
|
||||
);
|
||||
|
||||
if (
|
||||
me?.voiceState?.isConnected &&
|
||||
me.voiceState?.roomId === roomId &&
|
||||
me.voiceState?.serverId === room?.id
|
||||
) {
|
||||
if (me?.voiceState?.isConnected && me.voiceState?.roomId === roomId && me.voiceState?.serverId === room?.id) {
|
||||
const meId = me.id;
|
||||
const meOderId = me.oderId;
|
||||
const alreadyIncluded = remoteUsers.some(
|
||||
(user) => user.id === meId || user.oderId === meOderId
|
||||
);
|
||||
const alreadyIncluded = remoteUsers.some((user) => user.id === meId || user.oderId === meOderId);
|
||||
|
||||
if (!alreadyIncluded) {
|
||||
return [me, ...remoteUsers];
|
||||
@@ -884,8 +882,42 @@ export class RoomsSidePanelComponent {
|
||||
|
||||
voiceEnabled(): boolean {
|
||||
const room = this.currentRoom();
|
||||
const user = this.currentUser();
|
||||
|
||||
return room?.permissions?.allowVoice !== false;
|
||||
return !!room && !!user && resolveRoomPermission(room, user, 'joinVoice');
|
||||
}
|
||||
|
||||
canManageContextUser(user: User | null): boolean {
|
||||
const room = this.currentRoom();
|
||||
const currentUser = this.currentUser();
|
||||
|
||||
if (!room || !currentUser || !user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return this.canChangeUserRole(user) || this.canKickUser(user);
|
||||
}
|
||||
|
||||
canChangeUserRole(user: User | null): boolean {
|
||||
const room = this.currentRoom();
|
||||
const currentUser = this.currentUser();
|
||||
|
||||
if (!room || !currentUser || !user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return canManageMember(room, currentUser, user, 'manageRoles');
|
||||
}
|
||||
|
||||
canKickUser(user: User | null): boolean {
|
||||
const room = this.currentRoom();
|
||||
const currentUser = this.currentUser();
|
||||
|
||||
if (!room || !currentUser || !user) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return canManageMember(room, currentUser, user, 'kickMembers');
|
||||
}
|
||||
|
||||
getPeerLatency(user: User): number | null {
|
||||
@@ -934,9 +966,11 @@ export class RoomsSidePanelComponent {
|
||||
return true;
|
||||
}
|
||||
|
||||
return !!user?.voiceState?.isConnected
|
||||
&& user.voiceState.roomId === currentVoiceState.roomId
|
||||
&& user.voiceState.serverId === currentVoiceState.serverId;
|
||||
return (
|
||||
!!user?.voiceState?.isConnected &&
|
||||
user.voiceState.roomId === currentVoiceState.roomId &&
|
||||
user.voiceState.serverId === currentVoiceState.serverId
|
||||
);
|
||||
}
|
||||
|
||||
private getPeerKeysForUser(user: User | null, userId: string): string[] {
|
||||
@@ -944,9 +978,7 @@ export class RoomsSidePanelComponent {
|
||||
user?.oderId,
|
||||
user?.id,
|
||||
userId
|
||||
].filter(
|
||||
(candidate): candidate is string => !!candidate
|
||||
);
|
||||
].filter((candidate): candidate is string => !!candidate);
|
||||
}
|
||||
|
||||
private hasActiveVideoStream(stream: MediaStream | null): boolean {
|
||||
|
||||
Reference in New Issue
Block a user