refactor: stricter domain: access-control
This commit is contained in:
@@ -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);
|
||||
}
|
||||
Reference in New Issue
Block a user