refactor: Cleaning rooms store
This commit is contained in:
346
toju-app/src/app/store/rooms/room-settings.effects.ts
Normal file
346
toju-app/src/app/store/rooms/room-settings.effects.ts
Normal file
@@ -0,0 +1,346 @@
|
||||
/* eslint-disable @typescript-eslint/member-ordering */
|
||||
import { Injectable, inject } from '@angular/core';
|
||||
import {
|
||||
Actions,
|
||||
createEffect,
|
||||
ofType
|
||||
} from '@ngrx/effects';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { of, EMPTY } from 'rxjs';
|
||||
import {
|
||||
mergeMap,
|
||||
withLatestFrom,
|
||||
tap,
|
||||
catchError
|
||||
} from 'rxjs/operators';
|
||||
import { RoomsActions } from './rooms.actions';
|
||||
import { selectCurrentUser } from '../users/users.selectors';
|
||||
import { selectCurrentRoom, selectSavedRooms } from './rooms.selectors';
|
||||
import { RealtimeSessionFacade } from '../../core/realtime';
|
||||
import { DatabaseService } from '../../infrastructure/persistence';
|
||||
import { ServerDirectoryFacade } from '../../domains/server-directory';
|
||||
import {
|
||||
normalizeRoomAccessControl,
|
||||
resolveRoomPermission,
|
||||
withLegacyRoomPermissions
|
||||
} from '../../domains/access-control';
|
||||
import { Room, RoomSettings } from '../../shared-kernel';
|
||||
import {
|
||||
resolveRoom,
|
||||
getUserRoleForRoom,
|
||||
canManageChannelsInRoom
|
||||
} from './rooms.helpers';
|
||||
import { defaultChannels } from './room-channels.defaults';
|
||||
|
||||
/**
|
||||
* NgRx effects for room settings, permissions, channels, and icon updates.
|
||||
*/
|
||||
@Injectable()
|
||||
export class RoomSettingsEffects {
|
||||
private actions$ = inject(Actions);
|
||||
private store = inject(Store);
|
||||
private webrtc = inject(RealtimeSessionFacade);
|
||||
private db = inject(DatabaseService);
|
||||
private serverDirectory = inject(ServerDirectoryFacade);
|
||||
|
||||
/** Updates room settings (host/admin-only) and broadcasts changes to all peers. */
|
||||
updateRoomSettings$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(RoomsActions.updateRoomSettings),
|
||||
withLatestFrom(
|
||||
this.store.select(selectCurrentUser),
|
||||
this.store.select(selectCurrentRoom),
|
||||
this.store.select(selectSavedRooms)
|
||||
),
|
||||
mergeMap(([
|
||||
{ roomId, settings },
|
||||
currentUser,
|
||||
currentRoom,
|
||||
savedRooms
|
||||
]) => {
|
||||
if (!currentUser)
|
||||
return of(RoomsActions.updateRoomSettingsFailure({ error: 'Not logged in' }));
|
||||
|
||||
const room = resolveRoom(roomId, currentRoom, savedRooms);
|
||||
|
||||
if (!room)
|
||||
return of(RoomsActions.updateRoomSettingsFailure({ error: 'Room not found' }));
|
||||
|
||||
const currentUserRole = getUserRoleForRoom(room, currentUser, currentRoom);
|
||||
const isOwner = currentUserRole === 'host';
|
||||
const canManageRoom = isOwner || resolveRoomPermission(room, currentUser, 'manageServer');
|
||||
|
||||
if (!canManageRoom) {
|
||||
return of(
|
||||
RoomsActions.updateRoomSettingsFailure({
|
||||
error: 'Permission denied'
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const hasPasswordUpdate = Object.prototype.hasOwnProperty.call(settings, 'password');
|
||||
const normalizedPassword = typeof settings.password === 'string' ? settings.password.trim() : undefined;
|
||||
const nextHasPassword = typeof settings.hasPassword === 'boolean'
|
||||
? settings.hasPassword
|
||||
: (hasPasswordUpdate
|
||||
? !!normalizedPassword
|
||||
: (typeof room.hasPassword === 'boolean' ? room.hasPassword : !!room.password));
|
||||
const updatedSettings: RoomSettings = {
|
||||
name: settings.name ?? room.name,
|
||||
description: settings.description ?? room.description,
|
||||
topic: settings.topic ?? room.topic,
|
||||
isPrivate: settings.isPrivate ?? room.isPrivate,
|
||||
password: hasPasswordUpdate ? (normalizedPassword || '') : room.password,
|
||||
hasPassword: nextHasPassword,
|
||||
maxUsers: settings.maxUsers ?? room.maxUsers
|
||||
};
|
||||
const localRoomUpdates: Partial<Room> = {
|
||||
...updatedSettings,
|
||||
password: hasPasswordUpdate ? (normalizedPassword || undefined) : room.password,
|
||||
hasPassword: nextHasPassword
|
||||
};
|
||||
const sharedSettings: RoomSettings = {
|
||||
name: updatedSettings.name,
|
||||
description: updatedSettings.description,
|
||||
topic: updatedSettings.topic,
|
||||
isPrivate: updatedSettings.isPrivate,
|
||||
hasPassword: nextHasPassword,
|
||||
maxUsers: updatedSettings.maxUsers,
|
||||
password: hasPasswordUpdate ? (normalizedPassword || '') : undefined
|
||||
};
|
||||
|
||||
this.db.updateRoom(room.id, localRoomUpdates);
|
||||
|
||||
this.webrtc.broadcastMessage({
|
||||
type: 'room-settings-update',
|
||||
roomId: room.id,
|
||||
settings: sharedSettings
|
||||
});
|
||||
|
||||
if (canManageRoom) {
|
||||
this.serverDirectory.updateServer(room.id, {
|
||||
currentOwnerId: currentUser.id,
|
||||
actingRole: currentUserRole ?? undefined,
|
||||
name: updatedSettings.name,
|
||||
description: updatedSettings.description,
|
||||
isPrivate: updatedSettings.isPrivate,
|
||||
maxUsers: updatedSettings.maxUsers,
|
||||
password: hasPasswordUpdate ? (normalizedPassword || null) : undefined
|
||||
}, {
|
||||
sourceId: room.sourceId,
|
||||
sourceUrl: room.sourceUrl
|
||||
}).subscribe({
|
||||
error: () => {}
|
||||
});
|
||||
}
|
||||
|
||||
return of(RoomsActions.updateRoomSettingsSuccess({ roomId: room.id,
|
||||
settings: updatedSettings }));
|
||||
}),
|
||||
catchError((error) => of(RoomsActions.updateRoomSettingsFailure({ error: error.message })))
|
||||
)
|
||||
);
|
||||
|
||||
/** Persists and broadcasts channel add/remove/rename changes. */
|
||||
syncChannelChanges$ = createEffect(
|
||||
() =>
|
||||
this.actions$.pipe(
|
||||
ofType(RoomsActions.addChannel, RoomsActions.removeChannel, RoomsActions.renameChannel),
|
||||
withLatestFrom(
|
||||
this.store.select(selectCurrentUser),
|
||||
this.store.select(selectCurrentRoom)
|
||||
),
|
||||
tap(([
|
||||
, currentUser,
|
||||
currentRoom
|
||||
]) => {
|
||||
if (!currentUser || !currentRoom) {
|
||||
return;
|
||||
}
|
||||
|
||||
const role = getUserRoleForRoom(currentRoom, currentUser, currentRoom);
|
||||
|
||||
if (!canManageChannelsInRoom(currentRoom, currentUser, currentRoom, role)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const channels = currentRoom.channels ?? defaultChannels();
|
||||
|
||||
this.db.updateRoom(currentRoom.id, { channels });
|
||||
|
||||
this.webrtc.broadcastMessage({
|
||||
type: 'channels-update',
|
||||
roomId: currentRoom.id,
|
||||
channels
|
||||
});
|
||||
|
||||
this.serverDirectory.updateServer(currentRoom.id, {
|
||||
currentOwnerId: currentUser.id,
|
||||
actingRole: role ?? undefined,
|
||||
channels
|
||||
}, {
|
||||
sourceId: currentRoom.sourceId,
|
||||
sourceUrl: currentRoom.sourceUrl
|
||||
}).subscribe({
|
||||
error: () => {}
|
||||
});
|
||||
})
|
||||
),
|
||||
{ dispatch: false }
|
||||
);
|
||||
|
||||
/** Updates room permission grants (host-only) and broadcasts to peers. */
|
||||
updateRoomPermissions$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(RoomsActions.updateRoomPermissions),
|
||||
withLatestFrom(
|
||||
this.store.select(selectCurrentUser),
|
||||
this.store.select(selectCurrentRoom),
|
||||
this.store.select(selectSavedRooms)
|
||||
),
|
||||
mergeMap(([
|
||||
{ roomId, permissions },
|
||||
currentUser,
|
||||
currentRoom,
|
||||
savedRooms
|
||||
]) => {
|
||||
if (!currentUser)
|
||||
return EMPTY;
|
||||
|
||||
const room = resolveRoom(roomId, currentRoom, savedRooms);
|
||||
|
||||
if (!room)
|
||||
return EMPTY;
|
||||
|
||||
const nextRoom = withLegacyRoomPermissions(room, permissions);
|
||||
|
||||
return of(RoomsActions.updateRoomAccessControl({
|
||||
roomId: room.id,
|
||||
changes: {
|
||||
roles: nextRoom.roles,
|
||||
roleAssignments: nextRoom.roleAssignments,
|
||||
channelPermissions: nextRoom.channelPermissions,
|
||||
slowModeInterval: nextRoom.slowModeInterval
|
||||
}
|
||||
}));
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
/** Updates role-based access control and broadcasts to peers and directory. */
|
||||
updateRoomAccessControl$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(RoomsActions.updateRoomAccessControl),
|
||||
withLatestFrom(
|
||||
this.store.select(selectCurrentUser),
|
||||
this.store.select(selectCurrentRoom),
|
||||
this.store.select(selectSavedRooms)
|
||||
),
|
||||
mergeMap(([
|
||||
{ roomId, changes },
|
||||
currentUser,
|
||||
currentRoom,
|
||||
savedRooms
|
||||
]) => {
|
||||
if (!currentUser)
|
||||
return EMPTY;
|
||||
|
||||
const room = resolveRoom(roomId, currentRoom, savedRooms);
|
||||
|
||||
if (!room)
|
||||
return EMPTY;
|
||||
|
||||
const requiresRoleManagement = !!changes.roles || !!changes.roleAssignments || !!changes.channelPermissions;
|
||||
const requiresServerManagement = typeof changes.slowModeInterval === 'number';
|
||||
const isOwner = room.hostId === currentUser.id || room.hostId === currentUser.oderId;
|
||||
const canManageRoles = isOwner || resolveRoomPermission(room, currentUser, 'manageRoles');
|
||||
const canManageServer = isOwner || resolveRoomPermission(room, currentUser, 'manageServer');
|
||||
|
||||
if ((requiresRoleManagement && !canManageRoles) || (requiresServerManagement && !canManageServer)) {
|
||||
return EMPTY;
|
||||
}
|
||||
|
||||
const nextRoom = normalizeRoomAccessControl({
|
||||
...room,
|
||||
...changes
|
||||
});
|
||||
const nextChanges: Partial<Room> = {
|
||||
roles: nextRoom.roles,
|
||||
roleAssignments: nextRoom.roleAssignments,
|
||||
channelPermissions: nextRoom.channelPermissions,
|
||||
slowModeInterval: nextRoom.slowModeInterval,
|
||||
permissions: nextRoom.permissions,
|
||||
members: nextRoom.members
|
||||
};
|
||||
|
||||
this.webrtc.broadcastMessage({
|
||||
type: 'room-permissions-update',
|
||||
roomId: room.id,
|
||||
permissions: nextRoom.permissions,
|
||||
room: {
|
||||
roles: nextRoom.roles,
|
||||
roleAssignments: nextRoom.roleAssignments,
|
||||
channelPermissions: nextRoom.channelPermissions,
|
||||
slowModeInterval: nextRoom.slowModeInterval
|
||||
}
|
||||
});
|
||||
|
||||
this.serverDirectory.updateServer(room.id, {
|
||||
currentOwnerId: currentUser.id,
|
||||
roles: nextRoom.roles,
|
||||
roleAssignments: nextRoom.roleAssignments,
|
||||
channelPermissions: nextRoom.channelPermissions,
|
||||
slowModeInterval: nextRoom.slowModeInterval
|
||||
}, {
|
||||
sourceId: room.sourceId,
|
||||
sourceUrl: room.sourceUrl
|
||||
}).subscribe({
|
||||
error: () => {}
|
||||
});
|
||||
|
||||
return of(RoomsActions.updateRoom({ roomId: room.id,
|
||||
changes: nextChanges }));
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
/** Updates the server icon (permission-enforced) and broadcasts to peers. */
|
||||
updateServerIcon$ = createEffect(() =>
|
||||
this.actions$.pipe(
|
||||
ofType(RoomsActions.updateServerIcon),
|
||||
withLatestFrom(this.store.select(selectCurrentUser), this.store.select(selectCurrentRoom)),
|
||||
mergeMap(([
|
||||
{ roomId, icon },
|
||||
currentUser,
|
||||
currentRoom
|
||||
]) => {
|
||||
if (!currentUser || !currentRoom || currentRoom.id !== roomId) {
|
||||
return of(RoomsActions.updateServerIconFailure({ error: 'Not in room' }));
|
||||
}
|
||||
|
||||
const isOwner = currentRoom.hostId === currentUser.id;
|
||||
const canByRole = resolveRoomPermission(currentRoom, currentUser, 'manageIcon');
|
||||
|
||||
if (!isOwner && !canByRole) {
|
||||
return of(RoomsActions.updateServerIconFailure({ error: 'Permission denied' }));
|
||||
}
|
||||
|
||||
const iconUpdatedAt = Date.now();
|
||||
const changes: Partial<Room> = { icon,
|
||||
iconUpdatedAt };
|
||||
|
||||
this.db.updateRoom(roomId, changes);
|
||||
this.webrtc.broadcastMessage({
|
||||
type: 'server-icon-update',
|
||||
roomId,
|
||||
icon,
|
||||
iconUpdatedAt
|
||||
});
|
||||
|
||||
return of(RoomsActions.updateServerIconSuccess({ roomId,
|
||||
icon,
|
||||
iconUpdatedAt }));
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user