diff --git a/apps/server/src/migrations/mikro-orm/Migration20250120131625.ts b/apps/server/src/migrations/mikro-orm/Migration20250120131625.ts new file mode 100644 index 00000000000..3555fcc5674 --- /dev/null +++ b/apps/server/src/migrations/mikro-orm/Migration20250120131625.ts @@ -0,0 +1,69 @@ +/* eslint-disable no-console */ +/* eslint-disable filename-rules/match */ +import { Migration } from '@mikro-orm/migrations-mongodb'; + +export class Migration20250120131625 extends Migration { + public async up(): Promise { + const roomOwnerRoleUpdate = await this.getCollection('roles').updateOne( + { name: 'roomowner' }, + { + $addToSet: { + permissions: { + $each: ['ROOM_MEMBERS_CHANGE_ROLE'], + }, + }, + } + ); + + if (roomOwnerRoleUpdate.modifiedCount > 0) { + console.info('Permissions ROOM_MEMBERS_CHANGE_ROLE added to role roomowner.'); + } + + const roomAdminRoleUpdate = await this.getCollection('roles').updateOne( + { name: 'roomadmin' }, + { + $addToSet: { + permissions: { + $each: ['ROOM_MEMBERS_CHANGE_ROLE'], + }, + }, + } + ); + + if (roomAdminRoleUpdate.modifiedCount > 0) { + console.info('Permissions ROOM_MEMBERS_CHANGE_ROLE added to role roomadmin.'); + } + } + + public async down(): Promise { + const roomOwnerRoleUpdate = await this.getCollection('roles').updateOne( + { name: 'roomowner' }, + { + $pull: { + permissions: { + $in: ['ROOM_MEMBERS_CHANGE_ROLE'], + }, + }, + } + ); + + if (roomOwnerRoleUpdate.modifiedCount > 0) { + console.info('Rollback: Permission ROOM_CREATE removed from role roomowner.'); + } + + const roomAdminRoleUpdate = await this.getCollection('roles').updateOne( + { name: 'roomadmin' }, + { + $pull: { + permissions: { + $in: ['ROOM_MEMBERS_CHANGE_ROLE'], + }, + }, + } + ); + + if (roomAdminRoleUpdate.modifiedCount > 0) { + console.info('Rollback: Permission ROOM_DELETE removed from role roomadmin.'); + } + } +} diff --git a/apps/server/src/modules/room-membership/service/room-membership.service.spec.ts b/apps/server/src/modules/room-membership/service/room-membership.service.spec.ts index 01f9569ab5f..3be7d286a1b 100644 --- a/apps/server/src/modules/room-membership/service/room-membership.service.spec.ts +++ b/apps/server/src/modules/room-membership/service/room-membership.service.spec.ts @@ -1,10 +1,10 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { MongoMemoryDatabaseModule } from '@infra/database'; -import { Group, GroupService, GroupTypes } from '@modules/group'; +import { Group, GroupService, GroupTypes, GroupUser } from '@modules/group'; import { groupFactory } from '@modules/group/testing'; import { RoleDto, RoleService } from '@modules/role'; import { roleDtoFactory } from '@modules/role/testing'; -import { RoomService } from '@modules/room/domain'; +import { RoomService } from '@modules/room'; import { roomFactory } from '@modules/room/testing'; import { schoolFactory } from '@modules/school/testing'; import { UserService } from '@modules/user'; @@ -14,6 +14,7 @@ import { RoleName } from '@shared/domain/interface'; import { roleFactory } from '@testing/factory/role.factory'; import { userDoFactory } from '@testing/factory/user.do.factory'; import { userFactory } from '@testing/factory/user.factory'; +import { ObjectId } from 'bson'; import { RoomMembershipAuthorizable } from '../do/room-membership-authorizable.do'; import { RoomMembershipRepo } from '../repo/room-membership.repo'; import { roomMembershipFactory } from '../testing'; @@ -318,6 +319,97 @@ describe('RoomMembershipService', () => { }); }); + describe('changeRoleOfRoomMembers', () => { + describe('when roomMembership does not exist', () => { + it('should throw an exception', async () => { + roomMembershipRepo.findByRoomId.mockResolvedValue(null); + + await expect( + service.changeRoleOfRoomMembers(new ObjectId().toHexString(), [], RoleName.ROOMEDITOR) + ).rejects.toThrowError(BadRequestException); + }); + }); + + describe('when roomMembership exists', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const otherUser = userFactory.buildWithId(); + const userNotInRoom = userFactory.buildWithId(); + const school = schoolFactory.build(); + const viewerRole = roleFactory.buildWithId({ name: RoleName.ROOMVIEWER }); + const editorRole = roleFactory.buildWithId({ name: RoleName.ROOMEDITOR }); + const group = groupFactory.build({ + type: GroupTypes.ROOM, + organizationId: school.id, + users: [ + { userId: user.id, roleId: viewerRole.id }, + { userId: otherUser.id, roleId: viewerRole.id }, + ], + }); + const room = roomFactory.build({ schoolId: school.id }); + const roomMembership = roomMembershipFactory.build({ + roomId: room.id, + userGroupId: group.id, + schoolId: school.id, + }); + + roomMembershipRepo.findByRoomId.mockResolvedValue(roomMembership); + groupService.findById.mockResolvedValue(group); + roleService.findByName.mockResolvedValue(editorRole); + + return { + user, + otherUser, + userNotInRoom, + room, + roomMembership, + group, + viewerRole, + editorRole, + }; + }; + + it('should change role of user to editor', async () => { + const { user, room, group, editorRole } = setup(); + + await service.changeRoleOfRoomMembers(room.id, [user.id], RoleName.ROOMEDITOR); + + expect(groupService.save).toHaveBeenCalledWith( + expect.objectContaining({ + id: group.id, + users: expect.arrayContaining([{ userId: user.id, roleId: editorRole.id }]) as GroupUser[], + }) + ); + }); + + it('should not change role of other user', async () => { + const { user, otherUser, room, group, viewerRole } = setup(); + + await service.changeRoleOfRoomMembers(room.id, [user.id], RoleName.ROOMEDITOR); + + expect(groupService.save).toHaveBeenCalledWith( + expect.objectContaining({ + id: group.id, + users: expect.arrayContaining([{ userId: otherUser.id, roleId: viewerRole.id }]) as GroupUser[], + }) + ); + }); + + it('should ignore changing a user that is not in the room', async () => { + const { userNotInRoom, room, group } = setup(); + + await service.changeRoleOfRoomMembers(room.id, [userNotInRoom.id], RoleName.ROOMEDITOR); + + expect(groupService.save).toHaveBeenCalledWith( + expect.objectContaining({ + id: group.id, + users: expect.not.arrayContaining([expect.objectContaining({ userId: userNotInRoom.id })]) as GroupUser[], + }) + ); + }); + }); + }); + describe('deleteRoomMembership', () => { describe('when roomMembership does not exist', () => { const setup = () => { diff --git a/apps/server/src/modules/room-membership/service/room-membership.service.ts b/apps/server/src/modules/room-membership/service/room-membership.service.ts index 19c9731696a..6a8ac9113f7 100644 --- a/apps/server/src/modules/room-membership/service/room-membership.service.ts +++ b/apps/server/src/modules/room-membership/service/room-membership.service.ts @@ -92,7 +92,7 @@ export class RoomMembershipService { public async removeMembersFromRoom(roomId: EntityId, userIds: EntityId[]): Promise { const roomMembership = await this.roomMembershipRepo.findByRoomId(roomId); if (roomMembership === null) { - throw new BadRequestException('Room member not found'); + throw new BadRequestException('Room membership not found'); } const group = await this.groupService.findById(roomMembership.userGroupId); @@ -103,6 +103,26 @@ export class RoomMembershipService { await this.handleGuestRoleRemoval(userIds, roomMembership.schoolId); } + public async changeRoleOfRoomMembers(roomId: EntityId, userIds: EntityId[], roleName: RoleName): Promise { + const roomMembership = await this.roomMembershipRepo.findByRoomId(roomId); + if (roomMembership === null) { + throw new BadRequestException('Room membership not found'); + } + + const group = await this.groupService.findById(roomMembership.userGroupId); + const role = await this.roleService.findByName(roleName); + + group.users.forEach((groupUser) => { + if (userIds.includes(groupUser.userId)) { + groupUser.roleId = role.id; + } + }); + + await this.groupService.save(group); + + return Promise.resolve(); + } + public async getRoomMembershipAuthorizablesByUserId(userId: EntityId): Promise { const groupPage = await this.groupService.findGroups({ userId, groupTypes: [GroupTypes.ROOM] }); const groupIds = groupPage.data.map((group) => group.id); diff --git a/apps/server/src/modules/room/api/dto/request/change-room-role.body.params.ts b/apps/server/src/modules/room/api/dto/request/change-room-role.body.params.ts new file mode 100644 index 00000000000..05cab1574cb --- /dev/null +++ b/apps/server/src/modules/room/api/dto/request/change-room-role.body.params.ts @@ -0,0 +1,28 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { RoleName, RoomRole } from '@shared/domain/interface'; +import { IsArray, IsEnum, IsMongoId } from 'class-validator'; + +export type AssignableRoomRole = Exclude; +export enum AssignableRoomRoleEnum { + ROOMADMIN = RoleName.ROOMADMIN, + ROOMEDITOR = RoleName.ROOMEDITOR, + ROOMVIEWER = RoleName.ROOMVIEWER, +} + +export class ChangeRoomRoleBodyParams { + @ApiProperty({ + description: 'The IDs of the users', + required: true, + }) + @IsArray() + @IsMongoId({ each: true }) + public userIds!: string[]; + + @ApiProperty({ + description: 'The role to assign to the users. Must be a Room Role role other than ROOMOWNER.', + required: true, + enum: AssignableRoomRoleEnum, + }) + @IsEnum(AssignableRoomRoleEnum) + public roleName!: AssignableRoomRole; +} diff --git a/apps/server/src/modules/room/api/loggables/cant-change-roomowners-role.error.loggable.ts b/apps/server/src/modules/room/api/loggables/cant-change-roomowners-role.error.loggable.ts new file mode 100644 index 00000000000..fc2ca7ea71a --- /dev/null +++ b/apps/server/src/modules/room/api/loggables/cant-change-roomowners-role.error.loggable.ts @@ -0,0 +1,24 @@ +import { BadRequestException } from '@nestjs/common'; +import { ErrorLogMessage } from '@shared/common/error'; +import { Loggable } from '@shared/common/loggable'; + +export class CantChangeOwnersRoleLoggableException extends BadRequestException implements Loggable { + constructor(private readonly currentUserId: string, private readonly roomId: string) { + super(); + } + + public getLogMessage(): ErrorLogMessage { + const message: ErrorLogMessage = { + type: 'CANT_CHANGE_OWNERS_ROLE', + stack: this.stack, + data: { + currentUserId: this.currentUserId, + roomId: this.roomId, + errorMessage: + 'You cannot change the role of the room owner. If you want to change the owner, please transfer the ownership to another user instead.', + }, + }; + + return message; + } +} diff --git a/apps/server/src/modules/room/api/loggables/index.ts b/apps/server/src/modules/room/api/loggables/index.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/apps/server/src/modules/room/api/room.controller.ts b/apps/server/src/modules/room/api/room.controller.ts index 6377f54f20a..a92a047561b 100644 --- a/apps/server/src/modules/room/api/room.controller.ts +++ b/apps/server/src/modules/room/api/room.controller.ts @@ -33,6 +33,7 @@ import { RoomListResponse } from './dto/response/room-list.response'; import { RoomMemberListResponse } from './dto/response/room-member-list.response'; import { RoomMapper } from './mapper/room.mapper'; import { RoomUc } from './room.uc'; +import { ChangeRoomRoleBodyParams } from './dto/request/change-room-role.body.params'; @ApiTags('Room') @JwtAuthentication() @@ -164,6 +165,26 @@ export class RoomController { await this.roomUc.addMembersToRoom(currentUser.userId, urlParams.roomId, bodyParams.userIds); } + @Patch(':roomId/members/roles') + @ApiOperation({ summary: 'Change the roles that members have within the room' }) + @ApiResponse({ status: HttpStatus.OK, description: 'Adding successful', type: String }) + @ApiResponse({ status: HttpStatus.BAD_REQUEST, type: ApiValidationError }) + @ApiResponse({ status: HttpStatus.UNAUTHORIZED, type: UnauthorizedException }) + @ApiResponse({ status: HttpStatus.FORBIDDEN, type: ForbiddenException }) + @ApiResponse({ status: '5XX', type: ErrorResponse }) + public async changeRolesOfMembers( + @CurrentUser() currentUser: ICurrentUser, + @Param() urlParams: RoomUrlParams, + @Body() bodyParams: ChangeRoomRoleBodyParams + ): Promise { + await this.roomUc.changeRolesOfMembers( + currentUser.userId, + urlParams.roomId, + bodyParams.userIds, + bodyParams.roleName + ); + } + @Patch(':roomId/members/remove') @ApiOperation({ summary: 'Remove members from a room' }) @ApiResponse({ status: HttpStatus.OK, description: 'Removing successful', type: String }) diff --git a/apps/server/src/modules/room/api/room.uc.ts b/apps/server/src/modules/room/api/room.uc.ts index e559b3b5c0d..3debf8e4bb6 100644 --- a/apps/server/src/modules/room/api/room.uc.ts +++ b/apps/server/src/modules/room/api/room.uc.ts @@ -6,13 +6,14 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { FeatureDisabledLoggableException } from '@shared/common/loggable-exception'; import { Page, UserDO } from '@shared/domain/domainobject'; -import { IFindOptions, Permission } from '@shared/domain/interface'; +import { IFindOptions, Permission, RoleName } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { Room, RoomService } from '../domain'; import { RoomConfig } from '../room.config'; import { CreateRoomBodyParams } from './dto/request/create-room.body.params'; import { UpdateRoomBodyParams } from './dto/request/update-room.body.params'; import { RoomMemberResponse } from './dto/response/room-member.response'; +import { CantChangeOwnersRoleLoggableException } from './loggables/cant-change-roomowners-role.error.loggable'; @Injectable() export class RoomUc { @@ -125,12 +126,6 @@ export class RoomUc { return memberResponses; } - public async addMembersToRoom(currentUserId: EntityId, roomId: EntityId, userIds: Array): Promise { - this.checkFeatureEnabled(); - await this.checkRoomAuthorization(currentUserId, roomId, Action.write, [Permission.ROOM_MEMBERS_ADD]); - await this.roomMembershipService.addMembersToRoom(roomId, userIds); - } - private mapToMember(member: UserWithRoomRoles, user: UserDO): RoomMemberResponse { return new RoomMemberResponse({ userId: member.userId, @@ -142,6 +137,40 @@ export class RoomUc { }); } + public async addMembersToRoom(currentUserId: EntityId, roomId: EntityId, userIds: Array): Promise { + this.checkFeatureEnabled(); + await this.checkRoomAuthorization(currentUserId, roomId, Action.write, [Permission.ROOM_MEMBERS_ADD]); + await this.roomMembershipService.addMembersToRoom(roomId, userIds); + } + + public async changeRolesOfMembers( + currentUserId: EntityId, + roomId: EntityId, + userIds: Array, + roleName: RoleName + ): Promise { + this.checkFeatureEnabled(); + const roomAuthorizable = await this.checkRoomAuthorization(currentUserId, roomId, Action.write, [ + Permission.ROOM_MEMBERS_CHANGE_ROLE, + ]); + this.preventChangingOwnersRole(roomAuthorizable, userIds, currentUserId); + await this.roomMembershipService.changeRoleOfRoomMembers(roomId, userIds, roleName); + return Promise.resolve(); + } + + private preventChangingOwnersRole( + roomAuthorizable: RoomMembershipAuthorizable, + userIdsToChange: EntityId[], + currentUserId: EntityId + ): void { + const owner = roomAuthorizable.members.find((member) => + member.roles.some((role) => role.name === RoleName.ROOMOWNER) + ); + if (owner && userIdsToChange.includes(owner.userId)) { + throw new CantChangeOwnersRoleLoggableException(roomAuthorizable.roomId, currentUserId); + } + } + public async removeMembersFromRoom(currentUserId: EntityId, roomId: EntityId, userIds: EntityId[]): Promise { this.checkFeatureEnabled(); await this.checkRoomAuthorization(currentUserId, roomId, Action.write, [Permission.ROOM_MEMBERS_REMOVE]); diff --git a/apps/server/src/modules/room/api/test/room-change-role.api.spec.ts b/apps/server/src/modules/room/api/test/room-change-role.api.spec.ts new file mode 100644 index 00000000000..0f1e855dccb --- /dev/null +++ b/apps/server/src/modules/room/api/test/room-change-role.api.spec.ts @@ -0,0 +1,208 @@ +import { EntityManager } from '@mikro-orm/mongodb'; +import { GroupEntityTypes } from '@modules/group/entity/group.entity'; +import { groupEntityFactory } from '@modules/group/testing'; +import { roomMembershipEntityFactory } from '@modules/room-membership/testing/room-membership-entity.factory'; +import { ServerTestModule, serverConfig, type ServerConfig } from '@modules/server'; +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { RoleName } from '@shared/domain/interface/rolename.enum'; +import { cleanupCollections } from '@testing/cleanup-collections'; +import { roleFactory } from '@testing/factory/role.factory'; +import { schoolEntityFactory } from '@testing/factory/school-entity.factory'; +import { UserAndAccountTestFactory } from '@testing/factory/user-and-account.test.factory'; +import { userFactory } from '@testing/factory/user.factory'; +import { TestApiClient } from '@testing/test-api-client'; +import { roomEntityFactory } from '../../testing/room-entity.factory'; +import { RoomRolesTestFactory } from '../../testing/room-roles.test.factory'; +import { RoomMemberListResponse } from '../dto/response/room-member-list.response'; + +describe('Room Controller (API)', () => { + let app: INestApplication; + let em: EntityManager; + let testApiClient: TestApiClient; + let config: ServerConfig; + + beforeAll(async () => { + const moduleFixture = await Test.createTestingModule({ + imports: [ServerTestModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + em = app.get(EntityManager); + testApiClient = new TestApiClient(app, 'rooms'); + + config = serverConfig(); + }); + + beforeEach(async () => { + await cleanupCollections(em); + config.FEATURE_ROOMS_ENABLED = true; + + await em.clearCache('roles-cache-byname-roomeditor'); + await em.clearCache('roles-cache-byname-teacher'); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('PATCH /rooms/:roomId/members/roles', () => { + const setupRoomWithMembers = async () => { + const school = schoolEntityFactory.buildWithId(); + const { teacherAccount, teacherUser: owner } = UserAndAccountTestFactory.buildTeacher({ school }); + const teacherRole = owner.roles[0]; + const targetUser = userFactory.buildWithId({ school: owner.school, roles: [teacherRole] }); + const room = roomEntityFactory.buildWithId({ schoolId: owner.school.id }); + const teacherGuestRole = roleFactory.buildWithId({ name: RoleName.GUESTTEACHER }); + const studentGuestRole = roleFactory.buildWithId({ name: RoleName.GUESTSTUDENT }); + const { roomEditorRole, roomAdminRole, roomOwnerRole, roomViewerRole } = RoomRolesTestFactory.createRoomRoles(); + const userGroupEntity = groupEntityFactory.buildWithId({ + users: [ + { role: roomOwnerRole, user: owner }, + { role: roomViewerRole, user: targetUser }, + ], + type: GroupEntityTypes.ROOM, + organization: owner.school, + externalSource: undefined, + }); + + const roomMemberships = roomMembershipEntityFactory.build({ + userGroupId: userGroupEntity.id, + roomId: room.id, + schoolId: school.id, + }); + await em.persistAndFlush([ + room, + roomMemberships, + teacherAccount, + owner, + teacherRole, + teacherGuestRole, + studentGuestRole, + roomEditorRole, + roomAdminRole, + roomOwnerRole, + roomViewerRole, + targetUser, + targetUser, + userGroupEntity, + ]); + em.clear(); + + const loggedInClient = await testApiClient.login(teacherAccount); + + return { loggedInClient, room, targetUser, owner }; + }; + + describe('when the user is not authenticated', () => { + it('should return a 401 error', async () => { + const { room } = await setupRoomWithMembers(); + + const response = await testApiClient.patch(`/${room.id}/members/roles`); + + expect(response.status).toBe(HttpStatus.UNAUTHORIZED); + }); + }); + + describe('when the user has not the required permissions', () => { + const setupLoggedInUser = async () => { + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); + await em.persistAndFlush([teacherAccount, teacherUser]); + + const loggedInClient = await testApiClient.login(teacherAccount); + + return { loggedInClient }; + }; + + it('should return forbidden error', async () => { + const { room, targetUser } = await setupRoomWithMembers(); + const { loggedInClient } = await setupLoggedInUser(); + + const response = await loggedInClient.patch(`/${room.id}/members/roles`, { + userIds: [targetUser.id], + roleName: RoleName.ROOMEDITOR, + }); + + expect(response.status).toBe(HttpStatus.FORBIDDEN); + }); + }); + + describe('when the feature is disabled', () => { + it('should return a 403 error', async () => { + const { loggedInClient, room, targetUser } = await setupRoomWithMembers(); + config.FEATURE_ROOMS_ENABLED = false; + + const response = await loggedInClient.patch(`/${room.id}/members/roles`, { + userIds: [targetUser.id], + roleName: RoleName.ROOMEDITOR, + }); + + expect(response.status).toBe(HttpStatus.FORBIDDEN); + }); + }); + + describe('when the user has the required permissions', () => { + it('should return OK', async () => { + const { loggedInClient, room, targetUser } = await setupRoomWithMembers(); + + const response = await loggedInClient.patch(`/${room.id}/members/roles`, { + userIds: [targetUser.id], + roleName: RoleName.ROOMEDITOR, + }); + + expect(response.status).toBe(HttpStatus.OK); + }); + + it('should change the role of the user', async () => { + const { loggedInClient, room, targetUser } = await setupRoomWithMembers(); + + await loggedInClient.patch(`/${room.id}/members/roles`, { + userIds: [targetUser.id], + roleName: RoleName.ROOMEDITOR, + }); + + const updatedRoomMembership = await loggedInClient.get(`/${room.id}/members`); + const body = updatedRoomMembership.body as RoomMemberListResponse; + expect(body.data).toEqual( + expect.arrayContaining([ + expect.objectContaining({ userId: targetUser.id, roomRoleName: RoleName.ROOMEDITOR }), + ]) + ); + }); + + it('should return error when changing someones role to owner', async () => { + const { loggedInClient, room, targetUser } = await setupRoomWithMembers(); + + const response = await loggedInClient.patch(`/${room.id}/members/roles`, { + userIds: [targetUser.id], + roleName: RoleName.ROOMOWNER, + }); + + expect(response.status).toBe(HttpStatus.BAD_REQUEST); + }); + + it('should return error when changing someones role to a non-room role', async () => { + const { loggedInClient, room, targetUser } = await setupRoomWithMembers(); + + const response = await loggedInClient.patch(`/${room.id}/members/roles`, { + userIds: [targetUser.id], + roleName: RoleName.TEACHER, + }); + + expect(response.status).toBe(HttpStatus.BAD_REQUEST); + }); + + it('should return error when changing the owners role', async () => { + const { loggedInClient, room, owner } = await setupRoomWithMembers(); + + const response = await loggedInClient.patch(`/${room.id}/members/roles`, { + userIds: [owner.id], + roleName: RoleName.ROOMEDITOR, + }); + + expect(response.status).toBe(HttpStatus.BAD_REQUEST); + }); + }); + }); +}); diff --git a/apps/server/src/modules/room/testing/room-roles.test.factory.ts b/apps/server/src/modules/room/testing/room-roles.test.factory.ts new file mode 100644 index 00000000000..73f789b935d --- /dev/null +++ b/apps/server/src/modules/room/testing/room-roles.test.factory.ts @@ -0,0 +1,50 @@ +import { Role } from '@shared/domain/entity'; +import { Permission, RoleName } from '@shared/domain/interface'; +import { roleFactory } from '@testing/factory/role.factory'; + +export class RoomRolesTestFactory { + public static createRoomRoles(): { + roomOwnerRole: Role; + roomAdminRole: Role; + roomEditorRole: Role; + roomViewerRole: Role; + } { + const roomOwnerRole = roleFactory.buildWithId({ + name: RoleName.ROOMOWNER, + permissions: [ + Permission.ROOM_VIEW, + Permission.ROOM_EDIT, + Permission.ROOM_MEMBERS_ADD, + Permission.ROOM_MEMBERS_REMOVE, + Permission.ROOM_MEMBERS_CHANGE_ROLE, + Permission.ROOM_DELETE, + Permission.ROOM_CHANGE_OWNER, + ], + }); + const roomAdminRole = roleFactory.buildWithId({ + name: RoleName.ROOMADMIN, + permissions: [ + Permission.ROOM_VIEW, + Permission.ROOM_EDIT, + Permission.ROOM_MEMBERS_ADD, + Permission.ROOM_MEMBERS_REMOVE, + Permission.ROOM_MEMBERS_CHANGE_ROLE, + ], + }); + const roomEditorRole = roleFactory.buildWithId({ + name: RoleName.ROOMEDITOR, + permissions: [Permission.ROOM_VIEW, Permission.ROOM_EDIT], + }); + const roomViewerRole = roleFactory.buildWithId({ + name: RoleName.ROOMVIEWER, + permissions: [Permission.ROOM_VIEW], + }); + + return { + roomOwnerRole, + roomAdminRole, + roomEditorRole, + roomViewerRole, + }; + } +} diff --git a/apps/server/src/shared/domain/interface/permission.enum.ts b/apps/server/src/shared/domain/interface/permission.enum.ts index bd1c2b0d255..f677991d4a9 100644 --- a/apps/server/src/shared/domain/interface/permission.enum.ts +++ b/apps/server/src/shared/domain/interface/permission.enum.ts @@ -106,6 +106,7 @@ export enum Permission { ROOM_DELETE = 'ROOM_DELETE', ROOM_MEMBERS_ADD = 'ROOM_MEMBERS_ADD', ROOM_MEMBERS_REMOVE = 'ROOM_MEMBERS_REMOVE', + ROOM_MEMBERS_CHANGE_ROLE = 'ROOM_MEMBERS_CHANGE_ROLE', ROOM_CHANGE_OWNER = 'ROOM_CHANGE_OWNER', SCHOOL_CHAT_MANAGE = 'SCHOOL_CHAT_MANAGE', SCHOOL_CREATE = 'SCHOOL_CREATE', diff --git a/backup/setup/migrations.json b/backup/setup/migrations.json index ba3213b3cbf..f7a693c2cc9 100644 --- a/backup/setup/migrations.json +++ b/backup/setup/migrations.json @@ -340,5 +340,14 @@ "created_at": { "$date": "2024-11-22T09:29:01.351+0000" } + }, + { + "_id": { + "$oid": "678e4e7a4af7ab0304f28cbd" + }, + "name": "Migration20250120131625", + "created_at": { + "$date": "2025-01-20T13:24:10.786Z" + } } ] diff --git a/backup/setup/roles.json b/backup/setup/roles.json index 4bc6e75f88a..8e23aeb9e50 100644 --- a/backup/setup/roles.json +++ b/backup/setup/roles.json @@ -627,7 +627,8 @@ "ROOM_DELETE", "ROOM_MEMBERS_ADD", "ROOM_MEMBERS_REMOVE", - "ROOM_CHANGE_OWNER" + "ROOM_CHANGE_OWNER", + "ROOM_MEMBERS_CHANGE_ROLE" ] }, { @@ -639,7 +640,8 @@ "ROOM_VIEW", "ROOM_EDIT", "ROOM_MEMBERS_ADD", - "ROOM_MEMBERS_REMOVE" + "ROOM_MEMBERS_REMOVE", + "ROOM_MEMBERS_CHANGE_ROLE" ] }, {