refactor: stricter domain: access-control

This commit is contained in:
2026-04-11 13:25:26 +02:00
parent 6800c73292
commit 0b9a9f311e
16 changed files with 46 additions and 44 deletions

View File

@@ -0,0 +1,47 @@
import { BanEntry, User } from '../../../../shared-kernel';
type BanAwareUser = Pick<User, 'id' | 'oderId'> | null | undefined;
/** Build the set of user identifiers that may appear in room ban entries. */
export function getRoomBanCandidateIds(user: BanAwareUser, persistedUserId?: string | null): string[] {
const candidates = [
user?.id,
user?.oderId,
persistedUserId
].filter((value): value is string => typeof value === 'string' && value.trim().length > 0);
return Array.from(new Set(candidates));
}
/** Resolve the user identifier stored by a ban entry, with legacy fallback support. */
export function getRoomBanTargetId(ban: Pick<BanEntry, 'userId' | 'oderId'>): string {
if (typeof ban.userId === 'string' && ban.userId.trim().length > 0) {
return ban.userId;
}
return ban.oderId;
}
/** Return true when the given ban targets the provided user. */
export function isRoomBanMatch(
ban: Pick<BanEntry, 'userId' | 'oderId'>,
user: BanAwareUser,
persistedUserId?: string | null
): boolean {
const candidateIds = getRoomBanCandidateIds(user, persistedUserId);
if (candidateIds.length === 0) {
return false;
}
return candidateIds.includes(getRoomBanTargetId(ban));
}
/** Return true when any active ban entry targets the provided user. */
export function hasRoomBanForUser(
bans: Pick<BanEntry, 'userId' | 'oderId'>[],
user: BanAwareUser,
persistedUserId?: string | null
): boolean {
return bans.some((ban) => isRoomBanMatch(ban, user, persistedUserId));
}

View File

