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

@@ -9,7 +9,7 @@ infrastructure adapters and UI.
| Domain | Purpose | Public entry point | | Domain | Purpose | Public entry point |
| -------------------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- | | -------------------- | ------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------- |
| **attachment** | File upload/download, chunk transfer, persistence | `AttachmentFacade` | | **attachment** | File upload/download, chunk transfer, persistence | `AttachmentFacade` |
| **access-control** | Role, permission, moderation, and room access rules | `normalizeRoomAccessControl()`, `resolveRoomPermission()` | | **access-control** | Role, permission, ban matching, moderation, and room access rules | `normalizeRoomAccessControl()`, `resolveRoomPermission()`, `hasRoomBanForUser()` |
| **auth** | Login / register HTTP orchestration, user-bar UI | `AuthService` | | **auth** | Login / register HTTP orchestration, user-bar UI | `AuthService` |
| **chat** | Messaging rules, sync logic, GIF/Klipy integration, chat UI | `KlipyService`, `canEditMessage()`, `ChatMessagesComponent` | | **chat** | Messaging rules, sync logic, GIF/Klipy integration, chat UI | `KlipyService`, `canEditMessage()`, `ChatMessagesComponent` |
| **notifications** | Notification preferences, unread tracking, desktop alert orchestration | `NotificationsFacade` | | **notifications** | Notification preferences, unread tracking, desktop alert orchestration | `NotificationsFacade` |

View File

@@ -7,13 +7,18 @@ Role and permission rules for servers, including default system roles, role assi
``` ```
access-control/ access-control/
├── domain/ ├── domain/
│ ├── access-control.models.ts MemberIdentity and RoomPermissionDefinition domain types │ ├── models/
── access-control.constants.ts SYSTEM_ROLE_IDS and permission metadata │ └── access-control.model.ts MemberIdentity and RoomPermissionDefinition domain types
│ ├── constants/
│ │ └── access-control.constants.ts SYSTEM_ROLE_IDS and permission metadata
│ ├── util/
│ │ └── access-control.util.ts Internal helpers (normalization, identity matching, sorting)
│ └── rules/
│ ├── role.rules.ts Role defaults, normalization, ordering, create/update helpers │ ├── role.rules.ts Role defaults, normalization, ordering, create/update helpers
│ ├── role-assignment.rules.ts Assignment normalization and member-role lookups │ ├── role-assignment.rules.ts Assignment normalization and member-role lookups
│ ├── permission.rules.ts Permission resolution and moderation hierarchy checks │ ├── permission.rules.ts Permission resolution and moderation hierarchy checks
│ ├── room.rules.ts Legacy compatibility, room hydration, room-level normalization │ ├── room.rules.ts Legacy compatibility, room hydration, room-level normalization
└── access-control.logic.ts Public barrel for domain rules └── ban.rules.ts Ban matching and user-ban resolution
└── index.ts Domain barrel used by other layers └── index.ts Domain barrel used by other layers
``` ```
@@ -29,6 +34,8 @@ access-control/
| `canManageMember(...)` | Applies both permission checks and role hierarchy checks | | `canManageMember(...)` | Applies both permission checks and role hierarchy checks |
| `canManageRole(...)` | Prevents editing roles at or above the actor's highest role | | `canManageRole(...)` | Prevents editing roles at or above the actor's highest role |
| `normalizeRoomAccessControl(room)` | Produces a fully hydrated room with normalized roles, assignments, overrides, and legacy compatibility fields | | `normalizeRoomAccessControl(room)` | Produces a fully hydrated room with normalized roles, assignments, overrides, and legacy compatibility fields |
| `hasRoomBanForUser(bans, user, persistedUserId?)` | Returns true when any active ban entry targets the provided user |
| `isRoomBanMatch(ban, user, persistedUserId?)` | Returns true when a single ban entry targets the provided user |
## Layering ## Layering

View File

@@ -1,6 +0,0 @@
export * from './access-control.models';
export * from './access-control.constants';
export * from './role.rules';
export * from './role-assignment.rules';
export * from './permission.rules';
export * from './room.rules';

View File

