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, 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(); 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[] { 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); }