@@ -0,0 +1,248 @@
import {
ChannelPermissionOverride,
PermissionState,
Room,
RoomPermissionKey,
RoomRole
} from '../../../../shared-kernel';
import { SYSTEM_ROLE_IDS } from '../constants/access-control.constants';
import type { MemberIdentity } from '../models/access-control.model';
import {
buildRoleLookup,
getRolePermissionState,
matchesIdentity,
normalizePermissionState,
roleSortAscending,
compareText
} from '../util/access-control.util';
import { getAssignedRoleIds, getHighestAssignedRole } from './role-assignment.rules';
import { getRoomRoleById, normalizeRoomRoles } from './role.rules';
function resolveRolePermissionState(roles: readonly RoomRole[], assignedRoleIds: readonly string[], permission: RoomPermissionKey): PermissionState {
const roleLookup = buildRoleLookup(roles);
const assignedRoles = assignedRoleIds.map((roleId) => roleLookup.get(roleId)).filter((role): role is RoomRole => !!role);
const effectiveRoles = [roleLookup.get(SYSTEM_ROLE_IDS.everyone), ...assignedRoles]
.filter((role): role is RoomRole => !!role)
.sort(roleSortAscending);
let state: PermissionState = 'inherit';
for (const role of effectiveRoles) {
const nextState = getRolePermissionState(role, permission);
if (nextState !== 'inherit') {
state = nextState;
}
}
return state;
}
function resolveChannelOverrideState(
overrides: readonly ChannelPermissionOverride[],
roles: readonly RoomRole[],
assignedRoleIds: readonly string[],
identity: MemberIdentity,
channelId: string,
permission: RoomPermissionKey,
baseState: PermissionState
): PermissionState {
const roleLookup = buildRoleLookup(roles);
let state = baseState;
const everyoneOverride = overrides.find(
(override) =>
override.channelId === channelId &&
override.targetType === 'role' &&
override.targetId === SYSTEM_ROLE_IDS.everyone &&
override.permission === permission
);
if (everyoneOverride?.value && everyoneOverride.value !== 'inherit') {
state = everyoneOverride.value;
}
const orderedAssignedRoles = assignedRoleIds
.map((roleId) => roleLookup.get(roleId))
.filter((role): role is RoomRole => !!role)
.sort(roleSortAscending);
for (const role of orderedAssignedRoles) {
const override = overrides.find(
(candidateOverride) =>
candidateOverride.channelId === channelId &&
candidateOverride.targetType === 'role' &&
candidateOverride.targetId === role.id &&
candidateOverride.permission === permission
);
if (override?.value && override.value !== 'inherit') {
state = override.value;
}
}
const userOverride = overrides.find(
(override) =>
override.channelId === channelId &&
override.targetType === 'user' &&
override.permission === permission &&
(override.targetId === identity.id || override.targetId === identity.oderId)
);
if (userOverride?.value && userOverride.value !== 'inherit') {
state = userOverride.value;
}
return state;
}
export function normalizeChannelPermissionOverrides(
overrides: readonly ChannelPermissionOverride[] | undefined,
roles: readonly RoomRole[]
): ChannelPermissionOverride[] {
const validRoleIds = new Set(roles.map((role) => role.id));
const normalizedByKey = new Map<string, ChannelPermissionOverride>();
for (const override of overrides ?? []) {
if (!override || typeof override !== 'object') {
continue;
}
const channelId = typeof override.channelId === 'string' ? override.channelId.trim() : '';
const targetId = typeof override.targetId === 'string' ? override.targetId.trim() : '';
const targetType = override.targetType === 'role' || override.targetType === 'user' ? override.targetType : null;
const permission = override.permission;
const value = normalizePermissionState(override.value);
if (!channelId || !targetId || !targetType || !permission || value === 'inherit') {
continue;
}
if (targetType === 'role' && !validRoleIds.has(targetId)) {
continue;
}
normalizedByKey.set(`${channelId}:${targetType}:${targetId}:${permission}`, {
channelId,
targetType,
targetId,
permission,
value
});
}
return Array.from(normalizedByKey.values()).sort((firstOverride, secondOverride) => {
const channelCompare = compareText(firstOverride.channelId, secondOverride.channelId);
if (channelCompare !== 0) {
return channelCompare;
}
if (firstOverride.targetType !== secondOverride.targetType) {
return compareText(firstOverride.targetType, secondOverride.targetType);
}
const targetCompare = compareText(firstOverride.targetId, secondOverride.targetId);
if (targetCompare !== 0) {
return targetCompare;
}
return compareText(firstOverride.permission, secondOverride.permission);
});
}
export function resolveRoomPermission(
room: Room,
identity: MemberIdentity | null | undefined,
permission: RoomPermissionKey,
channelId?: string
): boolean {
if (!identity) {
return false;
}
if (room.hostId === identity.id || room.hostId === identity.oderId) {
return true;
}
const roles = normalizeRoomRoles(room.roles, room.permissions);
const assignedRoleIds = getAssignedRoleIds(room.roleAssignments, identity);
const roleState = resolveRolePermissionState(roles, assignedRoleIds, permission);
const channelState = channelId
? resolveChannelOverrideState(
normalizeChannelPermissionOverrides(room.channelPermissions, roles),
roles,
assignedRoleIds,
identity,
channelId,
permission,
roleState
)
: roleState;
return channelState === 'allow';
}
export function canManageMember(
room: Room,
actor: MemberIdentity | null | undefined,
target: MemberIdentity | null | undefined,
permission: 'kickMembers' | 'banMembers' | 'manageRoles'
): boolean {
if (!actor || !target) {
return false;
}
const isActorOwner = room.hostId === actor.id || room.hostId === actor.oderId;
const isTargetOwner = room.hostId === target.id || room.hostId === target.oderId;
const isSameIdentity = matchesIdentity(actor, {
userId: target.id || target.oderId || '',
oderId: target.oderId
});
if (isSameIdentity) {
return false;
}
if (isTargetOwner && !isActorOwner) {
return false;
}
if (isActorOwner) {
return true;
}
if (!resolveRoomPermission(room, actor, permission)) {
return false;
}
const actorRole = getHighestAssignedRole(room, actor);
const targetRole = getHighestAssignedRole(room, target);
return (actorRole?.position ?? 0) > (targetRole?.position ?? 0);
}
export function canManageRole(room: Room, actor: MemberIdentity | null | undefined, roleId: string): boolean {
if (!actor || !roleId) {
return false;
}
if (room.hostId === actor.id || room.hostId === actor.oderId) {
return true;
}
if (!resolveRoomPermission(room, actor, 'manageRoles')) {
return false;
}
const targetRole = getRoomRoleById(normalizeRoomRoles(room.roles, room.permissions), roleId);
const actorRole = getHighestAssignedRole(room, actor);
if (!targetRole) {
return false;
}
return (actorRole?.position ?? 0) > targetRole.position;
}