@@ -1,4 +1,4 @@
import type { RoomPermissionDefinition } from './access-control.models'; import type { RoomPermissionDefinition } from '../models/access-control.model';
export const SYSTEM_ROLE_IDS = { export const SYSTEM_ROLE_IDS = {
everyone: 'system-everyone', everyone: 'system-everyone',

View File

@@ -2,7 +2,7 @@ import type {
RoomMember, RoomMember,
RoomPermissionKey, RoomPermissionKey,
User User
} from '../../../shared-kernel'; } from '../../../../shared-kernel';
export interface RoomPermissionDefinition { export interface RoomPermissionDefinition {
key: RoomPermissionKey; key: RoomPermissionKey;

View File

@@ -1,4 +1,4 @@
import { BanEntry, User } from '../models/index'; import { BanEntry, User } from '../../../../shared-kernel';
type BanAwareUser = Pick<User, 'id' | 'oderId'> | null | undefined; type BanAwareUser = Pick<User, 'id' | 'oderId'> | null | undefined;

View File

@@ -4,9 +4,9 @@ import {
Room, Room,
RoomPermissionKey, RoomPermissionKey,
RoomRole RoomRole
} from '../../../shared-kernel'; } from '../../../../shared-kernel';
import { SYSTEM_ROLE_IDS } from './access-control.constants'; import { SYSTEM_ROLE_IDS } from '../constants/access-control.constants';
import type { MemberIdentity } from './access-control.models'; import type { MemberIdentity } from '../models/access-control.model';
import { import {
buildRoleLookup, buildRoleLookup,
getRolePermissionState, getRolePermissionState,
@@ -14,7 +14,7 @@ import {
normalizePermissionState, normalizePermissionState,
roleSortAscending, roleSortAscending,
compareText compareText
} from './access-control.internal'; } from '../util/access-control.util';
import { getAssignedRoleIds, getHighestAssignedRole } from './role-assignment.rules'; import { getAssignedRoleIds, getHighestAssignedRole } from './role-assignment.rules';
import { getRoomRoleById, normalizeRoomRoles } from './role.rules'; import { getRoomRoleById, normalizeRoomRoles } from './role.rules';

View File

@@ -3,9 +3,9 @@ import {
RoomMember, RoomMember,
RoomRole, RoomRole,
RoomRoleAssignment RoomRoleAssignment
} from '../../../shared-kernel'; } from '../../../../shared-kernel';
import { SYSTEM_ROLE_IDS } from './access-control.constants'; import { SYSTEM_ROLE_IDS } from '../constants/access-control.constants';
import type { MemberIdentity } from './access-control.models'; import type { MemberIdentity } from '../models/access-control.model';
import { import {
buildRoleLookup, buildRoleLookup,
compareText, compareText,
@@ -13,7 +13,7 @@ import {
matchesIdentity, matchesIdentity,
roleSortDescending, roleSortDescending,
uniqueStrings uniqueStrings
} from './access-control.internal'; } from '../util/access-control.util';
import { getRoomRoleById, normalizeRoomRoles } from './role.rules'; import { getRoomRoleById, normalizeRoomRoles } from './role.rules';
function sortAssignments(assignments: readonly RoomRoleAssignment[]): RoomRoleAssignment[] { function sortAssignments(assignments: readonly RoomRoleAssignment[]): RoomRoleAssignment[] {

View File

@@ -2,8 +2,8 @@ import {
RoomPermissionMatrix, RoomPermissionMatrix,
RoomPermissions, RoomPermissions,
RoomRole RoomRole
} from '../../../shared-kernel'; } from '../../../../shared-kernel';
import { SYSTEM_ROLE_IDS } from './access-control.constants'; import { SYSTEM_ROLE_IDS } from '../constants/access-control.constants';
import { import {
buildRoleLookup, buildRoleLookup,
buildSystemRole, buildSystemRole,
@@ -12,7 +12,7 @@ import {
normalizePermissionMatrix, normalizePermissionMatrix,
roleSortAscending, roleSortAscending,
roleSortDescending roleSortDescending
} from './access-control.internal'; } from '../util/access-control.util';
const ROLE_COLORS = { const ROLE_COLORS = {
everyone: '#6b7280', everyone: '#6b7280',

View File

@@ -7,14 +7,14 @@ import {
RoomRole, RoomRole,
RoomRoleAssignment, RoomRoleAssignment,
UserRole UserRole
} from '../../../shared-kernel'; } from '../../../../shared-kernel';
import { SYSTEM_ROLE_IDS } from './access-control.constants'; import { SYSTEM_ROLE_IDS } from '../constants/access-control.constants';
import { import {
getRolePermissionState, getRolePermissionState,
permissionStateToBoolean, permissionStateToBoolean,
resolveLegacyAllowState resolveLegacyAllowState
} from './access-control.internal'; } from '../util/access-control.util';
import type { MemberIdentity } from './access-control.models'; import type { MemberIdentity } from '../models/access-control.model';
import { import {
getAssignedRoleIds, getAssignedRoleIds,
normalizeRoomRoleAssignments, normalizeRoomRoleAssignments,

View File

@@ -5,8 +5,8 @@ import {
RoomRole, RoomRole,
RoomRoleAssignment, RoomRoleAssignment,
ROOM_PERMISSION_KEYS ROOM_PERMISSION_KEYS
} from '../../../shared-kernel'; } from '../../../../shared-kernel';
import type { MemberIdentity } from './access-control.models'; import type { MemberIdentity } from '../models/access-control.model';
export function normalizeName(name: string): string { export function normalizeName(name: string): string {
return name.trim().replace(/\s+/g, ' '); return name.trim().replace(/\s+/g, ' ');

View File

@@ -1,6 +1,7 @@
export * from './domain/access-control.models'; export * from './domain/models/access-control.model';
export * from './domain/access-control.constants'; export * from './domain/constants/access-control.constants';
export * from './domain/role.rules'; export * from './domain/rules/role.rules';
export * from './domain/role-assignment.rules'; export * from './domain/rules/role-assignment.rules';
export * from './domain/permission.rules'; export * from './domain/rules/permission.rules';
export * from './domain/room.rules'; export * from './domain/rules/room.rules';
export * from './domain/rules/ban.rules';

View File

@@ -40,7 +40,7 @@ import { type ServerInfo } from '../../domain/server-directory.models';
import { ServerDirectoryFacade } from '../../application/server-directory.facade'; import { ServerDirectoryFacade } from '../../application/server-directory.facade';
import { selectCurrentUser } from '../../../../store/users/users.selectors'; import { selectCurrentUser } from '../../../../store/users/users.selectors';
import { ConfirmDialogComponent } from '../../../../shared'; import { ConfirmDialogComponent } from '../../../../shared';
import { hasRoomBanForUser } from '../../../../core/helpers/room-ban.helpers'; import { hasRoomBanForUser } from '../../../access-control';
@Component({ @Component({
selector: 'app-server-search', selector: 'app-server-search',

View File

@@ -32,7 +32,7 @@ import { RoomsActions } from '../../store/rooms/rooms.actions';
import { DatabaseService } from '../../infrastructure/persistence'; import { DatabaseService } from '../../infrastructure/persistence';
import { NotificationsFacade } from '../../domains/notifications'; import { NotificationsFacade } from '../../domains/notifications';
import { type ServerInfo, ServerDirectoryFacade } from '../../domains/server-directory'; import { type ServerInfo, ServerDirectoryFacade } from '../../domains/server-directory';
import { hasRoomBanForUser } from '../../core/helpers/room-ban.helpers'; import { hasRoomBanForUser } from '../../domains/access-control';
import { import {
ConfirmDialogComponent, ConfirmDialogComponent,
ContextMenuComponent, ContextMenuComponent,

View File

@@ -40,7 +40,7 @@ import {
VoiceState VoiceState
} from '../../shared-kernel'; } from '../../shared-kernel';
import { NotificationAudioService, AppSound } from '../../core/services/notification-audio.service'; import { NotificationAudioService, AppSound } from '../../core/services/notification-audio.service';
import { hasRoomBanForUser } from '../../core/helpers/room-ban.helpers'; import { hasRoomBanForUser } from '../../domains/access-control';
import { RECONNECT_SOUND_GRACE_MS } from '../../core/constants'; import { RECONNECT_SOUND_GRACE_MS } from '../../core/constants';
import { VoiceSessionFacade } from '../../domains/voice-session'; import { VoiceSessionFacade } from '../../domains/voice-session';
import { import {

View File

@@ -41,7 +41,7 @@ import {
saveLastViewedChatToStorage saveLastViewedChatToStorage
} from '../../infrastructure/persistence'; } from '../../infrastructure/persistence';
import { ServerDirectoryFacade } from '../../domains/server-directory'; import { ServerDirectoryFacade } from '../../domains/server-directory';
import { hasRoomBanForUser } from '../../core/helpers/room-ban.helpers'; import { hasRoomBanForUser } from '../../domains/access-control';
import { Room } from '../../shared-kernel'; import { Room } from '../../shared-kernel';
import { import {
removeRoomMember, removeRoomMember,