Add access control rework
This commit is contained in:
@@ -1,69 +1,80 @@
|
||||
@if (server()) {
|
||||
<div class="space-y-3 max-w-xl">
|
||||
<div class="space-y-3 max-w-3xl">
|
||||
@if (members().length === 0) {
|
||||
<p class="text-sm text-muted-foreground text-center py-8">No other members found for this server</p>
|
||||
} @else {
|
||||
@for (member of members(); track member.oderId || member.id) {
|
||||
<div class="flex items-center gap-3 p-3 bg-secondary/50 rounded-lg">
|
||||
<app-user-avatar
|
||||
[name]="member.displayName || '?'"
|
||||
size="sm"
|
||||
/>
|
||||
<div class="flex-1 min-w-0">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<p class="text-sm font-medium text-foreground truncate">
|
||||
{{ member.displayName }}
|
||||
</p>
|
||||
@if (member.isOnline) {
|
||||
<span class="text-[10px] bg-emerald-500/20 text-emerald-400 px-1 py-0.5 rounded">Online</span>
|
||||
}
|
||||
@if (member.role === 'host') {
|
||||
<span class="text-[10px] bg-yellow-500/20 text-yellow-400 px-1 py-0.5 rounded">Owner</span>
|
||||
} @else if (member.role === 'admin') {
|
||||
<span class="text-[10px] bg-blue-500/20 text-blue-400 px-1 py-0.5 rounded">Admin</span>
|
||||
} @else if (member.role === 'moderator') {
|
||||
<span class="text-[10px] bg-green-500/20 text-green-400 px-1 py-0.5 rounded">Mod</span>
|
||||
}
|
||||
<div class="space-y-3 rounded-lg bg-secondary/50 p-3">
|
||||
<div class="flex items-center gap-3">
|
||||
<app-user-avatar
|
||||
[name]="member.displayName || '?'"
|
||||
size="sm"
|
||||
/>
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="flex items-center gap-1.5">
|
||||
<p class="truncate text-sm font-medium text-foreground">
|
||||
{{ member.displayName }}
|
||||
</p>
|
||||
@if (member.isOnline) {
|
||||
<span class="rounded bg-emerald-500/20 px-1 py-0.5 text-[10px] text-emerald-400">Online</span>
|
||||
}
|
||||
<span class="rounded bg-primary/10 px-1 py-0.5 text-[10px] text-primary">{{ member.displayRoleName }}</span>
|
||||
</div>
|
||||
<p class="text-xs text-muted-foreground">{{ member.username }}</p>
|
||||
</div>
|
||||
</div>
|
||||
@if (member.role !== 'host' && isAdmin()) {
|
||||
<div class="flex items-center gap-1">
|
||||
@if (canChangeRoles()) {
|
||||
<select
|
||||
[ngModel]="member.role"
|
||||
(ngModelChange)="changeRole(member, $event)"
|
||||
class="text-xs px-2 py-1 bg-secondary rounded border border-border text-foreground focus:outline-none focus:ring-1 focus:ring-primary"
|
||||
>
|
||||
<option value="member">Member</option>
|
||||
<option value="moderator">Moderator</option>
|
||||
<option value="admin">Admin</option>
|
||||
</select>
|
||||
}
|
||||
@if (canKickMembers()) {
|
||||
@if (canKickMembers(member)) {
|
||||
<button
|
||||
(click)="kickMember(member)"
|
||||
class="p-1 rounded hover:bg-destructive/20 text-muted-foreground hover:text-destructive transition-colors"
|
||||
class="rounded p-1 text-muted-foreground transition-colors hover:bg-destructive/20 hover:text-destructive"
|
||||
title="Kick"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideUserX"
|
||||
class="w-4 h-4"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
@if (canBanMembers()) {
|
||||
@if (canBanMembers(member)) {
|
||||
<button
|
||||
(click)="banMember(member)"
|
||||
class="p-1 rounded hover:bg-destructive/20 text-muted-foreground hover:text-destructive transition-colors"
|
||||
class="rounded p-1 text-muted-foreground transition-colors hover:bg-destructive/20 hover:text-destructive"
|
||||
title="Ban"
|
||||
>
|
||||
<ng-icon
|
||||
name="lucideBan"
|
||||
class="w-4 h-4"
|
||||
class="h-4 w-4"
|
||||
/>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@if (assignableRoles().length > 0 && canChangeRoles(member)) {
|
||||
<div class="space-y-2 border-t border-border/50 pt-3">
|
||||
<p class="text-xs font-medium uppercase tracking-[0.16em] text-muted-foreground">Assigned Roles</p>
|
||||
<div class="flex flex-wrap gap-2">
|
||||
@for (role of assignableRoles(); track role.id) {
|
||||
<label class="flex items-center gap-2 rounded-full border border-border bg-background/70 px-3 py-1 text-xs text-foreground">
|
||||
<input
|
||||
type="checkbox"
|
||||
[checked]="member.assignedRoleIds.includes(role.id)"
|
||||
(change)="toggleRole(member, role.id, $event)"
|
||||
class="h-3.5 w-3.5 accent-primary"
|
||||
/>
|
||||
<span
|
||||
class="inline-block h-2.5 w-2.5 rounded-full"
|
||||
[style.background]="role.color || '#94a3b8'"
|
||||
></span>
|
||||
<span>{{ role.name }}</span>
|
||||
</label>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
} @else if (assignableRoles().length > 0) {
|
||||
<p class="border-t border-border/50 pt-3 text-xs text-muted-foreground">
|
||||
You can view this member's roles, but you do not have permission to change them.
|
||||
</p>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
|
||||
@@ -14,16 +14,25 @@ import { lucideUserX, lucideBan } from '@ng-icons/lucide';
|
||||
import {
|
||||
Room,
|
||||
RoomMember,
|
||||
UserRole
|
||||
RoomRole
|
||||
} from '../../../../shared-kernel';
|
||||
import { RealtimeSessionFacade } from '../../../../core/realtime';
|
||||
import { RoomsActions } from '../../../../store/rooms/rooms.actions';
|
||||
import { UsersActions } from '../../../../store/users/users.actions';
|
||||
import { selectCurrentUser, selectUsersEntities } from '../../../../store/users/users.selectors';
|
||||
import { selectCurrentRoom } from '../../../../store/rooms/rooms.selectors';
|
||||
import { UserAvatarComponent } from '../../../../shared';
|
||||
import {
|
||||
canManageMember,
|
||||
findAssignableRoles,
|
||||
getDisplayRoleName,
|
||||
getRoleIdsForMember,
|
||||
normalizeRoomAccessControl,
|
||||
setRoleAssignmentsForMember
|
||||
} from '../../../../domains/access-control';
|
||||
|
||||
interface ServerMemberView extends RoomMember {
|
||||
assignedRoleIds: string[];
|
||||
displayRoleName: string;
|
||||
isOnline: boolean;
|
||||
}
|
||||
|
||||
@@ -46,20 +55,25 @@ interface ServerMemberView extends RoomMember {
|
||||
})
|
||||
export class MembersSettingsComponent {
|
||||
private store = inject(Store);
|
||||
private webrtcService = inject(RealtimeSessionFacade);
|
||||
|
||||
/** The currently selected server, passed from the parent. */
|
||||
server = input<Room | null>(null);
|
||||
/** Whether the current user is admin of this server. */
|
||||
isAdmin = input(false);
|
||||
accessRole = input<UserRole | null>(null);
|
||||
accessRole = input<string | null>(null);
|
||||
|
||||
currentUser = this.store.selectSignal(selectCurrentUser);
|
||||
currentRoom = this.store.selectSignal(selectCurrentRoom);
|
||||
usersEntities = this.store.selectSignal(selectUsersEntities);
|
||||
normalizedServer = computed(() => {
|
||||
const room = this.server();
|
||||
|
||||
return room ? normalizeRoomAccessControl(room) : null;
|
||||
});
|
||||
assignableRoles = computed<RoomRole[]>(() => findAssignableRoles(this.normalizedServer()?.roles ?? []));
|
||||
|
||||
members = computed<ServerMemberView[]>(() => {
|
||||
const room = this.server();
|
||||
const room = this.normalizedServer();
|
||||
const me = this.currentUser();
|
||||
const currentRoom = this.currentRoom();
|
||||
const usersEntities = this.usersEntities();
|
||||
@@ -78,6 +92,8 @@ export class MembersSettingsComponent {
|
||||
|
||||
return {
|
||||
...member,
|
||||
assignedRoleIds: getRoleIdsForMember(room, member),
|
||||
displayRoleName: getDisplayRoleName(room, member),
|
||||
avatarUrl: liveUser?.avatarUrl || member.avatarUrl,
|
||||
displayName: liveUser?.displayName || member.displayName,
|
||||
isOnline: !!liveUser && (liveUser.isOnline === true || liveUser.status !== 'offline')
|
||||
@@ -85,55 +101,47 @@ export class MembersSettingsComponent {
|
||||
});
|
||||
});
|
||||
|
||||
canChangeRoles(): boolean {
|
||||
const role = this.accessRole();
|
||||
canChangeRoles(member: ServerMemberView): boolean {
|
||||
const room = this.normalizedServer();
|
||||
const currentUser = this.currentUser();
|
||||
|
||||
return role === 'host' || role === 'admin';
|
||||
return !!room && !!currentUser && canManageMember(room, currentUser, member, 'manageRoles');
|
||||
}
|
||||
|
||||
canKickMembers(): boolean {
|
||||
const role = this.accessRole();
|
||||
canKickMembers(member: ServerMemberView): boolean {
|
||||
const room = this.normalizedServer();
|
||||
const currentUser = this.currentUser();
|
||||
|
||||
return role === 'host' || role === 'admin' || role === 'moderator';
|
||||
return !!room && !!currentUser && canManageMember(room, currentUser, member, 'kickMembers');
|
||||
}
|
||||
|
||||
canBanMembers(): boolean {
|
||||
const role = this.accessRole();
|
||||
canBanMembers(member: ServerMemberView): boolean {
|
||||
const room = this.normalizedServer();
|
||||
const currentUser = this.currentUser();
|
||||
|
||||
return role === 'host' || role === 'admin';
|
||||
return !!room && !!currentUser && canManageMember(room, currentUser, member, 'banMembers');
|
||||
}
|
||||
|
||||
changeRole(member: ServerMemberView, role: 'admin' | 'moderator' | 'member'): void {
|
||||
const room = this.server();
|
||||
toggleRole(member: ServerMemberView, roleId: string, event: Event): void {
|
||||
const room = this.normalizedServer();
|
||||
|
||||
if (!room)
|
||||
return;
|
||||
|
||||
const members = (room.members ?? []).map((existingMember) =>
|
||||
existingMember.id === member.id || existingMember.oderId === member.oderId
|
||||
? { ...existingMember,
|
||||
role }
|
||||
: existingMember
|
||||
);
|
||||
const checkbox = event.target as HTMLInputElement;
|
||||
const nextRoleIds = checkbox.checked
|
||||
? [...member.assignedRoleIds, roleId]
|
||||
: member.assignedRoleIds.filter((candidateRoleId) => candidateRoleId !== roleId);
|
||||
const roleAssignments = setRoleAssignmentsForMember(room.roleAssignments, member, nextRoleIds);
|
||||
|
||||
this.store.dispatch(RoomsActions.updateRoom({ roomId: room.id,
|
||||
changes: { members } }));
|
||||
|
||||
if (this.currentRoom()?.id === room.id) {
|
||||
this.store.dispatch(UsersActions.updateUserRole({ userId: member.id,
|
||||
role }));
|
||||
}
|
||||
|
||||
this.webrtcService.broadcastMessage({
|
||||
type: 'role-change',
|
||||
this.store.dispatch(RoomsActions.updateRoomAccessControl({
|
||||
roomId: room.id,
|
||||
targetUserId: member.id,
|
||||
role
|
||||
});
|
||||
changes: { roleAssignments }
|
||||
}));
|
||||
}
|
||||
|
||||
kickMember(member: ServerMemberView): void {
|
||||
const room = this.server();
|
||||
const room = this.normalizedServer();
|
||||
|
||||
if (!room)
|
||||
return;
|
||||
@@ -143,7 +151,7 @@ export class MembersSettingsComponent {
|
||||
}
|
||||
|
||||
banMember(member: ServerMemberView): void {
|
||||
const room = this.server();
|
||||
const room = this.normalizedServer();
|
||||
|
||||
if (!room)
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user