View File

@@ -0,0 +1,201 @@
import {
Room,
RoomMember,
RoomRole,
RoomRoleAssignment
} from '../../../../shared-kernel';
import { SYSTEM_ROLE_IDS } from '../constants/access-control.constants';
import type { MemberIdentity } from '../models/access-control.model';
import {
buildRoleLookup,
compareText,
memberIdentityKey,
matchesIdentity,
roleSortDescending,
uniqueStrings
} from '../util/access-control.util';
import { getRoomRoleById, normalizeRoomRoles } from './role.rules';
function sortAssignments(assignments: readonly RoomRoleAssignment[]): RoomRoleAssignment[] {
return [...assignments].sort((firstAssignment, secondAssignment) =>
compareText(firstAssignment.oderId || firstAssignment.userId, secondAssignment.oderId || secondAssignment.userId)
);
}
function resolveLegacyMemberRoleIds(member: RoomMember, validRoleIds: Set<string>): string[] {
if (Array.isArray(member.roleIds) && member.roleIds.length > 0) {
return uniqueStrings(member.roleIds).filter((roleId) => validRoleIds.has(roleId));
}
if (member.role === 'admin') {
return [SYSTEM_ROLE_IDS.admin];
}
if (member.role === 'moderator') {
return [SYSTEM_ROLE_IDS.moderator];
}
return [];
}
export function normalizeRoomRoleAssignments(
assignments: readonly RoomRoleAssignment[] | undefined,
members: readonly RoomMember[] | undefined,
roles: readonly RoomRole[]
): RoomRoleAssignment[] {
const validRoleIds = new Set(roles.map((role) => role.id).filter((roleId) => roleId !== SYSTEM_ROLE_IDS.everyone));
const normalizedByUserKey = new Map<string, RoomRoleAssignment>();
for (const assignment of assignments ?? []) {
if (!assignment || typeof assignment !== 'object') {
continue;
}
const userId = typeof assignment.userId === 'string' ? assignment.userId.trim() : '';
const oderId = typeof assignment.oderId === 'string' ? assignment.oderId.trim() : undefined;
const key = oderId || userId;
if (!key) {
continue;
}
const roleIds = uniqueStrings(assignment.roleIds ?? []).filter((roleId) => validRoleIds.has(roleId));
if (roleIds.length === 0) {
continue;
}
normalizedByUserKey.set(key, {
userId: userId || key,
oderId,
roleIds
});
}
if (normalizedByUserKey.size > 0) {
return sortAssignments(Array.from(normalizedByUserKey.values()));
}
for (const member of members ?? []) {
const key = memberIdentityKey(member);
if (!key) {
continue;
}
const roleIds = resolveLegacyMemberRoleIds(member, validRoleIds);
if (roleIds.length === 0) {
continue;
}
normalizedByUserKey.set(key, {
userId: member.id || key,
oderId: member.oderId || undefined,
roleIds
});
}
return sortAssignments(Array.from(normalizedByUserKey.values()));
}
export function getAssignedRoleIds(assignments: readonly RoomRoleAssignment[] | undefined, identity: MemberIdentity | null | undefined): string[] {
const assignment = (assignments ?? []).find((candidateAssignment) => matchesIdentity(identity, candidateAssignment));
return uniqueStrings(assignment?.roleIds ?? []);
}
export function getDisplayRoleName(room: Room, member: MemberIdentity | null | undefined): string {
if (!member) {
return 'Member';
}
if (room.hostId === member.id || room.hostId === member.oderId) {
return 'Owner';
}
const roles = normalizeRoomRoles(room.roles, room.permissions);
const roleLookup = buildRoleLookup(roles);
const assignedRoles = getAssignedRoleIds(room.roleAssignments, member)
.map((roleId) => roleLookup.get(roleId))
.filter((role): role is RoomRole => !!role)
.sort(roleSortDescending);
return assignedRoles[0]?.name || '@everyone';
}
export function getAssignedRoles(room: Room, identity: MemberIdentity | null | undefined): RoomRole[] {
const roles = normalizeRoomRoles(room.roles, room.permissions);
const roleLookup = buildRoleLookup(roles);
return getAssignedRoleIds(room.roleAssignments, identity)
.map((roleId) => roleLookup.get(roleId))
.filter((role): role is RoomRole => !!role)
.sort(roleSortDescending);
}
export function getHighestAssignedRole(room: Room, identity: MemberIdentity | null | undefined): RoomRole | null {
if (!identity) {
return null;
}
if (room.hostId === identity.id || room.hostId === identity.oderId) {
return null;
}
return getAssignedRoles(room, identity)[0] ?? getRoomRoleById(normalizeRoomRoles(room.roles, room.permissions), SYSTEM_ROLE_IDS.everyone) ?? null;
}
export function setRoleAssignmentsForMember(
assignments: readonly RoomRoleAssignment[] | undefined,
member: MemberIdentity,
roleIds: readonly string[]
): RoomRoleAssignment[] {
const nextAssignments = new Map<string, RoomRoleAssignment>();
const memberKey = memberIdentityKey(member);
for (const assignment of assignments ?? []) {
const key = memberIdentityKey({
id: assignment.userId,
oderId: assignment.oderId
});
if (!key || key === memberKey) {
continue;
}
nextAssignments.set(key, {
userId: assignment.userId,
oderId: assignment.oderId,
roleIds: uniqueStrings(assignment.roleIds)
});
}
const normalizedRoleIds = uniqueStrings(roleIds);
if (memberKey && normalizedRoleIds.length > 0) {
nextAssignments.set(memberKey, {
userId: member.id || member.oderId || memberKey,
oderId: member.oderId || undefined,
roleIds: normalizedRoleIds
});
}
return sortAssignments(Array.from(nextAssignments.values()));
}
export function removeRoleFromAssignments(assignments: readonly RoomRoleAssignment[] | undefined, roleId: string): RoomRoleAssignment[] {
return (assignments ?? [])
.map((assignment) => ({
...assignment,
roleIds: assignment.roleIds.filter((candidateRoleId) => candidateRoleId !== roleId)
}))
.filter((assignment) => assignment.roleIds.length > 0);
}
export function getRoleIdsForMember(room: Room, member: MemberIdentity | null | undefined): string[] {
return getAssignedRoleIds(
normalizeRoomRoleAssignments(room.roleAssignments, room.members, normalizeRoomRoles(room.roles, room.permissions)),
member
);
}

