diff --git a/apps/server/src/migrations/mikro-orm/Migration202410041210124.ts b/apps/server/src/migrations/mikro-orm/Migration202410041210124.ts index c18d21eb11c..cf10456dcdb 100644 --- a/apps/server/src/migrations/mikro-orm/Migration202410041210124.ts +++ b/apps/server/src/migrations/mikro-orm/Migration202410041210124.ts @@ -19,11 +19,11 @@ export class Migration202410041210124 extends Migration { async down(): Promise { // Remove ROOM_VIEWER role - await this.getCollection('roles').deleteOne({ name: 'ROOM_VIEWER' }); + await this.getCollection('roles').deleteOne({ name: 'room_viewer' }); console.info('Rollback: Removed ROOM_VIEWER role'); // Remove ROOM_EDITOR role - await this.getCollection('roles').deleteOne({ name: 'ROOM_EDITOR' }); + await this.getCollection('roles').deleteOne({ name: 'room_editor' }); console.info('Rollback: Removed ROOM_EDITOR role'); } } diff --git a/apps/server/src/migrations/mikro-orm/Migration20241111160412.ts b/apps/server/src/migrations/mikro-orm/Migration20241111160412.ts new file mode 100644 index 00000000000..e642e195e69 --- /dev/null +++ b/apps/server/src/migrations/mikro-orm/Migration20241111160412.ts @@ -0,0 +1,27 @@ +import { Migration } from '@mikro-orm/migrations-mongodb'; + +export class Migration20241111160412 extends Migration { + async up(): Promise { + // Rename ROOM_VIEWER role from room_viewer to roomviewer + await this.getCollection('roles').updateMany({ name: 'room_viewer' }, { $set: { name: 'roomviewer' } }); + + console.info('Renamed ROOM_VIEWER role from room_viewer to roomviewer'); + + // Rename ROOM_EDITOR role from room_editor to roomeditor + await this.getCollection('roles').updateMany({ name: 'room_editor' }, { $set: { name: 'roomeditor' } }); + + console.info('Renamed ROOM_EDITOR role from room_editor to roomeditor'); + } + + async down(): Promise { + // Rename ROOM_VIEWER role from roomviewer to room_viewer + await this.getCollection('roles').updateMany({ name: 'roomviewer' }, { $set: { name: 'room_viewer' } }); + + console.info('Rollback: Renamed ROOM_VIEWER role from roomviewer to room_viewer'); + + // Rename ROOM_EDITOR role from roomeditor to room_editor + await this.getCollection('roles').updateMany({ name: 'roomeditor' }, { $set: { name: 'room_editor' } }); + + console.info('Rollback: Renamed ROOM_EDITOR role from roomeditor to room_editor'); + } +} diff --git a/apps/server/src/migrations/mikro-orm/Migration20241112163538.ts b/apps/server/src/migrations/mikro-orm/Migration20241112163538.ts new file mode 100644 index 00000000000..26e50089fd7 --- /dev/null +++ b/apps/server/src/migrations/mikro-orm/Migration20241112163538.ts @@ -0,0 +1,31 @@ +import { Migration } from '@mikro-orm/migrations-mongodb'; + +export class Migration20241112163538 extends Migration { + async up(): Promise { + const collection = this.getCollection('room-members'); + + await collection.updateMany({ roomId: { $type: 'string' } }, [ + { + $set: { + roomId: { + $convert: { + input: '$roomId', + to: 'objectId', + onError: '$roomId', // Keep the original value if conversion fails + onNull: '$roomId', // Keep the original value if the input is null + }, + }, + }, + }, + ]); + console.info('Converted roomId from string to ObjectId'); + + await collection.updateMany({}, { $rename: { roomId: 'room' } }); + console.info('Renamed roomId to room'); + } + + async down(): Promise { + await Promise.resolve(); + console.error(`Migration down not implemented. You might need to restore database from backup!`); + } +} diff --git a/apps/server/src/migrations/mikro-orm/Migration20241113100535.ts b/apps/server/src/migrations/mikro-orm/Migration20241113100535.ts new file mode 100644 index 00000000000..d233b984596 --- /dev/null +++ b/apps/server/src/migrations/mikro-orm/Migration20241113100535.ts @@ -0,0 +1,67 @@ +import { Migration } from '@mikro-orm/migrations-mongodb'; + +export class Migration20241113100535 extends Migration { + async up(): Promise { + const teacherRoleUpdate = await this.getCollection('roles').updateOne( + { name: 'teacher' }, + { + $addToSet: { + permissions: { + $each: ['ROOM_CREATE'], + }, + }, + } + ); + + if (teacherRoleUpdate.modifiedCount > 0) { + console.info('Permissions ROOM_CREATE added to role teacher.'); + } + + const roomEditorRoleUpdate = await this.getCollection('roles').updateOne( + { name: 'roomeditor' }, + { + $addToSet: { + permissions: { + $each: ['ROOM_DELETE'], + }, + }, + } + ); + + if (roomEditorRoleUpdate.modifiedCount > 0) { + console.info('Permissions ROOM_DELETE added to role roomeditor.'); + } + } + + async down(): Promise { + const teacherRoleUpdate = await this.getCollection('roles').updateOne( + { name: 'teacher' }, + { + $pull: { + permissions: { + $in: ['ROOM_CREATE'], + }, + }, + } + ); + + if (teacherRoleUpdate.modifiedCount > 0) { + console.info('Rollback: Permission ROOM_CREATE added to role teacher.'); + } + + const roomEditorRoleUpdate = await this.getCollection('roles').updateOne( + { name: 'roomeditor' }, + { + $pull: { + permissions: { + $in: ['ROOM_DELETE'], + }, + }, + } + ); + + if (roomEditorRoleUpdate.modifiedCount > 0) { + console.info('Rollback: Permission ROOM_DELETE added to role roomeditor.'); + } + } +} diff --git a/apps/server/src/migrations/mikro-orm/Migration20241113152001.ts b/apps/server/src/migrations/mikro-orm/Migration20241113152001.ts new file mode 100644 index 00000000000..9914935c0a9 --- /dev/null +++ b/apps/server/src/migrations/mikro-orm/Migration20241113152001.ts @@ -0,0 +1,102 @@ +import { Migration } from '@mikro-orm/migrations-mongodb'; + +export class Migration20241113152001 extends Migration { + async up(): Promise { + const roomsToSchoolView = [ + { + $lookup: { + from: 'rooms', + localField: 'room', + foreignField: '_id', + as: 'roomDetails', + }, + }, + { + $unwind: '$roomDetails', + }, + { + $match: { + 'roomDetails.school': { $exists: false, $eq: null }, + }, + }, + { + $lookup: { + from: 'groups', + localField: 'userGroup', + foreignField: '_id', + as: 'groupDetails', + }, + }, + { + $unwind: '$groupDetails', + }, + { + $unwind: '$groupDetails.users', + }, + { + $lookup: { + from: 'roles', + localField: 'groupDetails.users.role', + foreignField: '_id', + as: 'roleDetails', + }, + }, + { + $unwind: '$roleDetails', + }, + { + $match: { + 'roleDetails.name': 'roomeditor', + }, + }, + { + $lookup: { + from: 'users', + localField: 'groupDetails.users.user', + foreignField: '_id', + as: 'userDetails', + }, + }, + { + $unwind: '$userDetails', + }, + { + $group: { + _id: '$userDetails.schoolId', + rooms: { $push: '$roomDetails._id' }, + }, + }, + { + $project: { + _id: 0, + school: '$_id', + rooms: 1, + }, + }, + ]; + + const mappings = await this.driver.aggregate('room-members', roomsToSchoolView); + + for await (const mapping of mappings) { + const schoolUpdate = await this.driver.nativeUpdate( + 'rooms', + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access + { _id: { $in: mapping.rooms } }, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access + { $set: { school: mapping.school } } + ); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access, @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-call + console.info(`Updated ${schoolUpdate.affectedRows} rooms with school ${mapping.school.toHexString()}`); + } + + if (mappings.length === 0) { + console.info(`No rooms without school to update`); + } + } + + async down(): Promise { + await Promise.resolve(); + console.error(`Migration down not implemented. You might need to restore database from backup!`); + } +} diff --git a/apps/server/src/migrations/mikro-orm/Migration20241127195120.ts b/apps/server/src/migrations/mikro-orm/Migration20241127195120.ts new file mode 100644 index 00000000000..0b74ac61e63 --- /dev/null +++ b/apps/server/src/migrations/mikro-orm/Migration20241127195120.ts @@ -0,0 +1,15 @@ +import { Migration } from '@mikro-orm/migrations-mongodb'; + +export class Migration20241127195120 extends Migration { + async up(): Promise { + const db = this.driver.getConnection().getDb(); + await db.renameCollection('room-members', 'room-memberships'); + console.info('Collection renamed from room-members to room-memberships'); + } + + async down(): Promise { + const db = this.driver.getConnection().getDb(); + await db.renameCollection('room-memberships', 'room-members'); + console.info('Collection renamed from room-memberships to room-members'); + } +} diff --git a/apps/server/src/migrations/mikro-orm/Migration20241128155801.ts b/apps/server/src/migrations/mikro-orm/Migration20241128155801.ts new file mode 100644 index 00000000000..725ef54ecd3 --- /dev/null +++ b/apps/server/src/migrations/mikro-orm/Migration20241128155801.ts @@ -0,0 +1,61 @@ +import { Migration } from '@mikro-orm/migrations-mongodb'; + +export class Migration20241128155801 extends Migration { + async up(): Promise { + const roomMembershipToSchoolView = [ + { + $match: { + school: { $exists: false, $eq: null }, + }, + }, + { + $lookup: { + from: 'rooms', + localField: 'room', + foreignField: '_id', + as: 'roomDetails', + }, + }, + { + $unwind: '$roomDetails', + }, + { + $group: { + _id: '$roomDetails.school', + roomMemberships: { $push: '$_id' }, + }, + }, + { + $project: { + _id: 0, + school: '$_id', + roomMemberships: 1, + }, + }, + ]; + + const mappings = await this.driver.aggregate('room-memberships', roomMembershipToSchoolView); + + for await (const mapping of mappings) { + const schoolUpdate = await this.driver.nativeUpdate( + 'room-memberships', + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access + { _id: { $in: mapping.roomMemberships } }, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access + { $set: { school: mapping.school } } + ); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access, @typescript-eslint/restrict-template-expressions, @typescript-eslint/no-unsafe-call + console.info(`Updated ${schoolUpdate.affectedRows} rooms with school ${mapping.school.toHexString()}`); + } + + if (mappings.length === 0) { + console.info(`No roomMemberships without school to update`); + } + } + + async down(): Promise { + await Promise.resolve(); + console.error(`Migration down not implemented. You might need to restore database from backup!`); + } +} diff --git a/apps/server/src/modules/board/board-api.module.ts b/apps/server/src/modules/board/board-api.module.ts index 3431851ef32..e4fa5ac9cb0 100644 --- a/apps/server/src/modules/board/board-api.module.ts +++ b/apps/server/src/modules/board/board-api.module.ts @@ -2,7 +2,7 @@ import { AuthorizationModule } from '@modules/authorization'; import { forwardRef, Module } from '@nestjs/common'; import { CourseRepo } from '@shared/repo/course'; import { LoggerModule } from '@src/core/logger'; -import { RoomMemberModule } from '../room-member'; +import { RoomMembershipModule } from '@src/modules/room-membership'; import { BoardModule } from './board.module'; import { BoardController, @@ -13,9 +13,10 @@ import { } from './controller'; import { BoardNodePermissionService } from './service'; import { BoardUc, CardUc, ColumnUc, ElementUc, SubmissionItemUc } from './uc'; +import { RoomModule } from '../room'; @Module({ - imports: [BoardModule, LoggerModule, RoomMemberModule, forwardRef(() => AuthorizationModule)], + imports: [BoardModule, LoggerModule, RoomMembershipModule, RoomModule, forwardRef(() => AuthorizationModule)], controllers: [BoardController, ColumnController, CardController, ElementController, BoardSubmissionController], providers: [BoardUc, BoardNodePermissionService, ColumnUc, CardUc, ElementUc, SubmissionItemUc, CourseRepo], }) diff --git a/apps/server/src/modules/board/board-ws-api.module.ts b/apps/server/src/modules/board/board-ws-api.module.ts index 1a39907dacd..663b1417b5d 100644 --- a/apps/server/src/modules/board/board-ws-api.module.ts +++ b/apps/server/src/modules/board/board-ws-api.module.ts @@ -3,15 +3,23 @@ import { UserModule } from '@modules/user'; import { forwardRef, Module } from '@nestjs/common'; import { CourseRepo } from '@shared/repo/course'; import { LoggerModule } from '@src/core/logger'; -import { RoomMemberModule } from '../room-member'; +import { RoomMembershipModule } from '../room-membership'; import { BoardModule } from './board.module'; import { BoardCollaborationGateway } from './gateway/board-collaboration.gateway'; import { MetricsService } from './metrics/metrics.service'; import { BoardNodePermissionService } from './service'; import { BoardUc, CardUc, ColumnUc, ElementUc } from './uc'; +import { RoomModule } from '../room'; @Module({ - imports: [BoardModule, forwardRef(() => AuthorizationModule), LoggerModule, UserModule, RoomMemberModule], + imports: [ + BoardModule, + forwardRef(() => AuthorizationModule), + LoggerModule, + UserModule, + RoomMembershipModule, + RoomModule, + ], providers: [ BoardCollaborationGateway, BoardNodePermissionService, diff --git a/apps/server/src/modules/board/board.module.ts b/apps/server/src/modules/board/board.module.ts index 7a2142e1204..e8d703431bd 100644 --- a/apps/server/src/modules/board/board.module.ts +++ b/apps/server/src/modules/board/board.module.ts @@ -10,7 +10,7 @@ import { CqrsModule } from '@nestjs/cqrs'; import { CourseRepo } from '@shared/repo/course'; import { LoggerModule } from '@src/core/logger'; import { AuthorizationModule } from '../authorization'; -import { RoomMemberModule } from '../room-member'; +import { RoomMembershipModule } from '../room-membership'; import { BoardNodeRule } from './authorisation/board-node.rule'; import { BoardNodeFactory } from './domain'; import { BoardNodeRepo } from './repo'; @@ -46,7 +46,7 @@ import { CqrsModule, CollaborativeTextEditorModule, AuthorizationModule, - RoomMemberModule, + RoomMembershipModule, ], providers: [ // TODO: move BoardDoAuthorizableService, BoardDoRepo, BoardDoService, BoardNodeRepo in separate module and move mediaboard related services in mediaboard module diff --git a/apps/server/src/modules/board/controller/api-test/board-context-in-rooms.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-context-in-rooms.api.spec.ts index 1d701b7a157..15b42ea15a8 100644 --- a/apps/server/src/modules/board/controller/api-test/board-context-in-rooms.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-context-in-rooms.api.spec.ts @@ -6,7 +6,7 @@ import { TestApiClient, cleanupCollections, groupEntityFactory, roleFactory, use import { Permission, RoleName } from '@shared/domain/interface'; import { accountFactory } from '@src/modules/account/testing'; import { GroupEntityTypes } from '@src/modules/group/entity'; -import { roomMemberEntityFactory } from '@src/modules/room-member/testing'; +import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing'; import { roomEntityFactory } from '@src/modules/room/testing'; import { columnBoardEntityFactory } from '../../testing'; import { BoardExternalReferenceType } from '../../domain'; @@ -48,11 +48,11 @@ describe('board get context in room (api)', () => { const noAccessAccount = accountFactory.withUser(noAccessUser).build(); const roleRoomEdit = roleFactory.buildWithId({ - name: RoleName.ROOM_EDITOR, + name: RoleName.ROOMEDITOR, permissions: [Permission.ROOM_EDIT], }); const roleRoomView = roleFactory.buildWithId({ - name: RoleName.ROOM_VIEWER, + name: RoleName.ROOMVIEWER, permissions: [Permission.ROOM_VIEW], }); @@ -66,7 +66,7 @@ describe('board get context in room (api)', () => { const room = roomEntityFactory.buildWithId(); - const roomMember = roomMemberEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); await em.persistAndFlush([ accountWithEditRole, @@ -79,7 +79,7 @@ describe('board get context in room (api)', () => { roleRoomView, userGroup, room, - roomMember, + roomMembership, ]); const columnBoardNode = columnBoardEntityFactory.build({ diff --git a/apps/server/src/modules/board/controller/api-test/board-copy-in-course.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-copy-in-course.api.spec.ts index 51a5cf227c1..854cf4e36e9 100644 --- a/apps/server/src/modules/board/controller/api-test/board-copy-in-course.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-copy-in-course.api.spec.ts @@ -6,7 +6,7 @@ import { TestApiClient, UserAndAccountTestFactory, cleanupCollections, courseFac import { CopyApiResponse, CopyElementType, CopyStatusEnum } from '@modules/copy-helper'; import { BoardNodeEntity } from '../../repo'; import { columnBoardEntityFactory } from '../../testing'; -import { BoardExternalReferenceType } from '../../domain'; +import { BoardExternalReferenceType, ColumnBoardProps } from '../../domain'; const baseRouteName = '/boards'; @@ -35,13 +35,14 @@ describe(`board copy with course relation (api)`, () => { }); describe('with valid user', () => { - const setup = async () => { + const setup = async (columnBoardProps: Partial = {}) => { const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); const course = courseFactory.build({ teachers: [teacherUser] }); await em.persistAndFlush([teacherAccount, teacherUser, course]); const columnBoardNode = columnBoardEntityFactory.build({ + ...columnBoardProps, context: { id: course.id, type: BoardExternalReferenceType.Course }, }); @@ -71,6 +72,7 @@ describe(`board copy with course relation (api)`, () => { id: expect.any(String), type: CopyElementType.COLUMNBOARD, status: CopyStatusEnum.SUCCESS, + destinationId: columnBoardNode.context?.id, }; expect(body).toEqual(expectedBody); @@ -81,6 +83,27 @@ describe(`board copy with course relation (api)`, () => { expect(result).toBeDefined(); }); + it('should set draft status on the board copy', async () => { + const { loggedInClient, columnBoardNode } = await setup({ isVisible: true }); + + const response = await loggedInClient.post(`${columnBoardNode.id}/copy`); + const body = response.body as CopyApiResponse; + + const expectedBody: CopyApiResponse = { + id: expect.any(String), + type: CopyElementType.COLUMNBOARD, + status: CopyStatusEnum.SUCCESS, + destinationId: columnBoardNode.context?.id, + }; + + expect(body).toEqual(expectedBody); + + // eslint-disable-next-line @typescript-eslint/no-non-null-assertion + const result = await em.findOneOrFail(BoardNodeEntity, body.id!); + + expect(result.isVisible).toBe(false); + }); + describe('with invalid id', () => { it('should return status 400', async () => { const { loggedInClient } = await setup(); diff --git a/apps/server/src/modules/board/controller/api-test/board-copy-in-room.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-copy-in-room.api.spec.ts new file mode 100644 index 00000000000..d833eb510b2 --- /dev/null +++ b/apps/server/src/modules/board/controller/api-test/board-copy-in-room.api.spec.ts @@ -0,0 +1,78 @@ +import { EntityManager } from '@mikro-orm/mongodb'; +import { INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Permission, RoleName } from '@shared/domain/interface'; +import { + cleanupCollections, + groupEntityFactory, + roleFactory, + TestApiClient, + UserAndAccountTestFactory, +} from '@shared/testing'; +import { GroupEntityTypes } from '@src/modules/group/entity'; +import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing'; +import { roomEntityFactory } from '@src/modules/room/testing'; +import { ServerTestModule } from '@src/modules/server'; +import { BoardExternalReferenceType, ColumnBoardProps } from '../../domain'; +import { columnBoardEntityFactory } from '../../testing'; + +const baseRouteName = '/boards'; + +describe(`board copy with room relation (api)`, () => { + let app: INestApplication; + let em: EntityManager; + let testApiClient: TestApiClient; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ServerTestModule], + }).compile(); + + app = module.createNestApplication(); + await app.init(); + em = module.get(EntityManager); + testApiClient = new TestApiClient(app, baseRouteName); + }); + + afterAll(async () => { + await app.close(); + }); + + beforeEach(async () => { + await cleanupCollections(em); + }); + + describe('with valid user', () => { + const setup = async (columnBoardProps: Partial = {}) => { + const room = roomEntityFactory.buildWithId(); + const role = roleFactory.buildWithId({ + name: RoleName.ROOMEDITOR, + permissions: [Permission.ROOM_EDIT], + }); + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); + const userGroup = groupEntityFactory.buildWithId({ + type: GroupEntityTypes.ROOM, + users: [{ role, user: teacherUser }], + }); + const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + const columnBoardNode = columnBoardEntityFactory.build({ + ...columnBoardProps, + context: { id: room.id, type: BoardExternalReferenceType.Room }, + }); + await em.persistAndFlush([room, roomMembership, teacherAccount, teacherUser, userGroup, role, columnBoardNode]); + em.clear(); + + const loggedInClient = await testApiClient.login(teacherAccount); + + return { loggedInClient, columnBoardNode }; + }; + + it('should return status 201', async () => { + const { loggedInClient, columnBoardNode } = await setup(); + + const response = await loggedInClient.post(`${columnBoardNode.id}/copy`); + + expect(response.status).toEqual(201); + }); + }); +}); diff --git a/apps/server/src/modules/board/controller/api-test/board-create-in-room.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-create-in-room.api.spec.ts index 50d573674ef..bb10e3fb33c 100644 --- a/apps/server/src/modules/board/controller/api-test/board-create-in-room.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-create-in-room.api.spec.ts @@ -7,7 +7,7 @@ import { RoleName } from '@shared/domain/interface/rolename.enum'; import { cleanupCollections, groupEntityFactory, roleFactory, TestApiClient, userFactory } from '@shared/testing'; import { accountFactory } from '@src/modules/account/testing'; import { GroupEntityTypes } from '@src/modules/group/entity'; -import { roomMemberEntityFactory } from '@src/modules/room-member/testing'; +import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing'; import { roomEntityFactory } from '@src/modules/room/testing'; import { BoardExternalReferenceType, BoardLayout } from '../../domain'; import { BoardNodeEntity } from '../../repo'; @@ -45,7 +45,7 @@ describe(`create board in room (api)`, () => { const user = userFactory.buildWithId(); const account = accountFactory.withUser(user).build(); - const role = roleFactory.buildWithId({ name: RoleName.ROOM_EDITOR, permissions: [Permission.ROOM_EDIT] }); + const role = roleFactory.buildWithId({ name: RoleName.ROOMEDITOR, permissions: [Permission.ROOM_EDIT] }); const userGroup = groupEntityFactory.buildWithId({ type: GroupEntityTypes.ROOM, @@ -54,9 +54,9 @@ describe(`create board in room (api)`, () => { const room = roomEntityFactory.buildWithId(); - const roomMember = roomMemberEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); - await em.persistAndFlush([account, user, role, userGroup, room, roomMember]); + await em.persistAndFlush([account, user, role, userGroup, room, roomMembership]); em.clear(); const loggedInClient = await testApiClient.login(account); @@ -161,7 +161,7 @@ describe(`create board in room (api)`, () => { const user = userFactory.buildWithId(); const account = accountFactory.withUser(user).build(); - const role = roleFactory.buildWithId({ name: RoleName.ROOM_EDITOR, permissions: [Permission.ROOM_VIEW] }); + const role = roleFactory.buildWithId({ name: RoleName.ROOMEDITOR, permissions: [Permission.ROOM_VIEW] }); const userGroup = groupEntityFactory.buildWithId({ type: GroupEntityTypes.ROOM, @@ -170,9 +170,9 @@ describe(`create board in room (api)`, () => { const room = roomEntityFactory.buildWithId(); - const roomMember = roomMemberEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); - await em.persistAndFlush([account, user, role, userGroup, room, roomMember]); + await em.persistAndFlush([account, user, role, userGroup, room, roomMembership]); em.clear(); const loggedInClient = await testApiClient.login(account); @@ -229,7 +229,7 @@ describe(`create board in room (api)`, () => { const user = userFactory.buildWithId(); const account = accountFactory.withUser(user).build(); - const role = roleFactory.buildWithId({ name: RoleName.ROOM_EDITOR, permissions: [Permission.ROOM_EDIT] }); + const role = roleFactory.buildWithId({ name: RoleName.ROOMEDITOR, permissions: [Permission.ROOM_EDIT] }); const userGroup = groupEntityFactory.buildWithId({ type: GroupEntityTypes.ROOM, @@ -238,9 +238,9 @@ describe(`create board in room (api)`, () => { const room = roomEntityFactory.buildWithId(); - const roomMember = roomMemberEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); - await em.persistAndFlush([account, user, role, userGroup, room, roomMember]); + await em.persistAndFlush([account, user, role, userGroup, room, roomMembership]); em.clear(); const loggedInClient = await testApiClient.login(account); diff --git a/apps/server/src/modules/board/controller/api-test/board-delete-in-room.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-delete-in-room.api.spec.ts index 59d819352c8..c46ae270dfd 100644 --- a/apps/server/src/modules/board/controller/api-test/board-delete-in-room.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-delete-in-room.api.spec.ts @@ -7,7 +7,7 @@ import { accountFactory } from '@src/modules/account/testing'; import { Permission, RoleName } from '@shared/domain/interface'; import { GroupEntityTypes } from '@src/modules/group/entity'; import { roomEntityFactory } from '@src/modules/room/testing'; -import { roomMemberEntityFactory } from '@src/modules/room-member/testing'; +import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing'; import { columnBoardEntityFactory, columnEntityFactory } from '../../testing'; import { BoardNodeEntity } from '../../repo'; import { BoardExternalReferenceType } from '../../domain'; @@ -48,8 +48,8 @@ describe(`board delete in room (api)`, () => { const noAccessUser = userFactory.buildWithId(); const noAccessAccount = accountFactory.withUser(noAccessUser).build(); - const roleRoomEdit = roleFactory.buildWithId({ name: RoleName.ROOM_EDITOR, permissions: [Permission.ROOM_EDIT] }); - const roleRoomView = roleFactory.buildWithId({ name: RoleName.ROOM_VIEWER, permissions: [Permission.ROOM_VIEW] }); + const roleRoomEdit = roleFactory.buildWithId({ name: RoleName.ROOMEDITOR, permissions: [Permission.ROOM_EDIT] }); + const roleRoomView = roleFactory.buildWithId({ name: RoleName.ROOMVIEWER, permissions: [Permission.ROOM_VIEW] }); const userGroup = groupEntityFactory.buildWithId({ type: GroupEntityTypes.ROOM, @@ -61,7 +61,7 @@ describe(`board delete in room (api)`, () => { const room = roomEntityFactory.buildWithId(); - const roomMember = roomMemberEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); await em.persistAndFlush([ accountWithEditRole, @@ -74,7 +74,7 @@ describe(`board delete in room (api)`, () => { roleRoomView, userGroup, room, - roomMember, + roomMembership, ]); const columnBoardNode = columnBoardEntityFactory.build({ diff --git a/apps/server/src/modules/board/controller/api-test/board-lookup-in-room.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-lookup-in-room.api.spec.ts index c6701f81c94..0f1dea74740 100644 --- a/apps/server/src/modules/board/controller/api-test/board-lookup-in-room.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-lookup-in-room.api.spec.ts @@ -7,7 +7,7 @@ import { cleanupCollections, groupEntityFactory, roleFactory, TestApiClient, use import { Permission, RoleName } from '@shared/domain/interface'; import { accountFactory } from '@src/modules/account/testing'; import { GroupEntityTypes } from '@src/modules/group/entity'; -import { roomMemberEntityFactory } from '@src/modules/room-member/testing'; +import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing'; import { roomEntityFactory } from '@src/modules/room/testing'; import { BoardExternalReferenceType, BoardLayout } from '../../domain'; import { BoardResponse } from '../dto'; @@ -49,8 +49,8 @@ describe(`board lookup in room (api)`, () => { const noAccessUser = userFactory.buildWithId(); const noAccessAccount = accountFactory.withUser(noAccessUser).build(); - const roleRoomEdit = roleFactory.buildWithId({ name: RoleName.ROOM_EDITOR, permissions: [Permission.ROOM_EDIT] }); - const roleRoomView = roleFactory.buildWithId({ name: RoleName.ROOM_VIEWER, permissions: [Permission.ROOM_VIEW] }); + const roleRoomEdit = roleFactory.buildWithId({ name: RoleName.ROOMEDITOR, permissions: [Permission.ROOM_EDIT] }); + const roleRoomView = roleFactory.buildWithId({ name: RoleName.ROOMVIEWER, permissions: [Permission.ROOM_VIEW] }); const userGroup = groupEntityFactory.buildWithId({ type: GroupEntityTypes.ROOM, @@ -62,7 +62,7 @@ describe(`board lookup in room (api)`, () => { const room = roomEntityFactory.buildWithId(); - const roomMember = roomMemberEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); await em.persistAndFlush([ accountWithEditRole, @@ -75,7 +75,7 @@ describe(`board lookup in room (api)`, () => { roleRoomView, userGroup, room, - roomMember, + roomMembership, ]); const columnBoardNode = columnBoardEntityFactory.build({ diff --git a/apps/server/src/modules/board/controller/api-test/board-update-title-in-room.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-update-title-in-room.api.spec.ts index d5988437838..168f2ac77f6 100644 --- a/apps/server/src/modules/board/controller/api-test/board-update-title-in-room.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-update-title-in-room.api.spec.ts @@ -7,7 +7,7 @@ import { TestApiClient, cleanupCollections, groupEntityFactory, roleFactory, use import { Permission, RoleName } from '@shared/domain/interface'; import { accountFactory } from '@src/modules/account/testing'; import { GroupEntityTypes } from '@src/modules/group/entity'; -import { roomMemberEntityFactory } from '@src/modules/room-member/testing'; +import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing'; import { roomEntityFactory } from '@src/modules/room/testing'; import { BoardNodeEntity } from '../../repo'; import { columnBoardEntityFactory } from '../../testing'; @@ -50,11 +50,11 @@ describe(`board update title with room relation (api)`, () => { const noAccessAccount = accountFactory.withUser(noAccessUser).build(); const roleRoomEdit = roleFactory.buildWithId({ - name: RoleName.ROOM_EDITOR, + name: RoleName.ROOMEDITOR, permissions: [Permission.ROOM_EDIT], }); const roleRoomView = roleFactory.buildWithId({ - name: RoleName.ROOM_VIEWER, + name: RoleName.ROOMVIEWER, permissions: [Permission.ROOM_VIEW], }); @@ -68,7 +68,7 @@ describe(`board update title with room relation (api)`, () => { const room = roomEntityFactory.buildWithId(); - const roomMember = roomMemberEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); await em.persistAndFlush([ accountWithEditRole, @@ -81,7 +81,7 @@ describe(`board update title with room relation (api)`, () => { roleRoomView, userGroup, room, - roomMember, + roomMembership, ]); const originalTitle = 'old title'; diff --git a/apps/server/src/modules/board/controller/api-test/board-visibility-in-room.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-visibility-in-room.api.spec.ts index c9a6aec631a..8bd6a4b9eab 100644 --- a/apps/server/src/modules/board/controller/api-test/board-visibility-in-room.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-visibility-in-room.api.spec.ts @@ -6,7 +6,7 @@ import { TestApiClient, cleanupCollections, groupEntityFactory, roleFactory, use import { Permission, RoleName } from '@shared/domain/interface'; import { accountFactory } from '@src/modules/account/testing'; import { GroupEntityTypes } from '@src/modules/group/entity'; -import { roomMemberEntityFactory } from '@src/modules/room-member/testing'; +import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing'; import { roomEntityFactory } from '@src/modules/room/testing'; import { BoardExternalReferenceType } from '../../domain'; import { columnBoardEntityFactory } from '../../testing'; @@ -49,11 +49,11 @@ describe(`board update visibility with room relation (api)`, () => { const noAccessAccount = accountFactory.withUser(noAccessUser).build(); const roleRoomEdit = roleFactory.buildWithId({ - name: RoleName.ROOM_EDITOR, + name: RoleName.ROOMEDITOR, permissions: [Permission.ROOM_EDIT], }); const roleRoomView = roleFactory.buildWithId({ - name: RoleName.ROOM_VIEWER, + name: RoleName.ROOMVIEWER, permissions: [Permission.ROOM_VIEW], }); @@ -67,7 +67,7 @@ describe(`board update visibility with room relation (api)`, () => { const room = roomEntityFactory.buildWithId(); - const roomMember = roomMemberEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); await em.persistAndFlush([ accountWithEditRole, @@ -80,7 +80,7 @@ describe(`board update visibility with room relation (api)`, () => { roleRoomView, userGroup, room, - roomMember, + roomMembership, ]); const columnBoardNode = columnBoardEntityFactory.build({ diff --git a/apps/server/src/modules/board/service/column-board.service.spec.ts b/apps/server/src/modules/board/service/column-board.service.spec.ts index a514242ea97..ac2986ff473 100644 --- a/apps/server/src/modules/board/service/column-board.service.spec.ts +++ b/apps/server/src/modules/board/service/column-board.service.spec.ts @@ -1,14 +1,14 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityId } from '@shared/domain/types'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ColumnBoardService } from './column-board.service'; +import { StorageLocation } from '@src/modules/files-storage/interface'; +import { CopyElementType, CopyStatus, CopyStatusEnum } from '../../copy-helper'; +import { BoardExternalReference, BoardExternalReferenceType, ColumnBoard } from '../domain'; import { BoardNodeRepo } from '../repo'; +import { columnBoardFactory } from '../testing'; import { BoardNodeService } from './board-node.service'; +import { ColumnBoardService } from './column-board.service'; import { ColumnBoardCopyService, ColumnBoardLinkService } from './internal'; -import { ColumnBoard, BoardExternalReference, BoardExternalReferenceType } from '../domain'; - -import { columnBoardFactory } from '../testing'; -import { CopyElementType, CopyStatus, CopyStatusEnum } from '../../copy-helper'; describe('ColumnBoardService', () => { let module: TestingModule; @@ -107,10 +107,12 @@ describe('ColumnBoardService', () => { columnBoardCopyService.copyColumnBoard.mockResolvedValueOnce(copyStatus); const result = await service.copyColumnBoard({ originalColumnBoardId: '1', - destinationExternalReference: { + targetExternalReference: { type: BoardExternalReferenceType.Course, id: '1', }, + sourceStorageLocationReference: { id: '1', type: StorageLocation.SCHOOL }, + targetStorageLocationReference: { id: '1', type: StorageLocation.SCHOOL }, userId: '1', }); diff --git a/apps/server/src/modules/board/service/column-board.service.ts b/apps/server/src/modules/board/service/column-board.service.ts index 3985af9bd17..3f225cb5285 100644 --- a/apps/server/src/modules/board/service/column-board.service.ts +++ b/apps/server/src/modules/board/service/column-board.service.ts @@ -4,7 +4,7 @@ import { EntityId } from '@shared/domain/types'; import { BoardExternalReference, BoardExternalReferenceType, ColumnBoard, isColumnBoard } from '../domain'; import { BoardNodeRepo } from '../repo'; import { BoardNodeService } from './board-node.service'; -import { ColumnBoardCopyService, ColumnBoardLinkService } from './internal'; +import { ColumnBoardCopyService, ColumnBoardLinkService, CopyColumnBoardParams } from './internal'; @Injectable() export class ColumnBoardService { @@ -44,13 +44,8 @@ export class ColumnBoardService { await this.boardNodeRepo.delete(boardNodes); } - async copyColumnBoard(props: { - originalColumnBoardId: EntityId; - destinationExternalReference: BoardExternalReference; - userId: EntityId; - copyTitle?: string; - }): Promise { - const copyStatus = await this.columnBoardCopyService.copyColumnBoard(props); + async copyColumnBoard(params: CopyColumnBoardParams): Promise { + const copyStatus = await this.columnBoardCopyService.copyColumnBoard(params); return copyStatus; } diff --git a/apps/server/src/modules/board/service/internal/board-context.service.spec.ts b/apps/server/src/modules/board/service/internal/board-context.service.spec.ts index a1f6f2341a5..489c09f8a8f 100644 --- a/apps/server/src/modules/board/service/internal/board-context.service.spec.ts +++ b/apps/server/src/modules/board/service/internal/board-context.service.spec.ts @@ -1,30 +1,30 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; +import { GroupTypes } from '@modules/group'; +import { RoomMembershipService } from '@modules/room-membership'; +import { roomMembershipFactory } from '@modules/room-membership/testing'; +import { roomFactory } from '@modules/room/testing'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission, RoleName } from '@shared/domain/interface'; import { CourseRepo } from '@shared/repo/course'; import { courseFactory, groupFactory, roleFactory, setupEntities, userFactory } from '@shared/testing'; -import { GroupTypes } from '@src/modules/group'; -import { RoomMemberService } from '@src/modules/room-member'; -import { roomMemberFactory } from '@src/modules/room-member/testing'; -import { roomFactory } from '@src/modules/room/testing'; import { BoardExternalReferenceType, BoardRoles, UserWithBoardRoles } from '../../domain'; import { columnBoardFactory, columnFactory } from '../../testing'; import { BoardContextService } from './board-context.service'; -describe(`${BoardContextService.name}`, () => { +describe(BoardContextService.name, () => { let module: TestingModule; let service: BoardContextService; let courseRepo: DeepMocked; - let roomMemberService: DeepMocked; + let roomMembershipService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ providers: [ BoardContextService, { - provide: RoomMemberService, - useValue: createMock(), + provide: RoomMembershipService, + useValue: createMock(), }, { provide: CourseRepo, @@ -34,7 +34,7 @@ describe(`${BoardContextService.name}`, () => { }).compile(); service = module.get(BoardContextService); - roomMemberService = module.get(RoomMemberService); + roomMembershipService = module.get(RoomMembershipService); courseRepo = module.get(CourseRepo); await setupEntities(); @@ -218,10 +218,10 @@ describe(`${BoardContextService.name}`, () => { describe('when user with editor role is associated with the room', () => { const setup = () => { const user = userFactory.buildWithId(); - const role = roleFactory.build({ name: RoleName.ROOM_EDITOR, permissions: [Permission.ROOM_EDIT] }); + const role = roleFactory.build({ name: RoleName.ROOMEDITOR, permissions: [Permission.ROOM_EDIT] }); const group = groupFactory.build({ type: GroupTypes.ROOM, users: [{ userId: user.id, roleId: role.id }] }); const room = roomFactory.build(); - roomMemberFactory.build({ roomId: room.id, userGroupId: group.id }); + roomMembershipFactory.build({ roomId: room.id, userGroupId: group.id }); const columnBoard = columnBoardFactory.build({ context: { id: room.id, type: BoardExternalReferenceType.Room }, }); @@ -232,7 +232,7 @@ describe(`${BoardContextService.name}`, () => { it('should return their information + editor role', async () => { const { columnBoard, role, user } = setup(); - roomMemberService.getRoomMemberAuthorizable.mockResolvedValue({ + roomMembershipService.getRoomMembershipAuthorizable.mockResolvedValue({ id: 'foo', roomId: columnBoard.context.id, members: [{ userId: user.id, roles: [role] }], @@ -253,10 +253,10 @@ describe(`${BoardContextService.name}`, () => { describe('when user with view role is associated with the room', () => { const setup = () => { const user = userFactory.buildWithId(); - const role = roleFactory.build({ name: RoleName.ROOM_VIEWER, permissions: [Permission.ROOM_VIEW] }); + const role = roleFactory.build({ name: RoleName.ROOMVIEWER, permissions: [Permission.ROOM_VIEW] }); const group = groupFactory.build({ type: GroupTypes.ROOM, users: [{ userId: user.id, roleId: role.id }] }); const room = roomFactory.build(); - roomMemberFactory.build({ roomId: room.id, userGroupId: group.id }); + roomMembershipFactory.build({ roomId: room.id, userGroupId: group.id }); const columnBoard = columnBoardFactory.build({ context: { id: room.id, type: BoardExternalReferenceType.Room }, }); @@ -267,7 +267,7 @@ describe(`${BoardContextService.name}`, () => { it('should return their information + reader role', async () => { const { columnBoard, role, user } = setup(); - roomMemberService.getRoomMemberAuthorizable.mockResolvedValue({ + roomMembershipService.getRoomMembershipAuthorizable.mockResolvedValue({ id: 'foo', roomId: columnBoard.context.id, members: [{ userId: user.id, roles: [role] }], @@ -291,7 +291,7 @@ describe(`${BoardContextService.name}`, () => { const role = roleFactory.build(); const group = groupFactory.build({ type: GroupTypes.ROOM, users: [{ userId: user.id, roleId: role.id }] }); const room = roomFactory.build(); - roomMemberFactory.build({ roomId: room.id, userGroupId: group.id }); + roomMembershipFactory.build({ roomId: room.id, userGroupId: group.id }); const columnBoard = columnBoardFactory.build({ context: { id: room.id, type: BoardExternalReferenceType.Room }, }); @@ -302,7 +302,7 @@ describe(`${BoardContextService.name}`, () => { it('should return their information + no role', async () => { const { columnBoard, role, user } = setup(); - roomMemberService.getRoomMemberAuthorizable.mockResolvedValue({ + roomMembershipService.getRoomMembershipAuthorizable.mockResolvedValue({ id: 'foo', roomId: columnBoard.context.id, members: [{ userId: user.id, roles: [role] }], diff --git a/apps/server/src/modules/board/service/internal/board-context.service.ts b/apps/server/src/modules/board/service/internal/board-context.service.ts index 66c78a1ac1f..efb1e51ddf2 100644 --- a/apps/server/src/modules/board/service/internal/board-context.service.ts +++ b/apps/server/src/modules/board/service/internal/board-context.service.ts @@ -2,13 +2,12 @@ import { Injectable } from '@nestjs/common'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { CourseRepo } from '@shared/repo/course'; -import { RoomMemberService } from '@src/modules/room-member'; -import { UserWithRoomRoles } from '@src/modules/room-member/do/room-member-authorizable.do'; +import { RoomMembershipService, UserWithRoomRoles } from '@src/modules/room-membership'; import { AnyBoardNode, BoardExternalReferenceType, BoardRoles, UserWithBoardRoles } from '../../domain'; @Injectable() export class BoardContextService { - constructor(private readonly courseRepo: CourseRepo, private readonly roomMemberService: RoomMemberService) {} + constructor(private readonly courseRepo: CourseRepo, private readonly roomMembershipService: RoomMembershipService) {} async getUsersWithBoardRoles(rootNode: AnyBoardNode): Promise { if (!('context' in rootNode)) { @@ -31,9 +30,9 @@ export class BoardContextService { } private async getFromRoom(roomId: EntityId): Promise { - const roomMemberAuthorizable = await this.roomMemberService.getRoomMemberAuthorizable(roomId); - const usersWithRoles: UserWithBoardRoles[] = roomMemberAuthorizable.members.map((member) => { - const roles = this.getBoardRolesFromRoomMember(member); + const roomMembershipAuthorizable = await this.roomMembershipService.getRoomMembershipAuthorizable(roomId); + const usersWithRoles: UserWithBoardRoles[] = roomMembershipAuthorizable.members.map((member) => { + const roles = this.getBoardRolesFromRoomMembership(member); return { userId: member.userId, roles, @@ -85,7 +84,7 @@ export class BoardContextService { return usersWithRoles; } - private getBoardRolesFromRoomMember(member: UserWithRoomRoles): BoardRoles[] { + private getBoardRolesFromRoomMembership(member: UserWithRoomRoles): BoardRoles[] { const isReader = member.roles.flatMap((role) => role.permissions ?? []).includes(Permission.ROOM_VIEW); const isEditor = member.roles.flatMap((role) => role.permissions ?? []).includes(Permission.ROOM_EDIT); diff --git a/apps/server/src/modules/board/service/internal/board-node-copy-context.spec.ts b/apps/server/src/modules/board/service/internal/board-node-copy-context.spec.ts index 412c4e83ecb..a65ef01e651 100644 --- a/apps/server/src/modules/board/service/internal/board-node-copy-context.spec.ts +++ b/apps/server/src/modules/board/service/internal/board-node-copy-context.spec.ts @@ -3,16 +3,14 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { StorageLocation } from '@modules/files-storage/interface'; import { FileRecordParentType } from '@src/infra/rabbitmq'; import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; -import { BoardNodeCopyContext } from './board-node-copy-context'; +import { BoardNodeCopyContext, BoardNodeCopyContextProps } from './board-node-copy-context'; describe(BoardNodeCopyContext.name, () => { describe('copyFilesOfParent', () => { const setup = () => { - const contextProps = { - sourceStorageLocationId: new ObjectId().toHexString(), - targetStorageLocationId: new ObjectId().toHexString(), - sourceStorageLocation: StorageLocation.SCHOOL, - targetStorageLocation: StorageLocation.SCHOOL, + const contextProps: BoardNodeCopyContextProps = { + sourceStorageLocationReference: { id: new ObjectId().toHexString(), type: StorageLocation.SCHOOL }, + targetStorageLocationReference: { id: new ObjectId().toHexString(), type: StorageLocation.SCHOOL }, userId: new ObjectId().toHexString(), filesStorageClientAdapterService: createMock(), }; @@ -34,14 +32,14 @@ describe(BoardNodeCopyContext.name, () => { source: { parentId: sourceParentId, parentType: FileRecordParentType.BoardNode, - storageLocationId: contextProps.sourceStorageLocationId, - storageLocation: contextProps.sourceStorageLocation, + storageLocationId: contextProps.sourceStorageLocationReference.id, + storageLocation: contextProps.sourceStorageLocationReference.type, }, target: { parentId: targetParentId, parentType: FileRecordParentType.BoardNode, - storageLocationId: contextProps.targetStorageLocationId, - storageLocation: contextProps.targetStorageLocation, + storageLocationId: contextProps.targetStorageLocationReference.id, + storageLocation: contextProps.targetStorageLocationReference.type, }, userId: contextProps.userId, }); diff --git a/apps/server/src/modules/board/service/internal/board-node-copy-context.ts b/apps/server/src/modules/board/service/internal/board-node-copy-context.ts index e9cab871c79..f32fe232534 100644 --- a/apps/server/src/modules/board/service/internal/board-node-copy-context.ts +++ b/apps/server/src/modules/board/service/internal/board-node-copy-context.ts @@ -5,12 +5,15 @@ import { EntityId } from '@shared/domain/types'; import { FileRecordParentType } from '@src/infra/rabbitmq'; import { CopyContext } from './board-node-copy.service'; +export type StorageLocationReference = { + id: EntityId; + type: StorageLocation; +}; + export type BoardNodeCopyContextProps = { - sourceStorageLocationId: EntityId; - targetStorageLocationId: EntityId; + sourceStorageLocationReference: StorageLocationReference; + targetStorageLocationReference: StorageLocationReference; userId: EntityId; - sourceStorageLocation: StorageLocation; - targetStorageLocation: StorageLocation; filesStorageClientAdapterService: FilesStorageClientAdapterService; }; @@ -22,14 +25,14 @@ export class BoardNodeCopyContext implements CopyContext { source: { parentId: sourceParentId, parentType: FileRecordParentType.BoardNode, - storageLocationId: this.props.sourceStorageLocationId, - storageLocation: this.props.sourceStorageLocation, + storageLocationId: this.props.sourceStorageLocationReference.id, + storageLocation: this.props.sourceStorageLocationReference.type, }, target: { parentId: targetParentId, parentType: FileRecordParentType.BoardNode, - storageLocationId: this.props.targetStorageLocationId, - storageLocation: this.props.targetStorageLocation, + storageLocationId: this.props.targetStorageLocationReference.id, + storageLocation: this.props.targetStorageLocationReference.type, }, userId: this.props.userId, }); diff --git a/apps/server/src/modules/board/service/internal/board-node-copy-general.service.spec.ts b/apps/server/src/modules/board/service/internal/board-node-copy-general.service.spec.ts index 1c6c4bfafe9..71ea6f8ab56 100644 --- a/apps/server/src/modules/board/service/internal/board-node-copy-general.service.spec.ts +++ b/apps/server/src/modules/board/service/internal/board-node-copy-general.service.spec.ts @@ -66,10 +66,8 @@ describe(BoardNodeCopyService.name, () => { const setup = () => { const contextProps: BoardNodeCopyContextProps = { - sourceStorageLocationId: new ObjectId().toHexString(), - sourceStorageLocation: StorageLocation.SCHOOL, - targetStorageLocationId: new ObjectId().toHexString(), - targetStorageLocation: StorageLocation.SCHOOL, + sourceStorageLocationReference: { id: new ObjectId().toHexString(), type: StorageLocation.SCHOOL }, + targetStorageLocationReference: { id: new ObjectId().toHexString(), type: StorageLocation.SCHOOL }, userId: new ObjectId().toHexString(), filesStorageClientAdapterService: createMock(), }; diff --git a/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts b/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts index 5c05465ad7e..44de4316a5c 100644 --- a/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts +++ b/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts @@ -40,7 +40,7 @@ import { submissionContainerElementFactory, submissionItemFactory, } from '../../testing'; -import { BoardNodeCopyContext } from './board-node-copy-context'; +import { BoardNodeCopyContext, BoardNodeCopyContextProps } from './board-node-copy-context'; import { BoardNodeCopyService } from './board-node-copy.service'; describe(BoardNodeCopyService.name, () => { @@ -98,11 +98,9 @@ describe(BoardNodeCopyService.name, () => { }); const setupContext = () => { - const contextProps = { - sourceStorageLocationId: new ObjectId().toHexString(), - sourceStorageLocation: StorageLocation.SCHOOL, - targetStorageLocationId: new ObjectId().toHexString(), - targetStorageLocation: StorageLocation.SCHOOL, + const contextProps: BoardNodeCopyContextProps = { + sourceStorageLocationReference: { id: new ObjectId().toHexString(), type: StorageLocation.SCHOOL }, + targetStorageLocationReference: { id: new ObjectId().toHexString(), type: StorageLocation.SCHOOL }, userId: new ObjectId().toHexString(), filesStorageClientAdapterService: createMock(), }; diff --git a/apps/server/src/modules/board/service/internal/column-board-copy.service.spec.ts b/apps/server/src/modules/board/service/internal/column-board-copy.service.spec.ts index 0a6792bdc46..e78022f73ef 100644 --- a/apps/server/src/modules/board/service/internal/column-board-copy.service.spec.ts +++ b/apps/server/src/modules/board/service/internal/column-board-copy.service.spec.ts @@ -1,17 +1,16 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; -import { UserService } from '@modules/user/service/user.service'; +import { CopyElementType, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client/service'; +import { StorageLocation } from '@modules/files-storage/interface'; import { Test, TestingModule } from '@nestjs/testing'; -import { CourseRepo } from '@shared/repo/course/course.repo'; -import { courseFactory, setupEntities, userDoFactory } from '@shared/testing'; -import { CopyElementType, CopyStatus, CopyStatusEnum } from '@src/modules/copy-helper'; -import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client/service/files-storage-client.service'; -import { BoardExternalReferenceType } from '../../domain'; -import { columnBoardFactory } from '../../testing'; +import { courseFactory, setupEntities } from '@shared/testing'; +import { BoardExternalReferenceType } from '../../domain/types'; +import { columnBoardFactory } from '../../testing/column-board.factory'; import { BoardNodeService } from '../board-node.service'; -import { ColumnBoardCopyService } from './column-board-copy.service'; +import { ColumnBoardCopyService, CopyColumnBoardParams } from './column-board-copy.service'; import { ColumnBoardTitleService } from './column-board-title.service'; -// Important: Don't move the BoardNodeCopyService import up to prevent import cycle! +// Warning: do not move the BoardNodeCopyService import up. Otherwise it will lead to dependency cycle. import { BoardNodeCopyService } from './board-node-copy.service'; describe(ColumnBoardCopyService.name, () => { @@ -19,8 +18,6 @@ describe(ColumnBoardCopyService.name, () => { let service: ColumnBoardCopyService; let boardNodeService: DeepMocked; - let courseRepo: DeepMocked; - let userService: DeepMocked; let boardNodeCopyService: DeepMocked; let columnBoardTitleService: DeepMocked; @@ -36,14 +33,6 @@ describe(ColumnBoardCopyService.name, () => { provide: ColumnBoardTitleService, useValue: createMock(), }, - { - provide: CourseRepo, - useValue: createMock(), - }, - { - provide: UserService, - useValue: createMock(), - }, { provide: BoardNodeCopyService, useValue: createMock(), @@ -52,17 +41,11 @@ describe(ColumnBoardCopyService.name, () => { provide: FilesStorageClientAdapterService, useValue: createMock(), }, - { - provide: ColumnBoardTitleService, - useValue: createMock(), - }, ], }).compile(); service = module.get(ColumnBoardCopyService); boardNodeService = module.get(BoardNodeService); - courseRepo = module.get(CourseRepo); - userService = module.get(UserService); boardNodeCopyService = module.get(BoardNodeCopyService); columnBoardTitleService = module.get(ColumnBoardTitleService); @@ -80,10 +63,7 @@ describe(ColumnBoardCopyService.name, () => { describe('copyColumnBoard', () => { const setup = () => { const userId = new ObjectId().toHexString(); - const user = userDoFactory.build({ id: userId }); - userService.findById.mockResolvedValueOnce(user); const course = courseFactory.buildWithId(); - courseRepo.findById.mockResolvedValueOnce(course); const originalBoard = columnBoardFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); @@ -99,66 +79,40 @@ describe(ColumnBoardCopyService.name, () => { }; boardNodeCopyService.copy.mockResolvedValueOnce(status); - return { originalBoard, userId }; - }; - - it('should find the original board', async () => { - const { originalBoard, userId } = setup(); - - await service.copyColumnBoard({ + const copyParams: CopyColumnBoardParams = { originalColumnBoardId: originalBoard.id, - destinationExternalReference: originalBoard.context, + targetExternalReference: originalBoard.context, + sourceStorageLocationReference: { id: course.school.id, type: StorageLocation.SCHOOL }, + targetStorageLocationReference: { id: course.school.id, type: StorageLocation.SCHOOL }, userId, - }); - - expect(boardNodeService.findByClassAndId).toHaveBeenCalled(); - }); - - it('should find the user', async () => { - const { originalBoard, userId } = setup(); + copyTitle: 'Board Copy', + }; - await service.copyColumnBoard({ - originalColumnBoardId: originalBoard.id, - destinationExternalReference: originalBoard.context, - userId, - }); + return { originalBoard, userId, copyParams }; + }; - expect(userService.findById).toHaveBeenCalled(); - }); + it('should find the original board', async () => { + const { copyParams } = setup(); - it('should find the course', async () => { - const { originalBoard, userId } = setup(); + await service.copyColumnBoard(copyParams); - await service.copyColumnBoard({ - originalColumnBoardId: originalBoard.id, - destinationExternalReference: originalBoard.context, - userId, - }); - - expect(courseRepo.findById).toHaveBeenCalled(); + expect(boardNodeService.findByClassAndId).toHaveBeenCalled(); }); it('should call service to copy the board', async () => { - const { originalBoard, userId } = setup(); + const { copyParams } = setup(); - await service.copyColumnBoard({ - originalColumnBoardId: originalBoard.id, - destinationExternalReference: originalBoard.context, - userId, - copyTitle: 'Another Title', - }); + await service.copyColumnBoard(copyParams); expect(boardNodeCopyService.copy).toHaveBeenCalled(); }); it('should set the title of the copied board', async () => { - const { originalBoard, userId } = setup(); + const { copyParams } = setup(); const copyTitle = 'Another Title'; await service.copyColumnBoard({ - originalColumnBoardId: originalBoard.id, - destinationExternalReference: originalBoard.context, - userId, + ...copyParams, copyTitle, }); @@ -166,56 +120,43 @@ describe(ColumnBoardCopyService.name, () => { }); it('should derive the title of the copied board', async () => { - const { originalBoard, userId } = setup(); + const { copyParams } = setup(); const derivedTitle = 'Derived Title'; columnBoardTitleService.deriveColumnBoardTitle.mockResolvedValueOnce(derivedTitle); await service.copyColumnBoard({ - originalColumnBoardId: originalBoard.id, - destinationExternalReference: originalBoard.context, - userId, + ...copyParams, + copyTitle: undefined, }); expect(boardNodeService.addRoot).toHaveBeenCalledWith(expect.objectContaining({ title: derivedTitle })); }); it('should set the context of the copied board', async () => { - const { originalBoard, userId } = setup(); - const destinationExternalReference = { + const { copyParams } = setup(); + const targetExternalReference = { id: new ObjectId().toHexString(), type: BoardExternalReferenceType.Course, }; - await service.copyColumnBoard({ - originalColumnBoardId: originalBoard.id, - destinationExternalReference, - userId, - }); + await service.copyColumnBoard({ ...copyParams, targetExternalReference }); expect(boardNodeService.addRoot).toHaveBeenCalledWith( - expect.objectContaining({ context: destinationExternalReference }) + expect.objectContaining({ context: targetExternalReference }) ); }); it('should return the copy status', async () => { - const { originalBoard, userId } = setup(); - const copyStatus = await service.copyColumnBoard({ - originalColumnBoardId: originalBoard.id, - destinationExternalReference: originalBoard.context, - userId, - }); + const { copyParams } = setup(); + const copyStatus = await service.copyColumnBoard(copyParams); expect(copyStatus).toBeDefined(); expect(copyStatus.copyEntity).toBeDefined(); }); it('should not affect the original board', async () => { - const { originalBoard, userId } = setup(); - const copyStatus = await service.copyColumnBoard({ - originalColumnBoardId: originalBoard.id, - destinationExternalReference: originalBoard.context, - userId, - }); + const { copyParams, originalBoard } = setup(); + const copyStatus = await service.copyColumnBoard(copyParams); expect(copyStatus.originalEntity).toBe(originalBoard); }); @@ -224,20 +165,26 @@ describe(ColumnBoardCopyService.name, () => { describe('when the copy response is not a ColumnBoard', () => { const setup = () => { const userId = new ObjectId().toHexString(); - const user = userDoFactory.build({ id: userId }); - userService.findById.mockResolvedValueOnce(user); const course = courseFactory.buildWithId(); - courseRepo.findById.mockResolvedValueOnce(course); const originalBoard = columnBoardFactory.build({ context: { id: course.id, type: BoardExternalReferenceType.Course }, }); boardNodeService.findByClassAndId.mockResolvedValueOnce(originalBoard); - return { originalBoard, userId }; + const copyParams: CopyColumnBoardParams = { + originalColumnBoardId: originalBoard.id, + targetExternalReference: originalBoard.context, + sourceStorageLocationReference: { id: course.school.id, type: StorageLocation.SCHOOL }, + targetStorageLocationReference: { id: course.school.id, type: StorageLocation.SCHOOL }, + userId, + copyTitle: 'Board Copy', + }; + + return { originalBoard, userId, copyParams }; }; it('should throw an error if the board is not a column board', async () => { - const { originalBoard, userId } = setup(); + const { originalBoard, copyParams } = setup(); const boardCopy = { ...originalBoard, id: new ObjectId().toHexString(), type: 'not-a-column-board' }; const status: CopyStatus = { @@ -247,13 +194,9 @@ describe(ColumnBoardCopyService.name, () => { }; boardNodeCopyService.copy.mockResolvedValueOnce(status); - await expect( - service.copyColumnBoard({ - originalColumnBoardId: originalBoard.id, - destinationExternalReference: originalBoard.context, - userId, - }) - ).rejects.toThrowError('expected copy of columnboard to be a columnboard'); + await expect(service.copyColumnBoard(copyParams)).rejects.toThrowError( + 'expected copy of columnboard to be a columnboard' + ); }); }); }); diff --git a/apps/server/src/modules/board/service/internal/column-board-copy.service.ts b/apps/server/src/modules/board/service/internal/column-board-copy.service.ts index a33c6a7b1e6..531ec8c3510 100644 --- a/apps/server/src/modules/board/service/internal/column-board-copy.service.ts +++ b/apps/server/src/modules/board/service/internal/column-board-copy.service.ts @@ -1,48 +1,40 @@ import { CopyStatus } from '@modules/copy-helper'; -import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; -import { StorageLocation } from '@modules/files-storage/interface'; -import { UserService } from '@modules/user'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client/service'; import { Injectable, InternalServerErrorException, NotImplementedException } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; -import { CourseRepo } from '@shared/repo/course'; import { BoardExternalReference, BoardExternalReferenceType, ColumnBoard, isColumnBoard } from '../../domain'; import { BoardNodeService } from '../board-node.service'; -import { BoardNodeCopyContext } from './board-node-copy-context'; +import { BoardNodeCopyContext, StorageLocationReference } from './board-node-copy-context'; import { BoardNodeCopyService } from './board-node-copy.service'; import { ColumnBoardTitleService } from './column-board-title.service'; +export type CopyColumnBoardParams = { + originalColumnBoardId: EntityId; + targetExternalReference: BoardExternalReference; + sourceStorageLocationReference: StorageLocationReference; + targetStorageLocationReference: StorageLocationReference; + userId: EntityId; + copyTitle?: string; +}; + @Injectable() export class ColumnBoardCopyService { constructor( private readonly boardNodeService: BoardNodeService, private readonly columnBoardTitleService: ColumnBoardTitleService, - private readonly courseRepo: CourseRepo, - private readonly userService: UserService, private readonly boardNodeCopyService: BoardNodeCopyService, private readonly filesStorageClientAdapterService: FilesStorageClientAdapterService ) {} - async copyColumnBoard(props: { - originalColumnBoardId: EntityId; - destinationExternalReference: BoardExternalReference; - userId: EntityId; - copyTitle?: string; - }): Promise { - const originalBoard = await this.boardNodeService.findByClassAndId(ColumnBoard, props.originalColumnBoardId); + async copyColumnBoard(params: CopyColumnBoardParams): Promise { + const originalBoard = await this.boardNodeService.findByClassAndId(ColumnBoard, params.originalColumnBoardId); - const user = await this.userService.findById(props.userId); - /* istanbul ignore next */ - if (originalBoard.context.type !== BoardExternalReferenceType.Course) { - throw new NotImplementedException('only courses are supported as board parents'); - } - const course = await this.courseRepo.findById(originalBoard.context.id); // TODO: get rid of this + this.checkSupportedExternalReferenceType(params.targetExternalReference.type); const copyContext = new BoardNodeCopyContext({ - sourceStorageLocationId: course.school.id, - targetStorageLocationId: user.schoolId, - sourceStorageLocation: StorageLocation.SCHOOL, - targetStorageLocation: StorageLocation.SCHOOL, - userId: props.userId, + sourceStorageLocationReference: params.sourceStorageLocationReference, + targetStorageLocationReference: params.targetStorageLocationReference, + userId: params.userId, filesStorageClientAdapterService: this.filesStorageClientAdapterService, }); @@ -53,18 +45,26 @@ export class ColumnBoardCopyService { throw new InternalServerErrorException('expected copy of columnboard to be a columnboard'); } - if (props.copyTitle) { - copyStatus.copyEntity.title = props.copyTitle; + if (params.copyTitle) { + copyStatus.copyEntity.title = params.copyTitle; } else { copyStatus.copyEntity.title = await this.columnBoardTitleService.deriveColumnBoardTitle( originalBoard.title, - props.destinationExternalReference + params.targetExternalReference ); } - copyStatus.copyEntity.context = props.destinationExternalReference; + copyStatus.copyEntity.context = params.targetExternalReference; + copyStatus.copyEntity.isVisible = false; await this.boardNodeService.addRoot(copyStatus.copyEntity); copyStatus.originalEntity = originalBoard; return copyStatus; } + + private checkSupportedExternalReferenceType(type: BoardExternalReferenceType) { + /* istanbul ignore next */ + if (type !== BoardExternalReferenceType.Course && type !== BoardExternalReferenceType.Room) { + throw new NotImplementedException('Only room and course external reference types are supported'); + } + } } diff --git a/apps/server/src/modules/board/service/internal/column-board-title.service.ts b/apps/server/src/modules/board/service/internal/column-board-title.service.ts index e43cbda1afe..07f6c1b6faa 100644 --- a/apps/server/src/modules/board/service/internal/column-board-title.service.ts +++ b/apps/server/src/modules/board/service/internal/column-board-title.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { CopyHelperService } from '@modules/copy-helper'; +import { CopyHelperService } from '@modules/copy-helper/service/copy-helper.service'; import { BoardExternalReference } from '../../domain'; import { ColumnBoardReferenceService } from './column-board-reference.service'; diff --git a/apps/server/src/modules/board/uc/board.uc.spec.ts b/apps/server/src/modules/board/uc/board.uc.spec.ts index 8b760cfa862..ff22353895b 100644 --- a/apps/server/src/modules/board/uc/board.uc.spec.ts +++ b/apps/server/src/modules/board/uc/board.uc.spec.ts @@ -7,7 +7,8 @@ import { CourseRepo } from '@shared/repo/course'; import { setupEntities, userFactory } from '@shared/testing'; import { courseFactory } from '@shared/testing/factory'; import { LegacyLogger } from '@src/core/logger'; -import { RoomMemberService } from '@src/modules/room-member'; +import { RoomService } from '@src/modules/room'; +import { RoomMembershipService } from '@src/modules/room-membership'; import { CopyElementType, CopyStatus, CopyStatusEnum } from '../../copy-helper'; import { BoardExternalReferenceType, BoardLayout, BoardNodeFactory, Column, ColumnBoard } from '../domain'; import { BoardNodePermissionService, BoardNodeService, ColumnBoardService } from '../service'; @@ -48,13 +49,17 @@ describe(BoardUc.name, () => { provide: CourseRepo, useValue: createMock(), }, + { + provide: RoomService, + useValue: createMock(), + }, { provide: BoardNodeFactory, useValue: createMock(), }, { - provide: RoomMemberService, - useValue: createMock(), + provide: RoomMembershipService, + useValue: createMock(), }, { provide: LegacyLogger, @@ -427,8 +432,15 @@ describe(BoardUc.name, () => { }); describe('copyBoard', () => { + const setup = () => { + const { user, board, boardId } = globalSetup(); + boardNodeService.findByClassAndId.mockResolvedValueOnce(board); + + return { user, board, boardId }; + }; + it('should call the service to find the user', async () => { - const { user, boardId } = globalSetup(); + const { user, boardId } = setup(); await uc.copyBoard(user.id, boardId); @@ -436,7 +448,7 @@ describe(BoardUc.name, () => { }); it('should call the service to find the board', async () => { - const { user, boardId } = globalSetup(); + const { user, boardId } = setup(); await uc.copyBoard(user.id, boardId); @@ -444,7 +456,7 @@ describe(BoardUc.name, () => { }); it('[deprecated] should call course repo to find the course', async () => { - const { user, boardId } = globalSetup(); + const { user, boardId } = setup(); await uc.copyBoard(user.id, boardId); @@ -452,8 +464,7 @@ describe(BoardUc.name, () => { }); it('should call Board Permission Service to check permission', async () => { - const { user, board } = globalSetup(); - boardNodeService.findByClassAndId.mockResolvedValueOnce(board); + const { user, board } = setup(); await uc.copyBoard(user.id, board.id); @@ -461,7 +472,7 @@ describe(BoardUc.name, () => { }); it('should call authorization to check course permissions', async () => { - const { user, boardId } = globalSetup(); + const { user, boardId } = setup(); const course = courseFactory.build(); // TODO should not use course repo @@ -471,12 +482,12 @@ describe(BoardUc.name, () => { expect(authorizationService.checkPermission).toHaveBeenCalledWith(user, course, { action: Action.write, - requiredPermissions: [], + requiredPermissions: ['COURSE_EDIT'], }); }); it('should call the service to copy the board', async () => { - const { user, boardId } = globalSetup(); + const { user, boardId } = setup(); await uc.copyBoard(user.id, boardId); @@ -486,7 +497,7 @@ describe(BoardUc.name, () => { }); it('should return the copy status', async () => { - const { user, boardId } = globalSetup(); + const { user, boardId } = setup(); const copyStatus: CopyStatus = { type: CopyElementType.BOARD, diff --git a/apps/server/src/modules/board/uc/board.uc.ts b/apps/server/src/modules/board/uc/board.uc.ts index 8beb95d5260..3dae0f22bb2 100644 --- a/apps/server/src/modules/board/uc/board.uc.ts +++ b/apps/server/src/modules/board/uc/board.uc.ts @@ -5,10 +5,13 @@ import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { CourseRepo } from '@shared/repo/course'; import { LegacyLogger } from '@src/core/logger'; -import { RoomMemberService } from '@src/modules/room-member'; +import { StorageLocation } from '@src/modules/files-storage/interface'; +import { RoomService } from '@src/modules/room'; +import { RoomMembershipService } from '@src/modules/room-membership'; import { CreateBoardBodyParams } from '../controller/dto'; import { BoardExternalReference, BoardExternalReferenceType, BoardNodeFactory, Column, ColumnBoard } from '../domain'; import { BoardNodePermissionService, BoardNodeService, ColumnBoardService } from '../service'; +import { StorageLocationReference } from '../service/internal'; @Injectable() export class BoardUc { @@ -16,11 +19,12 @@ export class BoardUc { @Inject(forwardRef(() => AuthorizationService)) // TODO is this needed? private readonly authorizationService: AuthorizationService, private readonly boardPermissionService: BoardNodePermissionService, - private readonly roomMemberService: RoomMemberService, + private readonly roomMembershipService: RoomMembershipService, private readonly boardNodeService: BoardNodeService, private readonly columnBoardService: ColumnBoardService, private readonly logger: LegacyLogger, private readonly courseRepo: CourseRepo, + private readonly roomService: RoomService, private readonly boardNodeFactory: BoardNodeFactory ) { this.logger.setContext(BoardUc.name); @@ -29,7 +33,7 @@ export class BoardUc { async createBoard(userId: EntityId, params: CreateBoardBodyParams): Promise { this.logger.debug({ action: 'createBoard', userId, title: params.title }); - await this.checkParentWritePermission(userId, { type: params.parentType, id: params.parentId }); + await this.checkReferenceWritePermission(userId, { type: params.parentType, id: params.parentId }); const board = this.boardNodeFactory.buildColumnBoard({ context: { type: params.parentType, id: params.parentId }, @@ -115,22 +119,19 @@ export class BoardUc { async copyBoard(userId: EntityId, boardId: EntityId): Promise { this.logger.debug({ action: 'copyBoard', userId, boardId }); - const user = await this.authorizationService.getUserWithPermissions(userId); const board = await this.boardNodeService.findByClassAndId(ColumnBoard, boardId); - // TODO - should not use course repo - const course = await this.courseRepo.findById(board.context.id); - await this.boardPermissionService.checkPermission(userId, board, Action.read); - this.authorizationService.checkPermission(user, course, { - action: Action.write, - requiredPermissions: [], // TODO - what permissions are required? COURSE_EDIT? - }); + await this.checkReferenceWritePermission(userId, board.context); + + const storageLocationReference = await this.getStorageLocationReference(board.context); const copyStatus = await this.columnBoardService.copyColumnBoard({ - userId, originalColumnBoardId: boardId, - destinationExternalReference: board.context, + targetExternalReference: board.context, + sourceStorageLocationReference: storageLocationReference, + targetStorageLocationReference: storageLocationReference, + userId, }); return copyStatus; @@ -144,7 +145,9 @@ export class BoardUc { return board; } - private async checkParentWritePermission(userId: EntityId, context: BoardExternalReference) { + // ---- Move to shared service? (see apps/server/src/modules/sharing/uc/share-token.uc.ts) + + private async checkReferenceWritePermission(userId: EntityId, context: BoardExternalReference) { const user = await this.authorizationService.getUserWithPermissions(userId); if (context.type === BoardExternalReferenceType.Course) { @@ -155,9 +158,9 @@ export class BoardUc { requiredPermissions: [Permission.COURSE_EDIT], }); } else if (context.type === BoardExternalReferenceType.Room) { - const roomMemberAuthorizable = await this.roomMemberService.getRoomMemberAuthorizable(context.id); + const roomMembershipAuthorizable = await this.roomMembershipService.getRoomMembershipAuthorizable(context.id); - this.authorizationService.checkPermission(user, roomMemberAuthorizable, { + this.authorizationService.checkPermission(user, roomMembershipAuthorizable, { action: Action.write, requiredPermissions: [], }); @@ -165,4 +168,20 @@ export class BoardUc { throw new Error(`Unsupported context type ${context.type as string}`); } } + + private async getStorageLocationReference(context: BoardExternalReference): Promise { + if (context.type === BoardExternalReferenceType.Course) { + const course = await this.courseRepo.findById(context.id); + + return { id: course.school.id, type: StorageLocation.SCHOOL }; + } + + if (context.type === BoardExternalReferenceType.Room) { + const room = await this.roomService.getSingleRoom(context.id); + + return { id: room.schoolId, type: StorageLocation.SCHOOL }; + } + /* istanbul ignore next */ + throw new Error(`Unsupported board reference type ${context.type as string}`); + } } diff --git a/apps/server/src/modules/board/uc/element.uc.spec.ts b/apps/server/src/modules/board/uc/element.uc.spec.ts index 2cf247b80ca..3309571196c 100644 --- a/apps/server/src/modules/board/uc/element.uc.spec.ts +++ b/apps/server/src/modules/board/uc/element.uc.spec.ts @@ -3,7 +3,8 @@ import { Action } from '@modules/authorization'; import { HttpService } from '@nestjs/axios'; import { Test, TestingModule } from '@nestjs/testing'; import { InputFormat } from '@shared/domain/types'; -import { setupEntities, userFactory } from '@shared/testing'; +import { setupEntities } from '@shared/testing/setup-entities'; +import { userFactory } from '@shared/testing/factory'; import { Logger } from '@src/core/logger'; import { RichTextContentBody } from '../controller/dto'; import { BoardNodeFactory } from '../domain'; diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/board-client/board-api-client/models/copy-api-response.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/board-client/board-api-client/models/copy-api-response.ts index 4921f0d6484..3c78d846edc 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/board-client/board-api-client/models/copy-api-response.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/board-client/board-api-client/models/copy-api-response.ts @@ -5,110 +5,106 @@ * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. * * The version of the OpenAPI document: 3.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ - - /** - * + * * @export * @interface CopyApiResponse */ export interface CopyApiResponse { - /** - * Id of copied element - * @type {string} - * @memberof CopyApiResponse - */ - 'id'?: string; - /** - * Title of copied element - * @type {string} - * @memberof CopyApiResponse - */ - 'title'?: string; - /** - * Type of copied element - * @type {string} - * @memberof CopyApiResponse - */ - 'type': CopyApiResponseType; - /** - * Id of destination course - * @type {string} - * @memberof CopyApiResponse - */ - 'destinationCourseId'?: string; - /** - * Copy progress status of copied element - * @type {string} - * @memberof CopyApiResponse - */ - 'status': CopyApiResponseStatus; - /** - * List of included sub elements with recursive type structure - * @type {Array} - * @memberof CopyApiResponse - */ - 'elements'?: Array; + /** + * Id of copied element + * @type {string} + * @memberof CopyApiResponse + */ + id?: string; + /** + * Title of copied element + * @type {string} + * @memberof CopyApiResponse + */ + title?: string; + /** + * Type of copied element + * @type {string} + * @memberof CopyApiResponse + */ + type: CopyApiResponseType; + /** + * Id of destination parent reference + * @type {string} + * @memberof CopyApiResponse + */ + destinationId?: string; + /** + * Copy progress status of copied element + * @type {string} + * @memberof CopyApiResponse + */ + status: CopyApiResponseStatus; + /** + * List of included sub elements with recursive type structure + * @type {Array} + * @memberof CopyApiResponse + */ + elements?: Array; } export const CopyApiResponseType = { - BOARD: 'BOARD', - CARD: 'CARD', - COLLABORATIVE_TEXT_EDITOR_ELEMENT: 'COLLABORATIVE_TEXT_EDITOR_ELEMENT', - COLUMN: 'COLUMN', - COLUMNBOARD: 'COLUMNBOARD', - CONTENT: 'CONTENT', - COURSE: 'COURSE', - COURSEGROUP_GROUP: 'COURSEGROUP_GROUP', - EXTERNAL_TOOL: 'EXTERNAL_TOOL', - EXTERNAL_TOOL_ELEMENT: 'EXTERNAL_TOOL_ELEMENT', - FILE: 'FILE', - FILE_ELEMENT: 'FILE_ELEMENT', - DRAWING_ELEMENT: 'DRAWING_ELEMENT', - FILE_GROUP: 'FILE_GROUP', - LEAF: 'LEAF', - LESSON: 'LESSON', - LESSON_CONTENT_ETHERPAD: 'LESSON_CONTENT_ETHERPAD', - LESSON_CONTENT_GEOGEBRA: 'LESSON_CONTENT_GEOGEBRA', - LESSON_CONTENT_GROUP: 'LESSON_CONTENT_GROUP', - LESSON_CONTENT_LERNSTORE: 'LESSON_CONTENT_LERNSTORE', - LESSON_CONTENT_NEXBOARD: 'LESSON_CONTENT_NEXBOARD', - LESSON_CONTENT_TASK: 'LESSON_CONTENT_TASK', - LESSON_CONTENT_TEXT: 'LESSON_CONTENT_TEXT', - LERNSTORE_MATERIAL: 'LERNSTORE_MATERIAL', - LERNSTORE_MATERIAL_GROUP: 'LERNSTORE_MATERIAL_GROUP', - LINK_ELEMENT: 'LINK_ELEMENT', - LTITOOL_GROUP: 'LTITOOL_GROUP', - MEDIA_BOARD: 'MEDIA_BOARD', - MEDIA_LINE: 'MEDIA_LINE', - MEDIA_EXTERNAL_TOOL_ELEMENT: 'MEDIA_EXTERNAL_TOOL_ELEMENT', - METADATA: 'METADATA', - RICHTEXT_ELEMENT: 'RICHTEXT_ELEMENT', - SUBMISSION_CONTAINER_ELEMENT: 'SUBMISSION_CONTAINER_ELEMENT', - SUBMISSION_ITEM: 'SUBMISSION_ITEM', - SUBMISSION_GROUP: 'SUBMISSION_GROUP', - TASK: 'TASK', - TASK_GROUP: 'TASK_GROUP', - TIME_GROUP: 'TIME_GROUP', - USER_GROUP: 'USER_GROUP' + BOARD: 'BOARD', + CARD: 'CARD', + COLLABORATIVE_TEXT_EDITOR_ELEMENT: 'COLLABORATIVE_TEXT_EDITOR_ELEMENT', + COLUMN: 'COLUMN', + COLUMNBOARD: 'COLUMNBOARD', + CONTENT: 'CONTENT', + COURSE: 'COURSE', + COURSEGROUP_GROUP: 'COURSEGROUP_GROUP', + EXTERNAL_TOOL: 'EXTERNAL_TOOL', + EXTERNAL_TOOL_ELEMENT: 'EXTERNAL_TOOL_ELEMENT', + FILE: 'FILE', + FILE_ELEMENT: 'FILE_ELEMENT', + DRAWING_ELEMENT: 'DRAWING_ELEMENT', + FILE_GROUP: 'FILE_GROUP', + LEAF: 'LEAF', + LESSON: 'LESSON', + LESSON_CONTENT_ETHERPAD: 'LESSON_CONTENT_ETHERPAD', + LESSON_CONTENT_GEOGEBRA: 'LESSON_CONTENT_GEOGEBRA', + LESSON_CONTENT_GROUP: 'LESSON_CONTENT_GROUP', + LESSON_CONTENT_LERNSTORE: 'LESSON_CONTENT_LERNSTORE', + LESSON_CONTENT_NEXBOARD: 'LESSON_CONTENT_NEXBOARD', + LESSON_CONTENT_TASK: 'LESSON_CONTENT_TASK', + LESSON_CONTENT_TEXT: 'LESSON_CONTENT_TEXT', + LERNSTORE_MATERIAL: 'LERNSTORE_MATERIAL', + LERNSTORE_MATERIAL_GROUP: 'LERNSTORE_MATERIAL_GROUP', + LINK_ELEMENT: 'LINK_ELEMENT', + LTITOOL_GROUP: 'LTITOOL_GROUP', + MEDIA_BOARD: 'MEDIA_BOARD', + MEDIA_LINE: 'MEDIA_LINE', + MEDIA_EXTERNAL_TOOL_ELEMENT: 'MEDIA_EXTERNAL_TOOL_ELEMENT', + METADATA: 'METADATA', + RICHTEXT_ELEMENT: 'RICHTEXT_ELEMENT', + SUBMISSION_CONTAINER_ELEMENT: 'SUBMISSION_CONTAINER_ELEMENT', + SUBMISSION_ITEM: 'SUBMISSION_ITEM', + SUBMISSION_GROUP: 'SUBMISSION_GROUP', + TASK: 'TASK', + TASK_GROUP: 'TASK_GROUP', + TIME_GROUP: 'TIME_GROUP', + USER_GROUP: 'USER_GROUP', } as const; export type CopyApiResponseType = typeof CopyApiResponseType[keyof typeof CopyApiResponseType]; export const CopyApiResponseStatus = { - SUCCESS: 'success', - FAILURE: 'failure', - NOT_DOING: 'not-doing', - NOT_IMPLEMENTED: 'not-implemented', - PARTIAL: 'partial' + SUCCESS: 'success', + FAILURE: 'failure', + NOT_DOING: 'not-doing', + NOT_IMPLEMENTED: 'not-implemented', + PARTIAL: 'partial', } as const; export type CopyApiResponseStatus = typeof CopyApiResponseStatus[keyof typeof CopyApiResponseStatus]; - - diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/copy-api-response.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/copy-api-response.ts index dd356d1e871..a4b72536f13 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/copy-api-response.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/copy-api-response.ts @@ -5,111 +5,107 @@ * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. * * The version of the OpenAPI document: 3.0 - * + * * * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). * https://openapi-generator.tech * Do not edit the class manually. */ - - /** - * + * * @export * @interface CopyApiResponse */ export interface CopyApiResponse { - /** - * Id of copied element - * @type {string} - * @memberof CopyApiResponse - */ - 'id'?: string; - /** - * Title of copied element - * @type {string} - * @memberof CopyApiResponse - */ - 'title'?: string; - /** - * Type of copied element - * @type {string} - * @memberof CopyApiResponse - */ - 'type': CopyApiResponseType; - /** - * Id of destination course - * @type {string} - * @memberof CopyApiResponse - */ - 'destinationCourseId'?: string; - /** - * Copy progress status of copied element - * @type {string} - * @memberof CopyApiResponse - */ - 'status': CopyApiResponseStatus; - /** - * List of included sub elements with recursive type structure - * @type {Array} - * @memberof CopyApiResponse - */ - 'elements'?: Array; + /** + * Id of copied element + * @type {string} + * @memberof CopyApiResponse + */ + id?: string; + /** + * Title of copied element + * @type {string} + * @memberof CopyApiResponse + */ + title?: string; + /** + * Type of copied element + * @type {string} + * @memberof CopyApiResponse + */ + type: CopyApiResponseType; + /** + * Id of destination parent reference + * @type {string} + * @memberof CopyApiResponse + */ + destinationId?: string; + /** + * Copy progress status of copied element + * @type {string} + * @memberof CopyApiResponse + */ + status: CopyApiResponseStatus; + /** + * List of included sub elements with recursive type structure + * @type {Array} + * @memberof CopyApiResponse + */ + elements?: Array; } export const CopyApiResponseType = { - BOARD: 'BOARD', - CARD: 'CARD', - COLLABORATIVE_TEXT_EDITOR_ELEMENT: 'COLLABORATIVE_TEXT_EDITOR_ELEMENT', - COLUMN: 'COLUMN', - COLUMNBOARD: 'COLUMNBOARD', - CONTENT: 'CONTENT', - COURSE: 'COURSE', - COURSEGROUP_GROUP: 'COURSEGROUP_GROUP', - DELETED_ELEMENT: 'DELETED_ELEMENT', - EXTERNAL_TOOL: 'EXTERNAL_TOOL', - EXTERNAL_TOOL_ELEMENT: 'EXTERNAL_TOOL_ELEMENT', - FILE: 'FILE', - FILE_ELEMENT: 'FILE_ELEMENT', - DRAWING_ELEMENT: 'DRAWING_ELEMENT', - FILE_GROUP: 'FILE_GROUP', - LEAF: 'LEAF', - LESSON: 'LESSON', - LESSON_CONTENT_ETHERPAD: 'LESSON_CONTENT_ETHERPAD', - LESSON_CONTENT_GEOGEBRA: 'LESSON_CONTENT_GEOGEBRA', - LESSON_CONTENT_GROUP: 'LESSON_CONTENT_GROUP', - LESSON_CONTENT_LERNSTORE: 'LESSON_CONTENT_LERNSTORE', - LESSON_CONTENT_NEXBOARD: 'LESSON_CONTENT_NEXBOARD', - LESSON_CONTENT_TASK: 'LESSON_CONTENT_TASK', - LESSON_CONTENT_TEXT: 'LESSON_CONTENT_TEXT', - LERNSTORE_MATERIAL: 'LERNSTORE_MATERIAL', - LERNSTORE_MATERIAL_GROUP: 'LERNSTORE_MATERIAL_GROUP', - LINK_ELEMENT: 'LINK_ELEMENT', - LTITOOL_GROUP: 'LTITOOL_GROUP', - MEDIA_BOARD: 'MEDIA_BOARD', - MEDIA_LINE: 'MEDIA_LINE', - MEDIA_EXTERNAL_TOOL_ELEMENT: 'MEDIA_EXTERNAL_TOOL_ELEMENT', - METADATA: 'METADATA', - RICHTEXT_ELEMENT: 'RICHTEXT_ELEMENT', - SUBMISSION_CONTAINER_ELEMENT: 'SUBMISSION_CONTAINER_ELEMENT', - SUBMISSION_ITEM: 'SUBMISSION_ITEM', - SUBMISSION_GROUP: 'SUBMISSION_GROUP', - TASK: 'TASK', - TASK_GROUP: 'TASK_GROUP', - TIME_GROUP: 'TIME_GROUP', - USER_GROUP: 'USER_GROUP' + BOARD: 'BOARD', + CARD: 'CARD', + COLLABORATIVE_TEXT_EDITOR_ELEMENT: 'COLLABORATIVE_TEXT_EDITOR_ELEMENT', + COLUMN: 'COLUMN', + COLUMNBOARD: 'COLUMNBOARD', + CONTENT: 'CONTENT', + COURSE: 'COURSE', + COURSEGROUP_GROUP: 'COURSEGROUP_GROUP', + DELETED_ELEMENT: 'DELETED_ELEMENT', + EXTERNAL_TOOL: 'EXTERNAL_TOOL', + EXTERNAL_TOOL_ELEMENT: 'EXTERNAL_TOOL_ELEMENT', + FILE: 'FILE', + FILE_ELEMENT: 'FILE_ELEMENT', + DRAWING_ELEMENT: 'DRAWING_ELEMENT', + FILE_GROUP: 'FILE_GROUP', + LEAF: 'LEAF', + LESSON: 'LESSON', + LESSON_CONTENT_ETHERPAD: 'LESSON_CONTENT_ETHERPAD', + LESSON_CONTENT_GEOGEBRA: 'LESSON_CONTENT_GEOGEBRA', + LESSON_CONTENT_GROUP: 'LESSON_CONTENT_GROUP', + LESSON_CONTENT_LERNSTORE: 'LESSON_CONTENT_LERNSTORE', + LESSON_CONTENT_NEXBOARD: 'LESSON_CONTENT_NEXBOARD', + LESSON_CONTENT_TASK: 'LESSON_CONTENT_TASK', + LESSON_CONTENT_TEXT: 'LESSON_CONTENT_TEXT', + LERNSTORE_MATERIAL: 'LERNSTORE_MATERIAL', + LERNSTORE_MATERIAL_GROUP: 'LERNSTORE_MATERIAL_GROUP', + LINK_ELEMENT: 'LINK_ELEMENT', + LTITOOL_GROUP: 'LTITOOL_GROUP', + MEDIA_BOARD: 'MEDIA_BOARD', + MEDIA_LINE: 'MEDIA_LINE', + MEDIA_EXTERNAL_TOOL_ELEMENT: 'MEDIA_EXTERNAL_TOOL_ELEMENT', + METADATA: 'METADATA', + RICHTEXT_ELEMENT: 'RICHTEXT_ELEMENT', + SUBMISSION_CONTAINER_ELEMENT: 'SUBMISSION_CONTAINER_ELEMENT', + SUBMISSION_ITEM: 'SUBMISSION_ITEM', + SUBMISSION_GROUP: 'SUBMISSION_GROUP', + TASK: 'TASK', + TASK_GROUP: 'TASK_GROUP', + TIME_GROUP: 'TIME_GROUP', + USER_GROUP: 'USER_GROUP', } as const; export type CopyApiResponseType = typeof CopyApiResponseType[keyof typeof CopyApiResponseType]; export const CopyApiResponseStatus = { - SUCCESS: 'success', - FAILURE: 'failure', - NOT_DOING: 'not-doing', - NOT_IMPLEMENTED: 'not-implemented', - PARTIAL: 'partial' + SUCCESS: 'success', + FAILURE: 'failure', + NOT_DOING: 'not-doing', + NOT_IMPLEMENTED: 'not-implemented', + PARTIAL: 'partial', } as const; export type CopyApiResponseStatus = typeof CopyApiResponseStatus[keyof typeof CopyApiResponseStatus]; - - diff --git a/apps/server/src/modules/copy-helper/dto/copy.response.ts b/apps/server/src/modules/copy-helper/dto/copy.response.ts index 549dcac7014..f255432eb1e 100644 --- a/apps/server/src/modules/copy-helper/dto/copy.response.ts +++ b/apps/server/src/modules/copy-helper/dto/copy.response.ts @@ -1,5 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { CopyElementType, CopyStatusEnum } from '@modules/copy-helper/types/copy.types'; +import { CopyElementType, CopyStatusEnum } from '../types/copy.types'; /** * DTO for returning a copy status document via api. @@ -29,9 +29,9 @@ export class CopyApiResponse { type: CopyElementType; @ApiPropertyOptional({ - description: 'Id of destination course', + description: 'Id of destination parent reference', }) - destinationCourseId?: string; + destinationId?: string; @ApiProperty({ type: 'string', diff --git a/apps/server/src/modules/copy-helper/mapper/copy.mapper.ts b/apps/server/src/modules/copy-helper/mapper/copy.mapper.ts index 581e33161bd..aa5e8d44af0 100644 --- a/apps/server/src/modules/copy-helper/mapper/copy.mapper.ts +++ b/apps/server/src/modules/copy-helper/mapper/copy.mapper.ts @@ -1,9 +1,11 @@ import { LessonCopyApiParams } from '@modules/learnroom/controller/dto/lesson/lesson-copy.params'; -import { LessonCopyParentParams } from '@modules/lesson/types'; +import { LessonCopyParentParams } from '@modules/lesson/types/lesson-copy-parent.params'; import { TaskCopyApiParams } from '@modules/task/controller/dto/task-copy.params'; -import { TaskCopyParentParams } from '@modules/task/types'; -import { LessonEntity, Task } from '@shared/domain/entity'; +import { TaskCopyParentParams } from '@modules/task/types/task-copy-parent.params'; +import { LessonEntity } from '@shared/domain/entity/lesson.entity'; +import { Task } from '@shared/domain/entity/task.entity'; import { EntityId } from '@shared/domain/types'; +import { ColumnBoard } from '@modules/board/domain/colum-board.do'; import { CopyApiResponse } from '../dto/copy.response'; import { CopyStatus, CopyStatusEnum } from '../types/copy.types'; @@ -18,7 +20,12 @@ export class CopyMapper { if (copyStatus.copyEntity) { const copyEntity = copyStatus.copyEntity as LessonEntity | Task; dto.id = copyEntity.id; - dto.destinationCourseId = copyEntity.course?.id; + if (copyEntity instanceof LessonEntity || copyEntity instanceof Task) { + dto.destinationId = copyEntity.course?.id; + } + if (copyEntity instanceof ColumnBoard) { + dto.destinationId = copyEntity.context?.id; + } } if (copyStatus.status !== CopyStatusEnum.SUCCESS && copyStatus.elements) { dto.elements = copyStatus.elements diff --git a/apps/server/src/modules/files-storage-client/files-storage-client.module.ts b/apps/server/src/modules/files-storage-client/files-storage-client.module.ts index 0c6095fc206..185654114c7 100644 --- a/apps/server/src/modules/files-storage-client/files-storage-client.module.ts +++ b/apps/server/src/modules/files-storage-client/files-storage-client.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; // The files-storage-client should not know the copy-helper -import { CopyHelperModule } from '@modules/copy-helper'; +import { CopyHelperModule } from '@modules/copy-helper/copy-helper.module'; import { CqrsModule } from '@nestjs/cqrs'; import { CopyFilesService, FilesStorageClientAdapterService, FilesStorageProducer } from './service'; diff --git a/apps/server/src/modules/files-storage-client/service/copy-files.service.ts b/apps/server/src/modules/files-storage-client/service/copy-files.service.ts index 2ca2466f1a5..8003065f094 100644 --- a/apps/server/src/modules/files-storage-client/service/copy-files.service.ts +++ b/apps/server/src/modules/files-storage-client/service/copy-files.service.ts @@ -1,4 +1,5 @@ -import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; +import { CopyHelperService } from '@modules/copy-helper/service/copy-helper.service'; +import { CopyElementType, CopyStatus, CopyStatusEnum } from '@modules/copy-helper/types/copy.types'; import { StorageLocation } from '@modules/files-storage/interface'; import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; diff --git a/apps/server/src/modules/learnroom/learnroom.module.ts b/apps/server/src/modules/learnroom/learnroom.module.ts index a8bdcbeba4c..c06158e4a38 100644 --- a/apps/server/src/modules/learnroom/learnroom.module.ts +++ b/apps/server/src/modules/learnroom/learnroom.module.ts @@ -20,7 +20,6 @@ import { UserRepo, } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; -import { BoardNodeRepo } from '../board/repo'; import { COURSE_REPO } from './domain'; import { CommonCartridgeExportMapper } from './mapper/common-cartridge-export.mapper'; import { CommonCartridgeImportMapper } from './mapper/common-cartridge-import.mapper'; @@ -66,7 +65,6 @@ import { CommonCartridgeFileValidatorPipe } from './utils'; useClass: DashboardRepo, }, BoardCopyService, - BoardNodeRepo, CommonCartridgeExportService, CommonCartridgeFileValidatorPipe, CommonCartridgeImportService, diff --git a/apps/server/src/modules/learnroom/service/board-copy.service.spec.ts b/apps/server/src/modules/learnroom/service/board-copy.service.spec.ts index 7fd621811db..0f82cd9f790 100644 --- a/apps/server/src/modules/learnroom/service/board-copy.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/board-copy.service.spec.ts @@ -22,7 +22,9 @@ import { userFactory, } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; +import { CopyColumnBoardParams } from '@src/modules/board/service/internal'; import { columnBoardFactory } from '@src/modules/board/testing'; +import { StorageLocation } from '@src/modules/files-storage/interface'; import { ColumnBoardNodeRepo } from '../repo'; import { BoardCopyService } from './board-copy.service'; @@ -101,28 +103,48 @@ describe('board copy service', () => { it('should return copy type "board"', async () => { const { destinationCourse, originalBoard, user } = setup(); - const status = await copyService.copyBoard({ originalBoard, user, destinationCourse }); + const status = await copyService.copyBoard({ + originalBoard, + user, + originalCourse: destinationCourse, + destinationCourse, + }); expect(status.type).toEqual(CopyElementType.BOARD); }); it('should set title copy status to "board"', async () => { const { destinationCourse, originalBoard, user } = setup(); - const status = await copyService.copyBoard({ originalBoard, user, destinationCourse }); + const status = await copyService.copyBoard({ + originalBoard, + user, + originalCourse: destinationCourse, + destinationCourse, + }); expect(status.title).toEqual('board'); }); it('should set original entity in status', async () => { const { destinationCourse, originalBoard, user } = setup(); - const status = await copyService.copyBoard({ originalBoard, user, destinationCourse }); + const status = await copyService.copyBoard({ + originalBoard, + user, + originalCourse: destinationCourse, + destinationCourse, + }); expect(status.originalEntity).toEqual(originalBoard); }); it('should create a copy', async () => { const { destinationCourse, originalBoard, user } = setup(); - const status = await copyService.copyBoard({ originalBoard, user, destinationCourse }); + const status = await copyService.copyBoard({ + originalBoard, + user, + originalCourse: destinationCourse, + destinationCourse, + }); const board = status.copyEntity as LegacyBoard; expect(board.id).not.toEqual(originalBoard.id); }); @@ -130,7 +152,12 @@ describe('board copy service', () => { it('should set destination course of copy', async () => { const { destinationCourse, originalBoard, user } = setup(); - const status = await copyService.copyBoard({ originalBoard, user, destinationCourse }); + const status = await copyService.copyBoard({ + originalBoard, + user, + originalCourse: destinationCourse, + destinationCourse, + }); const board = status.copyEntity as LegacyBoard; expect(board.course.id).toEqual(destinationCourse.id); }); @@ -158,7 +185,7 @@ describe('board copy service', () => { it('should call taskCopyService with original task', async () => { const { destinationCourse, originalBoard, user, originalTask } = setup(); - await copyService.copyBoard({ originalBoard, user, destinationCourse }); + await copyService.copyBoard({ originalBoard, user, originalCourse: destinationCourse, destinationCourse }); expect(taskCopyService.copyTask).toHaveBeenCalledWith({ originalTaskId: originalTask.id, destinationCourse, @@ -169,14 +196,19 @@ describe('board copy service', () => { it('should call copyHelperService', async () => { const { destinationCourse, originalBoard, user } = setup(); - await copyService.copyBoard({ originalBoard, user, destinationCourse }); + await copyService.copyBoard({ originalBoard, user, originalCourse: destinationCourse, destinationCourse }); expect(copyHelperService.deriveStatusFromElements).toHaveBeenCalledTimes(1); }); it('should add copy of task to board copy', async () => { const { destinationCourse, originalBoard, user } = setup(); - const status = await copyService.copyBoard({ originalBoard, user, destinationCourse }); + const status = await copyService.copyBoard({ + originalBoard, + user, + originalCourse: destinationCourse, + destinationCourse, + }); const board = status.copyEntity as LegacyBoard; expect(board.getElements().length).toEqual(1); }); @@ -184,7 +216,12 @@ describe('board copy service', () => { it('should add status of copying task to board copy status', async () => { const { destinationCourse, originalBoard, user, originalTask } = setup(); - const status = await copyService.copyBoard({ originalBoard, user, destinationCourse }); + const status = await copyService.copyBoard({ + originalBoard, + user, + originalCourse: destinationCourse, + destinationCourse, + }); const taskStatus = status.elements?.find( (el) => el.type === CopyElementType.TASK && el.title === originalTask.name ); @@ -221,13 +258,18 @@ describe('board copy service', () => { user, }; - await copyService.copyBoard({ originalBoard, user, destinationCourse }); + await copyService.copyBoard({ originalBoard, user, originalCourse: destinationCourse, destinationCourse }); expect(lessonCopyService.copyLesson).toHaveBeenCalledWith(expected); }); it('should add lessonCopy to board copy', async () => { const { destinationCourse, originalBoard, user } = setup(); - const status = await copyService.copyBoard({ originalBoard, user, destinationCourse }); + const status = await copyService.copyBoard({ + originalBoard, + user, + originalCourse: destinationCourse, + destinationCourse, + }); const board = status.copyEntity as LegacyBoard; expect(board.getElements().length).toEqual(1); @@ -235,7 +277,12 @@ describe('board copy service', () => { it('should add status of lessonCopy to board copy status', async () => { const { destinationCourse, originalBoard, user } = setup(); - const status = await copyService.copyBoard({ originalBoard, user, destinationCourse }); + const status = await copyService.copyBoard({ + originalBoard, + user, + originalCourse: destinationCourse, + destinationCourse, + }); const lessonStatus = status.elements?.find((el) => el.type === CopyElementType.LESSON); expect(lessonStatus).toBeDefined(); @@ -268,22 +315,29 @@ describe('board copy service', () => { it('should call columnBoardCopyService with original columnBoard', async () => { const { destinationCourse, originalBoard, user, columnBoardTarget } = setup(); - const expected = { + const expected: CopyColumnBoardParams = { originalColumnBoardId: columnBoardTarget.id, - destinationExternalReference: { + targetExternalReference: { type: BoardExternalReferenceType.Course, id: destinationCourse.id, }, + sourceStorageLocationReference: { id: destinationCourse.school.id, type: StorageLocation.SCHOOL }, + targetStorageLocationReference: { id: destinationCourse.school.id, type: StorageLocation.SCHOOL }, userId: user.id, }; - await copyService.copyBoard({ originalBoard, user, destinationCourse }); + await copyService.copyBoard({ originalBoard, user, originalCourse: destinationCourse, destinationCourse }); expect(columnBoardService.copyColumnBoard).toHaveBeenCalledWith(expected); }); it('should add columnBoard copy to board copy', async () => { const { destinationCourse, originalBoard, user } = setup(); - const status = await copyService.copyBoard({ originalBoard, user, destinationCourse }); + const status = await copyService.copyBoard({ + originalBoard, + originalCourse: destinationCourse, + user, + destinationCourse, + }); const board = status.copyEntity as LegacyBoard; expect(board.getElements().length).toEqual(1); @@ -291,7 +345,12 @@ describe('board copy service', () => { it('should add status of columnBoard copy to board copy status', async () => { const { destinationCourse, originalBoard, user } = setup(); - const status = await copyService.copyBoard({ originalBoard, user, destinationCourse }); + const status = await copyService.copyBoard({ + originalBoard, + originalCourse: destinationCourse, + user, + destinationCourse, + }); const lessonStatus = status.elements?.find((el) => el.type === CopyElementType.COLUMNBOARD); expect(lessonStatus).toBeDefined(); @@ -365,14 +424,14 @@ describe('board copy service', () => { it('should trigger swapping ids for board', async () => { const { destinationCourse, originalBoard, user, columnBoardCopy } = setup(); - await copyService.copyBoard({ originalBoard, user, destinationCourse }); + await copyService.copyBoard({ originalBoard, user, originalCourse: destinationCourse, destinationCourse }); expect(columnBoardService.swapLinkedIds).toHaveBeenCalledWith(columnBoardCopy.id, expect.anything()); }); it('should pass task for swapping ids', async () => { const { destinationCourse, originalBoard, user, originalTask, taskCopy } = setup(); - await copyService.copyBoard({ originalBoard, user, destinationCourse }); + await copyService.copyBoard({ originalBoard, user, originalCourse: destinationCourse, destinationCourse }); const map = columnBoardService.swapLinkedIds.mock.calls[0][1]; expect(map.get(originalTask.id)).toEqual(taskCopy.id); @@ -380,7 +439,7 @@ describe('board copy service', () => { it('should pass lesson for swapping ids', async () => { const { destinationCourse, originalBoard, user, originalLesson, lessonCopy } = setup(); - await copyService.copyBoard({ originalBoard, user, destinationCourse }); + await copyService.copyBoard({ originalBoard, user, originalCourse: destinationCourse, destinationCourse }); const map = columnBoardService.swapLinkedIds.mock.calls[0][1]; expect(map.get(originalLesson.id)).toEqual(lessonCopy.id); @@ -388,7 +447,7 @@ describe('board copy service', () => { it('should pass course for swapping ids', async () => { const { originalCourse, destinationCourse, originalBoard, user } = setup(); - await copyService.copyBoard({ originalBoard, user, destinationCourse }); + await copyService.copyBoard({ originalBoard, user, originalCourse: destinationCourse, destinationCourse }); const map = columnBoardService.swapLinkedIds.mock.calls[0][1]; expect(map.get(originalCourse.id)).toEqual(destinationCourse.id); @@ -417,7 +476,7 @@ describe('board copy service', () => { it('should call deriveStatusFromElements', async () => { const { destinationCourse, originalBoard, user } = setup(); - await copyService.copyBoard({ originalBoard, user, destinationCourse }); + await copyService.copyBoard({ originalBoard, user, originalCourse: destinationCourse, destinationCourse }); expect(copyHelperService.deriveStatusFromElements).toHaveBeenCalled(); }); @@ -425,7 +484,12 @@ describe('board copy service', () => { it('should use returned value from deriveStatusFromElements', async () => { const { destinationCourse, originalBoard, user } = setup(); copyHelperService.deriveStatusFromElements.mockReturnValue(CopyStatusEnum.PARTIAL); - const status = await copyService.copyBoard({ originalBoard, user, destinationCourse }); + const status = await copyService.copyBoard({ + originalBoard, + user, + originalCourse: destinationCourse, + destinationCourse, + }); expect(status.status).toEqual(CopyStatusEnum.PARTIAL); }); @@ -458,7 +522,12 @@ describe('board copy service', () => { it('should skip boardelements that contain a corrupted reference', async () => { const { destinationCourse, originalBoard, user } = setup(); - const status = await copyService.copyBoard({ originalBoard, user, destinationCourse }); + const status = await copyService.copyBoard({ + originalBoard, + user, + originalCourse: destinationCourse, + destinationCourse, + }); const board = status.copyEntity as LegacyBoard; expect(board.references).toHaveLength(0); @@ -489,7 +558,12 @@ describe('board copy service', () => { it('should return status fail', async () => { const { destinationCourse, originalBoard, user } = setup(); - const status = await copyService.copyBoard({ originalBoard, user, destinationCourse }); + const status = await copyService.copyBoard({ + originalBoard, + user, + originalCourse: destinationCourse, + destinationCourse, + }); expect(status.status).toEqual(CopyStatusEnum.FAIL); }); diff --git a/apps/server/src/modules/learnroom/service/board-copy.service.ts b/apps/server/src/modules/learnroom/service/board-copy.service.ts index b77dd910e6f..2678f13d3e4 100644 --- a/apps/server/src/modules/learnroom/service/board-copy.service.ts +++ b/apps/server/src/modules/learnroom/service/board-copy.service.ts @@ -22,11 +22,13 @@ import { import { EntityId } from '@shared/domain/types'; import { LegacyBoardRepo } from '@shared/repo'; import { LegacyLogger } from '@src/core/logger'; +import { StorageLocation } from '@src/modules/files-storage/interface'; import { sortBy } from 'lodash'; import { ColumnBoardNodeRepo } from '../repo'; -type BoardCopyParams = { +export type BoardCopyParams = { originalBoard: LegacyBoard; + originalCourse: Course; destinationCourse: Course; user: User; }; @@ -45,10 +47,10 @@ export class BoardCopyService { ) {} async copyBoard(params: BoardCopyParams): Promise { - const { originalBoard, user, destinationCourse } = params; + const { originalBoard, user, originalCourse, destinationCourse } = params; const boardElements: LegacyBoardElement[] = originalBoard.getElements(); - const elements: CopyStatus[] = await this.copyBoardElements(boardElements, user, destinationCourse); + const elements: CopyStatus[] = await this.copyBoardElements(boardElements, user, originalCourse, destinationCourse); const references: LegacyBoardElement[] = await this.extractReferences(elements); @@ -82,6 +84,7 @@ export class BoardCopyService { private async copyBoardElements( boardElements: LegacyBoardElement[], user: User, + originalCourse: Course, destinationCourse: Course ): Promise { const promises: Promise<[number, CopyStatus]>[] = boardElements.map((element, pos) => { @@ -101,7 +104,10 @@ export class BoardCopyService { element.boardElementType === LegacyBoardElementType.ColumnBoard && element.target instanceof ColumnBoardNode ) { - return this.copyColumnBoard(element.target, user, destinationCourse).then((status) => [pos, status]); + return this.copyColumnBoard(element.target, user, originalCourse, destinationCourse).then((status) => [ + pos, + status, + ]); } /* istanbul ignore next */ @@ -135,15 +141,18 @@ export class BoardCopyService { private async copyColumnBoard( columnBoard: ColumnBoardNode, user: User, + originalCourse: Course, destinationCourse: Course ): Promise { return this.columnBoardService.copyColumnBoard({ originalColumnBoardId: columnBoard.id, - userId: user.id, - destinationExternalReference: { + targetExternalReference: { id: destinationCourse.id, type: BoardExternalReferenceType.Course, }, + sourceStorageLocationReference: { id: originalCourse.school.id, type: StorageLocation.SCHOOL }, + targetStorageLocationReference: { id: destinationCourse.school.id, type: StorageLocation.SCHOOL }, + userId: user.id, }); } diff --git a/apps/server/src/modules/learnroom/service/course-copy.service.ts b/apps/server/src/modules/learnroom/service/course-copy.service.ts index 5edb0937621..66da0757f29 100644 --- a/apps/server/src/modules/learnroom/service/course-copy.service.ts +++ b/apps/server/src/modules/learnroom/service/course-copy.service.ts @@ -70,7 +70,12 @@ export class CourseCopyService { ); } - const boardStatus = await this.boardCopyService.copyBoard({ originalBoard, destinationCourse: courseCopy, user }); + const boardStatus = await this.boardCopyService.copyBoard({ + originalBoard, + originalCourse, + destinationCourse: courseCopy, + user, + }); const finishedCourseCopy = await this.finishCourseCopying(courseCopy); const courseStatus = this.deriveCourseStatus(originalCourse, finishedCourseCopy, boardStatus); diff --git a/apps/server/src/modules/room-member/do/room-member.do.spec.ts b/apps/server/src/modules/room-member/do/room-member.do.spec.ts deleted file mode 100644 index d7ed3fb03ac..00000000000 --- a/apps/server/src/modules/room-member/do/room-member.do.spec.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { EntityId } from '@shared/domain/types'; -import { roomMemberFactory } from '../testing'; -import { RoomMember, RoomMemberProps } from './room-member.do'; - -describe('RoomMember', () => { - let roomMember: RoomMember; - const roomMemberId: EntityId = 'roomMemberId'; - const roomMemberProps: RoomMemberProps = { - id: roomMemberId, - roomId: 'roomId', - userGroupId: 'userGroupId', - createdAt: new Date('2024-01-01'), - updatedAt: new Date('2024-01-01'), - }; - - beforeEach(() => { - roomMember = new RoomMember(roomMemberProps); - }); - - it('should props without domainObject', () => { - const mockDomainObject = roomMemberFactory.build(); - // this tests the hotfix for the mikro-orm issue - // eslint-disable-next-line @typescript-eslint/dot-notation - roomMember['domainObject'] = mockDomainObject; - - // eslint-disable-next-line @typescript-eslint/no-unused-vars - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const { domainObject, ...props } = roomMember.getProps(); - - expect(domainObject).toEqual(undefined); - expect(props).toEqual(roomMemberProps); - }); - - it('should get roomId', () => { - expect(roomMember.roomId).toEqual(roomMemberProps.roomId); - }); - - it('should get userGroupId', () => { - expect(roomMember.userGroupId).toEqual(roomMemberProps.userGroupId); - }); -}); diff --git a/apps/server/src/modules/room-member/index.ts b/apps/server/src/modules/room-member/index.ts deleted file mode 100644 index 772b42aa05a..00000000000 --- a/apps/server/src/modules/room-member/index.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { RoomMemberEntity } from './repo/entity'; -import { RoomMemberRepo } from './repo/room-member.repo'; -import { RoomMemberService } from './service/room-member.service'; - -export * from './do/room-member.do'; -export * from './room-member.module'; -export { RoomMemberEntity, RoomMemberRepo, RoomMemberService }; - -export { UserWithRoomRoles, RoomMemberAuthorizable } from './do/room-member-authorizable.do'; diff --git a/apps/server/src/modules/room-member/repo/entity/index.ts b/apps/server/src/modules/room-member/repo/entity/index.ts deleted file mode 100644 index 845b87253da..00000000000 --- a/apps/server/src/modules/room-member/repo/entity/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './room-member.entity'; diff --git a/apps/server/src/modules/room-member/repo/entity/room-member.entity.ts b/apps/server/src/modules/room-member/repo/entity/room-member.entity.ts deleted file mode 100644 index 95cbf802452..00000000000 --- a/apps/server/src/modules/room-member/repo/entity/room-member.entity.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { Entity, Property, Unique } from '@mikro-orm/core'; -import { AuthorizableObject } from '@shared/domain/domain-object'; -import { ObjectIdType } from '@shared/repo/types/object-id.type'; -import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; -import { EntityId } from '@shared/domain/types'; -import { RoomMember } from '../../do/room-member.do'; - -export interface RoomMemberEntityProps extends AuthorizableObject { - id: EntityId; - roomId: EntityId; - userGroupId: EntityId; - createdAt: Date; - updatedAt: Date; -} - -@Entity({ tableName: 'room-members' }) -@Unique({ properties: ['roomId', 'userGroupId'] }) -export class RoomMemberEntity extends BaseEntityWithTimestamps implements RoomMemberEntityProps { - @Property() - @Unique() - @Property({ type: ObjectIdType }) - roomId!: EntityId; - - @Property({ type: ObjectIdType }) - userGroupId!: EntityId; - - @Property({ persist: false }) - domainObject: RoomMember | undefined; -} diff --git a/apps/server/src/modules/room-member/repo/room-member-domain.mapper.spec.ts b/apps/server/src/modules/room-member/repo/room-member-domain.mapper.spec.ts deleted file mode 100644 index d2629ebef65..00000000000 --- a/apps/server/src/modules/room-member/repo/room-member-domain.mapper.spec.ts +++ /dev/null @@ -1,93 +0,0 @@ -import { RoomMember, RoomMemberProps } from '../do/room-member.do'; -import { roomMemberEntityFactory } from '../testing'; -import { RoomMemberEntity } from './entity'; -import { RoomMemberDomainMapper } from './room-member-domain.mapper'; - -describe('RoomMemberDomainMapper', () => { - describe('mapEntityToDo', () => { - it('should correctly map roomMemberEntity to RoomMember domain object', () => { - const roomMemberEntity = { - id: '1', - } as RoomMemberEntity; - - const result = RoomMemberDomainMapper.mapEntityToDo(roomMemberEntity); - - expect(result).toBeInstanceOf(RoomMember); - expect(result.getProps()).toEqual({ - id: '1', - }); - }); - - it('should return existing domainObject if present, regardless of entity properties', () => { - const existingRoomMember = new RoomMember({ - id: '1', - roomId: 'r1', - userGroupId: 'ug1', - createdAt: new Date('2023-01-01'), - updatedAt: new Date('2023-01-01'), - }); - - const roomMemberEntity = { - id: '1', - domainObject: existingRoomMember, - } as RoomMemberEntity; - - const result = RoomMemberDomainMapper.mapEntityToDo(roomMemberEntity); - - expect(result).toBe(existingRoomMember); - expect(result).toBeInstanceOf(RoomMember); - expect(result.getProps()).toEqual({ - id: '1', - roomId: 'r1', - userGroupId: 'ug1', - createdAt: new Date('2023-01-01'), - updatedAt: new Date('2023-01-01'), - }); - expect(result.getProps().id).toBe('1'); - expect(result.getProps().id).toBe(roomMemberEntity.id); - }); - - it('should wrap the actual entity reference in the domain object', () => { - const roomMemberEntity = { - id: '1', - } as RoomMemberEntity; - - const result = RoomMemberDomainMapper.mapEntityToDo(roomMemberEntity); - // @ts-expect-error check necessary - const { props } = result; - - expect(props === roomMemberEntity).toBe(true); - }); - }); - - describe('mapDoToEntity', () => { - describe('when domain object props are instanceof roomMemberEntity', () => { - it('should return the entity', () => { - const roomMemberEntity = roomMemberEntityFactory.build(); - const roomMember = new RoomMember(roomMemberEntity); - - const result = RoomMemberDomainMapper.mapDoToEntity(roomMember); - - expect(result).toBe(roomMemberEntity); - }); - }); - - describe('when domain object props are not instanceof roomMemberEntity', () => { - it('should convert them to an entity and return it', () => { - const roomMemberEntity: RoomMemberProps = { - id: '66d581c3ef74c548a4efea1d', - roomId: '66d581c3ef74c548a4efea1a', - userGroupId: '66d581c3ef74c548a4efea1b', - createdAt: new Date('2024-10-1'), - updatedAt: new Date('2024-10-1'), - }; - const room = new RoomMember(roomMemberEntity); - - const result = RoomMemberDomainMapper.mapDoToEntity(room); - - expect(result).toBeInstanceOf(RoomMemberEntity); - expect(result).toMatchObject(roomMemberEntity); - }); - }); - }); -}); diff --git a/apps/server/src/modules/room-member/repo/room-member-domain.mapper.ts b/apps/server/src/modules/room-member/repo/room-member-domain.mapper.ts deleted file mode 100644 index 73163a07769..00000000000 --- a/apps/server/src/modules/room-member/repo/room-member-domain.mapper.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { RoomMember } from '../do/room-member.do'; -import { RoomMemberEntity } from './entity'; - -export class RoomMemberDomainMapper { - static mapEntityToDo(roomMemberEntity: RoomMemberEntity): RoomMember { - // check identity map reference - if (roomMemberEntity.domainObject) { - return roomMemberEntity.domainObject; - } - - const roomMember = new RoomMember(roomMemberEntity); - - // attach to identity map - roomMemberEntity.domainObject = roomMember; - - return roomMember; - } - - static mapDoToEntity(roomMember: RoomMember): RoomMemberEntity { - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - const { props } = roomMember; - - if (!(props instanceof RoomMemberEntity)) { - const entity = new RoomMemberEntity(); - Object.assign(entity, props); - - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - roomMember.props = entity; - - return entity; - } - - return props; - } -} diff --git a/apps/server/src/modules/room-member/repo/room-member.repo.spec.ts b/apps/server/src/modules/room-member/repo/room-member.repo.spec.ts deleted file mode 100644 index 3cf9364019e..00000000000 --- a/apps/server/src/modules/room-member/repo/room-member.repo.spec.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { MongoMemoryDatabaseModule } from '@infra/database'; -import { NotFoundError } from '@mikro-orm/core'; -import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; -import { Test, TestingModule } from '@nestjs/testing'; -import { cleanupCollections } from '@shared/testing'; -import { RoomMember } from '../do/room-member.do'; -import { roomMemberEntityFactory, roomMemberFactory } from '../testing'; -import { RoomMemberEntity } from './entity'; -import { RoomMemberRepo } from './room-member.repo'; - -describe('RoomMemberRepo', () => { - let module: TestingModule; - let repo: RoomMemberRepo; - let em: EntityManager; - - beforeAll(async () => { - module = await Test.createTestingModule({ - imports: [MongoMemoryDatabaseModule.forRoot()], - providers: [RoomMemberRepo], - }).compile(); - - repo = module.get(RoomMemberRepo); - em = module.get(EntityManager); - }); - - afterAll(async () => { - await module.close(); - }); - - afterEach(async () => { - await cleanupCollections(em); - }); - - describe('findByRoomId', () => { - const setup = async () => { - const roomMemberEntity = roomMemberEntityFactory.buildWithId(); - await em.persistAndFlush([roomMemberEntity]); - em.clear(); - - return { roomMemberEntity }; - }; - - it('should find room member by roomId', async () => { - const { roomMemberEntity } = await setup(); - - const roomMember = await repo.findByRoomId(roomMemberEntity.roomId); - - expect(roomMember).toBeDefined(); - expect(roomMember?.getProps()).toEqual(roomMemberEntity); - }); - }); - - describe('findByRoomIds', () => { - const setup = async () => { - const roomId1 = new ObjectId().toHexString(); - const roomId2 = new ObjectId().toHexString(); - - const roomMemberEntities = [ - roomMemberEntityFactory.buildWithId({ roomId: roomId1 }), - roomMemberEntityFactory.buildWithId({ roomId: roomId1 }), - roomMemberEntityFactory.buildWithId({ roomId: roomId2 }), - ]; - - await em.persistAndFlush(roomMemberEntities); - em.clear(); - - return { roomMemberEntities, roomId1, roomId2 }; - }; - - it('should find room member by roomIds', async () => { - const { roomId1, roomId2 } = await setup(); - - const roomMembers = await repo.findByRoomIds([roomId1, roomId2]); - - expect(roomMembers).toHaveLength(3); - }); - }); - - describe('findByGroupId', () => { - const setup = async () => { - const groupId = new ObjectId().toHexString(); - const roomMemberEntities = [ - roomMemberEntityFactory.build({ userGroupId: groupId }), - roomMemberEntityFactory.build({ userGroupId: groupId }), - roomMemberEntityFactory.build({ userGroupId: new ObjectId().toHexString() }), - ]; - - await em.persistAndFlush(roomMemberEntities); - em.clear(); - - return { roomMemberEntities, groupId }; - }; - - it('should find room members by groupId', async () => { - const { groupId } = await setup(); - - const roomMembers = await repo.findByGroupId(groupId); - - expect(roomMembers).toHaveLength(2); - }); - }); - - describe('save', () => { - const setup = () => { - const roomMembers = roomMemberFactory.buildList(3); - return { roomMembers }; - }; - - it('should be able to persist a single room member', async () => { - const { roomMembers } = setup(); - - await repo.save(roomMembers[0]); - const result = await em.findOneOrFail(RoomMemberEntity, roomMembers[0].id); - - expect(roomMembers[0].getProps()).toMatchObject(result); - }); - - it('should be able to persist many room members', async () => { - const { roomMembers } = setup(); - - await repo.save(roomMembers); - const result = await em.find(RoomMemberEntity, { id: { $in: roomMembers.map((r) => r.id) } }); - - expect(result.length).toBe(roomMembers.length); - }); - }); - - describe('delete', () => { - const setup = async () => { - const roomMemberEntities = roomMemberEntityFactory.buildListWithId(3); - await em.persistAndFlush(roomMemberEntities); - const roomMembers = roomMemberEntities.map((entity) => new RoomMember(entity)); - em.clear(); - - return { roomMembers }; - }; - - it('should be able to delete a single room member', async () => { - const { roomMembers } = await setup(); - - await repo.delete(roomMembers[0]); - - await expect(em.findOneOrFail(RoomMemberEntity, roomMembers[0].id)).rejects.toThrow(NotFoundError); - }); - - it('should be able to delete many rooms', async () => { - const { roomMembers } = await setup(); - - await repo.delete(roomMembers); - - const remainingCount = await em.count(RoomMemberEntity); - expect(remainingCount).toBe(0); - }); - }); -}); diff --git a/apps/server/src/modules/room-member/repo/room-member.repo.ts b/apps/server/src/modules/room-member/repo/room-member.repo.ts deleted file mode 100644 index 022383fffaf..00000000000 --- a/apps/server/src/modules/room-member/repo/room-member.repo.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { Utils } from '@mikro-orm/core'; -import { EntityManager } from '@mikro-orm/mongodb'; -import { Injectable } from '@nestjs/common'; -import { EntityId } from '@shared/domain/types'; -import { RoomMember } from '../do/room-member.do'; -import { RoomMemberEntity } from './entity'; -import { RoomMemberDomainMapper } from './room-member-domain.mapper'; - -@Injectable() -export class RoomMemberRepo { - constructor(private readonly em: EntityManager) {} - - async findByRoomId(roomId: EntityId): Promise { - const roomMemberEntities = await this.em.findOne(RoomMemberEntity, { roomId }); - if (!roomMemberEntities) return null; - - const roomMembers = RoomMemberDomainMapper.mapEntityToDo(roomMemberEntities); - - return roomMembers; - } - - async findByRoomIds(roomIds: EntityId[]): Promise { - const entities = await this.em.find(RoomMemberEntity, { roomId: { $in: roomIds } }); - const roomMembers = entities.map((entity) => RoomMemberDomainMapper.mapEntityToDo(entity)); - - return roomMembers; - } - - async findByGroupId(groupId: EntityId): Promise { - const entities = await this.em.find(RoomMemberEntity, { userGroupId: groupId }); - const roomMembers = entities.map((entity) => RoomMemberDomainMapper.mapEntityToDo(entity)); - - return roomMembers; - } - - async findByGroupIds(groupIds: EntityId[]): Promise { - const entities = await this.em.find(RoomMemberEntity, { userGroupId: { $in: groupIds } }); - const roomMembers = entities.map((entity) => RoomMemberDomainMapper.mapEntityToDo(entity)); - - return roomMembers; - } - - async save(roomMember: RoomMember | RoomMember[]): Promise { - const roomMembers = Utils.asArray(roomMember); - - roomMembers.forEach((member) => { - const entity = RoomMemberDomainMapper.mapDoToEntity(member); - this.em.persist(entity); - }); - - await this.em.flush(); - } - - async delete(roomMember: RoomMember | RoomMember[]): Promise { - const roomMembers = Utils.asArray(roomMember); - - roomMembers.forEach((member) => { - const entity = RoomMemberDomainMapper.mapDoToEntity(member); - this.em.remove(entity); - }); - - await this.em.flush(); - } -} diff --git a/apps/server/src/modules/room-member/room-member.module.ts b/apps/server/src/modules/room-member/room-member.module.ts deleted file mode 100644 index b07ace89160..00000000000 --- a/apps/server/src/modules/room-member/room-member.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { GroupModule } from '@modules/group'; -import { Module } from '@nestjs/common'; -import { CqrsModule } from '@nestjs/cqrs'; -import { AuthorizationModule } from '../authorization'; -import { RoleModule } from '../role'; -import { RoomMemberRule } from './authorization/room-member.rule'; -import { RoomMemberRepo } from './repo/room-member.repo'; -import { RoomMemberService } from './service/room-member.service'; - -@Module({ - imports: [AuthorizationModule, CqrsModule, GroupModule, RoleModule], - providers: [RoomMemberService, RoomMemberRepo, RoomMemberRule], - exports: [RoomMemberService], -}) -export class RoomMemberModule {} diff --git a/apps/server/src/modules/room-member/service/room-member.service.ts b/apps/server/src/modules/room-member/service/room-member.service.ts deleted file mode 100644 index 4fdd5933e16..00000000000 --- a/apps/server/src/modules/room-member/service/room-member.service.ts +++ /dev/null @@ -1,133 +0,0 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; -import { RoleName } from '@shared/domain/interface'; -import { EntityId } from '@shared/domain/types'; -import { Group, GroupService, GroupTypes } from '@src/modules/group'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { RoleDto, RoleService } from '@src/modules/role'; -import { RoomMember } from '../do/room-member.do'; -import { RoomMemberRepo } from '../repo/room-member.repo'; -import { RoomMemberAuthorizable, UserWithRoomRoles } from '../do/room-member-authorizable.do'; - -@Injectable() -export class RoomMemberService { - constructor( - private readonly groupService: GroupService, - private readonly roomMembersRepo: RoomMemberRepo, - private readonly roleService: RoleService - ) {} - - private async createNewRoomMember( - roomId: EntityId, - userId: EntityId, - roleName: RoleName.ROOM_EDITOR | RoleName.ROOM_VIEWER, - schoolId?: EntityId - ) { - const group = await this.groupService.createGroup(`Room Members for Room ${roomId}`, GroupTypes.ROOM, schoolId); - await this.groupService.addUsersToGroup(group.id, [{ userId, roleName }]); - - const roomMember = new RoomMember({ - id: new ObjectId().toHexString(), - roomId, - userGroupId: group.id, - createdAt: new Date(), - updatedAt: new Date(), - }); - - await this.roomMembersRepo.save(roomMember); - - return roomMember; - } - - private buildRoomMemberAuthorizable(roomId: EntityId, group: Group, roleSet: RoleDto[]): RoomMemberAuthorizable { - const members = group.users.map((groupUser): UserWithRoomRoles => { - const roleDto = roleSet.find((role) => role.id === groupUser.roleId); - if (roleDto === undefined) throw new BadRequestException('Role not found'); - return { - roles: [roleDto], - userId: groupUser.userId, - }; - }); - - const roomMemberAuthorizable = new RoomMemberAuthorizable(roomId, members); - - return roomMemberAuthorizable; - } - - public async deleteRoomMember(roomId: EntityId) { - const roomMember = await this.roomMembersRepo.findByRoomId(roomId); - if (roomMember === null) return; - - const group = await this.groupService.findById(roomMember.userGroupId); - await this.groupService.delete(group); - await this.roomMembersRepo.delete(roomMember); - } - - public async addMembersToRoom( - roomId: EntityId, - userIdsAndRoles: Array<{ userId: EntityId; roleName: RoleName.ROOM_EDITOR | RoleName.ROOM_VIEWER }>, - schoolId?: EntityId - ): Promise { - const roomMember = await this.roomMembersRepo.findByRoomId(roomId); - if (roomMember === null) { - const firstUser = userIdsAndRoles.pop(); - if (firstUser === undefined) { - throw new BadRequestException('No user provided'); - } - const newRoomMember = await this.createNewRoomMember(roomId, firstUser.userId, firstUser.roleName, schoolId); - return newRoomMember.id; - } - - await this.groupService.addUsersToGroup(roomMember.userGroupId, userIdsAndRoles); - - return roomMember.id; - } - - public async removeMembersFromRoom(roomId: EntityId, userIds: EntityId[]): Promise { - const roomMember = await this.roomMembersRepo.findByRoomId(roomId); - if (roomMember === null) { - throw new BadRequestException('Room member not found'); - } - - const group = await this.groupService.findById(roomMember.userGroupId); - await this.groupService.removeUsersFromGroup(group.id, userIds); - } - - public async getRoomMemberAuthorizablesByUserId(userId: EntityId): Promise { - const groupPage = await this.groupService.findGroups({ userId, groupTypes: [GroupTypes.ROOM] }); - const groupIds = groupPage.data.map((group) => group.id); - const roomMembers = await this.roomMembersRepo.findByGroupIds(groupIds); - const roleIds = groupPage.data.flatMap((group) => group.users.map((groupUser) => groupUser.roleId)); - const roleSet = await this.roleService.findByIds(roleIds); - const roomMemberAuthorizables = roomMembers - .map((item) => { - const group = groupPage.data.find((g) => g.id === item.userGroupId); - if (!group) return null; - return this.buildRoomMemberAuthorizable(item.roomId, group, roleSet); - }) - .filter((item): item is RoomMemberAuthorizable => item !== null); - - return roomMemberAuthorizables; - } - - public async getRoomMemberAuthorizable(roomId: EntityId): Promise { - const roomMember = await this.roomMembersRepo.findByRoomId(roomId); - if (roomMember === null) { - return new RoomMemberAuthorizable(roomId, []); - } - const group = await this.groupService.findById(roomMember.userGroupId); - const roleSet = await this.roleService.findByIds(group.users.map((groupUser) => groupUser.roleId)); - - const members = group.users.map((groupUser): UserWithRoomRoles => { - const roleDto = roleSet.find((role) => role.id === groupUser.roleId); - if (roleDto === undefined) throw new BadRequestException('Role not found'); - return { - roles: [roleDto], - userId: groupUser.userId, - }; - }); - - const roomMemberAuthorizable = new RoomMemberAuthorizable(roomId, members); - - return roomMemberAuthorizable; - } -} diff --git a/apps/server/src/modules/room-member/testing/index.ts b/apps/server/src/modules/room-member/testing/index.ts deleted file mode 100644 index 7f1a2950ff5..00000000000 --- a/apps/server/src/modules/room-member/testing/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './room-member-entity.factory'; -export * from './room-member.factory'; diff --git a/apps/server/src/modules/room-member/authorization/room-member.rule.spec.ts b/apps/server/src/modules/room-membership/authorization/room-membership.rule.spec.ts similarity index 60% rename from apps/server/src/modules/room-member/authorization/room-member.rule.spec.ts rename to apps/server/src/modules/room-membership/authorization/room-membership.rule.spec.ts index 1ca26adb941..6946c83aa8d 100644 --- a/apps/server/src/modules/room-member/authorization/room-member.rule.spec.ts +++ b/apps/server/src/modules/room-membership/authorization/room-membership.rule.spec.ts @@ -1,22 +1,22 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain/interface'; import { roleDtoFactory, setupEntities, userFactory } from '@shared/testing'; -import { Action, AuthorizationHelper, AuthorizationInjectionService } from '@src/modules/authorization'; -import { RoomMemberAuthorizable } from '../do/room-member-authorizable.do'; -import { RoomMemberRule } from './room-member.rule'; +import { Action, AuthorizationHelper, AuthorizationInjectionService } from '@modules/authorization'; +import { RoomMembershipAuthorizable } from '../do/room-membership-authorizable.do'; +import { RoomMembershipRule } from './room-membership.rule'; -describe(RoomMemberRule.name, () => { - let service: RoomMemberRule; +describe(RoomMembershipRule.name, () => { + let service: RoomMembershipRule; let injectionService: AuthorizationInjectionService; beforeAll(async () => { await setupEntities(); const module: TestingModule = await Test.createTestingModule({ - providers: [RoomMemberRule, AuthorizationHelper, AuthorizationInjectionService], + providers: [RoomMembershipRule, AuthorizationHelper, AuthorizationInjectionService], }).compile(); - service = await module.get(RoomMemberRule); + service = await module.get(RoomMembershipRule); injectionService = await module.get(AuthorizationInjectionService); }); @@ -30,14 +30,14 @@ describe(RoomMemberRule.name, () => { describe('when entity is applicable', () => { const setup = () => { const user = userFactory.buildWithId(); - const roomMemberAuthorizable = new RoomMemberAuthorizable('', []); + const roomMembershipAuthorizable = new RoomMembershipAuthorizable('', []); - return { user, roomMemberAuthorizable }; + return { user, roomMembershipAuthorizable }; }; it('should return true', () => { - const { user, roomMemberAuthorizable } = setup(); - const result = service.isApplicable(user, roomMemberAuthorizable); + const { user, roomMembershipAuthorizable } = setup(); + const result = service.isApplicable(user, roomMembershipAuthorizable); expect(result).toStrictEqual(true); }); @@ -64,15 +64,15 @@ describe(RoomMemberRule.name, () => { const setup = () => { const user = userFactory.buildWithId(); const roleDto = roleDtoFactory.build({ permissions: [Permission.ROOM_VIEW] }); - const roomMemberAuthorizable = new RoomMemberAuthorizable('', [{ roles: [roleDto], userId: user.id }]); + const roomMembershipAuthorizable = new RoomMembershipAuthorizable('', [{ roles: [roleDto], userId: user.id }]); - return { user, roomMemberAuthorizable }; + return { user, roomMembershipAuthorizable }; }; it('should return "true" for read action', () => { - const { user, roomMemberAuthorizable } = setup(); + const { user, roomMembershipAuthorizable } = setup(); - const res = service.hasPermission(user, roomMemberAuthorizable, { + const res = service.hasPermission(user, roomMembershipAuthorizable, { action: Action.read, requiredPermissions: [], }); @@ -81,9 +81,9 @@ describe(RoomMemberRule.name, () => { }); it('should return "false" for write action', () => { - const { user, roomMemberAuthorizable } = setup(); + const { user, roomMembershipAuthorizable } = setup(); - const res = service.hasPermission(user, roomMemberAuthorizable, { + const res = service.hasPermission(user, roomMembershipAuthorizable, { action: Action.write, requiredPermissions: [], }); @@ -95,15 +95,15 @@ describe(RoomMemberRule.name, () => { describe('when user is not member of room', () => { const setup = () => { const user = userFactory.buildWithId(); - const roomMemberAuthorizable = new RoomMemberAuthorizable('', []); + const roomMembershipAuthorizable = new RoomMembershipAuthorizable('', []); - return { user, roomMemberAuthorizable }; + return { user, roomMembershipAuthorizable }; }; it('should return "false" for read action', () => { - const { user, roomMemberAuthorizable } = setup(); + const { user, roomMembershipAuthorizable } = setup(); - const res = service.hasPermission(user, roomMemberAuthorizable, { + const res = service.hasPermission(user, roomMembershipAuthorizable, { action: Action.read, requiredPermissions: [], }); @@ -112,9 +112,9 @@ describe(RoomMemberRule.name, () => { }); it('should return "false" for write action', () => { - const { user, roomMemberAuthorizable } = setup(); + const { user, roomMembershipAuthorizable } = setup(); - const res = service.hasPermission(user, roomMemberAuthorizable, { + const res = service.hasPermission(user, roomMembershipAuthorizable, { action: Action.write, requiredPermissions: [], }); diff --git a/apps/server/src/modules/room-member/authorization/room-member.rule.ts b/apps/server/src/modules/room-membership/authorization/room-membership.rule.ts similarity index 69% rename from apps/server/src/modules/room-member/authorization/room-member.rule.ts rename to apps/server/src/modules/room-membership/authorization/room-membership.rule.ts index 7a98c93a215..cfcd11c33af 100644 --- a/apps/server/src/modules/room-member/authorization/room-member.rule.ts +++ b/apps/server/src/modules/room-membership/authorization/room-membership.rule.ts @@ -1,22 +1,22 @@ import { Injectable } from '@nestjs/common'; import { User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; -import { AuthorizationInjectionService, Action, AuthorizationContext, Rule } from '@src/modules/authorization'; -import { RoomMemberAuthorizable } from '../do/room-member-authorizable.do'; +import { AuthorizationInjectionService, Action, AuthorizationContext, Rule } from '@modules/authorization'; +import { RoomMembershipAuthorizable } from '../do/room-membership-authorizable.do'; @Injectable() -export class RoomMemberRule implements Rule { +export class RoomMembershipRule implements Rule { constructor(private readonly authorisationInjectionService: AuthorizationInjectionService) { this.authorisationInjectionService.injectAuthorizationRule(this); } public isApplicable(user: User, object: unknown): boolean { - const isMatched = object instanceof RoomMemberAuthorizable; + const isMatched = object instanceof RoomMembershipAuthorizable; return isMatched; } - public hasPermission(user: User, object: RoomMemberAuthorizable, context: AuthorizationContext): boolean { + public hasPermission(user: User, object: RoomMembershipAuthorizable, context: AuthorizationContext): boolean { const { action } = context; const permissionsThisUserHas = object.members .filter((member) => member.userId === user.id) diff --git a/apps/server/src/modules/room-member/do/room-member-authorizable.do.ts b/apps/server/src/modules/room-membership/do/room-membership-authorizable.do.ts similarity index 79% rename from apps/server/src/modules/room-member/do/room-member-authorizable.do.ts rename to apps/server/src/modules/room-membership/do/room-membership-authorizable.do.ts index 1728d0045b9..61821fa4b82 100644 --- a/apps/server/src/modules/room-member/do/room-member-authorizable.do.ts +++ b/apps/server/src/modules/room-membership/do/room-membership-authorizable.do.ts @@ -1,13 +1,13 @@ import { AuthorizableObject } from '@shared/domain/domain-object'; import { EntityId } from '@shared/domain/types'; -import { RoleDto } from '@src/modules/role'; +import { RoleDto } from '@modules/role'; export type UserWithRoomRoles = { roles: RoleDto[]; userId: EntityId; }; -export class RoomMemberAuthorizable implements AuthorizableObject { +export class RoomMembershipAuthorizable implements AuthorizableObject { public readonly id: EntityId = ''; public readonly roomId: EntityId; diff --git a/apps/server/src/modules/room-membership/do/room-membership.do.spec.ts b/apps/server/src/modules/room-membership/do/room-membership.do.spec.ts new file mode 100644 index 00000000000..5033cdc33b4 --- /dev/null +++ b/apps/server/src/modules/room-membership/do/room-membership.do.spec.ts @@ -0,0 +1,47 @@ +import { EntityId } from '@shared/domain/types'; +import { roomMembershipFactory } from '../testing'; +import { RoomMembership, RoomMembershipProps } from './room-membership.do'; + +describe('RoomMembership', () => { + let roomMembership: RoomMembership; + const roomMemberId: EntityId = 'roomMemberId'; + const roomMembershipProps: RoomMembershipProps = { + id: roomMemberId, + roomId: 'roomId', + userGroupId: 'userGroupId', + schoolId: 'schoolId', + createdAt: new Date('2024-01-01'), + updatedAt: new Date('2024-01-01'), + }; + + beforeEach(() => { + roomMembership = new RoomMembership(roomMembershipProps); + }); + + it('should props without domainObject', () => { + const mockDomainObject = roomMembershipFactory.build(); + // this tests the hotfix for the mikro-orm issue + // eslint-disable-next-line @typescript-eslint/dot-notation + roomMembership['domainObject'] = mockDomainObject; + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const { domainObject, ...props } = roomMembership.getProps(); + + expect(domainObject).toEqual(undefined); + expect(props).toEqual(roomMembershipProps); + }); + + it('should get roomId', () => { + expect(roomMembership.roomId).toEqual(roomMembershipProps.roomId); + }); + + it('should get userGroupId', () => { + expect(roomMembership.userGroupId).toEqual(roomMembershipProps.userGroupId); + }); + + it('should get schoolId', () => { + expect(roomMembership.schoolId).toEqual(roomMembershipProps.schoolId); + }); +}); diff --git a/apps/server/src/modules/room-member/do/room-member.do.ts b/apps/server/src/modules/room-membership/do/room-membership.do.ts similarity index 69% rename from apps/server/src/modules/room-member/do/room-member.do.ts rename to apps/server/src/modules/room-membership/do/room-membership.do.ts index 8aeebfffbda..99062d69545 100644 --- a/apps/server/src/modules/room-member/do/room-member.do.ts +++ b/apps/server/src/modules/room-membership/do/room-membership.do.ts @@ -1,20 +1,21 @@ import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; import { EntityId } from '@shared/domain/types'; -export interface RoomMemberProps extends AuthorizableObject { +export interface RoomMembershipProps extends AuthorizableObject { id: EntityId; roomId: EntityId; userGroupId: EntityId; + schoolId: EntityId; createdAt: Date; updatedAt: Date; } -export class RoomMember extends DomainObject { - public constructor(props: RoomMemberProps) { +export class RoomMembership extends DomainObject { + public constructor(props: RoomMembershipProps) { super(props); } - public getProps(): RoomMemberProps { + public getProps(): RoomMembershipProps { // Note: Propagated hotfix. Will be resolved with mikro-orm update. Look at the comment in board-node.do.ts. // eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/ban-ts-comment @@ -31,4 +32,8 @@ export class RoomMember extends DomainObject { public get userGroupId(): EntityId { return this.props.userGroupId; } + + public get schoolId(): EntityId { + return this.props.schoolId; + } } diff --git a/apps/server/src/modules/room-membership/index.ts b/apps/server/src/modules/room-membership/index.ts new file mode 100644 index 00000000000..be6ccd4b3ac --- /dev/null +++ b/apps/server/src/modules/room-membership/index.ts @@ -0,0 +1,9 @@ +import { RoomMembershipEntity } from './repo/entity'; +import { RoomMembershipRepo } from './repo/room-membership.repo'; +import { RoomMembershipService } from './service/room-membership.service'; + +export * from './do/room-membership.do'; +export * from './room-membership.module'; +export { RoomMembershipEntity, RoomMembershipRepo, RoomMembershipService }; + +export { UserWithRoomRoles, RoomMembershipAuthorizable } from './do/room-membership-authorizable.do'; diff --git a/apps/server/src/modules/room-membership/repo/entity/index.ts b/apps/server/src/modules/room-membership/repo/entity/index.ts new file mode 100644 index 00000000000..43e58e0db14 --- /dev/null +++ b/apps/server/src/modules/room-membership/repo/entity/index.ts @@ -0,0 +1 @@ +export * from './room-membership.entity'; diff --git a/apps/server/src/modules/room-membership/repo/entity/room-membership.entity.ts b/apps/server/src/modules/room-membership/repo/entity/room-membership.entity.ts new file mode 100644 index 00000000000..eafbfd3aeab --- /dev/null +++ b/apps/server/src/modules/room-membership/repo/entity/room-membership.entity.ts @@ -0,0 +1,22 @@ +import { Entity, Property, Unique } from '@mikro-orm/core'; +import { ObjectIdType } from '@shared/repo/types/object-id.type'; +import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; +import { EntityId } from '@shared/domain/types'; +import { RoomMembership, RoomMembershipProps } from '../../do/room-membership.do'; + +@Entity({ tableName: 'room-memberships' }) +@Unique({ properties: ['roomId', 'userGroupId'] }) +export class RoomMembershipEntity extends BaseEntityWithTimestamps implements RoomMembershipProps { + @Unique() + @Property({ type: ObjectIdType, fieldName: 'room' }) + roomId!: EntityId; + + @Property({ type: ObjectIdType, fieldName: 'userGroup' }) + userGroupId!: EntityId; + + @Property({ type: ObjectIdType, fieldName: 'school' }) + schoolId!: EntityId; + + @Property({ persist: false }) + domainObject: RoomMembership | undefined; +} diff --git a/apps/server/src/modules/room-membership/repo/room-membership-domain.mapper.spec.ts b/apps/server/src/modules/room-membership/repo/room-membership-domain.mapper.spec.ts new file mode 100644 index 00000000000..a95d86fdb73 --- /dev/null +++ b/apps/server/src/modules/room-membership/repo/room-membership-domain.mapper.spec.ts @@ -0,0 +1,96 @@ +import { RoomMembership, RoomMembershipProps } from '../do/room-membership.do'; +import { roomMembershipEntityFactory } from '../testing'; +import { RoomMembershipEntity } from './entity'; +import { RoomMembershipDomainMapper } from './room-membership-domain.mapper'; + +describe('RoomMembershipDomainMapper', () => { + describe('mapEntityToDo', () => { + it('should correctly map roomMembershipEntity to RoomMembership domain object', () => { + const roomMembershipEntity = { + id: '1', + } as RoomMembershipEntity; + + const result = RoomMembershipDomainMapper.mapEntityToDo(roomMembershipEntity); + + expect(result).toBeInstanceOf(RoomMembership); + expect(result.getProps()).toEqual({ + id: '1', + }); + }); + + it('should return existing domainObject if present, regardless of entity properties', () => { + const existingRoomMembership = new RoomMembership({ + id: '1', + roomId: 'r1', + userGroupId: 'ug1', + schoolId: 's1', + createdAt: new Date('2023-01-01'), + updatedAt: new Date('2023-01-01'), + }); + + const roomMembershipEntity = { + id: '1', + domainObject: existingRoomMembership, + } as RoomMembershipEntity; + + const result = RoomMembershipDomainMapper.mapEntityToDo(roomMembershipEntity); + + expect(result).toBe(existingRoomMembership); + expect(result).toBeInstanceOf(RoomMembership); + expect(result.getProps()).toEqual({ + id: '1', + roomId: 'r1', + userGroupId: 'ug1', + schoolId: 's1', + createdAt: new Date('2023-01-01'), + updatedAt: new Date('2023-01-01'), + }); + expect(result.getProps().id).toBe('1'); + expect(result.getProps().id).toBe(roomMembershipEntity.id); + }); + + it('should wrap the actual entity reference in the domain object', () => { + const roomMembershipEntity = { + id: '1', + } as RoomMembershipEntity; + + const result = RoomMembershipDomainMapper.mapEntityToDo(roomMembershipEntity); + // @ts-expect-error check necessary + const { props } = result; + + expect(props === roomMembershipEntity).toBe(true); + }); + }); + + describe('mapDoToEntity', () => { + describe('when domain object props are instanceof roomMembershipEntity', () => { + it('should return the entity', () => { + const roomMembershipEntity = roomMembershipEntityFactory.build(); + const roomMembership = new RoomMembership(roomMembershipEntity); + + const result = RoomMembershipDomainMapper.mapDoToEntity(roomMembership); + + expect(result).toBe(roomMembershipEntity); + }); + }); + + describe('when domain object props are not instanceof roomMembershipEntity', () => { + it('should convert them to an entity and return it', () => { + const roomMembershipEntity: RoomMembershipProps = { + id: '66d581c3ef74c548a4efea1d', + roomId: '66d581c3ef74c548a4efea1a', + userGroupId: '66d581c3ef74c548a4efea1b', + schoolId: '66d581c3ef74c548a4efea1c', + createdAt: new Date('2024-10-1'), + updatedAt: new Date('2024-10-1'), + }; + const room = new RoomMembership(roomMembershipEntity); + + const result = RoomMembershipDomainMapper.mapDoToEntity(room); + + expect(result).toBeInstanceOf(RoomMembershipEntity); + expect(result).toMatchObject(roomMembershipEntity); + }); + }); + }); +}); diff --git a/apps/server/src/modules/room-membership/repo/room-membership-domain.mapper.ts b/apps/server/src/modules/room-membership/repo/room-membership-domain.mapper.ts new file mode 100644 index 00000000000..3538822a17a --- /dev/null +++ b/apps/server/src/modules/room-membership/repo/room-membership-domain.mapper.ts @@ -0,0 +1,37 @@ +import { RoomMembership } from '../do/room-membership.do'; +import { RoomMembershipEntity } from './entity'; + +export class RoomMembershipDomainMapper { + static mapEntityToDo(roomMembershipEntity: RoomMembershipEntity): RoomMembership { + // check identity map reference + if (roomMembershipEntity.domainObject) { + return roomMembershipEntity.domainObject; + } + + const roomMembership = new RoomMembership(roomMembershipEntity); + + // attach to identity map + roomMembershipEntity.domainObject = roomMembership; + + return roomMembership; + } + + static mapDoToEntity(roomMembership: RoomMembership): RoomMembershipEntity { + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + const { props } = roomMembership; + + if (!(props instanceof RoomMembershipEntity)) { + const entity = new RoomMembershipEntity(); + Object.assign(entity, props); + + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + roomMembership.props = entity; + + return entity; + } + + return props; + } +} diff --git a/apps/server/src/modules/room-membership/repo/room-membership.repo.spec.ts b/apps/server/src/modules/room-membership/repo/room-membership.repo.spec.ts new file mode 100644 index 00000000000..5fb6b3a0932 --- /dev/null +++ b/apps/server/src/modules/room-membership/repo/room-membership.repo.spec.ts @@ -0,0 +1,155 @@ +import { MongoMemoryDatabaseModule } from '@infra/database'; +import { NotFoundError } from '@mikro-orm/core'; +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; +import { cleanupCollections } from '@shared/testing'; +import { RoomMembership } from '../do/room-membership.do'; +import { roomMembershipEntityFactory, roomMembershipFactory } from '../testing'; +import { RoomMembershipEntity } from './entity'; +import { RoomMembershipRepo } from './room-membership.repo'; + +describe('RoomMembershipRepo', () => { + let module: TestingModule; + let repo: RoomMembershipRepo; + let em: EntityManager; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [MongoMemoryDatabaseModule.forRoot()], + providers: [RoomMembershipRepo], + }).compile(); + + repo = module.get(RoomMembershipRepo); + em = module.get(EntityManager); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(async () => { + await cleanupCollections(em); + }); + + describe('findByRoomId', () => { + const setup = async () => { + const roomMembershipEntity = roomMembershipEntityFactory.buildWithId(); + await em.persistAndFlush([roomMembershipEntity]); + em.clear(); + + return { roomMembershipEntity }; + }; + + it('should find room member by roomId', async () => { + const { roomMembershipEntity } = await setup(); + + const roomMembership = await repo.findByRoomId(roomMembershipEntity.roomId); + + expect(roomMembership).toBeDefined(); + expect(roomMembership?.getProps()).toEqual(roomMembershipEntity); + }); + }); + + describe('findByRoomIds', () => { + const setup = async () => { + const roomId1 = new ObjectId().toHexString(); + const roomId2 = new ObjectId().toHexString(); + + const roomMemberEntities = [ + roomMembershipEntityFactory.buildWithId({ roomId: roomId1 }), + roomMembershipEntityFactory.buildWithId({ roomId: roomId1 }), + roomMembershipEntityFactory.buildWithId({ roomId: roomId2 }), + ]; + + await em.persistAndFlush(roomMemberEntities); + em.clear(); + + return { roomMemberEntities, roomId1, roomId2 }; + }; + + it('should find room member by roomIds', async () => { + const { roomId1, roomId2 } = await setup(); + + const roomMemberships = await repo.findByRoomIds([roomId1, roomId2]); + + expect(roomMemberships).toHaveLength(3); + }); + }); + + describe('findByGroupId', () => { + const setup = async () => { + const groupId = new ObjectId().toHexString(); + const roomMemberEntities = [ + roomMembershipEntityFactory.build({ userGroupId: groupId }), + roomMembershipEntityFactory.build({ userGroupId: groupId }), + roomMembershipEntityFactory.build({ userGroupId: new ObjectId().toHexString() }), + ]; + + await em.persistAndFlush(roomMemberEntities); + em.clear(); + + return { roomMemberEntities, groupId }; + }; + + it('should find room members by groupId', async () => { + const { groupId } = await setup(); + + const roomMemberships = await repo.findByGroupId(groupId); + + expect(roomMemberships).toHaveLength(2); + }); + }); + + describe('save', () => { + const setup = () => { + const roomMemberships = roomMembershipFactory.buildList(3); + return { roomMemberships }; + }; + + it('should be able to persist a single room member', async () => { + const { roomMemberships } = setup(); + + await repo.save(roomMemberships[0]); + const result = await em.findOneOrFail(RoomMembershipEntity, roomMemberships[0].id); + + expect(roomMemberships[0].getProps()).toMatchObject(result); + }); + + it('should be able to persist many room members', async () => { + const { roomMemberships } = setup(); + + await repo.save(roomMemberships); + const result = await em.find(RoomMembershipEntity, { id: { $in: roomMemberships.map((r) => r.id) } }); + + expect(result.length).toBe(roomMemberships.length); + }); + }); + + describe('delete', () => { + const setup = async () => { + const roomMemberEntities = roomMembershipEntityFactory.buildListWithId(3); + await em.persistAndFlush(roomMemberEntities); + const roomMemberships = roomMemberEntities.map((entity) => new RoomMembership(entity)); + em.clear(); + + return { roomMemberships }; + }; + + it('should be able to delete a single room member', async () => { + const { roomMemberships } = await setup(); + + await repo.delete(roomMemberships[0]); + + await expect(em.findOneOrFail(RoomMembershipEntity, roomMemberships[0].id)).rejects.toThrow(NotFoundError); + }); + + it('should be able to delete many rooms', async () => { + const { roomMemberships } = await setup(); + + await repo.delete(roomMemberships); + + const remainingCount = await em.count(RoomMembershipEntity); + expect(remainingCount).toBe(0); + }); + }); +}); diff --git a/apps/server/src/modules/room-membership/repo/room-membership.repo.ts b/apps/server/src/modules/room-membership/repo/room-membership.repo.ts new file mode 100644 index 00000000000..5db8b3750ce --- /dev/null +++ b/apps/server/src/modules/room-membership/repo/room-membership.repo.ts @@ -0,0 +1,64 @@ +import { Utils } from '@mikro-orm/core'; +import { EntityManager } from '@mikro-orm/mongodb'; +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain/types'; +import { RoomMembership } from '../do/room-membership.do'; +import { RoomMembershipEntity } from './entity'; +import { RoomMembershipDomainMapper } from './room-membership-domain.mapper'; + +@Injectable() +export class RoomMembershipRepo { + constructor(private readonly em: EntityManager) {} + + async findByRoomId(roomId: EntityId): Promise { + const roomMembershipEntities = await this.em.findOne(RoomMembershipEntity, { roomId }); + if (!roomMembershipEntities) return null; + + const roomMemberships = RoomMembershipDomainMapper.mapEntityToDo(roomMembershipEntities); + + return roomMemberships; + } + + async findByRoomIds(roomIds: EntityId[]): Promise { + const entities = await this.em.find(RoomMembershipEntity, { roomId: { $in: roomIds } }); + const roomMemberships = entities.map((entity) => RoomMembershipDomainMapper.mapEntityToDo(entity)); + + return roomMemberships; + } + + async findByGroupId(groupId: EntityId): Promise { + const entities = await this.em.find(RoomMembershipEntity, { userGroupId: groupId }); + const roomMemberships = entities.map((entity) => RoomMembershipDomainMapper.mapEntityToDo(entity)); + + return roomMemberships; + } + + async findByGroupIds(groupIds: EntityId[]): Promise { + const entities = await this.em.find(RoomMembershipEntity, { userGroupId: { $in: groupIds } }); + const roomMemberships = entities.map((entity) => RoomMembershipDomainMapper.mapEntityToDo(entity)); + + return roomMemberships; + } + + async save(roomMembership: RoomMembership | RoomMembership[]): Promise { + const roomMemberships = Utils.asArray(roomMembership); + + roomMemberships.forEach((member) => { + const entity = RoomMembershipDomainMapper.mapDoToEntity(member); + this.em.persist(entity); + }); + + await this.em.flush(); + } + + async delete(roomMembership: RoomMembership | RoomMembership[]): Promise { + const roomMemberships = Utils.asArray(roomMembership); + + roomMemberships.forEach((member) => { + const entity = RoomMembershipDomainMapper.mapDoToEntity(member); + this.em.remove(entity); + }); + + await this.em.flush(); + } +} diff --git a/apps/server/src/modules/room-membership/room-membership.module.ts b/apps/server/src/modules/room-membership/room-membership.module.ts new file mode 100644 index 00000000000..a261d8ea475 --- /dev/null +++ b/apps/server/src/modules/room-membership/room-membership.module.ts @@ -0,0 +1,16 @@ +import { GroupModule } from '@modules/group'; +import { Module } from '@nestjs/common'; +import { CqrsModule } from '@nestjs/cqrs'; +import { AuthorizationModule } from '../authorization'; +import { RoleModule } from '../role'; +import { RoomModule } from '../room/room.module'; +import { RoomMembershipRule } from './authorization/room-membership.rule'; +import { RoomMembershipRepo } from './repo/room-membership.repo'; +import { RoomMembershipService } from './service/room-membership.service'; + +@Module({ + imports: [AuthorizationModule, CqrsModule, GroupModule, RoleModule, RoomModule], + providers: [RoomMembershipService, RoomMembershipRepo, RoomMembershipRule], + exports: [RoomMembershipService], +}) +export class RoomMembershipModule {} diff --git a/apps/server/src/modules/room-member/service/room-member.service.spec.ts b/apps/server/src/modules/room-membership/service/room-membership.service.spec.ts similarity index 51% rename from apps/server/src/modules/room-member/service/room-member.service.spec.ts rename to apps/server/src/modules/room-membership/service/room-membership.service.spec.ts index 69ba478a901..d9134cb2867 100644 --- a/apps/server/src/modules/room-member/service/room-member.service.spec.ts +++ b/apps/server/src/modules/room-membership/service/room-membership.service.spec.ts @@ -1,48 +1,55 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { GroupService, GroupTypes } from '@modules/group'; +import { RoleService } from '@modules/role'; +import { roomFactory } from '@modules/room/testing'; import { BadRequestException } from '@nestjs/common/exceptions'; import { Test, TestingModule } from '@nestjs/testing'; import { RoleName } from '@shared/domain/interface'; import { groupFactory, roleDtoFactory, userFactory } from '@shared/testing'; import { MongoMemoryDatabaseModule } from '@src/infra/database'; -import { GroupService, GroupTypes } from '@src/modules/group'; -import { RoleService } from '@src/modules/role'; -import { roomFactory } from '@src/modules/room/testing'; -import { RoomMemberAuthorizable } from '../do/room-member-authorizable.do'; -import { RoomMemberRepo } from '../repo/room-member.repo'; -import { roomMemberFactory } from '../testing'; -import { RoomMemberService } from './room-member.service'; - -describe('RoomMemberService', () => { +import { RoomService } from '@src/modules/room/domain'; +import { RoomMembershipAuthorizable } from '../do/room-membership-authorizable.do'; +import { RoomMembershipRepo } from '../repo/room-membership.repo'; +import { roomMembershipFactory } from '../testing'; +import { RoomMembershipService } from './room-membership.service'; + +describe('RoomMembershipService', () => { let module: TestingModule; - let service: RoomMemberService; - let roomMemberRepo: DeepMocked; + let service: RoomMembershipService; + let roomMembershipRepo: DeepMocked; let groupService: DeepMocked; let roleService: DeepMocked; + let roomService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ imports: [MongoMemoryDatabaseModule.forRoot()], providers: [ - RoomMemberService, + RoomMembershipService, { provide: GroupService, useValue: createMock(), }, { - provide: RoomMemberRepo, - useValue: createMock(), + provide: RoomMembershipRepo, + useValue: createMock(), }, { provide: RoleService, useValue: createMock(), }, + { + provide: RoomService, + useValue: createMock(), + }, ], }).compile(); - service = module.get(RoomMemberService); - roomMemberRepo = module.get(RoomMemberRepo); + service = module.get(RoomMembershipService); + roomMembershipRepo = module.get(RoomMembershipRepo); groupService = module.get(GroupService); roleService = module.get(RoleService); + roomService = module.get(RoomService); }); afterAll(async () => { @@ -54,16 +61,17 @@ describe('RoomMemberService', () => { }); describe('addMembersToRoom', () => { - describe('when room member does not exist', () => { + describe('when roomMembership does not exist', () => { const setup = () => { const user = userFactory.buildWithId(); const room = roomFactory.build(); const group = groupFactory.build({ type: GroupTypes.ROOM }); - roomMemberRepo.findByRoomId.mockResolvedValue(null); + roomMembershipRepo.findByRoomId.mockResolvedValue(null); groupService.createGroup.mockResolvedValue(group); groupService.addUserToGroup.mockResolvedValue(); - roomMemberRepo.save.mockResolvedValue(); + roomMembershipRepo.save.mockResolvedValue(); + roomService.getSingleRoom.mockResolvedValue(room); return { user, @@ -71,65 +79,77 @@ describe('RoomMemberService', () => { }; }; - it('should create new room member when not exists', async () => { + it('should create new roomMembership when not exists', async () => { + const { user, room } = setup(); + + await service.addMembersToRoom(room.id, [{ userId: user.id, roleName: RoleName.ROOMEDITOR }]); + + expect(roomMembershipRepo.save).toHaveBeenCalled(); + }); + + it('should save the schoolId of the room in the roomMembership', async () => { const { user, room } = setup(); - await service.addMembersToRoom(room.id, [{ userId: user.id, roleName: RoleName.ROOM_EDITOR }]); + await service.addMembersToRoom(room.id, [{ userId: user.id, roleName: RoleName.ROOMEDITOR }]); - expect(roomMemberRepo.save).toHaveBeenCalled(); + expect(roomMembershipRepo.save).toHaveBeenCalledWith( + expect.objectContaining({ + schoolId: room.schoolId, + }) + ); }); describe('when no user is provided', () => { it('should throw an exception', async () => { const { room } = setup(); - roomMemberRepo.findByRoomId.mockResolvedValue(null); + roomMembershipRepo.findByRoomId.mockResolvedValue(null); await expect(service.addMembersToRoom(room.id, [])).rejects.toThrow(); }); }); }); - describe('when room member exists', () => { + describe('when roomMembership exists', () => { const setup = () => { const user = userFactory.buildWithId(); const group = groupFactory.build({ type: GroupTypes.ROOM }); const room = roomFactory.build(); - const roomMember = roomMemberFactory.build({ roomId: room.id, userGroupId: group.id }); + const roomMembership = roomMembershipFactory.build({ roomId: room.id, userGroupId: group.id }); - roomMemberRepo.findByRoomId.mockResolvedValue(roomMember); + roomMembershipRepo.findByRoomId.mockResolvedValue(roomMembership); return { user, room, - roomMember, + roomMembership, group, }; }; - it('should add user to existing room member', async () => { + it('should add user to existing roomMembership', async () => { const { user, room, group } = setup(); - await service.addMembersToRoom(room.id, [{ userId: user.id, roleName: RoleName.ROOM_EDITOR }]); + await service.addMembersToRoom(room.id, [{ userId: user.id, roleName: RoleName.ROOMEDITOR }]); expect(groupService.addUsersToGroup).toHaveBeenCalledWith(group.id, [ - { userId: user.id, roleName: RoleName.ROOM_EDITOR }, + { userId: user.id, roleName: RoleName.ROOMEDITOR }, ]); }); }); }); describe('removeMembersFromRoom', () => { - describe('when room member does not exist', () => { + describe('when roomMembership does not exist', () => { const setup = () => { const user = userFactory.buildWithId(); const room = roomFactory.build(); const group = groupFactory.build({ type: GroupTypes.ROOM }); - roomMemberRepo.findByRoomId.mockResolvedValue(null); + roomMembershipRepo.findByRoomId.mockResolvedValue(null); groupService.createGroup.mockResolvedValue(group); groupService.addUserToGroup.mockResolvedValue(); - roomMemberRepo.save.mockResolvedValue(); + roomMembershipRepo.save.mockResolvedValue(); return { user, @@ -137,35 +157,35 @@ describe('RoomMemberService', () => { }; }; - describe('when roomMember does not exist', () => { + describe('when roomMembership does not exist', () => { it('should throw an exception', async () => { const { room } = setup(); - roomMemberRepo.findByRoomId.mockResolvedValue(null); + roomMembershipRepo.findByRoomId.mockResolvedValue(null); await expect(service.removeMembersFromRoom(room.id, [])).rejects.toThrowError(BadRequestException); }); }); }); - describe('when room member exists', () => { + describe('when roomMembership exists', () => { const setup = () => { const user = userFactory.buildWithId(); const group = groupFactory.build({ type: GroupTypes.ROOM }); const room = roomFactory.build(); - const roomMember = roomMemberFactory.build({ roomId: room.id, userGroupId: group.id }); + const roomMembership = roomMembershipFactory.build({ roomId: room.id, userGroupId: group.id }); - roomMemberRepo.findByRoomId.mockResolvedValue(roomMember); + roomMembershipRepo.findByRoomId.mockResolvedValue(roomMembership); groupService.findById.mockResolvedValue(group); return { user, room, - roomMember, + roomMembership, group, }; }; - it('should remove room member', async () => { + it('should remove roomMembership', async () => { const { user, room, group } = setup(); await service.removeMembersFromRoom(room.id, [user.id]); @@ -175,82 +195,82 @@ describe('RoomMemberService', () => { }); }); - describe('deleteRoomMember', () => { - describe('when room member does not exist', () => { + describe('deleteRoomMembership', () => { + describe('when roomMembership does not exist', () => { const setup = () => { - roomMemberRepo.findByRoomId.mockResolvedValue(null); + roomMembershipRepo.findByRoomId.mockResolvedValue(null); }; it('no nothing', async () => { setup(); - await service.deleteRoomMember('roomId'); + await service.deleteRoomMembership('roomId'); expect(groupService.delete).not.toHaveBeenCalled(); - expect(roomMemberRepo.delete).not.toHaveBeenCalled(); + expect(roomMembershipRepo.delete).not.toHaveBeenCalled(); }); }); - describe('when room member exists', () => { + describe('when roomMembership exists', () => { const setup = () => { const group = groupFactory.build(); - const roomMember = roomMemberFactory.build({ userGroupId: group.id }); - roomMemberRepo.findByRoomId.mockResolvedValue(roomMember); + const roomMembership = roomMembershipFactory.build({ userGroupId: group.id }); + roomMembershipRepo.findByRoomId.mockResolvedValue(roomMembership); groupService.findById.mockResolvedValue(group); - return { roomMember, group }; + return { roomMembership, group }; }; - it('should call delete group and room member', async () => { - const { roomMember, group } = setup(); - await service.deleteRoomMember(roomMember.roomId); + it('should call delete group and roomMembership', async () => { + const { roomMembership, group } = setup(); + await service.deleteRoomMembership(roomMembership.roomId); expect(groupService.delete).toHaveBeenCalledWith(group); - expect(roomMemberRepo.delete).toHaveBeenCalledWith(roomMember); + expect(roomMembershipRepo.delete).toHaveBeenCalledWith(roomMembership); }); }); }); - describe('getRoomMemberAuthorizable', () => { + describe('getRoomMembershipAuthorizable', () => { const setup = () => { const roomId = 'room123'; const userId = 'user456'; const groupId = 'group789'; const roleId = 'role101'; - const roomMember = roomMemberFactory.build({ roomId, userGroupId: groupId }); + const roomMembership = roomMembershipFactory.build({ roomId, userGroupId: groupId }); const group = groupFactory.build({ id: groupId, users: [{ userId, roleId }] }); const role = roleDtoFactory.build({ id: roleId }); - roomMemberRepo.findByRoomId.mockResolvedValue(roomMember); + roomMembershipRepo.findByRoomId.mockResolvedValue(roomMembership); groupService.findById.mockResolvedValue(group); roleService.findByIds.mockResolvedValue([role]); - return { roomId, userId, groupId, roleId, roomMember, group, role }; + return { roomId, userId, groupId, roleId, roomMembership, group, role }; }; - it('should return RoomMemberAuthorizable when room member exists', async () => { + it('should return RoomMembershipAuthorizable when roomMembership exists', async () => { const { roomId, userId, roleId } = setup(); - const result = await service.getRoomMemberAuthorizable(roomId); + const result = await service.getRoomMembershipAuthorizable(roomId); - expect(result).toBeInstanceOf(RoomMemberAuthorizable); + expect(result).toBeInstanceOf(RoomMembershipAuthorizable); expect(result.roomId).toBe(roomId); expect(result.members).toHaveLength(1); expect(result.members[0].userId).toBe(userId); expect(result.members[0].roles[0].id).toBe(roleId); }); - it('should return empty RoomMemberAuthorizable when room member not exists', async () => { + it('should return empty RoomMembershipAuthorizable when roomMembership not exists', async () => { const roomId = 'nonexistent'; - roomMemberRepo.findByRoomId.mockResolvedValue(null); + roomMembershipRepo.findByRoomId.mockResolvedValue(null); - const result = await service.getRoomMemberAuthorizable(roomId); + const result = await service.getRoomMembershipAuthorizable(roomId); - expect(result).toBeInstanceOf(RoomMemberAuthorizable); + expect(result).toBeInstanceOf(RoomMembershipAuthorizable); expect(result.roomId).toBe(roomId); expect(result.members).toHaveLength(0); }); }); - describe('getRoomMemberAuthorizablesByUserId', () => { + describe('getRoomMembershipAuthorizablesByUserId', () => { const setup = () => { const userId = 'user123'; const groupId1 = 'group456'; @@ -264,31 +284,31 @@ describe('RoomMemberService', () => { groupFactory.build({ id: groupId1, users: [{ userId, roleId: roleId1 }] }), groupFactory.build({ id: groupId2, users: [{ userId, roleId: roleId2 }] }), ]; - const roomMembers = [ - roomMemberFactory.build({ roomId: roomId1, userGroupId: groupId1 }), - roomMemberFactory.build({ roomId: roomId2, userGroupId: groupId2 }), + const roomMemberships = [ + roomMembershipFactory.build({ roomId: roomId1, userGroupId: groupId1 }), + roomMembershipFactory.build({ roomId: roomId2, userGroupId: groupId2 }), ]; const roles = [roleDtoFactory.build({ id: roleId1 }), roleDtoFactory.build({ id: roleId2 })]; groupService.findGroups.mockResolvedValue({ data: groups, total: groups.length }); - roomMemberRepo.findByGroupIds.mockResolvedValue(roomMembers); + roomMembershipRepo.findByGroupIds.mockResolvedValue(roomMemberships); roleService.findByIds.mockResolvedValue(roles); - return { userId, roomMembers, roles }; + return { userId, roomMemberships, roles }; }; - it('should return RoomMemberAuthorizables for user', async () => { - const { userId, roomMembers, roles } = setup(); + it('should return RoomMembershipAuthorizables for user', async () => { + const { userId, roomMemberships, roles } = setup(); - const result = await service.getRoomMemberAuthorizablesByUserId(userId); + const result = await service.getRoomMembershipAuthorizablesByUserId(userId); expect(result).toHaveLength(2); - expect(result[0]).toBeInstanceOf(RoomMemberAuthorizable); - expect(result[0].roomId).toBe(roomMembers[0].roomId); + expect(result[0]).toBeInstanceOf(RoomMembershipAuthorizable); + expect(result[0].roomId).toBe(roomMemberships[0].roomId); expect(result[0].members[0].userId).toBe(userId); expect(result[0].members[0].roles[0].id).toBe(roles[0].id); - expect(result[1]).toBeInstanceOf(RoomMemberAuthorizable); - expect(result[1].roomId).toBe(roomMembers[1].roomId); + expect(result[1]).toBeInstanceOf(RoomMembershipAuthorizable); + expect(result[1].roomId).toBe(roomMemberships[1].roomId); expect(result[1].members[0].userId).toBe(userId); expect(result[1].members[0].roles[0].id).toBe(roles[1].id); }); @@ -297,7 +317,7 @@ describe('RoomMemberService', () => { const { userId } = setup(); groupService.findGroups.mockResolvedValue({ data: [], total: 0 }); - const result = await service.getRoomMemberAuthorizablesByUserId(userId); + const result = await service.getRoomMembershipAuthorizablesByUserId(userId); expect(result).toHaveLength(0); }); 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 new file mode 100644 index 00000000000..75e6e6922eb --- /dev/null +++ b/apps/server/src/modules/room-membership/service/room-membership.service.ts @@ -0,0 +1,144 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { Group, GroupService, GroupTypes } from '@modules/group'; +import { RoleDto, RoleService } from '@modules/role'; +import { BadRequestException, Injectable } from '@nestjs/common'; +import { RoleName } from '@shared/domain/interface'; +import { EntityId } from '@shared/domain/types'; +import { RoomService } from '@src/modules/room/domain'; +import { RoomMembershipAuthorizable, UserWithRoomRoles } from '../do/room-membership-authorizable.do'; +import { RoomMembership } from '../do/room-membership.do'; +import { RoomMembershipRepo } from '../repo/room-membership.repo'; + +@Injectable() +export class RoomMembershipService { + constructor( + private readonly groupService: GroupService, + private readonly roomMembershipRepo: RoomMembershipRepo, + private readonly roleService: RoleService, + private readonly roomService: RoomService + ) {} + + private async createNewRoomMembership( + roomId: EntityId, + userId: EntityId, + roleName: RoleName.ROOMEDITOR | RoleName.ROOMVIEWER + ) { + const room = await this.roomService.getSingleRoom(roomId); + + const group = await this.groupService.createGroup( + `Room Members for Room ${roomId}`, + GroupTypes.ROOM, + room.schoolId + ); + await this.groupService.addUsersToGroup(group.id, [{ userId, roleName }]); + + const roomMembership = new RoomMembership({ + id: new ObjectId().toHexString(), + roomId, + userGroupId: group.id, + schoolId: room.schoolId, + createdAt: new Date(), + updatedAt: new Date(), + }); + + await this.roomMembershipRepo.save(roomMembership); + + return roomMembership; + } + + private buildRoomMembershipAuthorizable( + roomId: EntityId, + group: Group, + roleSet: RoleDto[] + ): RoomMembershipAuthorizable { + const members = group.users.map((groupUser): UserWithRoomRoles => { + const roleDto = roleSet.find((role) => role.id === groupUser.roleId); + if (roleDto === undefined) throw new BadRequestException('Role not found'); + return { + roles: [roleDto], + userId: groupUser.userId, + }; + }); + + const roomMembershipAuthorizable = new RoomMembershipAuthorizable(roomId, members); + + return roomMembershipAuthorizable; + } + + public async deleteRoomMembership(roomId: EntityId) { + const roomMembership = await this.roomMembershipRepo.findByRoomId(roomId); + if (roomMembership === null) return; + + const group = await this.groupService.findById(roomMembership.userGroupId); + await this.groupService.delete(group); + await this.roomMembershipRepo.delete(roomMembership); + } + + public async addMembersToRoom( + roomId: EntityId, + userIdsAndRoles: Array<{ userId: EntityId; roleName: RoleName.ROOMEDITOR | RoleName.ROOMVIEWER }> + ): Promise { + const roomMembership = await this.roomMembershipRepo.findByRoomId(roomId); + if (roomMembership === null) { + const firstUser = userIdsAndRoles.shift(); + if (firstUser === undefined) { + throw new BadRequestException('No user provided'); + } + const newRoomMembership = await this.createNewRoomMembership(roomId, firstUser.userId, firstUser.roleName); + return newRoomMembership.id; + } + + await this.groupService.addUsersToGroup(roomMembership.userGroupId, userIdsAndRoles); + + return roomMembership.id; + } + + 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'); + } + + const group = await this.groupService.findById(roomMembership.userGroupId); + await this.groupService.removeUsersFromGroup(group.id, userIds); + } + + public async getRoomMembershipAuthorizablesByUserId(userId: EntityId): Promise { + const groupPage = await this.groupService.findGroups({ userId, groupTypes: [GroupTypes.ROOM] }); + const groupIds = groupPage.data.map((group) => group.id); + const roomMemberships = await this.roomMembershipRepo.findByGroupIds(groupIds); + const roleIds = groupPage.data.flatMap((group) => group.users.map((groupUser) => groupUser.roleId)); + const roleSet = await this.roleService.findByIds(roleIds); + const roomMembershipAuthorizables = roomMemberships + .map((item) => { + const group = groupPage.data.find((g) => g.id === item.userGroupId); + if (!group) return null; + return this.buildRoomMembershipAuthorizable(item.roomId, group, roleSet); + }) + .filter((item): item is RoomMembershipAuthorizable => item !== null); + + return roomMembershipAuthorizables; + } + + public async getRoomMembershipAuthorizable(roomId: EntityId): Promise { + const roomMembership = await this.roomMembershipRepo.findByRoomId(roomId); + if (roomMembership === null) { + return new RoomMembershipAuthorizable(roomId, []); + } + const group = await this.groupService.findById(roomMembership.userGroupId); + const roleSet = await this.roleService.findByIds(group.users.map((groupUser) => groupUser.roleId)); + + const members = group.users.map((groupUser): UserWithRoomRoles => { + const roleDto = roleSet.find((role) => role.id === groupUser.roleId); + if (roleDto === undefined) throw new BadRequestException('Role not found'); + return { + roles: [roleDto], + userId: groupUser.userId, + }; + }); + + const roomMembershipAuthorizable = new RoomMembershipAuthorizable(roomId, members); + + return roomMembershipAuthorizable; + } +} diff --git a/apps/server/src/modules/room-membership/testing/index.ts b/apps/server/src/modules/room-membership/testing/index.ts new file mode 100644 index 00000000000..a29c3fae4a1 --- /dev/null +++ b/apps/server/src/modules/room-membership/testing/index.ts @@ -0,0 +1,2 @@ +export { roomMembershipEntityFactory } from './room-membership-entity.factory'; +export { roomMembershipFactory } from './room-membership.factory'; diff --git a/apps/server/src/modules/room-member/testing/room-member-entity.factory.ts b/apps/server/src/modules/room-membership/testing/room-membership-entity.factory.ts similarity index 50% rename from apps/server/src/modules/room-member/testing/room-member-entity.factory.ts rename to apps/server/src/modules/room-membership/testing/room-membership-entity.factory.ts index b63f70ecffd..8ea19809b55 100644 --- a/apps/server/src/modules/room-member/testing/room-member-entity.factory.ts +++ b/apps/server/src/modules/room-membership/testing/room-membership-entity.factory.ts @@ -1,14 +1,16 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { EntityFactory } from '@shared/testing/factory/entity.factory'; -import { RoomMemberEntity, RoomMemberEntityProps } from '../repo/entity/room-member.entity'; +import { RoomMembershipEntity } from '../repo/entity/room-membership.entity'; +import { RoomMembershipProps } from '../do/room-membership.do'; -export const roomMemberEntityFactory = EntityFactory.define( - RoomMemberEntity, +export const roomMembershipEntityFactory = EntityFactory.define( + RoomMembershipEntity, () => { return { id: new ObjectId().toHexString(), roomId: new ObjectId().toHexString(), userGroupId: new ObjectId().toHexString(), + schoolId: new ObjectId().toHexString(), createdAt: new Date(), updatedAt: new Date(), }; diff --git a/apps/server/src/modules/room-member/testing/room-member.factory.ts b/apps/server/src/modules/room-membership/testing/room-membership.factory.ts similarity index 51% rename from apps/server/src/modules/room-member/testing/room-member.factory.ts rename to apps/server/src/modules/room-membership/testing/room-membership.factory.ts index 829f0f1708c..64c294187cb 100644 --- a/apps/server/src/modules/room-member/testing/room-member.factory.ts +++ b/apps/server/src/modules/room-membership/testing/room-membership.factory.ts @@ -1,12 +1,13 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { BaseFactory } from '@shared/testing'; -import { RoomMember, RoomMemberProps } from '../do/room-member.do'; +import { RoomMembership, RoomMembershipProps } from '../do/room-membership.do'; -export const roomMemberFactory = BaseFactory.define(RoomMember, () => { - const props: RoomMemberProps = { +export const roomMembershipFactory = BaseFactory.define(RoomMembership, () => { + const props: RoomMembershipProps = { id: new ObjectId().toHexString(), roomId: new ObjectId().toHexString(), userGroupId: new ObjectId().toHexString(), + schoolId: new ObjectId().toHexString(), createdAt: new Date(), updatedAt: new Date(), }; diff --git a/apps/server/src/modules/room/api/dto/request/create-room.body.params.ts b/apps/server/src/modules/room/api/dto/request/create-room.body.params.ts index 286abeba50c..42e4e5b586e 100644 --- a/apps/server/src/modules/room/api/dto/request/create-room.body.params.ts +++ b/apps/server/src/modules/room/api/dto/request/create-room.body.params.ts @@ -1,10 +1,10 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { SanitizeHtml } from '@shared/controller'; -import { RoomCreateProps } from '@src/modules/room/domain'; -import { RoomColor } from '@src/modules/room/domain/type'; +import { NullToUndefined, SanitizeHtml } from '@shared/controller'; +import { RoomCreateProps } from '@modules/room/domain'; +import { RoomColor } from '@modules/room/domain/type'; import { IsDate, IsEnum, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; -export class CreateRoomBodyParams implements RoomCreateProps { +export class CreateRoomBodyParams implements Omit { @ApiProperty({ description: 'The name of the room', required: true, @@ -23,8 +23,9 @@ export class CreateRoomBodyParams implements RoomCreateProps { @IsEnum(RoomColor) color!: RoomColor; - @IsDate() @IsOptional() + @NullToUndefined() + @IsDate() @ApiPropertyOptional({ description: 'Start date of the room', required: false, @@ -32,8 +33,9 @@ export class CreateRoomBodyParams implements RoomCreateProps { }) startDate?: Date; - @IsDate() @IsOptional() + @NullToUndefined() + @IsDate() @ApiPropertyOptional({ description: 'End date of the room', required: false, diff --git a/apps/server/src/modules/room/api/dto/request/update-room.body.params.ts b/apps/server/src/modules/room/api/dto/request/update-room.body.params.ts index 8afebced7d3..71bf7ac10a1 100644 --- a/apps/server/src/modules/room/api/dto/request/update-room.body.params.ts +++ b/apps/server/src/modules/room/api/dto/request/update-room.body.params.ts @@ -1,7 +1,7 @@ +import { RoomUpdateProps } from '@modules/room/domain'; +import { RoomColor } from '@modules/room/domain/type'; import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { SanitizeHtml } from '@shared/controller'; -import { RoomUpdateProps } from '@src/modules/room/domain'; -import { RoomColor } from '@src/modules/room/domain/type'; +import { NullToUndefined, SanitizeHtml } from '@shared/controller'; import { IsDate, IsEnum, IsOptional, IsString, MaxLength, MinLength } from 'class-validator'; export class UpdateRoomBodyParams implements RoomUpdateProps { @@ -25,6 +25,7 @@ export class UpdateRoomBodyParams implements RoomUpdateProps { @IsDate() @IsOptional() + @NullToUndefined() @ApiPropertyOptional({ description: 'Start date of the room', required: false, @@ -34,6 +35,7 @@ export class UpdateRoomBodyParams implements RoomUpdateProps { @IsDate() @IsOptional() + @NullToUndefined() @ApiPropertyOptional({ description: 'Start date of the room', required: false, diff --git a/apps/server/src/modules/room/api/dto/response/room-board-item.response.ts b/apps/server/src/modules/room/api/dto/response/room-board-item.response.ts index 0840c1c1d87..f96d927741b 100644 --- a/apps/server/src/modules/room/api/dto/response/room-board-item.response.ts +++ b/apps/server/src/modules/room/api/dto/response/room-board-item.response.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { BoardLayout } from '@src/modules/board'; +import { BoardLayout } from '@modules/board'; export class RoomBoardItemResponse { @ApiProperty() diff --git a/apps/server/src/modules/room/api/dto/response/room-details.response.ts b/apps/server/src/modules/room/api/dto/response/room-details.response.ts index f0d39b68284..bb3a385941e 100644 --- a/apps/server/src/modules/room/api/dto/response/room-details.response.ts +++ b/apps/server/src/modules/room/api/dto/response/room-details.response.ts @@ -1,5 +1,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { RoomColor } from '@src/modules/room/domain/type'; +import { Permission } from '@shared/domain/interface'; +import { RoomColor } from '@modules/room/domain/type'; import { IsEnum } from 'class-validator'; export class RoomDetailsResponse { @@ -13,6 +14,9 @@ export class RoomDetailsResponse { @IsEnum(RoomColor) color: RoomColor; + @ApiProperty() + schoolId: string; + @ApiPropertyOptional({ type: Date }) startDate?: Date; @@ -25,14 +29,20 @@ export class RoomDetailsResponse { @ApiProperty({ type: Date }) updatedAt: Date; + @ApiProperty({ enum: Permission, isArray: true, enumName: 'Permission' }) + permissions: Permission[]; + constructor(room: RoomDetailsResponse) { this.id = room.id; this.name = room.name; this.color = room.color; + this.schoolId = room.schoolId; this.startDate = room.startDate; this.endDate = room.endDate; this.createdAt = room.createdAt; this.updatedAt = room.updatedAt; + + this.permissions = room.permissions; } } diff --git a/apps/server/src/modules/room/api/dto/response/room-item.response.ts b/apps/server/src/modules/room/api/dto/response/room-item.response.ts index 88b13ec3dab..3c8bffded23 100644 --- a/apps/server/src/modules/room/api/dto/response/room-item.response.ts +++ b/apps/server/src/modules/room/api/dto/response/room-item.response.ts @@ -13,6 +13,9 @@ export class RoomItemResponse { @IsEnum(RoomColor) color: RoomColor; + @ApiProperty() + schoolId: string; + @ApiPropertyOptional({ type: Date }) startDate?: Date; @@ -29,6 +32,7 @@ export class RoomItemResponse { this.id = room.id; this.name = room.name; this.color = room.color; + this.schoolId = room.schoolId; this.startDate = room.startDate; this.endDate = room.endDate; diff --git a/apps/server/src/modules/room/api/mapper/room.mapper.ts b/apps/server/src/modules/room/api/mapper/room.mapper.ts index 02fd43a659f..1f7d18ebc65 100644 --- a/apps/server/src/modules/room/api/mapper/room.mapper.ts +++ b/apps/server/src/modules/room/api/mapper/room.mapper.ts @@ -1,5 +1,6 @@ +import { ColumnBoard } from '@modules/board'; import { Page } from '@shared/domain/domainobject'; -import { ColumnBoard } from '@src/modules/board'; +import { Permission } from '@shared/domain/interface'; import { Room } from '../../domain/do/room.do'; import { RoomPaginationParams } from '../dto/request/room-pagination.params'; import { RoomBoardItemResponse } from '../dto/response/room-board-item.response'; @@ -14,6 +15,7 @@ export class RoomMapper { id: room.id, name: room.name, color: room.color, + schoolId: room.schoolId, startDate: room.startDate, endDate: room.endDate, createdAt: room.createdAt, @@ -32,15 +34,17 @@ export class RoomMapper { return response; } - static mapToRoomDetailsResponse(room: Room): RoomDetailsResponse { + static mapToRoomDetailsResponse(room: Room, permissions: Permission[]): RoomDetailsResponse { const response = new RoomDetailsResponse({ id: room.id, name: room.name, color: room.color, + schoolId: room.schoolId, startDate: room.startDate, endDate: room.endDate, createdAt: room.createdAt, updatedAt: room.updatedAt, + permissions, }); return response; diff --git a/apps/server/src/modules/room/api/room.controller.ts b/apps/server/src/modules/room/api/room.controller.ts index 2f54d0f65a8..b9a125787cc 100644 --- a/apps/server/src/modules/room/api/room.controller.ts +++ b/apps/server/src/modules/room/api/room.controller.ts @@ -11,6 +11,7 @@ import { Param, Patch, Post, + Put, Query, UnauthorizedException, } from '@nestjs/common'; @@ -89,9 +90,9 @@ export class RoomController { @CurrentUser() currentUser: ICurrentUser, @Param() urlParams: RoomUrlParams ): Promise { - const room = await this.roomUc.getSingleRoom(currentUser.userId, urlParams.roomId); + const { room, permissions } = await this.roomUc.getSingleRoom(currentUser.userId, urlParams.roomId); - const response = RoomMapper.mapToRoomDetailsResponse(room); + const response = RoomMapper.mapToRoomDetailsResponse(room, permissions); return response; } @@ -115,8 +116,8 @@ export class RoomController { return response; } - @Patch(':roomId') - @ApiOperation({ summary: 'Create a new room' }) + @Put(':roomId') + @ApiOperation({ summary: 'Update an existing room' }) @ApiResponse({ status: HttpStatus.OK, description: 'Returns the details of a room', type: RoomDetailsResponse }) @ApiResponse({ status: HttpStatus.BAD_REQUEST, type: ApiValidationError }) @ApiResponse({ status: HttpStatus.UNAUTHORIZED, type: UnauthorizedException }) @@ -128,9 +129,9 @@ export class RoomController { @Param() urlParams: RoomUrlParams, @Body() updateRoomParams: UpdateRoomBodyParams ): Promise { - const room = await this.roomUc.updateRoom(currentUser.userId, urlParams.roomId, updateRoomParams); + const { room, permissions } = await this.roomUc.updateRoom(currentUser.userId, urlParams.roomId, updateRoomParams); - const response = RoomMapper.mapToRoomDetailsResponse(room); + const response = RoomMapper.mapToRoomDetailsResponse(room, permissions); return response; } diff --git a/apps/server/src/modules/room/api/room.uc.spec.ts b/apps/server/src/modules/room/api/room.uc.spec.ts index ab0e958ae6c..8910130093e 100644 --- a/apps/server/src/modules/room/api/room.uc.spec.ts +++ b/apps/server/src/modules/room/api/room.uc.spec.ts @@ -1,6 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { AuthorizationService } from '@modules/authorization'; -import { RoomMemberRepo, RoomMemberService } from '@modules/room-member'; +import { RoomMembershipRepo, RoomMembershipService } from '@src/modules/room-membership'; import { UserService } from '@modules/user'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; @@ -8,7 +8,7 @@ import { FeatureDisabledLoggableException } from '@shared/common/loggable-except import { Page } from '@shared/domain/domainobject'; import { IFindOptions } from '@shared/domain/interface'; import { setupEntities, userFactory } from '@shared/testing'; -import { ColumnBoardService } from '@src/modules/board'; +import { ColumnBoardService } from '@modules/board'; import { Room, RoomService } from '../domain'; import { RoomColor } from '../domain/type'; import { roomFactory } from '../testing'; @@ -20,7 +20,7 @@ describe('RoomUc', () => { let configService: DeepMocked; let roomService: DeepMocked; let authorizationService: DeepMocked; - let roomMemberService: DeepMocked; + let roomMembershipService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ providers: [ @@ -34,8 +34,8 @@ describe('RoomUc', () => { useValue: createMock(), }, { - provide: RoomMemberService, - useValue: createMock(), + provide: RoomMembershipService, + useValue: createMock(), }, { provide: ColumnBoardService, @@ -46,8 +46,8 @@ describe('RoomUc', () => { useValue: createMock(), }, { - provide: RoomMemberRepo, - useValue: createMock(), + provide: RoomMembershipRepo, + useValue: createMock(), }, { provide: UserService, @@ -60,7 +60,7 @@ describe('RoomUc', () => { configService = module.get(ConfigService); roomService = module.get(RoomService); authorizationService = module.get(AuthorizationService); - roomMemberService = module.get(RoomMemberService); + roomMembershipService = module.get(RoomMembershipService); await setupEntities(); }); @@ -117,7 +117,7 @@ describe('RoomUc', () => { authorizationService.checkOneOfPermissions.mockReturnValue(undefined); const room = roomFactory.build(); roomService.createRoom.mockResolvedValue(room); - roomMemberService.addMembersToRoom.mockRejectedValue(new Error('test')); + roomMembershipService.addMembersToRoom.mockRejectedValue(new Error('test')); return { user, room }; }; diff --git a/apps/server/src/modules/room/api/room.uc.ts b/apps/server/src/modules/room/api/room.uc.ts index 1691df897a9..a80e2838c66 100644 --- a/apps/server/src/modules/room/api/room.uc.ts +++ b/apps/server/src/modules/room/api/room.uc.ts @@ -1,5 +1,5 @@ import { Action, AuthorizationService } from '@modules/authorization'; -import { RoomMemberService, UserWithRoomRoles } from '@modules/room-member'; +import { RoomMembershipAuthorizable, RoomMembershipService, UserWithRoomRoles } from '@src/modules/room-membership'; import { UserService } from '@modules/user'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; @@ -7,9 +7,11 @@ import { FeatureDisabledLoggableException } from '@shared/common/loggable-except import { Page, UserDO } from '@shared/domain/domainobject'; import { IFindOptions, Permission, RoleName, RoomRole } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { BoardExternalReferenceType, ColumnBoard, ColumnBoardService } from '@src/modules/board'; -import { Room, RoomCreateProps, RoomService, RoomUpdateProps } from '../domain'; +import { BoardExternalReferenceType, ColumnBoard, ColumnBoardService } from '@modules/board'; +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'; @Injectable() @@ -17,7 +19,7 @@ export class RoomUc { constructor( private readonly configService: ConfigService, private readonly roomService: RoomService, - private readonly roomMemberService: RoomMemberService, + private readonly roomMembershipService: RoomMembershipService, private readonly columnBoardService: ColumnBoardService, private readonly userService: UserService, private readonly authorizationService: AuthorizationService @@ -31,28 +33,31 @@ export class RoomUc { return rooms; } - public async createRoom(userId: EntityId, props: RoomCreateProps): Promise { + public async createRoom(userId: EntityId, props: CreateRoomBodyParams): Promise { this.checkFeatureEnabled(); - const user = await this.authorizationService.getUserWithPermissions(userId); - const room = await this.roomService.createRoom(props); - // NOTE: currently only teacher are allowed to create rooms. Could not find simpler way to check this. - this.authorizationService.checkOneOfPermissions(user, [Permission.COURSE_CREATE]); - await this.roomMemberService - .addMembersToRoom(room.id, [{ userId: user.id, roleName: RoleName.ROOM_EDITOR }], user.school.id) + const room = await this.roomService.createRoom({ ...props, schoolId: user.school.id }); + + this.authorizationService.checkOneOfPermissions(user, [Permission.ROOM_CREATE]); + + await this.roomMembershipService + .addMembersToRoom(room.id, [{ userId: user.id, roleName: RoleName.ROOMEDITOR }]) .catch(async (err) => { await this.roomService.deleteRoom(room); throw err; }); + return room; } - public async getSingleRoom(userId: EntityId, roomId: EntityId): Promise { + public async getSingleRoom(userId: EntityId, roomId: EntityId): Promise<{ room: Room; permissions: Permission[] }> { this.checkFeatureEnabled(); const room = await this.roomService.getSingleRoom(roomId); - await this.checkRoomAuthorization(userId, roomId, Action.read); - return room; + const roomMembershipAuthorizable = await this.checkRoomAuthorization(userId, roomId, Action.read); + const permissions = this.getPermissions(userId, roomMembershipAuthorizable); + + return { room, permissions }; } public async getRoomBoards(userId: EntityId, roomId: EntityId): Promise { @@ -72,38 +77,45 @@ export class RoomUc { return boards; } - public async updateRoom(userId: EntityId, roomId: EntityId, props: RoomUpdateProps): Promise { + public async updateRoom( + userId: EntityId, + roomId: EntityId, + props: UpdateRoomBodyParams + ): Promise<{ room: Room; permissions: Permission[] }> { this.checkFeatureEnabled(); const room = await this.roomService.getSingleRoom(roomId); - await this.checkRoomAuthorization(userId, roomId, Action.write); + const roomMembershipAuthorizable = await this.checkRoomAuthorization(userId, roomId, Action.write); + const permissions = this.getPermissions(userId, roomMembershipAuthorizable); + await this.roomService.updateRoom(room, props); - return room; + return { room, permissions }; } public async deleteRoom(userId: EntityId, roomId: EntityId): Promise { this.checkFeatureEnabled(); const room = await this.roomService.getSingleRoom(roomId); - await this.checkRoomAuthorization(userId, roomId, Action.write); + await this.checkRoomAuthorization(userId, roomId, Action.write, [Permission.ROOM_DELETE]); await this.roomService.deleteRoom(room); + await this.roomMembershipService.deleteRoomMembership(roomId); } public async getRoomMembers(userId: EntityId, roomId: EntityId): Promise { this.checkFeatureEnabled(); - const roomMemberAuthorizable = await this.roomMemberService.getRoomMemberAuthorizable(roomId); + const roomMembershipAuthorizable = await this.roomMembershipService.getRoomMembershipAuthorizable(roomId); const currentUser = await this.authorizationService.getUserWithPermissions(userId); - this.authorizationService.checkPermission(currentUser, roomMemberAuthorizable, { + this.authorizationService.checkPermission(currentUser, roomMembershipAuthorizable, { action: Action.read, requiredPermissions: [], }); - const userIds = roomMemberAuthorizable.members.map((member) => member.userId); + const userIds = roomMembershipAuthorizable.members.map((member) => member.userId); const users = await this.userService.findByIds(userIds); const memberResponses = users.map((user) => { - const member = roomMemberAuthorizable.members.find((item) => item.userId === user.id); + const member = roomMembershipAuthorizable.members.find((item) => item.userId === user.id); if (!member) { /* istanbul ignore next */ throw new Error('User not found in room members'); @@ -121,7 +133,7 @@ export class RoomUc { ): Promise { this.checkFeatureEnabled(); await this.checkRoomAuthorization(currentUserId, roomId, Action.write); - await this.roomMemberService.addMembersToRoom(roomId, userIdsAndRoles); + await this.roomMembershipService.addMembersToRoom(roomId, userIdsAndRoles); } private mapToMember(member: UserWithRoomRoles, user: UserDO) { @@ -137,12 +149,12 @@ export class RoomUc { public async removeMembersFromRoom(currentUserId: EntityId, roomId: EntityId, userIds: EntityId[]): Promise { this.checkFeatureEnabled(); await this.checkRoomAuthorization(currentUserId, roomId, Action.write); - await this.roomMemberService.removeMembersFromRoom(roomId, userIds); + await this.roomMembershipService.removeMembersFromRoom(roomId, userIds); } private async getAuthorizedRoomIds(userId: EntityId, action: Action): Promise { const user = await this.authorizationService.getUserWithPermissions(userId); - const roomAuthorizables = await this.roomMemberService.getRoomMemberAuthorizablesByUserId(userId); + const roomAuthorizables = await this.roomMembershipService.getRoomMembershipAuthorizablesByUserId(userId); const authorizedRoomIds = roomAuthorizables.filter((item) => this.authorizationService.hasPermission(user, item, { action, requiredPermissions: [] }) @@ -151,10 +163,26 @@ export class RoomUc { return authorizedRoomIds.map((item) => item.roomId); } - private async checkRoomAuthorization(userId: EntityId, roomId: EntityId, action: Action): Promise { - const roomMemberAuthorizable = await this.roomMemberService.getRoomMemberAuthorizable(roomId); + private async checkRoomAuthorization( + userId: EntityId, + roomId: EntityId, + action: Action, + requiredPermissions: Permission[] = [] + ): Promise { + const roomMembershipAuthorizable = await this.roomMembershipService.getRoomMembershipAuthorizable(roomId); const user = await this.authorizationService.getUserWithPermissions(userId); - this.authorizationService.checkPermission(user, roomMemberAuthorizable, { action, requiredPermissions: [] }); + this.authorizationService.checkPermission(user, roomMembershipAuthorizable, { action, requiredPermissions }); + + return roomMembershipAuthorizable; + } + + private getPermissions(userId: EntityId, roomMembershipAuthorizable: RoomMembershipAuthorizable): Permission[] { + const permissions = roomMembershipAuthorizable.members + .filter((member) => member.userId === userId) + .flatMap((member) => member.roles) + .flatMap((role) => role.permissions ?? []); + + return permissions; } private checkFeatureEnabled(): void { diff --git a/apps/server/src/modules/room/api/test/room-add-members.api.spec.ts b/apps/server/src/modules/room/api/test/room-add-members.api.spec.ts index 40f46dd24f1..5fab1cced7e 100644 --- a/apps/server/src/modules/room/api/test/room-add-members.api.spec.ts +++ b/apps/server/src/modules/room/api/test/room-add-members.api.spec.ts @@ -10,9 +10,9 @@ import { groupEntityFactory, roleFactory, } from '@shared/testing'; -import { GroupEntityTypes } from '@src/modules/group/entity/group.entity'; -import { roomMemberEntityFactory } from '@src/modules/room-member/testing/room-member-entity.factory'; -import { ServerTestModule, serverConfig, type ServerConfig } from '@src/modules/server'; +import { GroupEntityTypes } from '@modules/group/entity/group.entity'; +import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing/room-membership-entity.factory'; +import { ServerTestModule, serverConfig, type ServerConfig } from '@modules/server'; import { roomEntityFactory } from '../../testing/room-entity.factory'; describe('Room Controller (API)', () => { @@ -50,7 +50,7 @@ describe('Room Controller (API)', () => { const { teacherAccount: otherTeacherAccount, teacherUser: otherTeacherUser } = UserAndAccountTestFactory.buildTeacher({ school: teacherUser.school }); const role = roleFactory.buildWithId({ - name: RoleName.ROOM_EDITOR, + name: RoleName.ROOMEDITOR, permissions: [Permission.ROOM_VIEW, Permission.ROOM_EDIT], }); // TODO: add more than one user @@ -61,10 +61,10 @@ describe('Room Controller (API)', () => { externalSource: undefined, }); - const roomMembers = roomMemberEntityFactory.build({ userGroupId: userGroupEntity.id, roomId: room.id }); + const roomMemberships = roomMembershipEntityFactory.build({ userGroupId: userGroupEntity.id, roomId: room.id }); await em.persistAndFlush([ room, - roomMembers, + roomMemberships, teacherAccount, teacherUser, otherTeacherUser, @@ -120,7 +120,7 @@ describe('Room Controller (API)', () => { const { loggedInClient, room, otherTeacherUser } = await setupRoomWithMembers(); const response = await loggedInClient.patch(`/${room.id}/members/add`, { - userIdsAndRoles: [{ userId: otherTeacherUser.id, roleName: RoleName.ROOM_EDITOR }], + userIdsAndRoles: [{ userId: otherTeacherUser.id, roleName: RoleName.ROOMEDITOR }], }); expect(response.status).toBe(HttpStatus.OK); diff --git a/apps/server/src/modules/room/api/test/room-create.api.spec.ts b/apps/server/src/modules/room/api/test/room-create.api.spec.ts index a2a44adc20a..eeca260725b 100644 --- a/apps/server/src/modules/room/api/test/room-create.api.spec.ts +++ b/apps/server/src/modules/room/api/test/room-create.api.spec.ts @@ -2,9 +2,9 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { TestApiClient, UserAndAccountTestFactory, cleanupCollections, roleFactory } from '@shared/testing'; -import { ServerTestModule, serverConfig, type ServerConfig } from '@src/modules/server'; -import { RoomMemberEntity } from '@src/modules/room-member'; -import { GroupEntity } from '@src/modules/group/entity'; +import { ServerTestModule, serverConfig, type ServerConfig } from '@modules/server'; +import { RoomMembershipEntity } from '@src/modules/room-membership'; +import { GroupEntity } from '@modules/group/entity'; import { Permission, RoleName } from '@shared/domain/interface'; import { RoomEntity } from '../../repo'; @@ -69,7 +69,7 @@ describe('Room Controller (API)', () => { const setup = async () => { const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); const role = roleFactory.buildWithId({ - name: RoleName.ROOM_EDITOR, + name: RoleName.ROOMEDITOR, permissions: [Permission.ROOM_EDIT, Permission.ROOM_VIEW], }); await em.persistAndFlush([teacherAccount, teacherUser, role]); @@ -98,13 +98,13 @@ describe('Room Controller (API)', () => { const response = await loggedInClient.post(undefined, params); const roomId = (response.body as { id: string }).id; - const roomMember = await em.findOneOrFail(RoomMemberEntity, { roomId }); + const roomMembership = await em.findOneOrFail(RoomMembershipEntity, { roomId }); const userGroup = await em.findOneOrFail(GroupEntity, { - id: roomMember.userGroupId, + id: roomMembership.userGroupId, }); - expect(roomMember).toBeDefined(); + expect(roomMembership).toBeDefined(); expect(userGroup).toBeDefined(); expect(userGroup.users).toHaveLength(1); expect(userGroup.users[0].user.id).toBe(teacherUser.id); diff --git a/apps/server/src/modules/room/api/test/room-delete.api.spec.ts b/apps/server/src/modules/room/api/test/room-delete.api.spec.ts index b19d523f752..22f74c7edc8 100644 --- a/apps/server/src/modules/room/api/test/room-delete.api.spec.ts +++ b/apps/server/src/modules/room/api/test/room-delete.api.spec.ts @@ -1,4 +1,6 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { GroupEntityTypes } from '@modules/group/entity/group.entity'; +import { ServerTestModule, serverConfig, type ServerConfig } from '@modules/server'; import { HttpStatus, INestApplication, NotFoundException } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { Permission, RoleName } from '@shared/domain/interface'; @@ -9,9 +11,8 @@ import { groupEntityFactory, roleFactory, } from '@shared/testing'; -import { GroupEntityTypes } from '@src/modules/group/entity/group.entity'; -import { roomMemberEntityFactory } from '@src/modules/room-member/testing/room-member-entity.factory'; -import { ServerTestModule, serverConfig, type ServerConfig } from '@src/modules/server'; +import { RoomMembershipEntity } from '@src/modules/room-membership'; +import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing/room-membership-entity.factory'; import { RoomEntity } from '../../repo'; import { roomEntityFactory } from '../../testing/room-entity.factory'; @@ -95,7 +96,7 @@ describe('Room Controller (API)', () => { const setup = async () => { const room = roomEntityFactory.build(); const role = roleFactory.buildWithId({ - name: RoleName.ROOM_EDITOR, + name: RoleName.ROOMEDITOR, permissions: [Permission.ROOM_EDIT], }); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); @@ -103,8 +104,8 @@ describe('Room Controller (API)', () => { type: GroupEntityTypes.ROOM, users: [{ role, user: teacherUser }], }); - const roomMember = roomMemberEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); - await em.persistAndFlush([room, roomMember, teacherAccount, teacherUser, userGroup, role]); + const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + await em.persistAndFlush([room, roomMembership, teacherAccount, teacherUser, userGroup, role]); em.clear(); const loggedInClient = await testApiClient.login(teacherAccount); @@ -120,6 +121,16 @@ describe('Room Controller (API)', () => { expect(response.status).toBe(HttpStatus.NO_CONTENT); await expect(em.findOneOrFail(RoomEntity, room.id)).rejects.toThrow(NotFoundException); }); + + it('should delete the roomMembership', async () => { + const { loggedInClient, room } = await setup(); + + await expect(em.findOneOrFail(RoomMembershipEntity, { roomId: room.id })).resolves.not.toThrow(); + + const response = await loggedInClient.delete(room.id); + expect(response.status).toBe(HttpStatus.NO_CONTENT); + await expect(em.findOneOrFail(RoomMembershipEntity, { roomId: room.id })).rejects.toThrow(NotFoundException); + }); }); describe('when the room does not exist', () => { diff --git a/apps/server/src/modules/room/api/test/room-get-boards.api.spec.ts b/apps/server/src/modules/room/api/test/room-get-boards.api.spec.ts index b727b66355a..4f1646ec708 100644 --- a/apps/server/src/modules/room/api/test/room-get-boards.api.spec.ts +++ b/apps/server/src/modules/room/api/test/room-get-boards.api.spec.ts @@ -9,11 +9,11 @@ import { TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; -import { BoardExternalReferenceType } from '@src/modules/board'; -import { columnBoardEntityFactory } from '@src/modules/board/testing'; -import { GroupEntityTypes } from '@src/modules/group/entity'; -import { roomMemberEntityFactory } from '@src/modules/room-member/testing'; -import { serverConfig, ServerConfig, ServerTestModule } from '@src/modules/server'; +import { BoardExternalReferenceType } from '@modules/board'; +import { columnBoardEntityFactory } from '@modules/board/testing'; +import { GroupEntityTypes } from '@modules/group/entity'; +import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing'; +import { serverConfig, ServerConfig, ServerTestModule } from '@modules/server'; import { roomEntityFactory } from '../../testing'; describe('Room Controller (API)', () => { @@ -100,7 +100,7 @@ describe('Room Controller (API)', () => { }); const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); const role = roleFactory.buildWithId({ - name: RoleName.ROOM_VIEWER, + name: RoleName.ROOMVIEWER, permissions: [Permission.ROOM_VIEW], }); const userGroupEntity = groupEntityFactory.buildWithId({ @@ -109,8 +109,8 @@ describe('Room Controller (API)', () => { organization: studentUser.school, externalSource: undefined, }); - const roomMember = roomMemberEntityFactory.build({ userGroupId: userGroupEntity.id, roomId: room.id }); - await em.persistAndFlush([room, board, studentAccount, studentUser, role, userGroupEntity, roomMember]); + const roomMembership = roomMembershipEntityFactory.build({ userGroupId: userGroupEntity.id, roomId: room.id }); + await em.persistAndFlush([room, board, studentAccount, studentUser, role, userGroupEntity, roomMembership]); em.clear(); const loggedInClient = await testApiClient.login(studentAccount); diff --git a/apps/server/src/modules/room/api/test/room-get.api.spec.ts b/apps/server/src/modules/room/api/test/room-get.api.spec.ts index fe842db3d7f..719889d82ec 100644 --- a/apps/server/src/modules/room/api/test/room-get.api.spec.ts +++ b/apps/server/src/modules/room/api/test/room-get.api.spec.ts @@ -9,9 +9,9 @@ import { groupEntityFactory, roleFactory, } from '@shared/testing'; -import { GroupEntityTypes } from '@src/modules/group/entity/group.entity'; -import { roomMemberEntityFactory } from '@src/modules/room-member/testing'; -import { ServerTestModule, serverConfig, type ServerConfig } from '@src/modules/server'; +import { GroupEntityTypes } from '@modules/group/entity/group.entity'; +import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing'; +import { ServerTestModule, serverConfig, type ServerConfig } from '@modules/server'; import { roomEntityFactory } from '../../testing/room-entity.factory'; describe('Room Controller (API)', () => { @@ -95,7 +95,7 @@ describe('Room Controller (API)', () => { const room = roomEntityFactory.build(); const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); const role = roleFactory.buildWithId({ - name: RoleName.ROOM_VIEWER, + name: RoleName.ROOMVIEWER, permissions: [Permission.ROOM_VIEW], }); const userGroupEntity = groupEntityFactory.buildWithId({ @@ -104,8 +104,8 @@ describe('Room Controller (API)', () => { organization: studentUser.school, externalSource: undefined, }); - const roomMember = roomMemberEntityFactory.build({ userGroupId: userGroupEntity.id, roomId: room.id }); - await em.persistAndFlush([room, studentAccount, studentUser, role, userGroupEntity, roomMember]); + const roomMembership = roomMembershipEntityFactory.build({ userGroupId: userGroupEntity.id, roomId: room.id }); + await em.persistAndFlush([room, studentAccount, studentUser, role, userGroupEntity, roomMembership]); em.clear(); const loggedInClient = await testApiClient.login(studentAccount); @@ -114,10 +114,12 @@ describe('Room Controller (API)', () => { id: room.id, name: room.name, color: room.color, + schoolId: room.schoolId, startDate: room.startDate?.toISOString(), endDate: room.endDate?.toISOString(), createdAt: room.createdAt.toISOString(), updatedAt: room.updatedAt.toISOString(), + permissions: [Permission.ROOM_VIEW], }; return { loggedInClient, room, expectedResponse }; diff --git a/apps/server/src/modules/room/api/test/room-index.api.spec.ts b/apps/server/src/modules/room/api/test/room-index.api.spec.ts index ca0a1c22658..cbb68d0f38c 100644 --- a/apps/server/src/modules/room/api/test/room-index.api.spec.ts +++ b/apps/server/src/modules/room/api/test/room-index.api.spec.ts @@ -10,9 +10,9 @@ import { groupEntityFactory, roleFactory, } from '@shared/testing'; -import { GroupEntityTypes } from '@src/modules/group/entity/group.entity'; -import { roomMemberEntityFactory } from '@src/modules/room-member/testing/room-member-entity.factory'; -import { ServerTestModule, serverConfig, type ServerConfig } from '@src/modules/server'; +import { GroupEntityTypes } from '@modules/group/entity/group.entity'; +import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing/room-membership-entity.factory'; +import { ServerTestModule, serverConfig, type ServerConfig } from '@modules/server'; import { roomEntityFactory } from '../../testing/room-entity.factory'; import { RoomListResponse } from '../dto/response/room-list.response'; @@ -86,6 +86,7 @@ describe('Room Controller (API)', () => { id: room.id, name: room.name, color: room.color, + schoolId: room.schoolId, startDate: room.startDate?.toISOString(), endDate: room.endDate?.toISOString(), createdAt: room.createdAt.toISOString(), @@ -130,7 +131,7 @@ describe('Room Controller (API)', () => { const rooms = roomEntityFactory.buildListWithId(2); const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); const role = roleFactory.buildWithId({ - name: RoleName.ROOM_VIEWER, + name: RoleName.ROOMVIEWER, permissions: [Permission.ROOM_VIEW], }); const userGroupEntity = groupEntityFactory.buildWithId({ @@ -139,10 +140,10 @@ describe('Room Controller (API)', () => { organization: studentUser.school, externalSource: undefined, }); - const roomMembers = rooms.map((room) => - roomMemberEntityFactory.build({ userGroupId: userGroupEntity.id, roomId: room.id }) + const roomMemberships = rooms.map((room) => + roomMembershipEntityFactory.build({ userGroupId: userGroupEntity.id, roomId: room.id }) ); - await em.persistAndFlush([...rooms, ...roomMembers, studentAccount, studentUser, userGroupEntity]); + await em.persistAndFlush([...rooms, ...roomMemberships, studentAccount, studentUser, userGroupEntity]); em.clear(); const loggedInClient = await testApiClient.login(studentAccount); @@ -152,6 +153,7 @@ describe('Room Controller (API)', () => { id: room.id, name: room.name, color: room.color, + schoolId: room.schoolId, startDate: room.startDate?.toISOString(), endDate: room.endDate?.toISOString(), createdAt: room.createdAt.toISOString(), diff --git a/apps/server/src/modules/room/api/test/room-members.api.spec.ts b/apps/server/src/modules/room/api/test/room-members.api.spec.ts index 9e6663109b3..c509e59c41b 100644 --- a/apps/server/src/modules/room/api/test/room-members.api.spec.ts +++ b/apps/server/src/modules/room/api/test/room-members.api.spec.ts @@ -11,9 +11,9 @@ import { roleFactory, userFactory, } from '@shared/testing'; -import { GroupEntityTypes } from '@src/modules/group/entity/group.entity'; -import { roomMemberEntityFactory } from '@src/modules/room-member/testing/room-member-entity.factory'; -import { ServerTestModule, serverConfig, type ServerConfig } from '@src/modules/server'; +import { GroupEntityTypes } from '@modules/group/entity/group.entity'; +import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing/room-membership-entity.factory'; +import { ServerTestModule, serverConfig, type ServerConfig } from '@modules/server'; import { roomEntityFactory } from '../../testing/room-entity.factory'; import { RoomMemberListResponse } from '../dto/response/room-member.response'; @@ -50,11 +50,11 @@ describe('Room Controller (API)', () => { const room = roomEntityFactory.buildWithId(); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); const editRole = roleFactory.buildWithId({ - name: RoleName.ROOM_EDITOR, + name: RoleName.ROOMEDITOR, permissions: [Permission.ROOM_VIEW, Permission.ROOM_EDIT], }); const viewerRole = roleFactory.buildWithId({ - name: RoleName.ROOM_VIEWER, + name: RoleName.ROOMVIEWER, permissions: [Permission.ROOM_VIEW], }); const students = userFactory.buildList(2); @@ -71,10 +71,10 @@ describe('Room Controller (API)', () => { organization: teacherUser.school, externalSource: undefined, }); - const roomMembers = roomMemberEntityFactory.build({ userGroupId: userGroupEntity.id, roomId: room.id }); + const roomMemberships = roomMembershipEntityFactory.build({ userGroupId: userGroupEntity.id, roomId: room.id }); await em.persistAndFlush([ room, - roomMembers, + roomMemberships, teacherAccount, teacherUser, userGroupEntity, diff --git a/apps/server/src/modules/room/api/test/room-remove-members.api.spec.ts b/apps/server/src/modules/room/api/test/room-remove-members.api.spec.ts index f470f25c3d5..d87d0e68314 100644 --- a/apps/server/src/modules/room/api/test/room-remove-members.api.spec.ts +++ b/apps/server/src/modules/room/api/test/room-remove-members.api.spec.ts @@ -10,9 +10,9 @@ import { groupEntityFactory, roleFactory, } from '@shared/testing'; -import { GroupEntityTypes } from '@src/modules/group/entity/group.entity'; -import { roomMemberEntityFactory } from '@src/modules/room-member/testing/room-member-entity.factory'; -import { ServerTestModule, serverConfig, type ServerConfig } from '@src/modules/server'; +import { GroupEntityTypes } from '@modules/group/entity/group.entity'; +import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing/room-membership-entity.factory'; +import { ServerTestModule, serverConfig, type ServerConfig } from '@modules/server'; import { roomEntityFactory } from '../../testing/room-entity.factory'; describe('Room Controller (API)', () => { @@ -46,11 +46,11 @@ describe('Room Controller (API)', () => { describe('PATCH /rooms/:roomId/members/remove', () => { const setupRoomRoles = () => { const editorRole = roleFactory.buildWithId({ - name: RoleName.ROOM_EDITOR, + name: RoleName.ROOMEDITOR, permissions: [Permission.ROOM_VIEW, Permission.ROOM_EDIT], }); const viewerRole = roleFactory.buildWithId({ - name: RoleName.ROOM_VIEWER, + name: RoleName.ROOMVIEWER, permissions: [Permission.ROOM_VIEW], }); return { editorRole, viewerRole }; @@ -81,9 +81,9 @@ describe('Room Controller (API)', () => { externalSource: undefined, }); - const roomMembers = roomMemberEntityFactory.build({ userGroupId: userGroupEntity.id, roomId: room.id }); + const roomMemberships = roomMembershipEntityFactory.build({ userGroupId: userGroupEntity.id, roomId: room.id }); - await em.persistAndFlush([...Object.values(users), room, roomMembers, teacherAccount, userGroupEntity]); + await em.persistAndFlush([...Object.values(users), room, roomMemberships, teacherAccount, userGroupEntity]); em.clear(); const loggedInClient = await testApiClient.login(teacherAccount); diff --git a/apps/server/src/modules/room/api/test/room-update.api.spec.ts b/apps/server/src/modules/room/api/test/room-update.api.spec.ts index a756962f5af..782c23961d4 100644 --- a/apps/server/src/modules/room/api/test/room-update.api.spec.ts +++ b/apps/server/src/modules/room/api/test/room-update.api.spec.ts @@ -9,8 +9,8 @@ import { groupEntityFactory, roleFactory, } from '@shared/testing'; -import { roomMemberEntityFactory } from '@src/modules/room-member/testing'; -import { ServerTestModule, serverConfig, type ServerConfig } from '@src/modules/server'; +import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing'; +import { ServerTestModule, serverConfig, type ServerConfig } from '@modules/server'; import { RoomEntity } from '../../repo'; import { roomEntityFactory } from '../../testing'; @@ -42,11 +42,11 @@ describe('Room Controller (API)', () => { await app.close(); }); - describe('PATCH /rooms/:id', () => { + describe('PUT /rooms/:id', () => { describe('when the user is not authenticated', () => { it('should return a 401 error', async () => { const someId = new ObjectId().toHexString(); - const response = await testApiClient.patch(someId); + const response = await testApiClient.put(someId); expect(response.status).toBe(HttpStatus.UNAUTHORIZED); }); }); @@ -68,7 +68,7 @@ describe('Room Controller (API)', () => { const { loggedInClient } = await setup(); const someId = new ObjectId().toHexString(); const params = { name: 'Room #101', color: 'green' }; - const response = await loggedInClient.patch(someId, params); + const response = await loggedInClient.put(someId, params); expect(response.status).toBe(HttpStatus.FORBIDDEN); }); }); @@ -87,7 +87,7 @@ describe('Room Controller (API)', () => { it('should return a 400 error', async () => { const { loggedInClient } = await setup(); const params = { name: 'Room #101', color: 'green' }; - const response = await loggedInClient.patch('42', params); + const response = await loggedInClient.put('42', params); expect(response.status).toBe(HttpStatus.BAD_REQUEST); }); }); @@ -99,15 +99,15 @@ describe('Room Controller (API)', () => { endDate: new Date('2024-10-20'), }); const role = roleFactory.buildWithId({ - name: RoleName.ROOM_EDITOR, + name: RoleName.ROOMEDITOR, permissions: [Permission.ROOM_EDIT], }); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); const userGroup = groupEntityFactory.buildWithId({ users: [{ role, user: teacherUser }], }); - const roomMember = roomMemberEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); - await em.persistAndFlush([room, roomMember, teacherAccount, teacherUser, userGroup, role]); + const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + await em.persistAndFlush([room, roomMembership, teacherAccount, teacherUser, userGroup, role]); em.clear(); const loggedInClient = await testApiClient.login(teacherAccount); @@ -121,7 +121,7 @@ describe('Room Controller (API)', () => { const someId = new ObjectId().toHexString(); const params = { name: 'Room #101', color: 'green' }; - const response = await loggedInClient.patch(someId, params); + const response = await loggedInClient.put(someId, params); expect(response.status).toBe(HttpStatus.NOT_FOUND); }); @@ -132,7 +132,7 @@ describe('Room Controller (API)', () => { const { loggedInClient, room } = await setup(); const params = { name: 'Room #101', color: 'green' }; - const response = await loggedInClient.patch(room.id, params); + const response = await loggedInClient.put(room.id, params); expect(response.status).toBe(HttpStatus.OK); await expect(em.findOneOrFail(RoomEntity, room.id)).resolves.toMatchObject({ @@ -147,7 +147,7 @@ describe('Room Controller (API)', () => { const { loggedInClient, room } = await setup(); const params = { name: '', color: 'red' }; - const response = await loggedInClient.patch(room.id, params); + const response = await loggedInClient.put(room.id, params); expect(response.status).toBe(HttpStatus.BAD_REQUEST); }); @@ -158,7 +158,7 @@ describe('Room Controller (API)', () => { const { loggedInClient, room } = await setup(); const params = { name: 'Room #101', color: '' }; - const response = await loggedInClient.patch(room.id, params); + const response = await loggedInClient.put(room.id, params); expect(response.status).toBe(HttpStatus.BAD_REQUEST); }); @@ -169,7 +169,7 @@ describe('Room Controller (API)', () => { const { loggedInClient, room } = await setup(); const params = { name: 'Room #101', color: 'fancy-color' }; - const response = await loggedInClient.patch(room.id, params); + const response = await loggedInClient.put(room.id, params); expect(response.status).toBe(HttpStatus.BAD_REQUEST); }); @@ -181,7 +181,7 @@ describe('Room Controller (API)', () => { const { loggedInClient, room } = await setup(); const params = { name: 'Room #101', color: 'green', startDate: '2024-10-02' }; - const response = await loggedInClient.patch(room.id, params); + const response = await loggedInClient.put(room.id, params); expect(response.status).toBe(HttpStatus.OK); await expect(em.findOneOrFail(RoomEntity, room.id)).resolves.toMatchObject({ @@ -194,7 +194,7 @@ describe('Room Controller (API)', () => { it('should return a 400 error', async () => { const { loggedInClient, room } = await setup(); const params = { name: 'Room #101', color: 'green', startDate: 'invalid date' }; - const response = await loggedInClient.patch(room.id, params); + const response = await loggedInClient.put(room.id, params); expect(response.status).toBe(HttpStatus.BAD_REQUEST); }); }); @@ -204,23 +204,35 @@ describe('Room Controller (API)', () => { const { loggedInClient, room } = await setup(); const params = { name: 'Room #101', color: 'green', startDate: null }; - const response = await loggedInClient.patch(room.id, params); + const response = await loggedInClient.put(room.id, params); expect(response.status).toBe(HttpStatus.OK); - await expect(em.findOneOrFail(RoomEntity, room.id)).resolves.toMatchObject({ - id: room.id, - startDate: null, - }); + const resultRoom = await em.findOneOrFail(RoomEntity, room.id); + expect(resultRoom.startDate).toBe(undefined); }); }); }); + describe('when the startDate is omitted', () => { + it('should unset the property', async () => { + const { loggedInClient, room } = await setup(); + const params = { name: 'Room #101', color: 'green' }; + + const response = await loggedInClient.put(room.id, params); + + expect(response.status).toBe(HttpStatus.OK); + + const resultRoom = await em.findOneOrFail(RoomEntity, room.id); + expect(resultRoom.endDate).toBe(undefined); + }); + }); + describe('when an end date is given', () => { it('should update the room', async () => { const { loggedInClient, room } = await setup(); const params = { name: 'Room #101', color: 'green', endDate: '2024-10-18' }; - const response = await loggedInClient.patch(room.id, params); + const response = await loggedInClient.put(room.id, params); expect(response.status).toBe(HttpStatus.OK); await expect(em.findOneOrFail(RoomEntity, room.id)).resolves.toMatchObject({ @@ -233,7 +245,7 @@ describe('Room Controller (API)', () => { it('should return a 400 error', async () => { const { loggedInClient, room } = await setup(); const params = { name: 'Room #101', color: 'green', endDate: 'invalid date' }; - const response = await loggedInClient.patch(room.id, params); + const response = await loggedInClient.put(room.id, params); expect(response.status).toBe(HttpStatus.BAD_REQUEST); }); }); @@ -241,19 +253,32 @@ describe('Room Controller (API)', () => { describe('when the date is null', () => { it('should unset the property', async () => { const { loggedInClient, room } = await setup(); - const params = { name: 'Room #101', color: 'green', endDate: null }; + const params = { name: 'Room #101', color: 'green', startDate: '2024-10-02', endDate: null }; - const response = await loggedInClient.patch(room.id, params); + const response = await loggedInClient.put(room.id, params); expect(response.status).toBe(HttpStatus.OK); - await expect(em.findOneOrFail(RoomEntity, room.id)).resolves.toMatchObject({ - id: room.id, - endDate: null, - }); + + const resultRoom = await em.findOneOrFail(RoomEntity, room.id); + expect(resultRoom.endDate).toBe(undefined); }); }); }); + describe('when the endDate is omitted', () => { + it('should unset the property', async () => { + const { loggedInClient, room } = await setup(); + const params = { name: 'Room #101', color: 'green', startDate: '2024-10-02' }; + + const response = await loggedInClient.put(room.id, params); + + expect(response.status).toBe(HttpStatus.OK); + + const resultRoom = await em.findOneOrFail(RoomEntity, room.id); + expect(resultRoom.endDate).toBe(undefined); + }); + }); + describe('when the start date is before the end date', () => { it('should update the room', async () => { const { loggedInClient, room } = await setup(); @@ -264,7 +289,7 @@ describe('Room Controller (API)', () => { endDate: '2024-10-18', }; - const response = await loggedInClient.patch(room.id, params); + const response = await loggedInClient.put(room.id, params); expect(response.status).toBe(HttpStatus.OK); await expect(em.findOneOrFail(RoomEntity, room.id)).resolves.toMatchObject({ @@ -285,7 +310,7 @@ describe('Room Controller (API)', () => { endDate: '2024-10-05', }; - const response = await loggedInClient.patch(room.id, params); + const response = await loggedInClient.put(room.id, params); expect(response.status).toBe(HttpStatus.BAD_REQUEST); }); @@ -313,7 +338,7 @@ describe('Room Controller (API)', () => { const someId = new ObjectId().toHexString(); const params = { name: 'Room #101', color: 'green' }; - const response = await loggedInClient.patch(someId, params); + const response = await loggedInClient.put(someId, params); expect(response.status).toBe(HttpStatus.NOT_FOUND); }); @@ -324,7 +349,7 @@ describe('Room Controller (API)', () => { const { loggedInClient, room } = await setup(); const params = { name: 'Room #101', color: 'green' }; - const response = await loggedInClient.patch(room.id, params); + const response = await loggedInClient.put(room.id, params); expect(response.status).toBe(HttpStatus.FORBIDDEN); }); diff --git a/apps/server/src/modules/room/domain/do/room.do.spec.ts b/apps/server/src/modules/room/domain/do/room.do.spec.ts index 465fc4b95e8..7995aab7088 100644 --- a/apps/server/src/modules/room/domain/do/room.do.spec.ts +++ b/apps/server/src/modules/room/domain/do/room.do.spec.ts @@ -1,3 +1,4 @@ +import { ObjectId } from '@mikro-orm/mongodb'; import { EntityId } from '@shared/domain/types'; import { roomFactory } from '../../testing'; import { RoomColor } from '../type'; @@ -12,6 +13,7 @@ describe('Room', () => { color: RoomColor.BLUE, startDate: new Date('2024-01-01'), endDate: new Date('2024-12-31'), + schoolId: new ObjectId().toHexString(), createdAt: new Date('2024-01-01'), updatedAt: new Date('2024-01-01'), }; diff --git a/apps/server/src/modules/room/domain/do/room.do.ts b/apps/server/src/modules/room/domain/do/room.do.ts index 1cdd5093a63..af6ccf005d4 100644 --- a/apps/server/src/modules/room/domain/do/room.do.ts +++ b/apps/server/src/modules/room/domain/do/room.do.ts @@ -8,12 +8,13 @@ export interface RoomProps extends AuthorizableObject { color: RoomColor; startDate?: Date; endDate?: Date; + schoolId: EntityId; createdAt: Date; updatedAt: Date; } -export type RoomCreateProps = Pick; -export type RoomUpdateProps = RoomCreateProps; // will probably change in the future +export type RoomCreateProps = Pick; +export type RoomUpdateProps = Omit; export class Room extends DomainObject { public constructor(props: RoomProps) { @@ -46,11 +47,15 @@ export class Room extends DomainObject { this.props.color = value; } + public get schoolId(): EntityId { + return this.props.schoolId; + } + public get startDate(): Date | undefined { return this.props.startDate; } - public set startDate(value: Date) { + public set startDate(value: Date | undefined) { this.props.startDate = value; } @@ -58,7 +63,7 @@ export class Room extends DomainObject { return this.props.endDate; } - public set endDate(value: Date) { + public set endDate(value: Date | undefined) { this.props.endDate = value; } diff --git a/apps/server/src/modules/room/domain/service/room.service.spec.ts b/apps/server/src/modules/room/domain/service/room.service.spec.ts index 2eb114a4f1f..08191e7409d 100644 --- a/apps/server/src/modules/room/domain/service/room.service.spec.ts +++ b/apps/server/src/modules/room/domain/service/room.service.spec.ts @@ -1,4 +1,5 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { Page } from '@shared/domain/domainobject'; import { EntityId } from '@shared/domain/types'; @@ -70,6 +71,7 @@ describe('RoomService', () => { const props: RoomCreateProps = { name: 'room #1', color: RoomColor.ORANGE, + schoolId: new ObjectId().toHexString(), }; return { props }; }; diff --git a/apps/server/src/modules/room/domain/service/room.service.ts b/apps/server/src/modules/room/domain/service/room.service.ts index 4ec2c62c779..241b26a191a 100644 --- a/apps/server/src/modules/room/domain/service/room.service.ts +++ b/apps/server/src/modules/room/domain/service/room.service.ts @@ -26,7 +26,12 @@ export class RoomService { public async createRoom(props: RoomCreateProps): Promise { const roomProps: RoomProps = { id: new ObjectId().toHexString(), - ...props, + name: props.name, + color: props.color, + schoolId: props.schoolId, + // make sure that the dates are not null at runtime + startDate: props.startDate ?? undefined, + endDate: props.endDate ?? undefined, createdAt: new Date(), updatedAt: new Date(), }; @@ -46,7 +51,12 @@ export class RoomService { public async updateRoom(room: Room, props: RoomUpdateProps): Promise { this.validateTimeSpan(props, room.id); - Object.assign(room, props); + + room.name = props.name; + room.color = props.color; + // make sure that the dates are not null at runtime + room.startDate = props.startDate ?? undefined; + room.endDate = props.endDate ?? undefined; await this.roomRepo.save(room); } diff --git a/apps/server/src/modules/room/index.ts b/apps/server/src/modules/room/index.ts index 7be3bdc7474..31c66bccfac 100644 --- a/apps/server/src/modules/room/index.ts +++ b/apps/server/src/modules/room/index.ts @@ -2,3 +2,4 @@ export * from './domain'; export { RoomConfig } from './room.config'; export * from './room.module'; export * from './repo/entity'; +export { roomFactory } from './testing'; diff --git a/apps/server/src/modules/room/repo/entity/room.entity.ts b/apps/server/src/modules/room/repo/entity/room.entity.ts index 3a174be1d9d..0539f7c469c 100644 --- a/apps/server/src/modules/room/repo/entity/room.entity.ts +++ b/apps/server/src/modules/room/repo/entity/room.entity.ts @@ -1,5 +1,7 @@ import { Entity, Property } from '@mikro-orm/core'; import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; +import { EntityId } from '@shared/domain/types'; +import { ObjectIdType } from '@shared/repo/types/object-id.type'; import { Room, RoomProps } from '../../domain/do/room.do'; import { RoomColor } from '../../domain/type'; @@ -11,6 +13,9 @@ export class RoomEntity extends BaseEntityWithTimestamps implements RoomProps { @Property({ nullable: false }) color!: RoomColor; + @Property({ type: ObjectIdType, fieldName: 'school', nullable: false }) + schoolId!: EntityId; + @Property({ nullable: true }) startDate?: Date; diff --git a/apps/server/src/modules/room/repo/room-domain.mapper.spec.ts b/apps/server/src/modules/room/repo/room-domain.mapper.spec.ts index 22f25ad841c..8de67527eed 100644 --- a/apps/server/src/modules/room/repo/room-domain.mapper.spec.ts +++ b/apps/server/src/modules/room/repo/room-domain.mapper.spec.ts @@ -1,3 +1,4 @@ +import { ObjectId } from '@mikro-orm/mongodb'; import { Room, RoomProps } from '../domain/do/room.do'; import { RoomColor } from '../domain/type'; import { roomEntityFactory } from '../testing'; @@ -32,6 +33,7 @@ describe('RoomDomainMapper', () => { id: '1', name: 'Existing Room', color: RoomColor.GREEN, + schoolId: new ObjectId().toHexString(), startDate: new Date('2023-01-01'), endDate: new Date('2023-12-31'), createdAt: new Date('2023-01-01'), @@ -42,6 +44,7 @@ describe('RoomDomainMapper', () => { id: '2', name: 'Test Room', color: RoomColor.RED, + schoolId: new ObjectId().toHexString(), startDate: new Date('2023-02-01'), endDate: new Date('2023-11-30'), domainObject: existingRoom, @@ -55,6 +58,7 @@ describe('RoomDomainMapper', () => { id: '1', name: 'Existing Room', color: RoomColor.GREEN, + schoolId: existingRoom.schoolId, startDate: new Date('2023-01-01'), endDate: new Date('2023-12-31'), createdAt: new Date('2023-01-01'), @@ -69,6 +73,7 @@ describe('RoomDomainMapper', () => { id: '1', name: 'Test Room', color: RoomColor.RED, + schoolId: new ObjectId().toHexString(), startDate: new Date('2023-01-01'), endDate: new Date('2023-12-31'), } as RoomEntity; @@ -99,6 +104,7 @@ describe('RoomDomainMapper', () => { id: '66d581c3ef74c548a4efea1d', name: 'Test Room #1', color: RoomColor.RED, + schoolId: new ObjectId().toHexString(), startDate: new Date('2023-01-01'), endDate: new Date('2023-12-31'), createdAt: new Date('2024-10-1'), diff --git a/apps/server/src/modules/room/room-api.module.ts b/apps/server/src/modules/room/room-api.module.ts index 7405a83afe8..7711d7cd842 100644 --- a/apps/server/src/modules/room/room-api.module.ts +++ b/apps/server/src/modules/room/room-api.module.ts @@ -2,13 +2,13 @@ import { AuthorizationModule } from '@modules/authorization'; import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; import { BoardModule } from '../board'; -import { RoomMemberModule } from '../room-member/room-member.module'; +import { RoomMembershipModule } from '../room-membership/room-membership.module'; import { UserModule } from '../user'; import { RoomController, RoomUc } from './api'; import { RoomModule } from './room.module'; @Module({ - imports: [RoomModule, AuthorizationModule, LoggerModule, RoomMemberModule, BoardModule, UserModule], + imports: [RoomModule, AuthorizationModule, LoggerModule, RoomMembershipModule, BoardModule, UserModule], controllers: [RoomController], providers: [RoomUc], }) diff --git a/apps/server/src/modules/room/testing/room-entity.factory.ts b/apps/server/src/modules/room/testing/room-entity.factory.ts index fee0b2c80b0..ad2e979750e 100644 --- a/apps/server/src/modules/room/testing/room-entity.factory.ts +++ b/apps/server/src/modules/room/testing/room-entity.factory.ts @@ -9,6 +9,7 @@ export const roomEntityFactory = EntityFactory.define(Roo id: new ObjectId().toHexString(), name: `room #${sequence}`, color: [RoomColor.BLUE, RoomColor.RED, RoomColor.GREEN, RoomColor.MAGENTA][Math.floor(Math.random() * 4)], + schoolId: new ObjectId().toHexString(), startDate: new Date(), endDate: new Date(Date.now() + 1000 * 60 * 60 * 24 * 7), createdAt: new Date(), diff --git a/apps/server/src/modules/room/testing/room.factory.ts b/apps/server/src/modules/room/testing/room.factory.ts index 099c98b831c..7f1ef879488 100644 --- a/apps/server/src/modules/room/testing/room.factory.ts +++ b/apps/server/src/modules/room/testing/room.factory.ts @@ -8,6 +8,7 @@ export const roomFactory = BaseFactory.define(Room, ({ sequence id: new ObjectId().toHexString(), name: `room #${sequence}`, color: [RoomColor.BLUE, RoomColor.RED, RoomColor.GREEN, RoomColor.MAGENTA][Math.floor(Math.random() * 4)], + schoolId: new ObjectId().toHexString(), startDate: new Date(), createdAt: new Date(), updatedAt: new Date(), diff --git a/apps/server/src/modules/server/server.module.ts b/apps/server/src/modules/server/server.module.ts index c33c920148a..046df1f7497 100644 --- a/apps/server/src/modules/server/server.module.ts +++ b/apps/server/src/modules/server/server.module.ts @@ -29,7 +29,7 @@ import { RocketChatModule } from '@modules/rocketchat'; import { RoomApiModule } from '@modules/room/room-api.module'; import { RosterModule } from '@modules/roster/roster.module'; import { SchoolApiModule } from '@modules/school/school-api.module'; -import { SharingApiModule } from '@modules/sharing/sharing.module'; +import { SharingApiModule } from '@modules/sharing/sharing-api.module'; import { ShdApiModule } from '@modules/shd/shd.api.module'; import { SystemApiModule } from '@modules/system/system-api.module'; import { TaskApiModule } from '@modules/task/task-api.module'; diff --git a/apps/server/src/modules/sharing/controller/api-test/sharing-create-token.api.spec.ts b/apps/server/src/modules/sharing/controller/api-test/sharing-create-token.api.spec.ts index 68423d68574..d7aa58a65e4 100644 --- a/apps/server/src/modules/sharing/controller/api-test/sharing-create-token.api.spec.ts +++ b/apps/server/src/modules/sharing/controller/api-test/sharing-create-token.api.spec.ts @@ -94,13 +94,13 @@ describe(`share token creation (api)`, () => { }; describe('with the feature disabled', () => { - it('should return status 500', async () => { + it('should return status 403', async () => { Configuration.set('FEATURE_COURSE_SHARE', false); const { course } = await setup(); const response = await api.post({ parentId: course.id, parentType: ShareTokenParentType.Course }); - expect(response.status).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(response.status).toEqual(HttpStatus.FORBIDDEN); }); }); diff --git a/apps/server/src/modules/sharing/controller/api-test/sharing-import-room-board-token.api.spec.ts b/apps/server/src/modules/sharing/controller/api-test/sharing-import-room-board-token.api.spec.ts new file mode 100644 index 00000000000..799a1bcca32 --- /dev/null +++ b/apps/server/src/modules/sharing/controller/api-test/sharing-import-room-board-token.api.spec.ts @@ -0,0 +1,108 @@ +import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { EntityManager } from '@mikro-orm/mongodb'; +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { Permission, RoleName } from '@shared/domain/interface'; +import { + cleanupCollections, + groupEntityFactory, + roleFactory, + TestApiClient, + UserAndAccountTestFactory, +} from '@shared/testing'; +import { BoardExternalReferenceType } from '@src/modules/board'; +import { columnBoardEntityFactory } from '@src/modules/board/testing'; +import { GroupEntityTypes } from '@src/modules/group/entity'; +import { roomMembershipEntityFactory } from '@src/modules/room-membership/testing'; +import { roomEntityFactory } from '@src/modules/room/testing'; +import { ServerTestModule } from '@src/modules/server'; +import { ShareTokenParentType } from '../../domainobject/share-token.do'; +import { ShareTokenService } from '../../service'; + +describe('Sharing Controller (API)', () => { + let app: INestApplication; + let em: EntityManager; + let testApiClient: TestApiClient; + let shareTokenService: ShareTokenService; + + beforeAll(async () => { + const module = await Test.createTestingModule({ + imports: [ServerTestModule], + }).compile(); + + app = module.createNestApplication(); + await app.init(); + em = module.get(EntityManager); + testApiClient = new TestApiClient(app, 'sharetoken'); + shareTokenService = module.get(ShareTokenService); + }); + + beforeEach(async () => { + await cleanupCollections(em); + Configuration.set('FEATURE_COLUMN_BOARD_SHARE', true); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('POST /sharetoken/:token/import', () => { + const setup = async () => { + const room = roomEntityFactory.buildWithId(); + const role = roleFactory.buildWithId({ + name: RoleName.ROOMEDITOR, + permissions: [Permission.ROOM_EDIT], + }); + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); + const userGroup = groupEntityFactory.buildWithId({ + type: GroupEntityTypes.ROOM, + users: [{ role, user: teacherUser }], + }); + const roomMembership = roomMembershipEntityFactory.build({ roomId: room.id, userGroupId: userGroup.id }); + const board = columnBoardEntityFactory.build({ + context: { id: room.id, type: BoardExternalReferenceType.Room }, + }); + await em.persistAndFlush([room, roomMembership, teacherAccount, teacherUser, userGroup, role, board]); + em.clear(); + + const shareToken = await shareTokenService.createToken({ + parentId: board.id, + parentType: ShareTokenParentType.ColumnBoard, + }); + + const loggedInClient = await testApiClient.login(teacherAccount); + + return { loggedInClient, token: shareToken.token, room }; + }; + + describe('when the feature is disabled', () => { + beforeEach(() => { + Configuration.set('FEATURE_COLUMN_BOARD_SHARE', false); + }); + + it('should return a 403 error', async () => { + const { loggedInClient, token } = await setup(); + const response = await loggedInClient.post(`${token}/import`, { newName: 'NewName' }); + expect(response.status).toBe(HttpStatus.FORBIDDEN); + }); + }); + + describe('when the user has the required permissions', () => { + describe('when the destination is omitted', () => { + it('should return a 401 status', async () => { + const { loggedInClient, token } = await setup(); + const response = await loggedInClient.post(`${token}/import`, { newName: 'NewName' }); + expect(response.status).toBe(HttpStatus.BAD_REQUEST); + }); + }); + + describe('when the destination is valid', () => { + it('should return a 201 status', async () => { + const { loggedInClient, token, room } = await setup(); + const response = await loggedInClient.post(`${token}/import`, { newName: 'NewName', destinationId: room.id }); + expect(response.status).toBe(HttpStatus.CREATED); + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/sharing/controller/api-test/sharing-import-token.api.spec.ts b/apps/server/src/modules/sharing/controller/api-test/sharing-import-token.api.spec.ts index 9386b965857..ff24d2ed78b 100644 --- a/apps/server/src/modules/sharing/controller/api-test/sharing-import-token.api.spec.ts +++ b/apps/server/src/modules/sharing/controller/api-test/sharing-import-token.api.spec.ts @@ -1,211 +1,172 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { ICurrentUser, JwtAuthGuard } from '@infra/auth-guard'; import { EntityManager } from '@mikro-orm/mongodb'; import { CopyApiResponse, CopyElementType, CopyStatusEnum } from '@modules/copy-helper'; import { ServerTestModule } from '@modules/server'; -import { ExecutionContext, HttpStatus, INestApplication } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { ApiValidationError } from '@shared/common'; +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; import { Permission } from '@shared/domain/interface'; import { cleanupCollections, courseFactory, - mapUserToCurrentUser, roleFactory, schoolEntityFactory, + TestApiClient, + UserAndAccountTestFactory, userFactory, } from '@shared/testing'; -import { Request } from 'express'; -import request from 'supertest'; import { ShareTokenContext, ShareTokenContextType, ShareTokenParentType } from '../../domainobject/share-token.do'; import { ShareTokenService } from '../../service'; -import { ShareTokenImportBodyParams, ShareTokenResponse, ShareTokenUrlParams } from '../dto'; -const baseRouteName = '/sharetoken'; - -class API { - app: INestApplication; - - constructor(app: INestApplication) { - this.app = app; - } - - async post(urlParams: ShareTokenUrlParams, body: ShareTokenImportBodyParams) { - const response = await request(this.app.getHttpServer()) - .post(`${baseRouteName}/${urlParams.token}/import`) - .set('Accept', 'application/json') - .set('Authorization', 'jwt') - .send(body); - - return { - result: response.body as ShareTokenResponse, - error: response.body as ApiValidationError, - status: response.status, - }; - } -} - -describe(`share token import (api)`, () => { +describe(`Share Token Import (API)`, () => { let app: INestApplication; let em: EntityManager; - let currentUser: ICurrentUser; + let testApiClient: TestApiClient; let shareTokenService: ShareTokenService; - let api: API; beforeAll(async () => { - const module: TestingModule = await Test.createTestingModule({ + const module = await Test.createTestingModule({ imports: [ServerTestModule], - }) - .overrideGuard(JwtAuthGuard) - .useValue({ - canActivate(context: ExecutionContext) { - const req: Request = context.switchToHttp().getRequest(); - req.user = currentUser; - return true; - }, - }) - .compile(); + }).compile(); app = module.createNestApplication(); await app.init(); em = module.get(EntityManager); + testApiClient = new TestApiClient(app, 'sharetoken'); shareTokenService = module.get(ShareTokenService); - - api = new API(app); }); - afterAll(async () => { - await app.close(); - }); - - beforeEach(() => { + beforeEach(async () => { + await cleanupCollections(em); Configuration.set('FEATURE_COURSE_SHARE', true); }); - const setup = async (context?: ShareTokenContext) => { - await cleanupCollections(em); - const school = schoolEntityFactory.build(); - const roles = roleFactory.buildList(1, { - permissions: [Permission.COURSE_CREATE], - }); - const user = userFactory.build({ school, roles }); - const course = courseFactory.build({ teachers: [user] }); - await em.persistAndFlush([user, course]); - - const shareToken = await shareTokenService.createToken( - { - parentType: ShareTokenParentType.Course, - parentId: course.id, - }, - { context } - ); - - em.clear(); - - currentUser = mapUserToCurrentUser(user); - - return { - token: shareToken.token, - elementType: CopyElementType.COURSE, - }; - }; - - describe('with the feature disabled', () => { - it('should return status 500', async () => { - Configuration.set('FEATURE_COURSE_SHARE', false); - const { token } = await setup(); - - const response = await api.post({ token }, { newName: 'NewName' }); - - expect(response.status).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); - }); + afterAll(async () => { + await app.close(); }); - describe('with a valid token', () => { - it('should return status 201', async () => { - const { token } = await setup(); - - const response = await api.post({ token }, { newName: 'NewName' }); - - expect(response.status).toEqual(HttpStatus.CREATED); + describe('POST /sharetoken/:token/import', () => { + describe('when the user is not authenticated', () => { + it('should return a 401 error', async () => { + const token = 'aaLnAEZ0xqIW'; + const response = await testApiClient.post(`${token}/import`, { + newName: 'NewName', + }); + expect(response.status).toBe(HttpStatus.UNAUTHORIZED); + }); }); - it('should return a valid result', async () => { - const { token, elementType } = await setup(); - const newName = 'NewName'; - const response = await api.post({ token }, { newName }); - - const expectedResult: CopyApiResponse = { - id: expect.any(String), - type: elementType, - title: newName, - status: CopyStatusEnum.SUCCESS, + describe('when the user is valid', () => { + const setup = async (context?: ShareTokenContext) => { + const school = schoolEntityFactory.build(); + const roles = roleFactory.buildList(1, { + permissions: [Permission.COURSE_CREATE], + }); + const user = userFactory.build({ school, roles }); + const course = courseFactory.build({ teachers: [user] }); + await em.persistAndFlush([user, course]); + + const shareToken = await shareTokenService.createToken( + { + parentType: ShareTokenParentType.Course, + parentId: course.id, + }, + { context } + ); + + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); + await em.persistAndFlush([teacherAccount, teacherUser]); + em.clear(); + + const loggedInClient = await testApiClient.login(teacherAccount); + + return { + loggedInClient, + token: shareToken.token, + elementType: CopyElementType.COURSE, + }; }; - expect(response.result).toEqual(expect.objectContaining(expectedResult)); - }); - }); - - describe('with invalid token', () => { - it('should return status 404', async () => { - await setup(); - - const response = await api.post({ token: 'invalid_token' }, { newName: 'NewName' }); - - expect(response.status).toEqual(HttpStatus.NOT_FOUND); - }); - }); - - describe('with invalid context', () => { - const setup2 = async () => { - const school = schoolEntityFactory.build(); - const otherSchool = schoolEntityFactory.build(); - const roles = roleFactory.buildList(1, { - permissions: [Permission.COURSE_CREATE], + describe('with the feature disabled', () => { + beforeEach(() => { + Configuration.set('FEATURE_COURSE_SHARE', false); + }); + + it('should return a 403 error', async () => { + const { loggedInClient, token } = await setup(); + const response = await loggedInClient.post(`${token}/import`, { + newName: 'NewName', + }); + expect(response.status).toBe(HttpStatus.FORBIDDEN); + }); }); - const user = userFactory.build({ school, roles }); - const course = courseFactory.build({ teachers: [user] }); - await em.persistAndFlush([user, course, otherSchool]); - - const context = { - contextType: ShareTokenContextType.School, - contextId: otherSchool.id, - }; - - const shareToken = await shareTokenService.createToken( - { - parentType: ShareTokenParentType.Course, - parentId: course.id, - }, - { context } - ); + describe('with a valid token', () => { + it('should return status 201', async () => { + const { loggedInClient, token } = await setup(); + + const response = await loggedInClient.post(`${token}/import`, { + newName: 'NewName', + }); + + expect(response.status).toEqual(HttpStatus.CREATED); + }); + + it('should return a valid result', async () => { + const { loggedInClient, token, elementType } = await setup(); + const newName = 'NewName'; + const response = await loggedInClient.post(`${token}/import`, { + newName, + }); + const expectedResult: CopyApiResponse = { + id: expect.any(String), + type: elementType, + title: newName, + status: CopyStatusEnum.SUCCESS, + }; + + expect(response.body as CopyApiResponse).toEqual(expect.objectContaining(expectedResult)); + }); + }); - em.clear(); + describe('with invalid token', () => { + it('should return status 404', async () => { + const { loggedInClient } = await setup(); - currentUser = mapUserToCurrentUser(user); + const response = await loggedInClient.post(`invalid_token/import`, { + newName: 'NewName', + }); - return { - shareTokenFromDifferentCourse: shareToken.token, - }; - }; + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + }); + }); - it('should return status 403', async () => { - const { shareTokenFromDifferentCourse } = await setup2(); + describe('with invalid context', () => { + it('should return status 403', async () => { + const otherSchool = schoolEntityFactory.build(); + await em.persistAndFlush(otherSchool); - const response = await api.post({ token: shareTokenFromDifferentCourse }, { newName: 'NewName' }); + const { loggedInClient, token: tokenFromOtherSchool } = await setup({ + contextId: otherSchool.id, + contextType: ShareTokenContextType.School, + }); - expect(response.status).toEqual(HttpStatus.FORBIDDEN); - }); - }); + const response = await loggedInClient.post(`${tokenFromOtherSchool}/import`, { + newName: 'NewName', + }); - describe('with invalid new name', () => { - it('should return status 501', async () => { - const { token } = await setup(); - // @ts-expect-error invalid new name - const response = await api.post({ token }, { newName: 42 }); + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + }); + }); - expect(response.status).toEqual(HttpStatus.NOT_IMPLEMENTED); + describe('with invalid new name', () => { + it('should return status 501', async () => { + const { loggedInClient, token } = await setup(); + const response = await loggedInClient.post(`${token}/import`, { + newName: 42, + }); + expect(response.status).toEqual(HttpStatus.NOT_IMPLEMENTED); + }); + }); }); }); }); diff --git a/apps/server/src/modules/sharing/controller/api-test/sharing-lookup-token.api.spec.ts b/apps/server/src/modules/sharing/controller/api-test/sharing-lookup-token.api.spec.ts index 57498162304..adf854d9b3d 100644 --- a/apps/server/src/modules/sharing/controller/api-test/sharing-lookup-token.api.spec.ts +++ b/apps/server/src/modules/sharing/controller/api-test/sharing-lookup-token.api.spec.ts @@ -59,18 +59,12 @@ describe(`share token lookup (api)`, () => { }; }; - it('should return status 500', async () => { + it('should return status 403', async () => { const { token, loggedInClient } = await setup(); const response = await loggedInClient.get(token); - expect(response.status).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); - expect(response.body).toEqual({ - code: 500, - message: 'Import Course Feature not enabled', - title: 'Internal Server Error', - type: 'INTERNAL_SERVER_ERROR', - }); + expect(response.status).toEqual(HttpStatus.FORBIDDEN); }); }); diff --git a/apps/server/src/modules/sharing/controller/dto/share-token-import.body.params.ts b/apps/server/src/modules/sharing/controller/dto/share-token-import.body.params.ts index 420d00c3758..28453a6a4c7 100644 --- a/apps/server/src/modules/sharing/controller/dto/share-token-import.body.params.ts +++ b/apps/server/src/modules/sharing/controller/dto/share-token-import.body.params.ts @@ -15,9 +15,9 @@ export class ShareTokenImportBodyParams { @IsOptional() @IsString() @ApiProperty({ - description: 'Id of the course to which the lesson/task will be added', + description: 'Id of the parent to which the imported object will be added.', required: false, nullable: true, }) - destinationCourseId?: string; + destinationId?: string; } diff --git a/apps/server/src/modules/sharing/controller/share-token.controller.ts b/apps/server/src/modules/sharing/controller/share-token.controller.ts index 5320c5578f9..d85937cbb62 100644 --- a/apps/server/src/modules/sharing/controller/share-token.controller.ts +++ b/apps/server/src/modules/sharing/controller/share-token.controller.ts @@ -90,7 +90,7 @@ export class ShareTokenController { currentUser.userId, urlParams.token, body.newName, - body.destinationCourseId + body.destinationId ); const response = CopyMapper.mapToResponse(copyStatus); diff --git a/apps/server/src/modules/sharing/sharing-api.module.ts b/apps/server/src/modules/sharing/sharing-api.module.ts new file mode 100644 index 00000000000..3c30c03a883 --- /dev/null +++ b/apps/server/src/modules/sharing/sharing-api.module.ts @@ -0,0 +1,31 @@ +import { AuthorizationModule } from '@modules/authorization'; +import { BoardModule } from '@modules/board'; +import { LearnroomModule } from '@modules/learnroom'; +import { LessonModule } from '@modules/lesson'; +import { RoomModule } from '@modules/room'; +import { RoomMembershipModule } from '@src/modules/room-membership'; +import { SchoolModule } from '@modules/school'; +import { TaskModule } from '@modules/task'; +import { Module } from '@nestjs/common'; +import { LoggerModule } from '@src/core/logger'; +import { ShareTokenController } from './controller/share-token.controller'; +import { SharingModule } from './sharing.module'; +import { ShareTokenUC } from './uc'; + +@Module({ + imports: [ + SharingModule, + AuthorizationModule, + LearnroomModule, + LessonModule, + TaskModule, + BoardModule, + RoomMembershipModule, + RoomModule, + SchoolModule, + LoggerModule, + ], + controllers: [ShareTokenController], + providers: [ShareTokenUC], +}) +export class SharingApiModule {} diff --git a/apps/server/src/modules/sharing/sharing.module.ts b/apps/server/src/modules/sharing/sharing.module.ts index 71773a87a6f..df417337f1e 100644 --- a/apps/server/src/modules/sharing/sharing.module.ts +++ b/apps/server/src/modules/sharing/sharing.module.ts @@ -1,46 +1,17 @@ -import { AuthorizationModule } from '@modules/authorization'; -import { AuthorizationReferenceModule } from '@modules/authorization-reference/authorization-reference.module'; import { BoardModule } from '@modules/board'; import { LearnroomModule } from '@modules/learnroom'; import { LessonModule } from '@modules/lesson'; -import { SchoolModule } from '@modules/school'; import { TaskModule } from '@modules/task'; import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; -import { ShareTokenController } from './controller/share-token.controller'; +import { RoomModule } from '../room'; import { ShareTokenRepo } from './repo/share-token.repo'; import { ShareTokenService, TokenGenerator } from './service'; -import { ShareTokenUC } from './uc'; @Module({ - imports: [ - AuthorizationModule, - AuthorizationReferenceModule, - LoggerModule, - LearnroomModule, - LessonModule, - TaskModule, - BoardModule, - ], + imports: [LoggerModule, LearnroomModule, LessonModule, TaskModule, BoardModule, RoomModule], controllers: [], providers: [ShareTokenService, TokenGenerator, ShareTokenRepo], exports: [ShareTokenService], }) export class SharingModule {} - -@Module({ - imports: [ - SharingModule, - AuthorizationModule, - AuthorizationReferenceModule, - LearnroomModule, - LessonModule, - TaskModule, - LoggerModule, - BoardModule, - SchoolModule, - ], - controllers: [ShareTokenController], - providers: [ShareTokenUC], -}) -export class SharingApiModule {} diff --git a/apps/server/src/modules/sharing/uc/share-token.uc.spec.ts b/apps/server/src/modules/sharing/uc/share-token.uc.spec.ts index 4b33a73d438..193f4606285 100644 --- a/apps/server/src/modules/sharing/uc/share-token.uc.spec.ts +++ b/apps/server/src/modules/sharing/uc/share-token.uc.spec.ts @@ -3,15 +3,19 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { BoardExternalReferenceType, BoardNodeAuthorizableService, ColumnBoardService } from '@modules/board'; +import { CopyColumnBoardParams } from '@modules/board/service/internal'; import { boardNodeAuthorizableFactory, columnBoardFactory } from '@modules/board/testing'; import { CopyElementType, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; +import { StorageLocation } from '@modules/files-storage/interface'; import { CourseCopyService, CourseService } from '@modules/learnroom'; import { LessonCopyService, LessonService } from '@modules/lesson'; +import { RoomService } from '@modules/room'; import { SchoolService } from '@modules/school'; import { schoolFactory } from '@modules/school/testing'; import { TaskCopyService, TaskService } from '@modules/task'; -import { BadRequestException, InternalServerErrorException, NotImplementedException } from '@nestjs/common'; +import { BadRequestException, NotImplementedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; +import { FeatureDisabledLoggableException } from '@shared/common/loggable-exception'; import { Permission } from '@shared/domain/interface'; import { courseFactory, @@ -23,6 +27,7 @@ import { userFactory, } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; +import { RoomMembershipService } from '@src/modules/room-membership'; import { ShareTokenContextType, ShareTokenParentType, ShareTokenPayload } from '../domainobject/share-token.do'; import { ShareTokenService } from '../service'; import { ShareTokenUC } from './share-token.uc'; @@ -82,6 +87,14 @@ describe('ShareTokenUC', () => { provide: TaskService, useValue: createMock(), }, + { + provide: RoomService, + useValue: createMock(), + }, + { + provide: RoomMembershipService, + useValue: createMock(), + }, { provide: ColumnBoardService, useValue: createMock(), @@ -673,6 +686,8 @@ describe('ShareTokenUC', () => { const shareToken = shareTokenFactory.build({ payload }); service.lookupTokenWithParentName.mockResolvedValueOnce({ shareToken, parentName: columnBoard.title }); + columnBoardService.findById.mockResolvedValueOnce(columnBoard); + return { user, shareToken, columnBoard, course }; }; @@ -849,7 +864,7 @@ describe('ShareTokenUC', () => { Configuration.set('FEATURE_COURSE_SHARE', false); await expect(uc.importShareToken(user.id, shareToken.token, 'NewName')).rejects.toThrowError( - InternalServerErrorException + FeatureDisabledLoggableException ); }); @@ -922,11 +937,11 @@ describe('ShareTokenUC', () => { Configuration.set('FEATURE_LESSON_SHARE', false); await expect(uc.importShareToken(user.id, shareToken.token, 'NewName')).rejects.toThrowError( - InternalServerErrorException + FeatureDisabledLoggableException ); }); - it('should throw if the destinationCourseId is not passed', async () => { + it('should throw if the destinationId is not passed', async () => { const { user, shareToken } = setup(); await expect(uc.importShareToken(user.id, shareToken.token, 'NewName')).rejects.toThrowError( @@ -1008,11 +1023,11 @@ describe('ShareTokenUC', () => { Configuration.set('FEATURE_TASK_SHARE', false); await expect(uc.importShareToken(user.id, shareToken.token, 'NewName')).rejects.toThrowError( - InternalServerErrorException + FeatureDisabledLoggableException ); }); - it('should throw if the destinationCourseId is not passed', async () => { + it('should throw if the destinationId is not passed', async () => { const { user, shareToken } = setupTask(); await expect(uc.importShareToken(user.id, shareToken.token, 'NewName')).rejects.toThrowError( @@ -1070,9 +1085,10 @@ describe('ShareTokenUC', () => { const school = schoolEntityFactory.buildWithId(); const user = userFactory.buildWithId({ school }); const course = courseFactory.buildWithId(); - courseService.findById.mockResolvedValueOnce(course); + courseService.findById.mockResolvedValue(course); const columnBoard = columnBoardFactory.build(); + columnBoardService.findById.mockResolvedValue(columnBoard); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); const payload: ShareTokenPayload = { parentType: ShareTokenParentType.ColumnBoard, parentId: columnBoard.id }; @@ -1090,7 +1106,7 @@ describe('ShareTokenUC', () => { const { user, shareToken } = setup(); Configuration.set('FEATURE_COLUMN_BOARD_SHARE', false); await expect(uc.importShareToken(user.id, shareToken.token, 'NewName')).rejects.toThrowError( - InternalServerErrorException + FeatureDisabledLoggableException ); }); it('should check the permission to create the columnboard', async () => { @@ -1116,9 +1132,11 @@ describe('ShareTokenUC', () => { const { user, shareToken, course, columnBoard } = setup(); const newName = 'NewName'; await uc.importShareToken(user.id, shareToken.token, newName, course.id); - expect(columnBoardService.copyColumnBoard).toHaveBeenCalledWith({ + expect(columnBoardService.copyColumnBoard).toHaveBeenCalledWith({ originalColumnBoardId: columnBoard.id, - destinationExternalReference: { type: BoardExternalReferenceType.Course, id: course.id }, + targetExternalReference: { type: BoardExternalReferenceType.Course, id: course.id }, + sourceStorageLocationReference: { type: StorageLocation.SCHOOL, id: course.school.id }, + targetStorageLocationReference: { type: StorageLocation.SCHOOL, id: course.school.id }, userId: user.id, copyTitle: newName, }); diff --git a/apps/server/src/modules/sharing/uc/share-token.uc.ts b/apps/server/src/modules/sharing/uc/share-token.uc.ts index e8e0babd7bf..af4a079f10e 100644 --- a/apps/server/src/modules/sharing/uc/share-token.uc.ts +++ b/apps/server/src/modules/sharing/uc/share-token.uc.ts @@ -1,16 +1,26 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { + BoardExternalReference, + BoardExternalReferenceType, + BoardNodeAuthorizableService, + ColumnBoardService, +} from '@modules/board'; +import { StorageLocationReference } from '@modules/board/service/internal'; import { CopyStatus } from '@modules/copy-helper'; +import { StorageLocation } from '@modules/files-storage/interface'; import { CourseCopyService, CourseService } from '@modules/learnroom'; import { LessonCopyService, LessonService } from '@modules/lesson'; +import { RoomService } from '@modules/room'; +import { SchoolService } from '@modules/school'; import { TaskCopyService, TaskService } from '@modules/task'; -import { BadRequestException, Injectable, InternalServerErrorException, NotImplementedException } from '@nestjs/common'; -import { BoardNodeAuthorizableService, BoardExternalReferenceType, ColumnBoardService } from '@modules/board'; +import { BadRequestException, Injectable, NotImplementedException } from '@nestjs/common'; +import { FeatureDisabledLoggableException } from '@shared/common/loggable-exception'; import { Course, User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { LegacyLogger } from '@src/core/logger'; -import { SchoolService } from '@src/modules/school'; +import { RoomMembershipService } from '@src/modules/room-membership'; import { ShareTokenContext, ShareTokenContextType, @@ -29,10 +39,12 @@ export class ShareTokenUC { private readonly courseCopyService: CourseCopyService, private readonly lessonCopyService: LessonCopyService, private readonly taskCopyService: TaskCopyService, - private readonly columnBoardService: ColumnBoardService, private readonly courseService: CourseService, private readonly lessonService: LessonService, private readonly taskService: TaskService, + private readonly roomService: RoomService, + private readonly roomMembershipService: RoomMembershipService, + private readonly columnBoardService: ColumnBoardService, private readonly schoolService: SchoolService, private readonly boardNodeAuthorizableService: BoardNodeAuthorizableService, private readonly logger: LegacyLogger @@ -75,7 +87,7 @@ export class ShareTokenUC { this.checkFeatureEnabled(shareToken.payload.parentType); - await this.checkLookupPermission(userId, shareToken.payload.parentType); + await this.checkTokenLookupPermission(userId, shareToken.payload); if (shareToken.context) { await this.checkContextReadPermission(userId, shareToken.context); @@ -94,7 +106,7 @@ export class ShareTokenUC { userId: EntityId, token: string, newName: string, - destinationCourseId?: string + destinationId?: EntityId ): Promise { this.logger.debug({ action: 'importShareToken', userId, token, newName }); @@ -115,22 +127,22 @@ export class ShareTokenUC { result = await this.copyCourse(user, shareToken.payload.parentId, newName); break; case ShareTokenParentType.Lesson: - if (destinationCourseId === undefined) { - throw new BadRequestException('Destination course id is required to copy lesson'); + if (destinationId === undefined) { + throw new BadRequestException('Cannot copy lesson without destination course reference'); } - result = await this.copyLesson(user, shareToken.payload.parentId, destinationCourseId, newName); + result = await this.copyLesson(user, shareToken.payload.parentId, destinationId, newName); break; case ShareTokenParentType.Task: - if (destinationCourseId === undefined) { - throw new BadRequestException('Destination course id is required to copy task'); + if (destinationId === undefined) { + throw new BadRequestException('Cannot copy task without destination course reference'); } - result = await this.copyTask(user, shareToken.payload.parentId, destinationCourseId, newName); + result = await this.copyTask(user, shareToken.payload.parentId, destinationId, newName); break; case ShareTokenParentType.ColumnBoard: - if (destinationCourseId === undefined) { - throw new BadRequestException('Destination course id is required to copy task'); + if (destinationId === undefined) { + throw new BadRequestException('Cannot copy board without destination course or room reference'); } - result = await this.copyColumnBoard(user, shareToken.payload.parentId, destinationCourseId, newName); + result = await this.copyColumnBoard(user, shareToken.payload.parentId, destinationId, newName); break; } @@ -180,14 +192,26 @@ export class ShareTokenUC { private async copyColumnBoard( user: User, originalColumnBoardId: string, - courseId: string, + destinationId: EntityId, copyTitle?: string ): Promise { - await this.checkCourseWritePermission(user, courseId, Permission.COURSE_EDIT); + const originalBoard = await this.columnBoardService.findById(originalColumnBoardId, 0); + + const targetExternalReference: BoardExternalReference = { + id: destinationId, + type: originalBoard.context.type, + }; + + await this.checkBoardContextWritePermission(user, targetExternalReference); + + const sourceStorageLocationReference = await this.getStorageLocationReference(originalBoard.context); + const targetStorageLocationReference = await this.getStorageLocationReference(targetExternalReference); const copyStatus = this.columnBoardService.copyColumnBoard({ originalColumnBoardId, - destinationExternalReference: { type: BoardExternalReferenceType.Course, id: courseId }, + targetExternalReference, + sourceStorageLocationReference, + targetStorageLocationReference, userId: user.id, copyTitle, }); @@ -206,7 +230,7 @@ export class ShareTokenUC { await this.checkTaskWritePermission(user, payload.parentId, Permission.HOMEWORK_CREATE); break; case ShareTokenParentType.ColumnBoard: - await this.checkColumnBoardWritePermission(user, payload.parentId, Permission.COURSE_EDIT); + await this.checkColumnBoardSharePermission(user, payload.parentId); break; default: } @@ -225,6 +249,16 @@ export class ShareTokenUC { }; } + private async checkRoomWritePermission(user: User, roomId: EntityId, permissions: Permission[] = []) { + const roomMembershipAuthorizable = await this.roomMembershipService.getRoomMembershipAuthorizable(roomId); + + this.authorizationService.checkPermission( + user, + roomMembershipAuthorizable, + AuthorizationContextBuilder.write(permissions) + ); + } + private async checkLessonWritePermission(user: User, lessonId: EntityId, permission: Permission) { const lesson = await this.lessonService.findById(lessonId); this.authorizationService.checkPermission(user, lesson, AuthorizationContextBuilder.write([permission])); @@ -235,14 +269,15 @@ export class ShareTokenUC { this.authorizationService.checkPermission(user, task, AuthorizationContextBuilder.write([permission])); } - private async checkColumnBoardWritePermission(user: User, boardNodeId: EntityId, permission: Permission) { - const columBoard = await this.columnBoardService.findById(boardNodeId); - const boardNodeAuthorizableService = await this.boardNodeAuthorizableService.getBoardAuthorizable(columBoard); + private async checkColumnBoardSharePermission(user: User, boardNodeId: EntityId) { + const columBoard = await this.columnBoardService.findById(boardNodeId, 0); + const boardNodeAuthorizable = await this.boardNodeAuthorizableService.getBoardAuthorizable(columBoard); + const permissions = columBoard.context.type === BoardExternalReferenceType.Course ? [Permission.COURSE_EDIT] : []; this.authorizationService.checkPermission( user, - boardNodeAuthorizableService, - AuthorizationContextBuilder.write([permission]) + boardNodeAuthorizable, + AuthorizationContextBuilder.write(permissions) ); } @@ -263,11 +298,11 @@ export class ShareTokenUC { } } - private async checkLookupPermission(userId: EntityId, parentType: ShareTokenParentType) { + private async checkTokenLookupPermission(userId: EntityId, payload: ShareTokenPayload) { const user = await this.authorizationService.getUserWithPermissions(userId); let requiredPermissions: Permission[] = []; // eslint-disable-next-line default-case - switch (parentType) { + switch (payload.parentType) { case ShareTokenParentType.Course: requiredPermissions = [Permission.COURSE_CREATE]; break; @@ -277,9 +312,12 @@ export class ShareTokenUC { case ShareTokenParentType.Task: requiredPermissions = [Permission.HOMEWORK_CREATE]; break; - case ShareTokenParentType.ColumnBoard: - requiredPermissions = [Permission.COURSE_EDIT]; + case ShareTokenParentType.ColumnBoard: { + const columnBoard = await this.columnBoardService.findById(payload.parentId, 0); + requiredPermissions = + columnBoard.context.type === BoardExternalReferenceType.Course ? [Permission.COURSE_EDIT] : []; break; + } } this.authorizationService.checkAllPermissions(user, requiredPermissions); } @@ -295,29 +333,58 @@ export class ShareTokenUC { case ShareTokenParentType.Course: // Configuration.get is the deprecated way to read envirment variables if (!(Configuration.get('FEATURE_COURSE_SHARE') as boolean)) { - throw new InternalServerErrorException('Import Course Feature not enabled'); + throw new FeatureDisabledLoggableException('FEATURE_COURSE_SHARE'); } break; case ShareTokenParentType.Lesson: // Configuration.get is the deprecated way to read envirment variables if (!(Configuration.get('FEATURE_LESSON_SHARE') as boolean)) { - throw new InternalServerErrorException('Import Lesson Feature not enabled'); + throw new FeatureDisabledLoggableException('FEATURE_LESSON_SHARE'); } break; case ShareTokenParentType.Task: // Configuration.get is the deprecated way to read envirment variables if (!(Configuration.get('FEATURE_TASK_SHARE') as boolean)) { - throw new InternalServerErrorException('Import Task Feature not enabled'); + throw new FeatureDisabledLoggableException('FEATURE_TASK_SHARE'); } break; case ShareTokenParentType.ColumnBoard: // Configuration.get is the deprecated way to read envirment variables if (!(Configuration.get('FEATURE_COLUMN_BOARD_SHARE') as boolean)) { - throw new InternalServerErrorException('Import Task Feature not enabled'); + throw new FeatureDisabledLoggableException('FEATURE_COLUMN_BOARD_SHARE'); } break; default: throw new NotImplementedException('Import Feature not implemented'); } } + + // ---- Move to shared service? (see apps/server/src/modules/board/uc/board.uc.ts) + + private async checkBoardContextWritePermission(user: User, boardContext: BoardExternalReference) { + if (boardContext.type === BoardExternalReferenceType.Course) { + await this.checkCourseWritePermission(user, boardContext.id, Permission.COURSE_EDIT); + } else if (boardContext.type === BoardExternalReferenceType.Room) { + await this.checkRoomWritePermission(user, boardContext.id); + } else { + /* istanbul ignore next */ + throw new Error(`Unsupported board reference type ${boardContext.type as string}`); + } + } + + private async getStorageLocationReference(boardContext: BoardExternalReference): Promise { + if (boardContext.type === BoardExternalReferenceType.Course) { + const course = await this.courseService.findById(boardContext.id); + + return { id: course.school.id, type: StorageLocation.SCHOOL }; + } + + if (boardContext.type === BoardExternalReferenceType.Room) { + const room = await this.roomService.getSingleRoom(boardContext.id); + + return { id: room.schoolId, type: StorageLocation.SCHOOL }; + } + /* istanbul ignore next */ + throw new Error(`Unsupported board reference type ${boardContext.type as string}`); + } } diff --git a/apps/server/src/shared/controller/transformer/index.ts b/apps/server/src/shared/controller/transformer/index.ts index 889f33130c7..1a037feef76 100644 --- a/apps/server/src/shared/controller/transformer/index.ts +++ b/apps/server/src/shared/controller/transformer/index.ts @@ -4,3 +4,4 @@ export * from './single-value-to-array.transformer'; export * from './sanitize-html.transformer'; export { PolymorphicArrayTransform } from './polymorphic-array.transformer'; export { StringToObject } from './string-to-object.transformer'; +export { NullToUndefined } from './null-to-undefined.transformer'; diff --git a/apps/server/src/shared/controller/transformer/null-to-undefined.transformer.spec.ts b/apps/server/src/shared/controller/transformer/null-to-undefined.transformer.spec.ts new file mode 100644 index 00000000000..490e8745be6 --- /dev/null +++ b/apps/server/src/shared/controller/transformer/null-to-undefined.transformer.spec.ts @@ -0,0 +1,35 @@ +import { plainToClass } from 'class-transformer'; +import { NullToUndefined } from './null-to-undefined.transformer'; + +describe('NullToUndefined Decorator', () => { + describe('when transforming an optionl value', () => { + class WithOptionalDto { + @NullToUndefined() + optionalDate?: Date; + } + + it('should transform from `null` to `undefined`', () => { + const props = { optionalDate: null }; + const instance = plainToClass(WithOptionalDto, props); + expect(instance.optionalDate).toEqual(undefined); + }); + + it('should transform from `undefined` to `undefined`', () => { + const props = { optionalDate: undefined }; + const instance = plainToClass(WithOptionalDto, props); + expect(instance.optionalDate).toEqual(undefined); + }); + + it('should transform from omitted property to `undefined`', () => { + const props = {}; + const instance = plainToClass(WithOptionalDto, props); + expect(instance.optionalDate).toEqual(undefined); + }); + + it('should transform from value to value', () => { + const props = { optionalDate: new Date() }; + const instance = plainToClass(WithOptionalDto, props); + expect(instance.optionalDate).toEqual(props.optionalDate); + }); + }); +}); diff --git a/apps/server/src/shared/controller/transformer/null-to-undefined.transformer.ts b/apps/server/src/shared/controller/transformer/null-to-undefined.transformer.ts new file mode 100644 index 00000000000..1be36c10825 --- /dev/null +++ b/apps/server/src/shared/controller/transformer/null-to-undefined.transformer.ts @@ -0,0 +1,12 @@ +import { Transform } from 'class-transformer'; + +/** + * Decorator to replace a null value by undefined. + * Can be used to make optinal values consistent. + * Place after IsOptional decorator. + * It will return undefined if the value is null. + * @returns + */ +export function NullToUndefined(): PropertyDecorator { + return Transform(({ value }): unknown => (value === null ? undefined : value)); +} diff --git a/apps/server/src/shared/domain/entity/all-entities.ts b/apps/server/src/shared/domain/entity/all-entities.ts index 45d69efea23..ff570b7c6ac 100644 --- a/apps/server/src/shared/domain/entity/all-entities.ts +++ b/apps/server/src/shared/domain/entity/all-entities.ts @@ -11,6 +11,7 @@ import { ExternalToolPseudonymEntity, PseudonymEntity } from '@modules/pseudonym import { RegistrationPinEntity } from '@modules/registration-pin/entity'; import { RocketChatUserEntity } from '@modules/rocketchat-user/entity'; import { RoomEntity } from '@modules/room/repo/entity'; +import { RoomMembershipEntity } from '@modules/room-membership/repo/entity/room-membership.entity'; import { ShareToken } from '@modules/sharing/entity/share-token.entity'; import { SystemEntity } from '@modules/system/entity/system.entity'; import { TldrawDrawing } from '@modules/tldraw/entities'; @@ -19,7 +20,6 @@ import { ExternalToolEntity } from '@modules/tool/external-tool/entity'; import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; import { ImportUser } from '@modules/user-import/entity'; import { MediaSourceEntity, MediaUserLicenseEntity, UserLicenseEntity } from '@modules/user-license/entity'; -import { RoomMemberEntity } from '@src/modules/room-member/repo/entity/room-member.entity'; import { ColumnBoardNode } from './column-board-node.entity'; import { Course } from './course.entity'; import { CourseGroup } from './coursegroup.entity'; @@ -77,7 +77,7 @@ export const ALL_ENTITIES = [ RocketChatUserEntity, Role, RoomEntity, - RoomMemberEntity, + RoomMembershipEntity, SchoolEntity, SchoolExternalToolEntity, SchoolNews, diff --git a/apps/server/src/shared/domain/interface/permission.enum.ts b/apps/server/src/shared/domain/interface/permission.enum.ts index 125e027653f..c5bed37ad11 100644 --- a/apps/server/src/shared/domain/interface/permission.enum.ts +++ b/apps/server/src/shared/domain/interface/permission.enum.ts @@ -100,8 +100,10 @@ export enum Permission { ROLE_CREATE = 'ROLE_CREATE', ROLE_EDIT = 'ROLE_EDIT', ROLE_VIEW = 'ROLE_VIEW', + ROOM_CREATE = 'ROOM_CREATE', ROOM_EDIT = 'ROOM_EDIT', ROOM_VIEW = 'ROOM_VIEW', + ROOM_DELETE = 'ROOM_DELETE', SCHOOL_CHAT_MANAGE = 'SCHOOL_CHAT_MANAGE', SCHOOL_CREATE = 'SCHOOL_CREATE', SCHOOL_EDIT = 'SCHOOL_EDIT', diff --git a/apps/server/src/shared/domain/interface/rolename.enum.ts b/apps/server/src/shared/domain/interface/rolename.enum.ts index 0fa35e22862..e354109efd3 100644 --- a/apps/server/src/shared/domain/interface/rolename.enum.ts +++ b/apps/server/src/shared/domain/interface/rolename.enum.ts @@ -11,8 +11,8 @@ export enum RoleName { GUESTTEACHER = 'guestTeacher', GUESTSTUDENT = 'guestStudent', HELPDESK = 'helpdesk', - ROOM_VIEWER = 'room_viewer', - ROOM_EDITOR = 'room_editor', + ROOMVIEWER = 'roomviewer', + ROOMEDITOR = 'roomeditor', STUDENT = 'student', SUPERHERO = 'superhero', TEACHER = 'teacher', @@ -32,7 +32,7 @@ export type IUserRoleName = | RoleName.DEMOSTUDENT | RoleName.DEMOTEACHER; -export const RoomRoleArray = [RoleName.ROOM_EDITOR, RoleName.ROOM_VIEWER] as const; +export const RoomRoleArray = [RoleName.ROOMEDITOR, RoleName.ROOMVIEWER] as const; export type RoomRole = typeof RoomRoleArray[number]; export const GuestRoleArray = [RoleName.GUESTSTUDENT, RoleName.GUESTTEACHER] as const; diff --git a/apps/server/src/shared/domain/types/task.types.ts b/apps/server/src/shared/domain/types/task.types.ts index c9ce4ebb698..a5812f5e522 100644 --- a/apps/server/src/shared/domain/types/task.types.ts +++ b/apps/server/src/shared/domain/types/task.types.ts @@ -1,5 +1,5 @@ -import type { Course, LessonEntity, SchoolEntity, Submission, User } from '@shared/domain/entity'; -import type { InputFormat } from '@shared/domain/types'; +import type { Course, LessonEntity, SchoolEntity, Submission, User } from '../entity'; +import type { InputFormat } from './input-format.types'; interface ITask { name: string; diff --git a/apps/server/src/shared/testing/user-role-permissions.ts b/apps/server/src/shared/testing/user-role-permissions.ts index 378ba8859d1..e8e49a85dc2 100644 --- a/apps/server/src/shared/testing/user-role-permissions.ts +++ b/apps/server/src/shared/testing/user-role-permissions.ts @@ -103,6 +103,7 @@ export const teacherPermissions = [ Permission.TOPIC_EDIT, Permission.START_MEETING, Permission.CONTEXT_TOOL_ADMIN, + Permission.ROOM_CREATE, ]; export const adminPermissions = [ diff --git a/backup/setup/migrations.json b/backup/setup/migrations.json index f31be452adb..1679b78b973 100644 --- a/backup/setup/migrations.json +++ b/backup/setup/migrations.json @@ -262,11 +262,38 @@ }, { "_id": { - "$oid": "674847ac3c76d17b0c01c155" + "$oid": "673237e9b0955dcff4cde3d9" }, - "name": "Migration20241127134513", + "name": "Migration20241111160412", "created_at": { - "$date": "2024-11-28T10:37:51.515Z" + "$date": "2024-11-11T16:59:21.768Z" + } + }, + { + "_id": { + "$oid": "673387c13aba1e283484119d" + }, + "name": "Migration20241112163538", + "created_at": { + "$date": "2024-11-12T16:52:17.292Z" + } + }, + { + "_id": { + "$oid": "67347bb8b1bcb78aecbab90f" + }, + "name": "Migration20241113100535", + "created_at": { + "$date": "2024-11-13T10:13:12.411Z" + } + }, + { + "_id": { + "$oid": "67361aa7776f2f3e5a519735" + }, + "name": "Migration20241113152001", + "created_at": { + "$date": "2024-11-14T15:43:35.024Z" } }, { @@ -277,5 +304,32 @@ "created_at": { "$date": "2024-11-25T09:32:22.556Z" } + }, + { + "_id": { + "$oid": "674847ac3c76d17b0c01c155" + }, + "name": "Migration20241127134513", + "created_at": { + "$date": "2024-11-28T10:37:51.515Z" + } + }, + { + "_id": { + "$oid": "67477a7455d881b78f7a79fa" + }, + "name": "Migration20241127195120", + "created_at": { + "$date": "2024-11-27T20:00:52.582Z" + } + }, + { + "_id": { + "$oid": "6748b0f451c62e9dc6899983" + }, + "name": "Migration20241128155801", + "created_at": { + "$date": "2024-11-28T18:05:40.839Z" + } } ] diff --git a/backup/setup/roles.json b/backup/setup/roles.json index 800ad0bc742..81c1b5bc4af 100644 --- a/backup/setup/roles.json +++ b/backup/setup/roles.json @@ -214,7 +214,7 @@ }, "name": "teacher", "updatedAt": { - "$date": "2024-04-19T10:32:51.070Z" + "$date": "2024-11-12T16:35:53.763Z" }, "createdAt": { "$date": "2017-01-01T00:06:37.148Z" @@ -263,7 +263,8 @@ "USERGROUP_CREATE", "USERGROUP_EDIT", "USER_CHANGE_OWN_NAME", - "USER_CREATE" + "USER_CREATE", + "ROOM_CREATE" ], "__v": 2 }, @@ -586,7 +587,7 @@ "_id": { "$oid": "6720b8621b61c9dd7ebd193b" }, - "name": "room_viewer", + "name": "roomviewer", "permissions": [ "ROOM_VIEW" ] @@ -595,10 +596,11 @@ "_id": { "$oid": "6720b8621b61c9dd7ebd193c" }, - "name": "room_editor", + "name": "roomeditor", "permissions": [ "ROOM_VIEW", - "ROOM_EDIT" + "ROOM_EDIT", + "ROOM_DELETE" ] }, {