192 lines
5.8 KiB
TypeScript
192 lines
5.8 KiB
TypeScript
import type {
|
|
AccessRolePayload,
|
|
PermissionStatePayload,
|
|
RoleAssignmentPayload,
|
|
ServerPayload,
|
|
ServerPermissionKeyPayload
|
|
} from '../cqrs/types';
|
|
import { normalizeServerRoleAssignments, normalizeServerRoles } from '../cqrs/relations';
|
|
|
|
const SYSTEM_ROLE_IDS = {
|
|
everyone: 'system-everyone'
|
|
} as const;
|
|
|
|
interface ServerIdentity {
|
|
userId: string;
|
|
oderId?: string;
|
|
}
|
|
|
|
function getServerRoles(server: Pick<ServerPayload, 'roles'>): AccessRolePayload[] {
|
|
return normalizeServerRoles(server.roles);
|
|
}
|
|
|
|
function getServerAssignments(server: Pick<ServerPayload, 'roleAssignments' | 'roles'>): RoleAssignmentPayload[] {
|
|
return normalizeServerRoleAssignments(server.roleAssignments, getServerRoles(server));
|
|
}
|
|
|
|
function matchesIdentity(identity: ServerIdentity, assignment: RoleAssignmentPayload): boolean {
|
|
return assignment.userId === identity.userId
|
|
|| assignment.oderId === identity.userId
|
|
|| (!!identity.oderId && (assignment.userId === identity.oderId || assignment.oderId === identity.oderId));
|
|
}
|
|
|
|
function resolveAssignedRoleIds(server: Pick<ServerPayload, 'roleAssignments' | 'roles'>, identity: ServerIdentity): string[] {
|
|
const assignment = getServerAssignments(server).find((candidateAssignment) => matchesIdentity(identity, candidateAssignment));
|
|
|
|
return assignment?.roleIds ?? [];
|
|
}
|
|
|
|
function compareRolePosition(firstRole: AccessRolePayload, secondRole: AccessRolePayload): number {
|
|
if (firstRole.position !== secondRole.position) {
|
|
return firstRole.position - secondRole.position;
|
|
}
|
|
|
|
return firstRole.name.localeCompare(secondRole.name, undefined, { sensitivity: 'base' });
|
|
}
|
|
|
|
function resolveRolePermissionState(
|
|
roles: readonly AccessRolePayload[],
|
|
assignedRoleIds: readonly string[],
|
|
permission: ServerPermissionKeyPayload
|
|
): PermissionStatePayload {
|
|
const roleLookup = new Map(roles.map((role) => [role.id, role]));
|
|
const effectiveRoles = [roleLookup.get(SYSTEM_ROLE_IDS.everyone), ...assignedRoleIds.map((roleId) => roleLookup.get(roleId))]
|
|
.filter((role): role is AccessRolePayload => !!role)
|
|
.sort(compareRolePosition);
|
|
|
|
let state: PermissionStatePayload = 'inherit';
|
|
|
|
for (const role of effectiveRoles) {
|
|
const nextState = role.permissions?.[permission] ?? 'inherit';
|
|
|
|
if (nextState !== 'inherit') {
|
|
state = nextState;
|
|
}
|
|
}
|
|
|
|
return state;
|
|
}
|
|
|
|
function resolveHighestRole(
|
|
server: Pick<ServerPayload, 'roleAssignments' | 'roles'>,
|
|
identity: ServerIdentity
|
|
): AccessRolePayload | null {
|
|
const roles = getServerRoles(server);
|
|
const assignedRoleIds = resolveAssignedRoleIds(server, identity);
|
|
const roleLookup = new Map(roles.map((role) => [role.id, role]));
|
|
const assignedRoles = assignedRoleIds
|
|
.map((roleId) => roleLookup.get(roleId))
|
|
.filter((role): role is AccessRolePayload => !!role)
|
|
.sort((firstRole, secondRole) => compareRolePosition(secondRole, firstRole));
|
|
|
|
return assignedRoles[0] ?? roleLookup.get(SYSTEM_ROLE_IDS.everyone) ?? null;
|
|
}
|
|
|
|
export function isServerOwner(server: Pick<ServerPayload, 'ownerId'>, actorUserId: string): boolean {
|
|
return server.ownerId === actorUserId;
|
|
}
|
|
|
|
export function resolveServerPermission(
|
|
server: Pick<ServerPayload, 'ownerId' | 'roleAssignments' | 'roles'>,
|
|
actorUserId: string,
|
|
permission: ServerPermissionKeyPayload,
|
|
actorOderId?: string
|
|
): boolean {
|
|
if (isServerOwner(server, actorUserId)) {
|
|
return true;
|
|
}
|
|
|
|
const roles = getServerRoles(server);
|
|
const assignedRoleIds = resolveAssignedRoleIds(server, {
|
|
userId: actorUserId,
|
|
oderId: actorOderId
|
|
});
|
|
|
|
return resolveRolePermissionState(roles, assignedRoleIds, permission) === 'allow';
|
|
}
|
|
|
|
export function canManageServerUpdate(
|
|
server: Pick<ServerPayload, 'ownerId' | 'roleAssignments' | 'roles'>,
|
|
actorUserId: string,
|
|
updates: Record<string, unknown>,
|
|
actorOderId?: string
|
|
): boolean {
|
|
if (isServerOwner(server, actorUserId)) {
|
|
return true;
|
|
}
|
|
|
|
if (typeof updates['ownerId'] === 'string' || typeof updates['ownerPublicKey'] === 'string') {
|
|
return false;
|
|
}
|
|
|
|
const requiredPermissions = new Set<ServerPermissionKeyPayload>();
|
|
|
|
if (
|
|
Array.isArray(updates['roles'])
|
|
|| Array.isArray(updates['roleAssignments'])
|
|
|| Array.isArray(updates['channelPermissions'])
|
|
) {
|
|
requiredPermissions.add('manageRoles');
|
|
}
|
|
|
|
if (Array.isArray(updates['channels'])) {
|
|
requiredPermissions.add('manageChannels');
|
|
}
|
|
|
|
if (typeof updates['icon'] === 'string') {
|
|
requiredPermissions.add('manageIcon');
|
|
}
|
|
|
|
if (
|
|
typeof updates['name'] === 'string'
|
|
|| typeof updates['description'] === 'string'
|
|
|| typeof updates['isPrivate'] === 'boolean'
|
|
|| typeof updates['maxUsers'] === 'number'
|
|
|| typeof updates['password'] === 'string'
|
|
|| typeof updates['passwordHash'] === 'string'
|
|
|| typeof updates['slowModeInterval'] === 'number'
|
|
) {
|
|
requiredPermissions.add('manageServer');
|
|
}
|
|
|
|
return Array.from(requiredPermissions).every((permission) =>
|
|
resolveServerPermission(server, actorUserId, permission, actorOderId)
|
|
);
|
|
}
|
|
|
|
export function canModerateServerMember(
|
|
server: Pick<ServerPayload, 'ownerId' | 'roleAssignments' | 'roles'>,
|
|
actorUserId: string,
|
|
targetUserId: string,
|
|
permission: 'kickMembers' | 'banMembers' | 'manageBans',
|
|
actorOderId?: string,
|
|
targetOderId?: string
|
|
): boolean {
|
|
if (!actorUserId || !targetUserId || actorUserId === targetUserId) {
|
|
return false;
|
|
}
|
|
|
|
if (isServerOwner(server, targetUserId) && !isServerOwner(server, actorUserId)) {
|
|
return false;
|
|
}
|
|
|
|
if (isServerOwner(server, actorUserId)) {
|
|
return true;
|
|
}
|
|
|
|
if (!resolveServerPermission(server, actorUserId, permission, actorOderId)) {
|
|
return false;
|
|
}
|
|
|
|
const actorRole = resolveHighestRole(server, {
|
|
userId: actorUserId,
|
|
oderId: actorOderId
|
|
});
|
|
const targetRole = resolveHighestRole(server, {
|
|
userId: targetUserId,
|
|
oderId: targetOderId
|
|
});
|
|
|
|
return (actorRole?.position ?? 0) > (targetRole?.position ?? 0);
|
|
}
|