View File

@@ -0,0 +1,171 @@
import {
RoomPermissionMatrix,
RoomPermissions,
RoomRole
} from '../../../../shared-kernel';
import { SYSTEM_ROLE_IDS } from '../constants/access-control.constants';
import {
buildRoleLookup,
buildSystemRole,
nextRolePosition,
normalizeName,
normalizePermissionMatrix,
roleSortAscending,
roleSortDescending
} from '../util/access-control.util';
const ROLE_COLORS = {
everyone: '#6b7280',
moderator: '#10b981',
admin: '#60a5fa'
} as const;
function resolveNormalizedRolePosition(
position: unknown,
fallbackPosition: number | undefined,
existingRoles: readonly RoomRole[],
defaultRoles: readonly RoomRole[]
): number {
if (typeof position === 'number' && Number.isFinite(position)) {
return position;
}
if (typeof fallbackPosition === 'number') {
return fallbackPosition;
}
return nextRolePosition(existingRoles.length > 0 ? existingRoles : defaultRoles);
}
function normalizeRoomRoleEntry(
role: RoomRole | null | undefined,
defaultsById: Map<string, RoomRole>,
existingRoles: readonly RoomRole[],
defaultRoles: readonly RoomRole[]
): RoomRole | null {
if (!role) {
return null;
}
const id = typeof role.id === 'string' ? role.id.trim() : '';
const fallbackRole = defaultsById.get(id);
const name = normalizeName(typeof role.name === 'string' ? role.name : (fallbackRole?.name ?? 'Role'));
if (!id || !name) {
return null;
}
return {
id,
name,
position: resolveNormalizedRolePosition(role.position, fallbackRole?.position, existingRoles, defaultRoles),
color: typeof role.color === 'string' && role.color.trim() ? role.color.trim() : fallbackRole?.color,
isSystem: typeof role.isSystem === 'boolean' ? role.isSystem : fallbackRole?.isSystem,
permissions: normalizePermissionMatrix(role.permissions ?? fallbackRole?.permissions)
};
}
export function buildDefaultRoomRoles(legacyPermissions?: RoomPermissions): RoomRole[] {
const everyonePermissions: RoomPermissionMatrix = {
joinVoice: legacyPermissions?.allowVoice === false ? 'deny' : 'allow',
shareScreen: legacyPermissions?.allowScreenShare === false ? 'deny' : 'allow',
uploadFiles: legacyPermissions?.allowFileUploads === false ? 'deny' : 'allow'
};
const moderatorPermissions: RoomPermissionMatrix = {
kickMembers: 'allow',
deleteMessages: 'allow',
manageChannels: legacyPermissions?.moderatorsManageRooms ? 'allow' : 'inherit',
manageIcon: legacyPermissions?.moderatorsManageIcon ? 'allow' : 'inherit'
};
const adminPermissions: RoomPermissionMatrix = {
kickMembers: 'allow',
banMembers: 'allow',
manageBans: 'allow',
deleteMessages: 'allow',
manageChannels: legacyPermissions?.adminsManageRooms ? 'allow' : 'inherit',
manageIcon: legacyPermissions?.adminsManageIcon ? 'allow' : 'inherit'
};
return [
buildSystemRole(SYSTEM_ROLE_IDS.everyone, '@everyone', 0, everyonePermissions, ROLE_COLORS.everyone),
buildSystemRole(SYSTEM_ROLE_IDS.moderator, 'Moderator', 200, moderatorPermissions, ROLE_COLORS.moderator),
buildSystemRole(SYSTEM_ROLE_IDS.admin, 'Admin', 300, adminPermissions, ROLE_COLORS.admin)
];
}
export function sortRolesForDisplay(roles: readonly RoomRole[]): RoomRole[] {
return [...roles].sort(roleSortDescending);
}
export function normalizeRoomRoles(roles: readonly RoomRole[] | undefined, legacyPermissions?: RoomPermissions): RoomRole[] {
const defaultRoles = buildDefaultRoomRoles(legacyPermissions);
const defaultsById = buildRoleLookup(defaultRoles);
const normalizedById = new Map<string, RoomRole>();
for (const role of roles ?? []) {
const normalizedRole = normalizeRoomRoleEntry(role, defaultsById, Array.from(normalizedById.values()), defaultRoles);
if (normalizedRole) {
normalizedById.set(normalizedRole.id, normalizedRole);
}
}
for (const [roleId, role] of defaultsById) {
if (!normalizedById.has(roleId)) {
normalizedById.set(roleId, role);
}
}
return Array.from(normalizedById.values()).sort(roleSortAscending);
}
export function getRoomRoleById(roles: readonly RoomRole[] | undefined, roleId: string): RoomRole | undefined {
return (roles ?? []).find((role) => role.id === roleId);
}
export function createCustomRoomRole(name: string, roles: readonly RoomRole[]): RoomRole {
const normalizedName = normalizeName(name) || 'New Role';
return {
id: `role-${crypto.randomUUID()}`,
name: normalizedName,
position: nextRolePosition(roles),
permissions: {}
};
}
export function reorderRoles(roles: readonly RoomRole[], orderedRoleIds: readonly string[]): RoomRole[] {
const roleLookup = buildRoleLookup(roles);
const systemRoles = roles.filter((role) => role.isSystem);
const customRoles = orderedRoleIds.map((roleId) => roleLookup.get(roleId)).filter((role): role is RoomRole => !!role && !role.isSystem);
const remainingCustomRoles = roles.filter((role) => !role.isSystem && !orderedRoleIds.includes(role.id));
const orderedRoles = sortRolesForDisplay(systemRoles).concat(customRoles)
.concat(sortRolesForDisplay(remainingCustomRoles));
return orderedRoles
.map((role, index) => ({
...role,
position: (orderedRoles.length - index - 1) * 100
}))
.sort(roleSortAscending);
}
export function withUpdatedRole(roles: readonly RoomRole[], roleId: string, updates: Partial<RoomRole>): RoomRole[] {
return normalizeRoomRoles(
roles.map((role) => {
if (role.id !== roleId) {
return role;
}
return {
...role,
...updates,
permissions: normalizePermissionMatrix(updates.permissions ?? role.permissions)
};
})
);
}
export function findAssignableRoles(roles: readonly RoomRole[]): RoomRole[] {
return sortRolesForDisplay(roles).filter((role) => role.id !== SYSTEM_ROLE_IDS.everyone);
}

View File

@@ -0,0 +1,209 @@
import {
PermissionState,
Room,
RoomMember,
RoomPermissionKey,
RoomPermissions,
RoomRole,
RoomRoleAssignment,
UserRole
} from '../../../../shared-kernel';
import { SYSTEM_ROLE_IDS } from '../constants/access-control.constants';
import {
getRolePermissionState,
permissionStateToBoolean,
resolveLegacyAllowState
} from '../util/access-control.util';
import type { MemberIdentity } from '../models/access-control.model';
import {
getAssignedRoleIds,
normalizeRoomRoleAssignments,
removeRoleFromAssignments
} from './role-assignment.rules';
import {
getRoomRoleById,
normalizeRoomRoles,
withUpdatedRole
} from './role.rules';
import { normalizeChannelPermissionOverrides, resolveRoomPermission } from './permission.rules';
function applyRolePermissionChanges(
roles: readonly RoomRole[],
role: RoomRole | null | undefined,
changes: Partial<Record<RoomPermissionKey, PermissionState | undefined>>
): RoomRole[] {
if (!role) {
return [...roles];
}
return withUpdatedRole(roles, role.id, {
permissions: {
...role.permissions,
...changes
}
});
}
function getEveryoneLegacyPermissionChanges(
role: RoomRole,
permissions: Partial<RoomPermissions>
): Partial<Record<RoomPermissionKey, PermissionState | undefined>> {
return {
joinVoice: resolveLegacyAllowState(permissions.allowVoice, role.permissions?.joinVoice, 'deny'),
shareScreen: resolveLegacyAllowState(permissions.allowScreenShare, role.permissions?.shareScreen, 'deny'),
uploadFiles: resolveLegacyAllowState(permissions.allowFileUploads, role.permissions?.uploadFiles, 'deny')
};
}
function getModeratorLegacyPermissionChanges(
role: RoomRole,
permissions: Partial<RoomPermissions>
): Partial<Record<RoomPermissionKey, PermissionState | undefined>> {
return {
manageChannels: resolveLegacyAllowState(permissions.moderatorsManageRooms, role.permissions?.manageChannels, 'inherit'),
manageIcon: resolveLegacyAllowState(permissions.moderatorsManageIcon, role.permissions?.manageIcon, 'inherit')
};
}
function getAdminLegacyPermissionChanges(
role: RoomRole,
permissions: Partial<RoomPermissions>
): Partial<Record<RoomPermissionKey, PermissionState | undefined>> {
return {
manageChannels: resolveLegacyAllowState(permissions.adminsManageRooms, role.permissions?.manageChannels, 'inherit'),
manageIcon: resolveLegacyAllowState(permissions.adminsManageIcon, role.permissions?.manageIcon, 'inherit')
};
}
export function resolveLegacyRole(room: Room, identity: MemberIdentity | null | undefined): UserRole {
if (!identity) {
return 'member';
}
if (room.hostId === identity.id || room.hostId === identity.oderId) {
return 'host';
}
const assignedRoleIds = getAssignedRoleIds(room.roleAssignments, identity);
if (assignedRoleIds.includes(SYSTEM_ROLE_IDS.admin)) {
return 'admin';
}
if (assignedRoleIds.includes(SYSTEM_ROLE_IDS.moderator)) {
return 'moderator';
}
if (
resolveRoomPermission(room, identity, 'manageRoles') ||
resolveRoomPermission(room, identity, 'banMembers') ||
resolveRoomPermission(room, identity, 'manageBans') ||
resolveRoomPermission(room, identity, 'manageServer')
) {
return 'admin';
}
if (
resolveRoomPermission(room, identity, 'kickMembers') ||
resolveRoomPermission(room, identity, 'deleteMessages') ||
resolveRoomPermission(room, identity, 'manageChannels') ||
resolveRoomPermission(room, identity, 'manageIcon')
) {
return 'moderator';
}
return 'member';
}
export function deriveLegacyRoomPermissions(room: Pick<Room, 'roles' | 'permissions' | 'slowModeInterval'>): RoomPermissions {
const roles = normalizeRoomRoles(room.roles, room.permissions);
const everyoneRole = getRoomRoleById(roles, SYSTEM_ROLE_IDS.everyone);
const moderatorRole = getRoomRoleById(roles, SYSTEM_ROLE_IDS.moderator);
const adminRole = getRoomRoleById(roles, SYSTEM_ROLE_IDS.admin);
return {
allowVoice: permissionStateToBoolean(getRolePermissionState(everyoneRole, 'joinVoice'), true),
allowScreenShare: permissionStateToBoolean(getRolePermissionState(everyoneRole, 'shareScreen'), true),
allowFileUploads: permissionStateToBoolean(getRolePermissionState(everyoneRole, 'uploadFiles'), true),
adminsManageRooms: getRolePermissionState(adminRole, 'manageChannels') === 'allow',
moderatorsManageRooms: getRolePermissionState(moderatorRole, 'manageChannels') === 'allow',
adminsManageIcon: getRolePermissionState(adminRole, 'manageIcon') === 'allow',
moderatorsManageIcon: getRolePermissionState(moderatorRole, 'manageIcon') === 'allow',
slowModeInterval: room.slowModeInterval ?? room.permissions?.slowModeInterval ?? 0
};
}
export function withLegacyRoomPermissions(room: Room, permissions: Partial<RoomPermissions>): Room {
const roles = normalizeRoomRoles(room.roles, room.permissions);
const everyoneRole = getRoomRoleById(roles, SYSTEM_ROLE_IDS.everyone);
const moderatorRole = getRoomRoleById(roles, SYSTEM_ROLE_IDS.moderator);
const adminRole = getRoomRoleById(roles, SYSTEM_ROLE_IDS.admin);
let nextRoles = applyRolePermissionChanges(roles, everyoneRole, everyoneRole ? getEveryoneLegacyPermissionChanges(everyoneRole, permissions) : {});
nextRoles = applyRolePermissionChanges(
nextRoles,
moderatorRole,
moderatorRole ? getModeratorLegacyPermissionChanges(moderatorRole, permissions) : {}
);
nextRoles = applyRolePermissionChanges(nextRoles, adminRole, adminRole ? getAdminLegacyPermissionChanges(adminRole, permissions) : {});
return normalizeRoomAccessControl({
...room,
roles: nextRoles,
slowModeInterval: permissions.slowModeInterval ?? room.slowModeInterval ?? room.permissions?.slowModeInterval ?? 0
});
}
export function hydrateRoomMembers(room: Room): RoomMember[] {
const roles = normalizeRoomRoles(room.roles, room.permissions);
const roleAssignments = normalizeRoomRoleAssignments(room.roleAssignments, room.members, roles);
return (room.members ?? []).map((member) => {
const roleIds = getAssignedRoleIds(roleAssignments, member);
const hydratedRoom: Room = {
...room,
roles,
roleAssignments
};
return {
...member,
roleIds,
role: resolveLegacyRole(hydratedRoom, member)
};
});
}
export function normalizeRoomAccessControl(room: Room): Room {
const roles = normalizeRoomRoles(room.roles, room.permissions);
const roleAssignments = normalizeRoomRoleAssignments(room.roleAssignments, room.members, roles);
const channelPermissions = normalizeChannelPermissionOverrides(room.channelPermissions, roles);
const slowModeInterval = room.slowModeInterval ?? room.permissions?.slowModeInterval ?? 0;
const nextRoom: Room = {
...room,
roles,
roleAssignments,
channelPermissions,
slowModeInterval
};
nextRoom.permissions = deriveLegacyRoomPermissions(nextRoom);
nextRoom.members = hydrateRoomMembers(nextRoom);
return nextRoom;
}
export function removeRole(
roles: readonly RoomRole[],
assignments: readonly RoomRoleAssignment[] | undefined,
roleId: string
): { roles: RoomRole[]; roleAssignments: RoomRoleAssignment[] } {
const nextRoles = roles.filter((role) => role.id !== roleId || role.isSystem);
return {
roles: normalizeRoomRoles(nextRoles),
roleAssignments: removeRoleFromAssignments(assignments, roleId)
};
}