From d2edfc0e8fa6ea440b78da9927cffd06a717b131 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= <103562092+MarvinOehlerkingCap@users.noreply.github.com> Date: Wed, 12 Jun 2024 14:00:26 +0200 Subject: [PATCH] N21-1967 Adjust Filestorage for instance files (#5051) * adds fileStorageLocation and fileStorageId --- .../infra/rabbitmq/exchange/files-storage.ts | 1 + .../mikro-orm/Migration20240604131554.ts | 44 ++++++ .../mikro-orm/Migration20240605065231.ts | 23 +++ .../mikro-orm/Migration20240606142059.ts | 39 +++++ .../authorization-reference.module.ts | 2 + .../authorization/authorization.module.ts | 4 + .../domain/rules/board-do.rule.ts | 24 ++- .../rules/context-external-tool.rule.ts | 18 +-- .../domain/rules/course-group.rule.ts | 16 +- .../authorization/domain/rules/course.rule.ts | 10 +- .../domain/rules/external-tool.rule.spec.ts | 148 ++++++++++++++++++ .../domain/rules/external-tool.rule.ts | 22 +++ .../authorization/domain/rules/group.rule.ts | 8 +- .../authorization/domain/rules/index.ts | 2 + .../domain/rules/instance.rule.spec.ts | 142 +++++++++++++++++ .../domain/rules/instance.rule.ts | 22 +++ .../domain/rules/legacy-school.rule.ts | 11 +- .../authorization/domain/rules/lesson.rule.ts | 14 +- .../domain/rules/school-external-tool.rule.ts | 18 +-- .../rules/school-system-options.rule.ts | 12 +- .../domain/rules/school.rule.spec.ts | 1 - .../authorization/domain/rules/school.rule.ts | 5 +- .../domain/rules/submission.rule.spec.ts | 11 +- .../domain/rules/submission.rule.ts | 8 +- .../authorization/domain/rules/system.rule.ts | 10 +- .../authorization/domain/rules/task.rule.ts | 16 +- .../domain/rules/team.rule.spec.ts | 6 +- .../authorization/domain/rules/team.rule.ts | 12 +- .../domain/rules/user-login-migration.rule.ts | 6 +- .../authorization/domain/rules/user.rule.ts | 8 +- .../domain/service/reference.loader.spec.ts | 27 +++- .../domain/service/reference.loader.ts | 12 +- .../domain/service/rule-manager.spec.ts | 16 ++ .../domain/service/rule-manager.ts | 8 +- .../allowed-authorization-object-type.enum.ts | 2 + .../domain/type/rule.interface.ts | 4 +- .../api-test/files-security.api.spec.ts | 19 +-- .../files-storage-copy-files.api.spec.ts | 77 +++++---- .../files-storage-delete-files.api.spec.ts | 70 ++++----- .../files-storage-download-upload.api.spec.ts | 77 +++++---- .../files-storage-list-files.api.spec.ts | 50 +++--- .../files-storage-preview.api.spec.ts | 13 +- .../files-storage-rename-file.api.spec.ts | 12 +- .../files-storage-restore-files.api.spec.ts | 61 ++++---- .../controller/api-test/mocks.ts | 3 +- .../controller/dto/file-storage.params.ts | 8 +- .../controller/files-storage.consumer.spec.ts | 28 ++-- .../controller/files-storage.controller.ts | 12 +- .../entity/filerecord.entity.spec.ts | 50 +++--- .../files-storage/entity/filerecord.entity.ts | 41 +++-- .../files-storage/helper/file-name.spec.ts | 10 +- .../files-storage/helper/file-record.spec.ts | 19 ++- .../files-storage/helper/file-record.ts | 3 +- .../modules/files-storage/helper/path.spec.ts | 22 +-- .../src/modules/files-storage/helper/path.ts | 20 +-- .../mapper/files-storage.mapper.spec.ts | 10 +- .../mapper/files-storage.mapper.ts | 45 +++--- .../files-storage/mapper/preview.builder.ts | 6 +- .../files-storage/repo/filerecord-scope.ts | 12 +- .../repo/filerecord.repo.integration.spec.ts | 101 ++++++++---- .../files-storage/repo/filerecord.repo.ts | 24 ++- .../files-storage-copy.service.spec.ts | 34 ++-- .../files-storage-delete.service.spec.ts | 17 +- .../files-storage-download.service.spec.ts | 19 +-- .../service/files-storage-get.service.spec.ts | 21 +-- ...les-storage-remove-creator.service.spec.ts | 17 +- .../files-storage-restore.service.spec.ts | 32 ++-- .../files-storage-update.service.spec.ts | 21 +-- .../files-storage-upload.service.spec.ts | 15 +- .../service/files-storage.service.ts | 15 +- .../service/preview.service.spec.ts | 43 ++--- .../files-storage/service/preview.service.ts | 6 +- .../uc/files-storage-copy.uc.spec.ts | 40 ++--- .../uc/files-storage-delete.uc.spec.ts | 20 +-- .../files-storage-download-preview.uc.spec.ts | 6 +- .../uc/files-storage-download.uc.spec.ts | 6 +- .../uc/files-storage-get.uc.spec.ts | 15 +- .../uc/files-storage-restore.uc.spec.ts | 21 +-- .../uc/files-storage-update.uc.spec.ts | 6 +- .../uc/files-storage-upload.uc.spec.ts | 59 +++++-- .../files-storage/uc/files-storage.uc.ts | 45 +++++- .../src/modules/instance/domain/index.ts | 1 + .../src/modules/instance/domain/instance.ts | 11 ++ .../src/modules/instance/entity/index.ts | 1 + .../instance/entity/instance.entity.ts | 23 +++ apps/server/src/modules/instance/index.ts | 4 + .../src/modules/instance/instance.module.ts | 9 ++ .../server/src/modules/instance/repo/index.ts | 1 + .../instance/repo/instance-repo.service.ts | 38 +++++ .../instance/repo/instance.repo.spec.ts | 119 ++++++++++++++ .../src/modules/instance/service/index.ts | 1 + .../instance/service/instance.service.spec.ts | 57 +++++++ .../instance/service/instance.service.ts | 16 ++ .../src/modules/instance/testing/index.ts | 2 + .../testing/instance-entity.factory.ts | 10 ++ .../instance/testing/instance.factory.ts | 10 ++ .../learnroom/repo/mikro-orm/course.repo.ts | 2 +- .../external-tool/external-tool.module.ts | 3 + ...external-tool-authorizable.service.spec.ts | 57 +++++++ .../external-tool-authorizable.service.ts | 16 ++ .../tool/external-tool/service/index.ts | 1 + apps/server/src/modules/tool/index.ts | 1 + .../src/shared/domain/entity/all-entities.ts | 4 +- .../domain/interface/permission.enum.ts | 1 + ...ase-domain-object.repo.integration.spec.ts | 2 +- .../shared/repo/base-domain-object.repo.ts | 4 +- .../testing/factory/filerecord.factory.ts | 10 +- .../shared/testing/user-role-permissions.ts | 2 +- backup/setup/instances.json | 26 +++ backup/setup/migrations.json | 39 ++++- backup/setup/roles.json | 3 +- 111 files changed, 1775 insertions(+), 682 deletions(-) create mode 100644 apps/server/src/migrations/mikro-orm/Migration20240604131554.ts create mode 100644 apps/server/src/migrations/mikro-orm/Migration20240605065231.ts create mode 100644 apps/server/src/migrations/mikro-orm/Migration20240606142059.ts create mode 100644 apps/server/src/modules/authorization/domain/rules/external-tool.rule.spec.ts create mode 100644 apps/server/src/modules/authorization/domain/rules/external-tool.rule.ts create mode 100644 apps/server/src/modules/authorization/domain/rules/instance.rule.spec.ts create mode 100644 apps/server/src/modules/authorization/domain/rules/instance.rule.ts create mode 100644 apps/server/src/modules/instance/domain/index.ts create mode 100644 apps/server/src/modules/instance/domain/instance.ts create mode 100644 apps/server/src/modules/instance/entity/index.ts create mode 100644 apps/server/src/modules/instance/entity/instance.entity.ts create mode 100644 apps/server/src/modules/instance/index.ts create mode 100644 apps/server/src/modules/instance/instance.module.ts create mode 100644 apps/server/src/modules/instance/repo/index.ts create mode 100644 apps/server/src/modules/instance/repo/instance-repo.service.ts create mode 100644 apps/server/src/modules/instance/repo/instance.repo.spec.ts create mode 100644 apps/server/src/modules/instance/service/index.ts create mode 100644 apps/server/src/modules/instance/service/instance.service.spec.ts create mode 100644 apps/server/src/modules/instance/service/instance.service.ts create mode 100644 apps/server/src/modules/instance/testing/index.ts create mode 100644 apps/server/src/modules/instance/testing/instance-entity.factory.ts create mode 100644 apps/server/src/modules/instance/testing/instance.factory.ts create mode 100644 apps/server/src/modules/tool/external-tool/service/external-tool-authorizable.service.spec.ts create mode 100644 apps/server/src/modules/tool/external-tool/service/external-tool-authorizable.service.ts create mode 100644 backup/setup/instances.json diff --git a/apps/server/src/infra/rabbitmq/exchange/files-storage.ts b/apps/server/src/infra/rabbitmq/exchange/files-storage.ts index 15a9139fec7..75adc45193a 100644 --- a/apps/server/src/infra/rabbitmq/exchange/files-storage.ts +++ b/apps/server/src/infra/rabbitmq/exchange/files-storage.ts @@ -25,6 +25,7 @@ export enum FileRecordParentType { 'Submission' = 'submissions', 'Grading' = 'gradings', 'BoardNode' = 'boardnodes', + 'ExternalTool' = 'externaltools', } export interface CopyFilesOfParentParams { diff --git a/apps/server/src/migrations/mikro-orm/Migration20240604131554.ts b/apps/server/src/migrations/mikro-orm/Migration20240604131554.ts new file mode 100644 index 00000000000..15e088a2e51 --- /dev/null +++ b/apps/server/src/migrations/mikro-orm/Migration20240604131554.ts @@ -0,0 +1,44 @@ +import { Migration } from '@mikro-orm/migrations-mongodb'; +import * as process from 'node:process'; + +export class Migration20240604131554 extends Migration { + async up(): Promise { + // eslint-disable-next-line no-process-env + if (process.env.SC_THEME === 'n21') { + await this.driver.nativeInsert('instances', { + name: 'nbc', + }); + console.info('Instance was added for nbc'); + } + + // eslint-disable-next-line no-process-env + if (process.env.SC_THEME === 'brb') { + await this.driver.nativeInsert('instances', { + name: 'brb', + }); + console.info('Instance was added for brb'); + } + + // eslint-disable-next-line no-process-env + if (process.env.SC_THEME === 'thr') { + await this.driver.nativeInsert('instances', { + name: 'thr', + }); + console.info('Instance was added for thr'); + } + + // eslint-disable-next-line no-process-env + if (process.env.SC_THEME === 'default') { + await this.driver.nativeInsert('instances', { + name: 'dbc', + }); + console.info('Instance was added for default'); + } + } + + async down(): Promise { + await this.getCollection('instances').drop(); + + console.info('Collection "instances" was dropped'); + } +} diff --git a/apps/server/src/migrations/mikro-orm/Migration20240605065231.ts b/apps/server/src/migrations/mikro-orm/Migration20240605065231.ts new file mode 100644 index 00000000000..1372492b13f --- /dev/null +++ b/apps/server/src/migrations/mikro-orm/Migration20240605065231.ts @@ -0,0 +1,23 @@ +import { Migration } from '@mikro-orm/migrations-mongodb'; + +export class Migration20240605065231 extends Migration { + async up(): Promise { + const filerecords = await this.driver.nativeUpdate( + 'filerecords', + {}, + { $rename: { schoolId: 'storageLocationId' }, $set: { storageLocation: 'school' } } + ); + + console.info(`${filerecords.affectedRows} Filerecords were migrated to "storageLocationId" and "storageLocation"`); + } + + async down(): Promise { + const filerecords = await this.driver.nativeUpdate( + 'filerecords', + {}, + { $rename: { storageLocationId: 'schoolId' }, $unset: { storageLocation: '' } } + ); + + console.info(`${filerecords.affectedRows} Filerecords were rolled back to use "schoolId"`); + } +} diff --git a/apps/server/src/migrations/mikro-orm/Migration20240606142059.ts b/apps/server/src/migrations/mikro-orm/Migration20240606142059.ts new file mode 100644 index 00000000000..3bbbd9395b9 --- /dev/null +++ b/apps/server/src/migrations/mikro-orm/Migration20240606142059.ts @@ -0,0 +1,39 @@ +import { Migration } from '@mikro-orm/migrations-mongodb'; + +export class Migration20240606142059 extends Migration { + async up(): Promise { + const superheroRole = await this.driver.nativeUpdate( + 'roles', + { name: 'superhero' }, + { + $addToSet: { + permissions: { + $each: ['INSTANCE_VIEW'], + }, + }, + } + ); + + if (superheroRole.affectedRows > 0) { + console.info('Permissions INSTANCE_VIEW was added to role superhero.'); + } + } + + async down(): Promise { + const superheroRole = await this.driver.nativeUpdate( + 'roles', + { name: 'superhero' }, + { + $pull: { + permissions: { + $in: ['INSTANCE_VIEW'], + }, + }, + } + ); + + if (superheroRole.affectedRows > 0) { + console.info('Permissions INSTANCE_VIEW was removed to role superhero.'); + } + } +} diff --git a/apps/server/src/modules/authorization/authorization-reference.module.ts b/apps/server/src/modules/authorization/authorization-reference.module.ts index 455d26fb27b..a5d59753c58 100644 --- a/apps/server/src/modules/authorization/authorization-reference.module.ts +++ b/apps/server/src/modules/authorization/authorization-reference.module.ts @@ -1,4 +1,5 @@ import { BoardModule } from '@modules/board'; +import { InstanceModule } from '@modules/instance'; import { LessonModule } from '@modules/lesson'; import { ToolModule } from '@modules/tool'; import { forwardRef, Module } from '@nestjs/common'; @@ -29,6 +30,7 @@ import { AuthorizationHelper, AuthorizationReferenceService, ReferenceLoader } f forwardRef(() => ToolModule), forwardRef(() => BoardModule), LoggerModule, + InstanceModule, ], providers: [ AuthorizationHelper, diff --git a/apps/server/src/modules/authorization/authorization.module.ts b/apps/server/src/modules/authorization/authorization.module.ts index 5cc951ac889..dfd6038cd8b 100644 --- a/apps/server/src/modules/authorization/authorization.module.ts +++ b/apps/server/src/modules/authorization/authorization.module.ts @@ -8,7 +8,9 @@ import { ContextExternalToolRule, CourseGroupRule, CourseRule, + ExternalToolRule, GroupRule, + InstanceRule, LegacySchoolRule, LessonRule, SchoolExternalToolRule, @@ -49,6 +51,8 @@ import { FeathersAuthorizationService, FeathersAuthProvider } from './feathers'; LegacySchoolRule, SystemRule, SchoolSystemOptionsRule, + ExternalToolRule, + InstanceRule, ], exports: [FeathersAuthorizationService, AuthorizationService, SystemRule], }) diff --git a/apps/server/src/modules/authorization/domain/rules/board-do.rule.ts b/apps/server/src/modules/authorization/domain/rules/board-do.rule.ts index e3856781d09..726fcd12bd7 100644 --- a/apps/server/src/modules/authorization/domain/rules/board-do.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/board-do.rule.ts @@ -14,43 +14,39 @@ import { AuthorizationHelper } from '../service/authorization.helper'; import { Action, AuthorizationContext, Rule } from '../type'; @Injectable() -export class BoardDoRule implements Rule { +export class BoardDoRule implements Rule { constructor(private readonly authorizationHelper: AuthorizationHelper) {} - public isApplicable(user: User, boardDoAuthorizable: unknown): boolean { - const isMatched = boardDoAuthorizable instanceof BoardDoAuthorizable; + public isApplicable(user: User, object: unknown): boolean { + const isMatched = object instanceof BoardDoAuthorizable; return isMatched; } - public hasPermission(user: User, boardDoAuthorizable: BoardDoAuthorizable, context: AuthorizationContext): boolean { + public hasPermission(user: User, object: BoardDoAuthorizable, context: AuthorizationContext): boolean { const hasPermission = this.authorizationHelper.hasAllPermissions(user, context.requiredPermissions); if (!hasPermission) { return false; } - const userWithBoardRoles = boardDoAuthorizable.users.find(({ userId }) => userId === user.id); + const userWithBoardRoles = object.users.find(({ userId }) => userId === user.id); if (!userWithBoardRoles) { return false; } - if ( - boardDoAuthorizable.rootDo instanceof ColumnBoard && - !boardDoAuthorizable.rootDo.isVisible && - !this.isBoardEditor(userWithBoardRoles) - ) { + if (object.rootDo instanceof ColumnBoard && !object.rootDo.isVisible && !this.isBoardEditor(userWithBoardRoles)) { return false; } - if (this.shouldProcessSubmissionItem(boardDoAuthorizable)) { - return this.hasPermissionForSubmissionItem(user, userWithBoardRoles, boardDoAuthorizable, context); + if (this.shouldProcessSubmissionItem(object)) { + return this.hasPermissionForSubmissionItem(user, userWithBoardRoles, object, context); } - if (this.shouldProcessDrawingElementFile(boardDoAuthorizable, context)) { + if (this.shouldProcessDrawingElementFile(object, context)) { return this.hasPermissionForDrawingElementFile(userWithBoardRoles); } - if (this.shouldProcessDrawingElement(boardDoAuthorizable)) { + if (this.shouldProcessDrawingElement(object)) { return this.hasPermissionForDrawingElement(userWithBoardRoles, context); } diff --git a/apps/server/src/modules/authorization/domain/rules/context-external-tool.rule.ts b/apps/server/src/modules/authorization/domain/rules/context-external-tool.rule.ts index 0aee8334aac..373d6debf41 100644 --- a/apps/server/src/modules/authorization/domain/rules/context-external-tool.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/context-external-tool.rule.ts @@ -1,34 +1,34 @@ -import { Injectable } from '@nestjs/common'; import { ContextExternalTool } from '@modules/tool/context-external-tool/domain'; import { ContextExternalToolEntity } from '@modules/tool/context-external-tool/entity'; +import { Injectable } from '@nestjs/common'; import { User } from '@shared/domain/entity'; -import { AuthorizationContext, Rule } from '../type'; import { AuthorizationHelper } from '../service/authorization.helper'; +import { AuthorizationContext, Rule } from '../type'; @Injectable() -export class ContextExternalToolRule implements Rule { +export class ContextExternalToolRule implements Rule { constructor(private readonly authorizationHelper: AuthorizationHelper) {} - public isApplicable(user: User, entity: ContextExternalToolEntity | ContextExternalTool): boolean { - const isMatched: boolean = entity instanceof ContextExternalToolEntity || entity instanceof ContextExternalTool; + public isApplicable(user: User, object: unknown): boolean { + const isMatched: boolean = object instanceof ContextExternalToolEntity || object instanceof ContextExternalTool; return isMatched; } public hasPermission( user: User, - entity: ContextExternalToolEntity | ContextExternalTool, + object: ContextExternalToolEntity | ContextExternalTool, context: AuthorizationContext ): boolean { let hasPermission: boolean; - if (entity instanceof ContextExternalToolEntity) { + if (object instanceof ContextExternalToolEntity) { hasPermission = this.authorizationHelper.hasAllPermissions(user, context.requiredPermissions) && - user.school.id === entity.schoolTool.school.id; + user.school.id === object.schoolTool.school.id; } else { hasPermission = this.authorizationHelper.hasAllPermissions(user, context.requiredPermissions) && - user.school.id === entity.schoolToolRef.schoolId; + user.school.id === object.schoolToolRef.schoolId; } return hasPermission; } diff --git a/apps/server/src/modules/authorization/domain/rules/course-group.rule.ts b/apps/server/src/modules/authorization/domain/rules/course-group.rule.ts index 863d7072ec8..cd21258a802 100644 --- a/apps/server/src/modules/authorization/domain/rules/course-group.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/course-group.rule.ts @@ -1,26 +1,26 @@ import { Injectable } from '@nestjs/common'; import { CourseGroup, User } from '@shared/domain/entity'; -import { CourseRule } from './course.rule'; -import { Action, AuthorizationContext, Rule } from '../type'; import { AuthorizationHelper } from '../service/authorization.helper'; +import { Action, AuthorizationContext, Rule } from '../type'; +import { CourseRule } from './course.rule'; @Injectable() -export class CourseGroupRule implements Rule { +export class CourseGroupRule implements Rule { constructor(private readonly authorizationHelper: AuthorizationHelper, private readonly courseRule: CourseRule) {} - public isApplicable(user: User, entity: CourseGroup): boolean { - const isMatched = entity instanceof CourseGroup; + public isApplicable(user: User, object: unknown): boolean { + const isMatched = object instanceof CourseGroup; return isMatched; } - public hasPermission(user: User, entity: CourseGroup, context: AuthorizationContext): boolean { + public hasPermission(user: User, object: CourseGroup, context: AuthorizationContext): boolean { const { requiredPermissions } = context; const hasAllPermissions = this.authorizationHelper.hasAllPermissions(user, requiredPermissions); const hasPermission = - this.authorizationHelper.hasAccessToEntity(user, entity, ['students']) || - this.courseRule.hasPermission(user, entity.course, { action: Action.write, requiredPermissions: [] }); + this.authorizationHelper.hasAccessToEntity(user, object, ['students']) || + this.courseRule.hasPermission(user, object.course, { action: Action.write, requiredPermissions: [] }); return hasAllPermissions && hasPermission; } diff --git a/apps/server/src/modules/authorization/domain/rules/course.rule.ts b/apps/server/src/modules/authorization/domain/rules/course.rule.ts index e1155d0587b..f4c3b51f84a 100644 --- a/apps/server/src/modules/authorization/domain/rules/course.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/course.rule.ts @@ -5,22 +5,22 @@ import { AuthorizationHelper } from '../service/authorization.helper'; import { Action, AuthorizationContext, Rule } from '../type'; @Injectable() -export class CourseRule implements Rule { +export class CourseRule implements Rule { constructor(private readonly authorizationHelper: AuthorizationHelper) {} - public isApplicable(user: User, entity: unknown): boolean { - const isMatched = entity instanceof CourseEntity || entity instanceof Course; + public isApplicable(user: User, object: unknown): boolean { + const isMatched = object instanceof CourseEntity || object instanceof Course; return isMatched; } - public hasPermission(user: User, entity: CourseEntity | Course, context: AuthorizationContext): boolean { + public hasPermission(user: User, object: CourseEntity | Course, context: AuthorizationContext): boolean { const { action, requiredPermissions } = context; const hasPermission = this.authorizationHelper.hasAllPermissions(user, requiredPermissions) && this.authorizationHelper.hasAccessToEntity( user, - entity, + object, action === Action.read ? ['teachers', 'substitutionTeachers', 'students'] : ['teachers', 'substitutionTeachers'] ); diff --git a/apps/server/src/modules/authorization/domain/rules/external-tool.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/external-tool.rule.spec.ts new file mode 100644 index 00000000000..131032b0e35 --- /dev/null +++ b/apps/server/src/modules/authorization/domain/rules/external-tool.rule.spec.ts @@ -0,0 +1,148 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ExternalTool } from '@modules/tool/external-tool/domain'; +import { externalToolFactory } from '@modules/tool/external-tool/testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { User } from '@shared/domain/entity'; +import { Permission } from '@shared/domain/interface'; +import { setupEntities, userFactory } from '@shared/testing'; +import { AuthorizationHelper } from '../service/authorization.helper'; +import { Action } from '../type'; +import { ExternalToolRule } from './external-tool.rule'; + +describe(ExternalToolRule.name, () => { + let module: TestingModule; + let rule: ExternalToolRule; + + let authorizationHelper: DeepMocked; + + beforeAll(async () => { + await setupEntities(); + + module = await Test.createTestingModule({ + providers: [ + ExternalToolRule, + { + provide: AuthorizationHelper, + useValue: createMock(), + }, + ], + }).compile(); + + rule = module.get(ExternalToolRule); + authorizationHelper = module.get(AuthorizationHelper); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('isApplicable', () => { + describe('when the object is an external tool', () => { + const setup = () => { + const user: User = userFactory.build(); + const object: ExternalTool = externalToolFactory.build(); + + return { + object, + user, + }; + }; + + it('should return true', () => { + const { user, object } = setup(); + + const result = rule.isApplicable(user, object); + + expect(result).toEqual(true); + }); + }); + + describe('when the object is not an external tool', () => { + it('should return false', () => { + const user: User = userFactory.build(); + + const result = rule.isApplicable(user, user); + + expect(result).toEqual(false); + }); + }); + }); + + describe('hasPermission', () => { + describe('when the user has the permission', () => { + const setup = () => { + const user: User = userFactory.build(); + const object: ExternalTool = externalToolFactory.build(); + + authorizationHelper.hasAllPermissions.mockReturnValueOnce(true); + + return { + object, + user, + }; + }; + + it('should check all permissions', () => { + const { user, object } = setup(); + + rule.hasPermission(user, object, { + action: Action.read, + requiredPermissions: [Permission.TOOL_ADMIN], + }); + + expect(authorizationHelper.hasAllPermissions).toHaveBeenCalledWith(user, [Permission.TOOL_ADMIN]); + }); + + it('should return true', () => { + const { user, object } = setup(); + + const result = rule.hasPermission(user, object, { + action: Action.read, + requiredPermissions: [Permission.TOOL_ADMIN], + }); + + expect(result).toEqual(true); + }); + }); + + describe('when the user does not have the permission', () => { + const setup = () => { + const user: User = userFactory.build(); + const object: ExternalTool = externalToolFactory.build(); + + authorizationHelper.hasAllPermissions.mockReturnValueOnce(false); + + return { + object, + user, + }; + }; + + it('should check all permissions', () => { + const { user, object } = setup(); + + rule.hasPermission(user, object, { + action: Action.read, + requiredPermissions: [Permission.TOOL_ADMIN], + }); + + expect(authorizationHelper.hasAllPermissions).toHaveBeenCalledWith(user, [Permission.TOOL_ADMIN]); + }); + + it('should return false', () => { + const { user, object } = setup(); + + const result = rule.hasPermission(user, object, { + action: Action.read, + requiredPermissions: [Permission.TOOL_ADMIN], + }); + + expect(result).toEqual(false); + }); + }); + }); +}); diff --git a/apps/server/src/modules/authorization/domain/rules/external-tool.rule.ts b/apps/server/src/modules/authorization/domain/rules/external-tool.rule.ts new file mode 100644 index 00000000000..922846dbf21 --- /dev/null +++ b/apps/server/src/modules/authorization/domain/rules/external-tool.rule.ts @@ -0,0 +1,22 @@ +import { ExternalTool } from '@modules/tool/external-tool/domain'; +import { Injectable } from '@nestjs/common'; +import { User } from '@shared/domain/entity'; +import { AuthorizationHelper } from '../service/authorization.helper'; +import { AuthorizationContext, Rule } from '../type'; + +@Injectable() +export class ExternalToolRule implements Rule { + constructor(private readonly authorizationHelper: AuthorizationHelper) {} + + public isApplicable(user: User, object: unknown): boolean { + const isMatched: boolean = object instanceof ExternalTool; + + return isMatched; + } + + public hasPermission(user: User, object: ExternalTool, context: AuthorizationContext): boolean { + const hasPermission: boolean = this.authorizationHelper.hasAllPermissions(user, context.requiredPermissions); + + return hasPermission; + } +} diff --git a/apps/server/src/modules/authorization/domain/rules/group.rule.ts b/apps/server/src/modules/authorization/domain/rules/group.rule.ts index 9f9b82e15e5..4c07f3e060f 100644 --- a/apps/server/src/modules/authorization/domain/rules/group.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/group.rule.ts @@ -8,16 +8,16 @@ import { AuthorizationContext, Rule } from '../type'; export class GroupRule implements Rule { constructor(private readonly authorizationHelper: AuthorizationHelper) {} - public isApplicable(user: User, domainObject: Group): boolean { - const isMatched: boolean = domainObject instanceof Group; + public isApplicable(user: User, object: unknown): boolean { + const isMatched: boolean = object instanceof Group; return isMatched; } - public hasPermission(user: User, domainObject: Group, context: AuthorizationContext): boolean { + public hasPermission(user: User, object: Group, context: AuthorizationContext): boolean { const hasPermission: boolean = this.authorizationHelper.hasAllPermissions(user, context.requiredPermissions) && - (domainObject.organizationId ? user.school.id === domainObject.organizationId : true); + (object.organizationId ? user.school.id === object.organizationId : true); return hasPermission; } diff --git a/apps/server/src/modules/authorization/domain/rules/index.ts b/apps/server/src/modules/authorization/domain/rules/index.ts index a83b2348514..f18bdbd9e20 100644 --- a/apps/server/src/modules/authorization/domain/rules/index.ts +++ b/apps/server/src/modules/authorization/domain/rules/index.ts @@ -18,3 +18,5 @@ export * from './user.rule'; export * from './group.rule'; export { SystemRule } from './system.rule'; export { SchoolSystemOptionsRule } from './school-system-options.rule'; +export { InstanceRule } from './instance.rule'; +export { ExternalToolRule } from './external-tool.rule'; diff --git a/apps/server/src/modules/authorization/domain/rules/instance.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/instance.rule.spec.ts new file mode 100644 index 00000000000..b5dd35bbc4a --- /dev/null +++ b/apps/server/src/modules/authorization/domain/rules/instance.rule.spec.ts @@ -0,0 +1,142 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { instanceFactory } from '@modules/instance/testing'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Permission } from '@shared/domain/interface'; +import { setupEntities, userFactory } from '@shared/testing'; +import { AuthorizationHelper } from '../service/authorization.helper'; +import { Action, AuthorizationContext } from '../type'; +import { InstanceRule } from './instance.rule'; + +describe(InstanceRule.name, () => { + let module: TestingModule; + let rule: InstanceRule; + + let authorizationHelper: DeepMocked; + + beforeAll(async () => { + await setupEntities(); + + module = await Test.createTestingModule({ + providers: [ + InstanceRule, + { + provide: AuthorizationHelper, + useValue: createMock(), + }, + ], + }).compile(); + + rule = module.get(InstanceRule); + authorizationHelper = module.get(AuthorizationHelper); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('isApplicable', () => { + describe('when the entity is applicable', () => { + const setup = () => { + const user = userFactory.build(); + const instance = instanceFactory.build(); + + return { + user, + instance, + }; + }; + + it('should return true', () => { + const { user, instance } = setup(); + + const result = rule.isApplicable(user, instance); + + expect(result).toEqual(true); + }); + }); + + describe('when the entity is not applicable', () => { + const setup = () => { + const user = userFactory.build(); + const notInstance = userFactory.build(); + + return { + user, + notInstance, + }; + }; + + it('should return false', () => { + const { user, notInstance } = setup(); + + const result = rule.isApplicable(user, notInstance as unknown as InstanceRule); + + expect(result).toEqual(false); + }); + }); + }); + + describe('hasPermission', () => { + describe('when the user has all permissions', () => { + const setup = () => { + const user = userFactory.build(); + const instance = instanceFactory.build(); + const context: AuthorizationContext = { + action: Action.write, + requiredPermissions: [Permission.FILESTORAGE_VIEW], + }; + + authorizationHelper.hasAllPermissions.mockReturnValue(true); + + return { + user, + instance, + context, + }; + }; + + it('should check all permissions', () => { + const { user, instance, context } = setup(); + + rule.hasPermission(user, instance, context); + + expect(authorizationHelper.hasAllPermissions).toHaveBeenCalledWith(user, context.requiredPermissions); + }); + + it('should return true', () => { + const { user, instance, context } = setup(); + + const result = rule.hasPermission(user, instance, context); + + expect(result).toEqual(true); + }); + }); + + describe('when the user has no permission', () => { + const setup = () => { + const user = userFactory.build(); + const instance = instanceFactory.build(); + const context: AuthorizationContext = { + action: Action.write, + requiredPermissions: [Permission.FILESTORAGE_VIEW], + }; + + authorizationHelper.hasAllPermissions.mockReturnValue(false); + + return { + user, + instance, + context, + }; + }; + + it('should return false', () => { + const { user, instance, context } = setup(); + + const result = rule.hasPermission(user, instance, context); + + expect(result).toEqual(false); + }); + }); + }); +}); diff --git a/apps/server/src/modules/authorization/domain/rules/instance.rule.ts b/apps/server/src/modules/authorization/domain/rules/instance.rule.ts new file mode 100644 index 00000000000..4aaba878552 --- /dev/null +++ b/apps/server/src/modules/authorization/domain/rules/instance.rule.ts @@ -0,0 +1,22 @@ +import { Instance } from '@modules/instance'; +import { Injectable } from '@nestjs/common'; +import { User } from '@shared/domain/entity'; +import { AuthorizationHelper } from '../service/authorization.helper'; +import { AuthorizationContext, Rule } from '../type'; + +@Injectable() +export class InstanceRule implements Rule { + constructor(private readonly authorizationHelper: AuthorizationHelper) {} + + public isApplicable(user: User, object: unknown): boolean { + const isMatched: boolean = object instanceof Instance; + + return isMatched; + } + + public hasPermission(user: User, entity: Instance, context: AuthorizationContext): boolean { + const hasPermission: boolean = this.authorizationHelper.hasAllPermissions(user, context.requiredPermissions); + + return hasPermission; + } +} diff --git a/apps/server/src/modules/authorization/domain/rules/legacy-school.rule.ts b/apps/server/src/modules/authorization/domain/rules/legacy-school.rule.ts index 564577d9038..fe8eb466fbd 100644 --- a/apps/server/src/modules/authorization/domain/rules/legacy-school.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/legacy-school.rule.ts @@ -1,6 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { AuthorizableObject } from '@shared/domain/domain-object'; -import { BaseDO, LegacySchoolDo } from '@shared/domain/domainobject'; +import { LegacySchoolDo } from '@shared/domain/domainobject'; import { User } from '@shared/domain/entity'; import { AuthorizationHelper } from '../service/authorization.helper'; import { AuthorizationContext, Rule } from '../type'; @@ -9,18 +8,18 @@ import { AuthorizationContext, Rule } from '../type'; * @deprecated because it uses the deprecated LegacySchoolDo. */ @Injectable() -export class LegacySchoolRule implements Rule { +export class LegacySchoolRule implements Rule { constructor(private readonly authorizationHelper: AuthorizationHelper) {} - public isApplicable(user: User, object: AuthorizableObject | BaseDO): boolean { + public isApplicable(user: User, object: unknown): boolean { const isMatched = object instanceof LegacySchoolDo; return isMatched; } - public hasPermission(user: User, entity: LegacySchoolDo, context: AuthorizationContext): boolean { + public hasPermission(user: User, object: LegacySchoolDo, context: AuthorizationContext): boolean { const hasPermission = - this.authorizationHelper.hasAllPermissions(user, context.requiredPermissions) && user.school.id === entity.id; + this.authorizationHelper.hasAllPermissions(user, context.requiredPermissions) && user.school.id === object.id; return hasPermission; } diff --git a/apps/server/src/modules/authorization/domain/rules/lesson.rule.ts b/apps/server/src/modules/authorization/domain/rules/lesson.rule.ts index 1f59f98ad49..48e0cfa92e6 100644 --- a/apps/server/src/modules/authorization/domain/rules/lesson.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/lesson.rule.ts @@ -1,32 +1,32 @@ import { Injectable, NotImplementedException } from '@nestjs/common'; import { Course, CourseGroup, LessonEntity, User } from '@shared/domain/entity'; -import { Action, AuthorizationContext, Rule } from '../type'; import { AuthorizationHelper } from '../service/authorization.helper'; +import { Action, AuthorizationContext, Rule } from '../type'; import { CourseGroupRule } from './course-group.rule'; import { CourseRule } from './course.rule'; @Injectable() -export class LessonRule implements Rule { +export class LessonRule implements Rule { constructor( private readonly authorizationHelper: AuthorizationHelper, private readonly courseRule: CourseRule, private readonly courseGroupRule: CourseGroupRule ) {} - public isApplicable(user: User, entity: LessonEntity): boolean { - const isMatched = entity instanceof LessonEntity; + public isApplicable(user: User, object: unknown): boolean { + const isMatched = object instanceof LessonEntity; return isMatched; } - public hasPermission(user: User, entity: LessonEntity, context: AuthorizationContext): boolean { + public hasPermission(user: User, object: LessonEntity, context: AuthorizationContext): boolean { const { action, requiredPermissions } = context; let hasLessonPermission = false; if (action === Action.read) { - hasLessonPermission = this.lessonReadPermission(user, entity); + hasLessonPermission = this.lessonReadPermission(user, object); } else if (action === Action.write) { - hasLessonPermission = this.lessonWritePermission(user, entity); + hasLessonPermission = this.lessonWritePermission(user, object); } else { throw new NotImplementedException('Action is not supported.'); } diff --git a/apps/server/src/modules/authorization/domain/rules/school-external-tool.rule.ts b/apps/server/src/modules/authorization/domain/rules/school-external-tool.rule.ts index 46126fe0589..f8241bcefc7 100644 --- a/apps/server/src/modules/authorization/domain/rules/school-external-tool.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/school-external-tool.rule.ts @@ -1,34 +1,34 @@ -import { Injectable } from '@nestjs/common'; import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; +import { Injectable } from '@nestjs/common'; import { User } from '@shared/domain/entity'; -import { AuthorizationContext, Rule } from '../type'; import { AuthorizationHelper } from '../service/authorization.helper'; +import { AuthorizationContext, Rule } from '../type'; @Injectable() -export class SchoolExternalToolRule implements Rule { +export class SchoolExternalToolRule implements Rule { constructor(private readonly authorizationHelper: AuthorizationHelper) {} - public isApplicable(user: User, entity: SchoolExternalToolEntity | SchoolExternalTool): boolean { - const isMatched: boolean = entity instanceof SchoolExternalToolEntity || entity instanceof SchoolExternalTool; + public isApplicable(user: User, object: unknown): boolean { + const isMatched: boolean = object instanceof SchoolExternalToolEntity || object instanceof SchoolExternalTool; return isMatched; } public hasPermission( user: User, - entity: SchoolExternalToolEntity | SchoolExternalTool, + object: SchoolExternalToolEntity | SchoolExternalTool, context: AuthorizationContext ): boolean { let hasPermission: boolean; - if (entity instanceof SchoolExternalToolEntity) { + if (object instanceof SchoolExternalToolEntity) { hasPermission = this.authorizationHelper.hasAllPermissions(user, context.requiredPermissions) && - user.school.id === entity.school.id; + user.school.id === object.school.id; } else { hasPermission = this.authorizationHelper.hasAllPermissions(user, context.requiredPermissions) && - user.school.id === entity.schoolId; + user.school.id === object.schoolId; } return hasPermission; } diff --git a/apps/server/src/modules/authorization/domain/rules/school-system-options.rule.ts b/apps/server/src/modules/authorization/domain/rules/school-system-options.rule.ts index 89a84a5d98f..475697553db 100644 --- a/apps/server/src/modules/authorization/domain/rules/school-system-options.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/school-system-options.rule.ts @@ -1,4 +1,4 @@ -import { AnyProvisioningOptions, SchoolSystemOptions } from '@modules/legacy-school'; +import { SchoolSystemOptions } from '@modules/legacy-school'; import { Injectable } from '@nestjs/common'; import { User } from '@shared/domain/entity'; import { AuthorizationHelper } from '../service/authorization.helper'; @@ -8,18 +8,18 @@ import { AuthorizationContext, Rule } from '../type'; export class SchoolSystemOptionsRule implements Rule { constructor(private readonly authorizationHelper: AuthorizationHelper) {} - public isApplicable(user: User, domainObject: SchoolSystemOptions): boolean { - const isMatched: boolean = domainObject instanceof SchoolSystemOptions; + public isApplicable(user: User, object: unknown): boolean { + const isMatched: boolean = object instanceof SchoolSystemOptions; return isMatched; } - public hasPermission(user: User, domainObject: SchoolSystemOptions, context: AuthorizationContext): boolean { + public hasPermission(user: User, object: SchoolSystemOptions, context: AuthorizationContext): boolean { const hasPermissions: boolean = this.authorizationHelper.hasAllPermissions(user, context.requiredPermissions); - const isAtSchool: boolean = user.school.id === domainObject.schoolId; + const isAtSchool: boolean = user.school.id === object.schoolId; - const hasSystem: boolean = user.school.systems.getIdentifiers().includes(domainObject.systemId); + const hasSystem: boolean = user.school.systems.getIdentifiers().includes(object.systemId); const isAuthorized: boolean = hasPermissions && isAtSchool && hasSystem; diff --git a/apps/server/src/modules/authorization/domain/rules/school.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/school.rule.spec.ts index e5ff909f491..07758a92d01 100644 --- a/apps/server/src/modules/authorization/domain/rules/school.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/school.rule.spec.ts @@ -56,7 +56,6 @@ describe('SchoolRule', () => { it('should return false', () => { const { user, someRandomObject } = setup(); - // @ts-expect-error Testcase const result = rule.isApplicable(user, someRandomObject); expect(result).toBe(false); diff --git a/apps/server/src/modules/authorization/domain/rules/school.rule.ts b/apps/server/src/modules/authorization/domain/rules/school.rule.ts index 4f31405d762..d960e62c3dd 100644 --- a/apps/server/src/modules/authorization/domain/rules/school.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/school.rule.ts @@ -1,15 +1,14 @@ import { Injectable } from '@nestjs/common'; -import { AuthorizableObject } from '@shared/domain/domain-object'; import { User } from '@shared/domain/entity'; import { School } from '@src/modules/school/domain/do'; import { AuthorizationHelper } from '../service/authorization.helper'; import { AuthorizationContext, Rule } from '../type'; @Injectable() -export class SchoolRule implements Rule { +export class SchoolRule implements Rule { constructor(private readonly authorizationHelper: AuthorizationHelper) {} - public isApplicable(user: User, object: AuthorizableObject): boolean { + public isApplicable(user: User, object: unknown): boolean { const isApplicable = object instanceof School; return isApplicable; diff --git a/apps/server/src/modules/authorization/domain/rules/submission.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/submission.rule.spec.ts index 8b054671970..041d583a13f 100644 --- a/apps/server/src/modules/authorization/domain/rules/submission.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/submission.rule.spec.ts @@ -1,3 +1,4 @@ +import { NotImplementedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain/interface'; import { @@ -9,14 +10,13 @@ import { taskFactory, userFactory, } from '@shared/testing'; -import { NotImplementedException } from '@nestjs/common'; -import { Action, AuthorizationContext } from '../type'; import { AuthorizationHelper } from '../service/authorization.helper'; -import { SubmissionRule } from './submission.rule'; -import { TaskRule } from './task.rule'; +import { Action, AuthorizationContext } from '../type'; +import { CourseGroupRule } from './course-group.rule'; import { CourseRule } from './course.rule'; import { LessonRule } from './lesson.rule'; -import { CourseGroupRule } from './course-group.rule'; +import { SubmissionRule } from './submission.rule'; +import { TaskRule } from './task.rule'; const buildUserWithPermission = (permission) => { const role = roleFactory.buildWithId({ permissions: [permission] }); @@ -72,7 +72,6 @@ describe('SubmissionRule', () => { it('should return false', () => { const { user, task } = setup(); - // @ts-expect-error Testcase const result = submissionRule.isApplicable(user, task); expect(result).toBe(false); diff --git a/apps/server/src/modules/authorization/domain/rules/submission.rule.ts b/apps/server/src/modules/authorization/domain/rules/submission.rule.ts index 6bff9504f5c..fda0bcca7ad 100644 --- a/apps/server/src/modules/authorization/domain/rules/submission.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/submission.rule.ts @@ -1,15 +1,15 @@ import { Injectable, NotImplementedException } from '@nestjs/common'; import { Submission, User } from '@shared/domain/entity'; -import { Action, AuthorizationContext, Rule } from '../type'; import { AuthorizationHelper } from '../service/authorization.helper'; +import { Action, AuthorizationContext, Rule } from '../type'; import { TaskRule } from './task.rule'; @Injectable() -export class SubmissionRule implements Rule { +export class SubmissionRule implements Rule { constructor(private readonly authorizationHelper: AuthorizationHelper, private readonly taskRule: TaskRule) {} - public isApplicable(user: User, entity: Submission): boolean { - const isMatched = entity instanceof Submission; + public isApplicable(user: User, object: unknown): boolean { + const isMatched = object instanceof Submission; return isMatched; } diff --git a/apps/server/src/modules/authorization/domain/rules/system.rule.ts b/apps/server/src/modules/authorization/domain/rules/system.rule.ts index 8e63dae744c..253224097b6 100644 --- a/apps/server/src/modules/authorization/domain/rules/system.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/system.rule.ts @@ -8,21 +8,21 @@ import { Action, AuthorizationContext, Rule } from '../type'; export class SystemRule implements Rule { constructor(private readonly authorizationHelper: AuthorizationHelper) {} - public isApplicable(user: User, domainObject: System): boolean { - const isMatched: boolean = domainObject instanceof System; + public isApplicable(user: User, object: unknown): boolean { + const isMatched: boolean = object instanceof System; return isMatched; } - public hasPermission(user: User, domainObject: System, context: AuthorizationContext): boolean { + public hasPermission(user: User, object: System, context: AuthorizationContext): boolean { const hasPermissions: boolean = this.authorizationHelper.hasAllPermissions(user, context.requiredPermissions); - const hasAccess: boolean = user.school.systems.getIdentifiers().includes(domainObject.id); + const hasAccess: boolean = user.school.systems.getIdentifiers().includes(object.id); let isAuthorized: boolean = hasPermissions && hasAccess; if (context.action === Action.write) { - isAuthorized = isAuthorized && this.canEdit(domainObject); + isAuthorized = isAuthorized && this.canEdit(object); } return isAuthorized; diff --git a/apps/server/src/modules/authorization/domain/rules/task.rule.ts b/apps/server/src/modules/authorization/domain/rules/task.rule.ts index 3ebc04d9f71..f24dee67740 100644 --- a/apps/server/src/modules/authorization/domain/rules/task.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/task.rule.ts @@ -1,25 +1,25 @@ import { Injectable } from '@nestjs/common'; import { Task, User } from '@shared/domain/entity'; -import { Action, AuthorizationContext, Rule } from '../type'; import { AuthorizationHelper } from '../service/authorization.helper'; +import { Action, AuthorizationContext, Rule } from '../type'; import { CourseRule } from './course.rule'; import { LessonRule } from './lesson.rule'; @Injectable() -export class TaskRule implements Rule { +export class TaskRule implements Rule { constructor( private readonly authorizationHelper: AuthorizationHelper, private readonly courseRule: CourseRule, private readonly lessonRule: LessonRule ) {} - public isApplicable(user: User, entity: Task): boolean { - const isMatched = entity instanceof Task; + public isApplicable(user: User, object: unknown): boolean { + const isMatched = object instanceof Task; return isMatched; } - public hasPermission(user: User, entity: Task, context: AuthorizationContext): boolean { + public hasPermission(user: User, object: Task, context: AuthorizationContext): boolean { let { action } = context; const { requiredPermissions } = context; const hasRequiredPermission = this.authorizationHelper.hasAllPermissions(user, requiredPermissions); @@ -27,12 +27,12 @@ export class TaskRule implements Rule { return false; } - const isCreator = this.authorizationHelper.hasAccessToEntity(user, entity, ['creator']); - if (entity.isDraft()) { + const isCreator = this.authorizationHelper.hasAccessToEntity(user, object, ['creator']); + if (object.isDraft()) { action = Action.write; } - const hasParentPermission = this.hasParentPermission(user, entity, action); + const hasParentPermission = this.hasParentPermission(user, object, action); // TODO why parent permission has OR cond? const result = isCreator || hasParentPermission; diff --git a/apps/server/src/modules/authorization/domain/rules/team.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/team.rule.spec.ts index da99354a49b..d331adb80f9 100644 --- a/apps/server/src/modules/authorization/domain/rules/team.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/team.rule.spec.ts @@ -1,9 +1,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain/interface'; -import { roleFactory, setupEntities, userFactory, teamFactory } from '@shared/testing'; +import { roleFactory, setupEntities, teamFactory, userFactory } from '@shared/testing'; +import { AuthorizationContextBuilder } from '../mapper'; import { AuthorizationHelper } from '../service/authorization.helper'; import { TeamRule } from './team.rule'; -import { AuthorizationContextBuilder } from '../mapper'; describe('TeamRule', () => { let rule: TeamRule; @@ -54,7 +54,7 @@ describe('TeamRule', () => { it('should return false', () => { const { user } = setup(); - // @ts-expect-error test with wrong instance + const result = rule.isApplicable(user, user); expect(result).toBe(false); diff --git a/apps/server/src/modules/authorization/domain/rules/team.rule.ts b/apps/server/src/modules/authorization/domain/rules/team.rule.ts index 2d8f5e90edf..48cca110e2a 100644 --- a/apps/server/src/modules/authorization/domain/rules/team.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/team.rule.ts @@ -1,19 +1,19 @@ import { Injectable } from '@nestjs/common'; import { TeamEntity, TeamUserEntity, User } from '@shared/domain/entity'; -import { AuthorizationContext, Rule } from '../type'; import { AuthorizationHelper } from '../service/authorization.helper'; +import { AuthorizationContext, Rule } from '../type'; @Injectable() -export class TeamRule implements Rule { +export class TeamRule implements Rule { constructor(private readonly authorizationHelper: AuthorizationHelper) {} - public isApplicable(user: User, entity: TeamEntity): boolean { - return entity instanceof TeamEntity; + public isApplicable(user: User, object: unknown): boolean { + return object instanceof TeamEntity; } - public hasPermission(user: User, entity: TeamEntity, context: AuthorizationContext): boolean { + public hasPermission(user: User, object: TeamEntity, context: AuthorizationContext): boolean { let hasPermission = false; - const isTeamUser = entity.teamUsers.find((teamUser: TeamUserEntity) => teamUser.user.id === user.id); + const isTeamUser = object.teamUsers.find((teamUser: TeamUserEntity) => teamUser.user.id === user.id); if (isTeamUser) { hasPermission = this.authorizationHelper.hasAllPermissionsByRole(isTeamUser.role, context.requiredPermissions); } diff --git a/apps/server/src/modules/authorization/domain/rules/user-login-migration.rule.ts b/apps/server/src/modules/authorization/domain/rules/user-login-migration.rule.ts index 3ae82d02505..05ca647e7ab 100644 --- a/apps/server/src/modules/authorization/domain/rules/user-login-migration.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/user-login-migration.rule.ts @@ -1,15 +1,15 @@ import { Injectable } from '@nestjs/common'; import { UserLoginMigrationDO } from '@shared/domain/domainobject'; import { User } from '@shared/domain/entity'; -import { AuthorizationContext, Rule } from '../type'; import { AuthorizationHelper } from '../service/authorization.helper'; +import { AuthorizationContext, Rule } from '../type'; @Injectable() export class UserLoginMigrationRule implements Rule { constructor(private readonly authorizationHelper: AuthorizationHelper) {} - public isApplicable(user: User, entity: UserLoginMigrationDO): boolean { - const isMatched: boolean = entity instanceof UserLoginMigrationDO; + public isApplicable(user: User, object: unknown): boolean { + const isMatched: boolean = object instanceof UserLoginMigrationDO; return isMatched; } diff --git a/apps/server/src/modules/authorization/domain/rules/user.rule.ts b/apps/server/src/modules/authorization/domain/rules/user.rule.ts index 2a1365881e1..66ea8e44052 100644 --- a/apps/server/src/modules/authorization/domain/rules/user.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/user.rule.ts @@ -1,14 +1,14 @@ import { Injectable } from '@nestjs/common'; import { User } from '@shared/domain/entity'; -import { AuthorizationContext, Rule } from '../type'; import { AuthorizationHelper } from '../service/authorization.helper'; +import { AuthorizationContext, Rule } from '../type'; @Injectable() -export class UserRule implements Rule { +export class UserRule implements Rule { constructor(private readonly authorizationHelper: AuthorizationHelper) {} - public isApplicable(user: User, entity: User): boolean { - const isMatched = entity instanceof User; + public isApplicable(user: User, object: unknown): boolean { + const isMatched = object instanceof User; return isMatched; } diff --git a/apps/server/src/modules/authorization/domain/service/reference.loader.spec.ts b/apps/server/src/modules/authorization/domain/service/reference.loader.spec.ts index 77e04a3c1ba..997afd6ce99 100644 --- a/apps/server/src/modules/authorization/domain/service/reference.loader.spec.ts +++ b/apps/server/src/modules/authorization/domain/service/reference.loader.spec.ts @@ -1,8 +1,9 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { BoardDoAuthorizableService } from '@modules/board'; +import { InstanceService } from '@modules/instance'; import { LessonService } from '@modules/lesson'; -import { ContextExternalToolAuthorizableService } from '@modules/tool'; +import { ContextExternalToolAuthorizableService, ExternalToolAuthorizableService } from '@modules/tool'; import { NotImplementedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityId } from '@shared/domain/types'; @@ -33,6 +34,8 @@ describe('reference.loader', () => { let schoolExternalToolRepo: DeepMocked; let boardNodeAuthorizableService: DeepMocked; let contextExternalToolAuthorizableService: DeepMocked; + let externalToolAuthorizableService: DeepMocked; + let instanceService: DeepMocked; const entityId: EntityId = new ObjectId().toHexString(); beforeAll(async () => { @@ -85,6 +88,14 @@ describe('reference.loader', () => { provide: ContextExternalToolAuthorizableService, useValue: createMock(), }, + { + provide: ExternalToolAuthorizableService, + useValue: createMock(), + }, + { + provide: InstanceService, + useValue: createMock(), + }, ], }).compile(); @@ -100,6 +111,8 @@ describe('reference.loader', () => { schoolExternalToolRepo = await module.get(SchoolExternalToolRepo); boardNodeAuthorizableService = await module.get(BoardDoAuthorizableService); contextExternalToolAuthorizableService = await module.get(ContextExternalToolAuthorizableService); + externalToolAuthorizableService = await module.get(ExternalToolAuthorizableService); + instanceService = await module.get(InstanceService); }); afterEach(() => { @@ -171,12 +184,24 @@ describe('reference.loader', () => { expect(schoolExternalToolRepo.findById).toBeCalledWith(entityId); }); + it('should call externalToolAuthorizableService.findById', async () => { + await service.loadAuthorizableObject(AuthorizableReferenceType.ExternalTool, entityId); + + expect(externalToolAuthorizableService.findById).toBeCalledWith(entityId); + }); + it('should call findNodeService.findById', async () => { await service.loadAuthorizableObject(AuthorizableReferenceType.BoardNode, entityId); expect(boardNodeAuthorizableService.findById).toBeCalledWith(entityId); }); + it('should call instanceService.findById', async () => { + await service.loadAuthorizableObject(AuthorizableReferenceType.Instance, entityId); + + expect(instanceService.findById).toBeCalledWith(entityId); + }); + it('should return authorizable object', async () => { const user = userFactory.build(); userRepo.findById.mockResolvedValue(user); diff --git a/apps/server/src/modules/authorization/domain/service/reference.loader.ts b/apps/server/src/modules/authorization/domain/service/reference.loader.ts index 066d737cfcb..1ed06a1c7cb 100644 --- a/apps/server/src/modules/authorization/domain/service/reference.loader.ts +++ b/apps/server/src/modules/authorization/domain/service/reference.loader.ts @@ -2,6 +2,7 @@ import { BoardDoAuthorizableService } from '@modules/board'; import { LessonService } from '@modules/lesson'; import { ContextExternalToolAuthorizableService } from '@modules/tool'; +import { ExternalToolAuthorizableService } from '@modules/tool/external-tool/service'; import { Injectable, NotImplementedException } from '@nestjs/common'; import { AuthorizableObject } from '@shared/domain/domain-object'; import { BaseDO } from '@shared/domain/domainobject'; @@ -16,6 +17,7 @@ import { TeamsRepo, UserRepo, } from '@shared/repo'; +import { InstanceService } from '../../../instance'; import { AuthorizableReferenceType } from '../type'; type RepoType = @@ -29,7 +31,9 @@ type RepoType = | SubmissionRepo | TaskRepo | TeamsRepo - | UserRepo; + | UserRepo + | ExternalToolAuthorizableService + | InstanceService; interface RepoLoader { repo: RepoType; @@ -51,7 +55,9 @@ export class ReferenceLoader { private readonly submissionRepo: SubmissionRepo, private readonly schoolExternalToolRepo: SchoolExternalToolRepo, private readonly boardNodeAuthorizableService: BoardDoAuthorizableService, - private readonly contextExternalToolAuthorizableService: ContextExternalToolAuthorizableService + private readonly contextExternalToolAuthorizableService: ContextExternalToolAuthorizableService, + private readonly externalToolAuthorizableService: ExternalToolAuthorizableService, + private readonly instanceService: InstanceService ) { this.repos.set(AuthorizableReferenceType.Task, { repo: this.taskRepo }); this.repos.set(AuthorizableReferenceType.Course, { repo: this.courseRepo }); @@ -66,6 +72,8 @@ export class ReferenceLoader { this.repos.set(AuthorizableReferenceType.ContextExternalToolEntity, { repo: this.contextExternalToolAuthorizableService, }); + this.repos.set(AuthorizableReferenceType.ExternalTool, { repo: this.externalToolAuthorizableService }); + this.repos.set(AuthorizableReferenceType.Instance, { repo: this.instanceService }); } private resolveRepo(type: AuthorizableReferenceType): RepoLoader { diff --git a/apps/server/src/modules/authorization/domain/service/rule-manager.spec.ts b/apps/server/src/modules/authorization/domain/service/rule-manager.spec.ts index a5d08f5c061..67e6e428b9a 100644 --- a/apps/server/src/modules/authorization/domain/service/rule-manager.spec.ts +++ b/apps/server/src/modules/authorization/domain/service/rule-manager.spec.ts @@ -9,6 +9,7 @@ import { CourseGroupRule, CourseRule, GroupRule, + InstanceRule, LegacySchoolRule, LessonRule, SchoolExternalToolRule, @@ -21,6 +22,7 @@ import { UserLoginMigrationRule, UserRule, } from '../rules'; +import { ExternalToolRule } from '../rules/external-tool.rule'; import { RuleManager } from './rule-manager'; describe('RuleManager', () => { @@ -41,6 +43,8 @@ describe('RuleManager', () => { let groupRule: DeepMocked; let systemRule: DeepMocked; let schoolSystemOptionsRule: DeepMocked; + let externalToolRule: DeepMocked; + let instanceRule: DeepMocked; beforeAll(async () => { await setupEntities(); @@ -64,6 +68,8 @@ describe('RuleManager', () => { { provide: SchoolRule, useValue: createMock() }, { provide: SystemRule, useValue: createMock() }, { provide: SchoolSystemOptionsRule, useValue: createMock() }, + { provide: ExternalToolRule, useValue: createMock() }, + { provide: InstanceRule, useValue: createMock() }, ], }).compile(); @@ -84,6 +90,8 @@ describe('RuleManager', () => { groupRule = await module.get(GroupRule); systemRule = await module.get(SystemRule); schoolSystemOptionsRule = await module.get(SchoolSystemOptionsRule); + externalToolRule = await module.get(ExternalToolRule); + instanceRule = await module.get(InstanceRule); }); afterEach(() => { @@ -118,6 +126,8 @@ describe('RuleManager', () => { groupRule.isApplicable.mockReturnValueOnce(false); systemRule.isApplicable.mockReturnValueOnce(false); schoolSystemOptionsRule.isApplicable.mockReturnValueOnce(false); + externalToolRule.isApplicable.mockReturnValueOnce(false); + instanceRule.isApplicable.mockReturnValueOnce(false); return { user, object, context }; }; @@ -143,6 +153,8 @@ describe('RuleManager', () => { expect(groupRule.isApplicable).toBeCalled(); expect(systemRule.isApplicable).toBeCalled(); expect(schoolSystemOptionsRule.isApplicable).toBeCalled(); + expect(externalToolRule.isApplicable).toBeCalled(); + expect(instanceRule.isApplicable).toBeCalled(); }); it('should return CourseRule', () => { @@ -176,6 +188,8 @@ describe('RuleManager', () => { groupRule.isApplicable.mockReturnValueOnce(false); systemRule.isApplicable.mockReturnValueOnce(false); schoolSystemOptionsRule.isApplicable.mockReturnValueOnce(false); + externalToolRule.isApplicable.mockReturnValueOnce(false); + instanceRule.isApplicable.mockReturnValueOnce(false); return { user, object, context }; }; @@ -209,6 +223,8 @@ describe('RuleManager', () => { groupRule.isApplicable.mockReturnValueOnce(false); systemRule.isApplicable.mockReturnValueOnce(false); schoolSystemOptionsRule.isApplicable.mockReturnValueOnce(false); + externalToolRule.isApplicable.mockReturnValueOnce(false); + instanceRule.isApplicable.mockReturnValueOnce(false); return { user, object, context }; }; diff --git a/apps/server/src/modules/authorization/domain/service/rule-manager.ts b/apps/server/src/modules/authorization/domain/service/rule-manager.ts index 5330ccac8f5..143c5edbdc0 100644 --- a/apps/server/src/modules/authorization/domain/service/rule-manager.ts +++ b/apps/server/src/modules/authorization/domain/service/rule-manager.ts @@ -8,6 +8,7 @@ import { CourseGroupRule, CourseRule, GroupRule, + InstanceRule, LegacySchoolRule, LessonRule, SchoolExternalToolRule, @@ -20,6 +21,7 @@ import { UserLoginMigrationRule, UserRule, } from '../rules'; +import { ExternalToolRule } from '../rules/external-tool.rule'; import type { AuthorizationContext, Rule } from '../type'; @Injectable() @@ -42,7 +44,9 @@ export class RuleManager { private readonly taskRule: TaskRule, private readonly teamRule: TeamRule, private readonly userLoginMigrationRule: UserLoginMigrationRule, - private readonly userRule: UserRule + private readonly userRule: UserRule, + private readonly externalToolRule: ExternalToolRule, + private readonly instanceRule: InstanceRule ) { this.rules = [ this.boardDoRule, @@ -61,6 +65,8 @@ export class RuleManager { this.teamRule, this.userLoginMigrationRule, this.userRule, + this.externalToolRule, + this.instanceRule, ]; } diff --git a/apps/server/src/modules/authorization/domain/type/allowed-authorization-object-type.enum.ts b/apps/server/src/modules/authorization/domain/type/allowed-authorization-object-type.enum.ts index 01f24b21985..c9b78436b65 100644 --- a/apps/server/src/modules/authorization/domain/type/allowed-authorization-object-type.enum.ts +++ b/apps/server/src/modules/authorization/domain/type/allowed-authorization-object-type.enum.ts @@ -10,4 +10,6 @@ export enum AuthorizableReferenceType { 'SchoolExternalToolEntity' = 'school-external-tools', 'BoardNode' = 'boardnodes', 'ContextExternalToolEntity' = 'context-external-tools', + 'ExternalTool' = 'external-tools', + 'Instance' = 'instances', } diff --git a/apps/server/src/modules/authorization/domain/type/rule.interface.ts b/apps/server/src/modules/authorization/domain/type/rule.interface.ts index b6077f83d70..f4632e1aea9 100644 --- a/apps/server/src/modules/authorization/domain/type/rule.interface.ts +++ b/apps/server/src/modules/authorization/domain/type/rule.interface.ts @@ -3,7 +3,7 @@ import { BaseDO } from '@shared/domain/domainobject'; import { User } from '@shared/domain/entity'; import { AuthorizationContext } from './authorization-context.interface'; -export interface Rule { - isApplicable(user: User, object: T, context?: AuthorizationContext): boolean; +export interface Rule { + isApplicable(user: User, object: unknown, context?: AuthorizationContext): boolean; hasPermission(user: User, object: T, context: AuthorizationContext): boolean; } diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-security.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-security.api.spec.ts index 3de38711ad4..b30164089a2 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-security.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-security.api.spec.ts @@ -5,19 +5,17 @@ import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; -import { Permission } from '@shared/domain/interface'; import { cleanupCollections, fileRecordFactory, mapUserToCurrentUser, - roleFactory, schoolEntityFactory, - userFactory, + UserAndAccountTestFactory, } from '@shared/testing'; import NodeClam from 'clamscan'; import { Request } from 'express'; import request from 'supertest'; -import { FileRecord, FileRecordParentType } from '../../entity'; +import { FileRecord, FileRecordParentType, StorageLocation } from '../../entity'; import { FilesStorageTestModule } from '../../files-storage-test.module'; import { FileRecordListResponse, ScanResultParams } from '../dto'; @@ -81,12 +79,9 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); const school = schoolEntityFactory.build(); - const roles = roleFactory.buildList(1, { - permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], - }); - const user = userFactory.build({ school, roles }); + const { studentUser: user, studentAccount: account } = UserAndAccountTestFactory.buildStudent({ school }); - await em.persistAndFlush([user]); + await em.persistAndFlush([user, account]); em.clear(); currentUser = mapUserToCurrentUser(user); @@ -96,7 +91,8 @@ describe(`${baseRouteName} (api)`, () => { describe('with bad request data', () => { it('should return status 400 for invalid token', async () => { const fileRecord = fileRecordFactory.build({ - schoolId: validId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId: validId, parentId: validId, parentType: FileRecordParentType.School, }); @@ -112,7 +108,8 @@ describe(`${baseRouteName} (api)`, () => { describe(`with valid request data`, () => { it('should return right type of data', async () => { const fileRecord = fileRecordFactory.build({ - schoolId: validId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId: validId, parentId: validId, parentType: FileRecordParentType.School, }); diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-copy-files.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-copy-files.api.spec.ts index 810765c99d7..7aec7cbfe60 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-copy-files.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-copy-files.api.spec.ts @@ -1,28 +1,26 @@ import { createMock } from '@golevelup/ts-jest'; import { AntivirusService } from '@infra/antivirus'; import { S3ClientAdapter } from '@infra/s3-client'; -import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { EntityManager } from '@mikro-orm/mongodb'; import { ICurrentUser } from '@modules/authentication'; import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; -import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { cleanupCollections, courseFactory, fileRecordFactory, mapUserToCurrentUser, - roleFactory, schoolEntityFactory, - userFactory, + UserAndAccountTestFactory, } from '@shared/testing'; import NodeClam from 'clamscan'; import { Request } from 'express'; import FileType from 'file-type-cjs/file-type-cjs-index'; import request from 'supertest'; -import { FileRecordParentType } from '../../entity'; +import { FileRecordParentType, StorageLocation } from '../../entity'; import { FilesStorageTestModule } from '../../files-storage-test.module'; import { FILES_STORAGE_S3_CONNECTION } from '../../files-storage.config'; import { CopyFileParams, CopyFilesOfParentParams, FileRecordListResponse, FileRecordResponse } from '../dto'; @@ -128,13 +126,10 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); const school = schoolEntityFactory.build(); - const roles = roleFactory.buildList(1, { - permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], - }); - const user = userFactory.build({ school, roles }); + const { studentUser: user, studentAccount: account } = UserAndAccountTestFactory.buildStudent({ school }); const targetParent = courseFactory.build({ teachers: [user] }); - await em.persistAndFlush([user, school, targetParent]); + await em.persistAndFlush([user, school, targetParent, account]); em.clear(); currentUser = mapUserToCurrentUser(user); @@ -143,7 +138,8 @@ describe(`${baseRouteName} (api)`, () => { copyFilesParams = { target: { - schoolId: validId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId: validId, parentId: targetParentId, parentType: FileRecordParentType.Course, }, @@ -151,18 +147,18 @@ describe(`${baseRouteName} (api)`, () => { }); it('should return status 400 for invalid schoolId', async () => { - const response = await api.copy(`/123/users/${validId}`, copyFilesParams); + const response = await api.copy(`/school/123/users/${validId}`, copyFilesParams); expect(response.error.validationErrors).toEqual([ { - errors: ['schoolId must be a mongodb id'], - field: ['schoolId'], + errors: ['storageLocationId must be a mongodb id'], + field: ['storageLocationId'], }, ]); expect(response.status).toEqual(400); }); it('should return status 400 for invalid parentId', async () => { - const response = await api.copy(`/${validId}/users/123`, copyFilesParams); + const response = await api.copy(`/school/${validId}/users/123`, copyFilesParams); expect(response.error.validationErrors).toEqual([ { errors: ['parentId must be a mongodb id'], @@ -173,7 +169,7 @@ describe(`${baseRouteName} (api)`, () => { }); it('should return status 400 for invalid parentType', async () => { - const response = await api.copy(`/${validId}/cookies/${validId}`, copyFilesParams); + const response = await api.copy(`/school/${validId}/cookies/${validId}`, copyFilesParams); expect(response.error.validationErrors).toEqual([ { errors: [`parentType must be one of the following values: ${availableParentTypes}`], @@ -186,12 +182,13 @@ describe(`${baseRouteName} (api)`, () => { it('should return status 400 for invalid parentType', async () => { copyFilesParams = { target: { - schoolId: 'invalidObjectId', + storageLocation: StorageLocation.SCHOOL, + storageLocationId: 'invalidObjectId', parentId: 'invalidObjectId', parentType: FileRecordParentType.Task, }, }; - const response = await api.copy(`/${validId}/users/${validId}`, copyFilesParams); + const response = await api.copy(`/school/${validId}/users/${validId}`, copyFilesParams); expect(response.status).toEqual(400); }); }); @@ -200,13 +197,10 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); const school = schoolEntityFactory.build(); - const roles = roleFactory.buildList(1, { - permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], - }); - const user = userFactory.build({ school, roles }); + const { studentUser: user, studentAccount: account } = UserAndAccountTestFactory.buildStudent({ school }); const targetParent = courseFactory.build({ teachers: [user] }); - await em.persistAndFlush([user, school, targetParent]); + await em.persistAndFlush([user, school, targetParent, account]); em.clear(); currentUser = mapUserToCurrentUser(user); @@ -215,7 +209,8 @@ describe(`${baseRouteName} (api)`, () => { copyFilesParams = { target: { - schoolId: validId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId: validId, parentId: targetParentId, parentType: FileRecordParentType.Course, }, @@ -225,16 +220,16 @@ describe(`${baseRouteName} (api)`, () => { }); it('should return status 200 for successful request', async () => { - await api.postUploadFile(`/file/upload/${validId}/schools/${validId}`, 'test1.txt'); + await api.postUploadFile(`/file/upload/school/${validId}/schools/${validId}`, 'test1.txt'); - const response = await api.copy(`/${validId}/schools/${validId}`, copyFilesParams); + const response = await api.copy(`/school/${validId}/schools/${validId}`, copyFilesParams); expect(response.status).toEqual(201); }); it('should return right type of data', async () => { - await api.postUploadFile(`/file/upload/${validId}/schools/${validId}`, 'test1.txt'); - const { result } = await api.copy(`/${validId}/schools/${validId}`, copyFilesParams); + await api.postUploadFile(`/file/upload/school/${validId}/schools/${validId}`, 'test1.txt'); + const { result } = await api.copy(`/school/${validId}/schools/${validId}`, copyFilesParams); expect(Array.isArray(result.data)).toBe(true); expect(result.data[0]).toBeDefined(); @@ -255,13 +250,10 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); const school = schoolEntityFactory.build(); - const roles = roleFactory.buildList(1, { - permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], - }); - const user = userFactory.build({ school, roles }); + const { studentUser: user, studentAccount: account } = UserAndAccountTestFactory.buildStudent({ school }); const targetParent = courseFactory.build({ teachers: [user] }); - await em.persistAndFlush([user, school, targetParent]); + await em.persistAndFlush([user, school, targetParent, account]); em.clear(); currentUser = mapUserToCurrentUser(user); @@ -270,7 +262,8 @@ describe(`${baseRouteName} (api)`, () => { validId = user.school.id; copyFileParams = { target: { - schoolId: validId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId: validId, parentId: targetParentId, parentType: FileRecordParentType.Course, }, @@ -296,13 +289,10 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); const school = schoolEntityFactory.build(); - const roles = roleFactory.buildList(1, { - permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], - }); - const user = userFactory.build({ school, roles }); + const { studentUser: user, studentAccount: account } = UserAndAccountTestFactory.buildStudent({ school }); const targetParent = courseFactory.build({ teachers: [user] }); - await em.persistAndFlush([user, school, targetParent]); + await em.persistAndFlush([user, school, targetParent, account]); em.clear(); currentUser = mapUserToCurrentUser(user); @@ -311,14 +301,18 @@ describe(`${baseRouteName} (api)`, () => { validId = user.school.id; copyFileParams = { target: { - schoolId: validId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId: validId, parentId: targetParentId, parentType: FileRecordParentType.Course, }, fileNamePrefix: 'copy from', }; - const { result } = await api.postUploadFile(`/file/upload/${school.id}/schools/${school.id}`, 'test1.txt'); + const { result } = await api.postUploadFile( + `/file/upload/school/${school.id}/schools/${school.id}`, + 'test1.txt' + ); fileRecordId = result.id; }); @@ -341,7 +335,6 @@ describe(`${baseRouteName} (api)`, () => { it('should return elements not equal of requested scope', async () => { const otherFileRecords = fileRecordFactory.buildList(3, { - schoolId: new ObjectId().toHexString(), parentType: FileRecordParentType.School, }); diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-delete-files.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-delete-files.api.spec.ts index 295ac8c6ac3..9f6c7511abf 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-delete-files.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-delete-files.api.spec.ts @@ -7,16 +7,14 @@ import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; -import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { cleanupCollections, fileRecordFactory, mapUserToCurrentUser, - roleFactory, schoolEntityFactory, - userFactory, + UserAndAccountTestFactory, } from '@shared/testing'; import NodeClam from 'clamscan'; import { Request } from 'express'; @@ -126,12 +124,9 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); const school = schoolEntityFactory.build(); - const roles = roleFactory.buildList(1, { - permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW, Permission.FILESTORAGE_REMOVE], - }); - const user = userFactory.build({ school, roles }); + const { studentUser: user, studentAccount: account } = UserAndAccountTestFactory.buildStudent({ school }); - await em.persistAndFlush([user]); + await em.persistAndFlush([user, account]); em.clear(); currentUser = mapUserToCurrentUser(user); @@ -139,18 +134,18 @@ describe(`${baseRouteName} (api)`, () => { }); it('should return status 400 for invalid schoolId', async () => { - const response = await api.delete(`/123/users/${validId}`); + const response = await api.delete(`/school/123/users/${validId}`); expect(response.error.validationErrors).toEqual([ { - errors: ['schoolId must be a mongodb id'], - field: ['schoolId'], + errors: ['storageLocationId must be a mongodb id'], + field: ['storageLocationId'], }, ]); expect(response.status).toEqual(400); }); it('should return status 400 for invalid parentId', async () => { - const response = await api.delete(`/${validId}/users/123`); + const response = await api.delete(`/school/${validId}/users/123`); expect(response.error.validationErrors).toEqual([ { errors: ['parentId must be a mongodb id'], @@ -161,7 +156,7 @@ describe(`${baseRouteName} (api)`, () => { }); it('should return status 400 for invalid parentType', async () => { - const response = await api.delete(`/${validId}/cookies/${validId}`); + const response = await api.delete(`/school/${validId}/cookies/${validId}`); expect(response.error.validationErrors).toEqual([ { errors: [`parentType must be one of the following values: ${availableParentTypes}`], @@ -178,12 +173,9 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); const school = schoolEntityFactory.build(); - const roles = roleFactory.buildList(1, { - permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW, Permission.FILESTORAGE_REMOVE], - }); - const user = userFactory.build({ school, roles }); + const { studentUser: user, studentAccount: account } = UserAndAccountTestFactory.buildStudent({ school }); - await em.persistAndFlush([user]); + await em.persistAndFlush([user, account]); em.clear(); currentUser = mapUserToCurrentUser(user); @@ -193,17 +185,17 @@ describe(`${baseRouteName} (api)`, () => { }); it('should return status 200 for successful request', async () => { - await api.postUploadFile(`/file/upload/${validId}/schools/${validId}`, 'test1.txt'); + await api.postUploadFile(`/file/upload/school/${validId}/schools/${validId}`, 'test1.txt'); - const response = await api.delete(`/${validId}/schools/${validId}`); + const response = await api.delete(`/school/${validId}/schools/${validId}`); expect(response.status).toEqual(200); }); it('should return right type of data', async () => { - await api.postUploadFile(`/file/upload/${validId}/schools/${validId}`, 'test1.txt'); + await api.postUploadFile(`/file/upload/school/${validId}/schools/${validId}`, 'test1.txt'); - const { result } = await api.delete(`/${validId}/schools/${validId}`); + const { result } = await api.delete(`/school/${validId}/schools/${validId}`); expect(Array.isArray(result.data)).toBe(true); expect(result.data[0]).toBeDefined(); @@ -227,17 +219,17 @@ describe(`${baseRouteName} (api)`, () => { it('should return elements of requested scope', async () => { const otherParentId = new ObjectId().toHexString(); const uploadResponse = await Promise.all([ - api.postUploadFile(`/file/upload/${validId}/schools/${validId}`, 'test1.txt'), - api.postUploadFile(`/file/upload/${validId}/schools/${validId}`, 'test2.txt'), - api.postUploadFile(`/file/upload/${validId}/schools/${validId}`, 'test3.txt'), - api.postUploadFile(`/file/upload/${validId}/schools/${otherParentId}`, 'other1.txt'), - api.postUploadFile(`/file/upload/${validId}/schools/${otherParentId}`, 'other2.txt'), - api.postUploadFile(`/file/upload/${validId}/schools/${otherParentId}`, 'other3.txt'), + api.postUploadFile(`/file/upload/school/${validId}/schools/${validId}`, 'test1.txt'), + api.postUploadFile(`/file/upload/school/${validId}/schools/${validId}`, 'test2.txt'), + api.postUploadFile(`/file/upload/school/${validId}/schools/${validId}`, 'test3.txt'), + api.postUploadFile(`/file/upload/school/${validId}/schools/${otherParentId}`, 'other1.txt'), + api.postUploadFile(`/file/upload/school/${validId}/schools/${otherParentId}`, 'other2.txt'), + api.postUploadFile(`/file/upload/school/${validId}/schools/${otherParentId}`, 'other3.txt'), ]); const fileRecords = uploadResponse.map(({ result }) => result); - const { result } = await api.delete(`/${validId}/schools/${validId}`); + const { result } = await api.delete(`/school/${validId}/schools/${validId}`); const resultData: FileRecordResponse[] = result.data; const ids: EntityId[] = resultData.map((o) => o.id); @@ -253,12 +245,9 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); const school = schoolEntityFactory.build(); - const roles = roleFactory.buildList(1, { - permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW, Permission.FILESTORAGE_REMOVE], - }); - const user = userFactory.build({ school, roles }); + const { studentUser: user, studentAccount: account } = UserAndAccountTestFactory.buildStudent({ school }); - await em.persistAndFlush([user]); + await em.persistAndFlush([user, account]); em.clear(); }); @@ -280,17 +269,17 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); const school = schoolEntityFactory.build(); - const roles = roleFactory.buildList(1, { - permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW, Permission.FILESTORAGE_REMOVE], - }); - const user = userFactory.build({ school, roles }); + const { studentUser: user, studentAccount: account } = UserAndAccountTestFactory.buildStudent({ school }); - await em.persistAndFlush([user]); + await em.persistAndFlush([user, account]); em.clear(); currentUser = mapUserToCurrentUser(user); - const { result } = await api.postUploadFile(`/file/upload/${school.id}/schools/${school.id}`, 'test1.txt'); + const { result } = await api.postUploadFile( + `/file/upload/school/${school.id}/schools/${school.id}`, + 'test1.txt' + ); fileRecordId = result.id; }); @@ -323,7 +312,6 @@ describe(`${baseRouteName} (api)`, () => { it('should return elements of requested scope', async () => { const otherFileRecords = fileRecordFactory.buildList(3, { - schoolId: new ObjectId().toHexString(), parentType: FileRecordParentType.School, }); diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-download-upload.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-download-upload.api.spec.ts index 276e84b6eb1..4b550b516c5 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-download-upload.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-download-upload.api.spec.ts @@ -7,14 +7,12 @@ import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; -import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { cleanupCollections, mapUserToCurrentUser, - roleFactory, schoolEntityFactory, - userFactory, + UserAndAccountTestFactory, } from '@shared/testing'; import NodeClam from 'clamscan'; import { Request } from 'express'; @@ -146,12 +144,9 @@ describe('files-storage controller (API)', () => { jest.resetAllMocks(); await cleanupCollections(em); const school = schoolEntityFactory.build(); - const roles = roleFactory.buildList(1, { - permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], - }); - const user = userFactory.build({ school, roles }); + const { studentUser: user, studentAccount: account } = UserAndAccountTestFactory.buildStudent({ school }); - await em.persistAndFlush([user, school]); + await em.persistAndFlush([user, school, account]); em.clear(); validId = school.id; currentUser = mapUserToCurrentUser(user); @@ -162,19 +157,19 @@ describe('files-storage controller (API)', () => { describe('upload action', () => { describe('with bad request data', () => { it('should return status 400 for invalid schoolId', async () => { - const response = await api.postUploadFile(`/file/upload/123/users/${validId}`); + const response = await api.postUploadFile(`/file/upload/school/123/users/${validId}`); expect(response.error.validationErrors).toEqual([ { - errors: ['schoolId must be a mongodb id'], - field: ['schoolId'], + errors: ['storageLocationId must be a mongodb id'], + field: ['storageLocationId'], }, ]); expect(response.status).toEqual(400); }); it('should return status 400 for invalid parentId', async () => { - const response = await api.postUploadFile(`/file/upload/${validId}/users/123`); + const response = await api.postUploadFile(`/file/upload/school/${validId}/users/123`); expect(response.error.validationErrors).toEqual([ { @@ -186,7 +181,7 @@ describe('files-storage controller (API)', () => { }); it('should return status 400 for invalid parentType', async () => { - const response = await api.postUploadFile(`/file/upload/${validId}/cookies/${validId}`); + const response = await api.postUploadFile(`/file/upload/school/${validId}/cookies/${validId}`); expect(response.status).toEqual(400); }); @@ -194,13 +189,13 @@ describe('files-storage controller (API)', () => { describe(`with valid request data`, () => { it('should return status 201 for successful upload', async () => { - const response = await api.postUploadFile(`/file/upload/${validId}/schools/${validId}`); + const response = await api.postUploadFile(`/file/upload/school/${validId}/schools/${validId}`); expect(response.status).toEqual(201); }); it('should return the new created file record', async () => { - const { result } = await api.postUploadFile(`/file/upload/${validId}/schools/${validId}`); + const { result } = await api.postUploadFile(`/file/upload/school/${validId}/schools/${validId}`); expect(result).toStrictEqual( expect.objectContaining({ @@ -217,9 +212,9 @@ describe('files-storage controller (API)', () => { }); it('should set iterator number to file name if file already exist', async () => { - await api.postUploadFile(`/file/upload/${validId}/schools/${validId}`); + await api.postUploadFile(`/file/upload/school/${validId}/schools/${validId}`); - const { result } = await api.postUploadFile(`/file/upload/${validId}/schools/${validId}`); + const { result } = await api.postUploadFile(`/file/upload/school/${validId}/schools/${validId}`); expect(result.name).toEqual('test (1).txt'); }); @@ -233,19 +228,19 @@ describe('files-storage controller (API)', () => { }; describe('with bad request data', () => { it('should return status 400 for invalid schoolId', async () => { - const response = await api.postUploadFromUrl(`/file/upload-from-url/123/users/${validId}`, body); + const response = await api.postUploadFromUrl(`/file/upload-from-url/school/123/users/${validId}`, body); expect(response.error.validationErrors).toEqual([ { - errors: ['schoolId must be a mongodb id'], - field: ['schoolId'], + errors: ['storageLocationId must be a mongodb id'], + field: ['storageLocationId'], }, ]); expect(response.status).toEqual(400); }); it('should return status 400 for invalid parentId', async () => { - const response = await api.postUploadFromUrl(`/file/upload-from-url/${validId}/users/123`, body); + const response = await api.postUploadFromUrl(`/file/upload-from-url/school/${validId}/users/123`, body); expect(response.error.validationErrors).toEqual([ { @@ -257,7 +252,10 @@ describe('files-storage controller (API)', () => { }); it('should return status 400 for invalid parentType', async () => { - const response = await api.postUploadFromUrl(`/file/upload-from-url/${validId}/cookies/${validId}`, body); + const response = await api.postUploadFromUrl( + `/file/upload-from-url/school/${validId}/cookies/${validId}`, + body + ); expect(response.error.validationErrors).toEqual([ { @@ -269,7 +267,7 @@ describe('files-storage controller (API)', () => { }); it('should return status 400 for empty url and fileName', async () => { - const response = await api.postUploadFromUrl(`/file/upload-from-url/${validId}/schools/${validId}`, {}); + const response = await api.postUploadFromUrl(`/file/upload-from-url/school/${validId}/schools/${validId}`, {}); expect(response.error.validationErrors).toEqual([ { @@ -291,7 +289,7 @@ describe('files-storage controller (API)', () => { const expectedResponse = TestHelper.createFile({ contentRange: 'bytes 0-3/4' }); s3ClientAdapter.get.mockResolvedValueOnce(expectedResponse); - const uploadResponse = await api.postUploadFile(`/file/upload/${validId}/schools/${validId}`); + const uploadResponse = await api.postUploadFile(`/file/upload/school/${validId}/schools/${validId}`); const { result } = uploadResponse; body = { url: `http://localhost:${appPort}/file/download/${result.id}/${result.name}`, @@ -300,13 +298,19 @@ describe('files-storage controller (API)', () => { }); it('should return status 201 for successful upload', async () => { - const response = await api.postUploadFromUrl(`/file/upload-from-url/${validId}/schools/${validId}`, body); + const response = await api.postUploadFromUrl( + `/file/upload-from-url/school/${validId}/schools/${validId}`, + body + ); expect(response.status).toEqual(201); }); it('should return the new created file record', async () => { - const { result } = await api.postUploadFromUrl(`/file/upload-from-url/${validId}/schools/${validId}`, body); + const { result } = await api.postUploadFromUrl( + `/file/upload-from-url/school/${validId}/schools/${validId}`, + body + ); expect(result).toStrictEqual( expect.objectContaining({ id: expect.any(String), @@ -328,18 +332,21 @@ describe('files-storage controller (API)', () => { s3ClientAdapter.get.mockResolvedValueOnce(expectedResponse1).mockResolvedValueOnce(expectedResponse2); - const uploadResponse = await api.postUploadFile(`/file/upload/${validId}/schools/${validId}`); + const uploadResponse = await api.postUploadFile(`/file/upload/school/${validId}/schools/${validId}`); const { result } = uploadResponse; body = { url: `http://localhost:${appPort}/file/download/${result.id}/${result.name}`, fileName: 'test.txt', }; - await api.postUploadFromUrl(`/file/upload-from-url/${validId}/schools/${validId}`, body); + await api.postUploadFromUrl(`/file/upload-from-url/school/${validId}/schools/${validId}`, body); }); it('should set iterator number to file name if file already exist', async () => { - const { result } = await api.postUploadFromUrl(`/file/upload-from-url/${validId}/schools/${validId}`, body); + const { result } = await api.postUploadFromUrl( + `/file/upload-from-url/school/${validId}/schools/${validId}`, + body + ); expect(result.name).toEqual('test (2).txt'); }); @@ -362,7 +369,7 @@ describe('files-storage controller (API)', () => { }); it('should return status 404 for wrong filename', async () => { - const { result } = await api.postUploadFile(`/file/upload/${validId}/schools/${validId}`); + const { result } = await api.postUploadFile(`/file/upload/school/${validId}/schools/${validId}`); const response = await api.getDownloadFile(`/file/download/${result.id}/wrong-name.txt`); expect(response.error.message).toEqual(ErrorType.FILE_NOT_FOUND); @@ -379,7 +386,9 @@ describe('files-storage controller (API)', () => { describe(`with valid request data`, () => { describe('when mimetype is not application/pdf', () => { const setup = async () => { - const { result: uploadedFile } = await api.postUploadFile(`/file/upload/${validId}/schools/${validId}`); + const { result: uploadedFile } = await api.postUploadFile( + `/file/upload/school/${validId}/schools/${validId}` + ); const expectedResponse = TestHelper.createFile({ contentRange: 'bytes 0-3/4', mimeType: 'image/webp' }); s3ClientAdapter.get.mockResolvedValueOnce(expectedResponse); @@ -420,7 +429,9 @@ describe('files-storage controller (API)', () => { describe('when mimetype is application/pdf', () => { const setup = async () => { - const { result: uploadedFile } = await api.postUploadFile(`/file/upload/${validId}/schools/${validId}`); + const { result: uploadedFile } = await api.postUploadFile( + `/file/upload/school/${validId}/schools/${validId}` + ); const expectedResponse = TestHelper.createFile({ contentRange: 'bytes 0-3/4', mimeType: 'application/pdf' }); s3ClientAdapter.get.mockResolvedValueOnce(expectedResponse); @@ -453,7 +464,7 @@ describe('files-storage controller (API)', () => { const expectedResponse = TestHelper.createFile({ contentRange: 'bytes 0-3/4' }); s3ClientAdapter.get.mockResolvedValueOnce(expectedResponse); - const { result } = await api.postUploadFile(`/file/upload/${validId}/schools/${validId}`); + const { result } = await api.postUploadFile(`/file/upload/school/${validId}/schools/${validId}`); const newRecord = await em.findOneOrFail(FileRecord, result.id); return { newRecord }; diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-list-files.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-list-files.api.spec.ts index 9efc1e4351f..14dafcf4266 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-list-files.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-list-files.api.spec.ts @@ -5,21 +5,18 @@ import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; - -import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { cleanupCollections, fileRecordFactory, mapUserToCurrentUser, - roleFactory, schoolEntityFactory, - userFactory, + UserAndAccountTestFactory, } from '@shared/testing'; import NodeClam from 'clamscan'; import { Request } from 'express'; import request from 'supertest'; -import { FileRecordParentType, PreviewStatus } from '../../entity'; +import { FileRecordParentType, PreviewStatus, StorageLocation } from '../../entity'; import { FilesStorageTestModule } from '../../files-storage-test.module'; import { FileRecordListResponse, FileRecordResponse } from '../dto'; import { availableParentTypes } from './mocks'; @@ -84,12 +81,9 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); const school = schoolEntityFactory.build(); - const roles = roleFactory.buildList(1, { - permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], - }); - const user = userFactory.build({ school, roles }); + const { studentUser: user, studentAccount: account } = UserAndAccountTestFactory.buildStudent({ school }); - await em.persistAndFlush([user]); + await em.persistAndFlush([user, account]); em.clear(); currentUser = mapUserToCurrentUser(user); @@ -97,18 +91,18 @@ describe(`${baseRouteName} (api)`, () => { }); it('should return status 400 for invalid schoolId', async () => { - const response = await api.get(`/123/users/${validId}`); + const response = await api.get(`/school/123/users/${validId}`); expect(response.error.validationErrors).toEqual([ { - errors: ['schoolId must be a mongodb id'], - field: ['schoolId'], + errors: ['storageLocationId must be a mongodb id'], + field: ['storageLocationId'], }, ]); expect(response.status).toEqual(400); }); it('should return status 400 for invalid parentId', async () => { - const response = await api.get(`/${validId}/users/123`); + const response = await api.get(`/school/${validId}/users/123`); expect(response.error.validationErrors).toEqual([ { errors: ['parentId must be a mongodb id'], @@ -119,7 +113,7 @@ describe(`${baseRouteName} (api)`, () => { }); it('should return status 400 for invalid parentType', async () => { - const response = await api.get(`/${validId}/cookies/${validId}`); + const response = await api.get(`/school/${validId}/cookies/${validId}`); expect(response.error.validationErrors).toEqual([ { errors: [`parentType must be one of the following values: ${availableParentTypes}`], @@ -134,12 +128,9 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); const school = schoolEntityFactory.build(); - const roles = roleFactory.buildList(1, { - permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], - }); - const user = userFactory.build({ school, roles }); + const { studentUser: user, studentAccount: account } = UserAndAccountTestFactory.buildStudent({ school }); - await em.persistAndFlush([user]); + await em.persistAndFlush([user, account]); em.clear(); currentUser = mapUserToCurrentUser(user); @@ -147,13 +138,13 @@ describe(`${baseRouteName} (api)`, () => { }); it('should return status 200 for successful request', async () => { - const response = await api.get(`/${validId}/schools/${validId}`); + const response = await api.get(`/school/${validId}/schools/${validId}`); expect(response.status).toEqual(200); }); it('should return a paginated result as default', async () => { - const { result } = await api.get(`/${validId}/schools/${validId}`); + const { result } = await api.get(`/school/${validId}/schools/${validId}`); expect(result).toEqual({ total: 0, @@ -164,7 +155,7 @@ describe(`${baseRouteName} (api)`, () => { }); it('should pass the pagination qurey params', async () => { - const { result } = await api.get(`/${validId}/schools/${validId}`, { limit: 100, skip: 100 }); + const { result } = await api.get(`/school/${validId}/schools/${validId}`, { limit: 100, skip: 100 }); expect(result.limit).toEqual(100); expect(result.skip).toEqual(100); @@ -172,7 +163,8 @@ describe(`${baseRouteName} (api)`, () => { it('should return right type of data', async () => { const fileRecords = fileRecordFactory.buildList(1, { - schoolId: validId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId: validId, parentId: validId, parentType: FileRecordParentType.School, }); @@ -180,7 +172,7 @@ describe(`${baseRouteName} (api)`, () => { await em.persistAndFlush(fileRecords); em.clear(); - const { result } = await api.get(`/${validId}/schools/${validId}`); + const { result } = await api.get(`/school/${validId}/schools/${validId}`); expect(Array.isArray(result.data)).toBe(true); expect(result.data[0]).toBeDefined(); @@ -202,19 +194,21 @@ describe(`${baseRouteName} (api)`, () => { it('should return elements of requested scope', async () => { const fileRecords = fileRecordFactory.buildList(3, { - schoolId: validId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId: validId, parentId: validId, parentType: FileRecordParentType.School, }); const otherFileRecords = fileRecordFactory.buildList(3, { - schoolId: validId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId: validId, parentType: FileRecordParentType.School, }); await em.persistAndFlush([...otherFileRecords, ...fileRecords]); em.clear(); - const { result } = await api.get(`/${validId}/schools/${validId}`); + const { result } = await api.get(`/school/${validId}/schools/${validId}`); const resultData: FileRecordResponse[] = result.data; const ids: EntityId[] = resultData.map((o) => o.id); diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-preview.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-preview.api.spec.ts index 42e41e189a9..ae1667f7b38 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-preview.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-preview.api.spec.ts @@ -8,14 +8,12 @@ import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { ExecutionContext, INestApplication, NotFoundException, StreamableFile } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; -import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { cleanupCollections, mapUserToCurrentUser, - roleFactory, schoolEntityFactory, - userFactory, + UserAndAccountTestFactory, } from '@shared/testing'; import NodeClam from 'clamscan'; import { Request } from 'express'; @@ -160,16 +158,13 @@ describe('File Controller (API) - preview', () => { jest.resetAllMocks(); await cleanupCollections(em); const school = schoolEntityFactory.build(); - const roles = roleFactory.buildList(1, { - permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW], - }); - const user = userFactory.build({ school, roles }); + const { studentUser: user, studentAccount: account } = UserAndAccountTestFactory.buildStudent({ school }); - await em.persistAndFlush([user, school]); + await em.persistAndFlush([user, school, account]); em.clear(); schoolId = school.id; currentUser = mapUserToCurrentUser(user); - uploadPath = `/file/upload/${schoolId}/schools/${schoolId}`; + uploadPath = `/file/upload/school/${schoolId}/schools/${schoolId}`; jest.spyOn(FileType, 'fileTypeStream').mockImplementation((readable) => Promise.resolve(readable)); antivirusService.checkStream.mockResolvedValueOnce({ virus_detected: false }); diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-rename-file.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-rename-file.api.spec.ts index 2933a53822e..1a9a09a4f68 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-rename-file.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-rename-file.api.spec.ts @@ -5,14 +5,12 @@ import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; -import { Permission } from '@shared/domain/interface'; import { cleanupCollections, fileRecordFactory, mapUserToCurrentUser, - roleFactory, schoolEntityFactory, - userFactory, + UserAndAccountTestFactory, } from '@shared/testing'; import NodeClam from 'clamscan'; import { Request } from 'express'; @@ -81,11 +79,9 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); const school = schoolEntityFactory.build(); - const roles = roleFactory.buildList(1, { - permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW, Permission.FILESTORAGE_EDIT], - }); - const user = userFactory.build({ school, roles }); - await em.persistAndFlush([user]); + const { studentUser: user, studentAccount: account } = UserAndAccountTestFactory.buildStudent({ school }); + + await em.persistAndFlush([user, account]); const fileParams = { schoolId: school.id, diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-restore-files.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-restore-files.api.spec.ts index a92691fef5c..0879a4c39c0 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-restore-files.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-restore-files.api.spec.ts @@ -7,8 +7,6 @@ import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; - -import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { cleanupCollections, @@ -16,6 +14,7 @@ import { mapUserToCurrentUser, roleFactory, schoolEntityFactory, + UserAndAccountTestFactory, userFactory, } from '@shared/testing'; import NodeClam from 'clamscan'; @@ -162,18 +161,18 @@ describe(`${baseRouteName} (api)`, () => { }); it('should return status 400 for invalid schoolId', async () => { - const response = await api.restore(`/123/users/${validId}`); + const response = await api.restore(`/school/123/users/${validId}`); expect(response.error.validationErrors).toEqual([ { - errors: ['schoolId must be a mongodb id'], - field: ['schoolId'], + errors: ['storageLocationId must be a mongodb id'], + field: ['storageLocationId'], }, ]); expect(response.status).toEqual(400); }); it('should return status 400 for invalid parentId', async () => { - const response = await api.restore(`/${validId}/users/123`); + const response = await api.restore(`/school/${validId}/users/123`); expect(response.error.validationErrors).toEqual([ { errors: ['parentId must be a mongodb id'], @@ -184,7 +183,7 @@ describe(`${baseRouteName} (api)`, () => { }); it('should return status 400 for invalid parentType', async () => { - const response = await api.restore(`/${validId}/cookies/${validId}`); + const response = await api.restore(`/school/${validId}/cookies/${validId}`); expect(response.error.validationErrors).toEqual([ { errors: [`parentType must be one of the following values: ${availableParentTypes}`], @@ -201,12 +200,9 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); const school = schoolEntityFactory.build(); - const roles = roleFactory.buildList(1, { - permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW, Permission.FILESTORAGE_REMOVE], - }); - const user = userFactory.build({ school, roles }); + const { studentUser: user, studentAccount: account } = UserAndAccountTestFactory.buildStudent({ school }); - await em.persistAndFlush([user]); + await em.persistAndFlush([user, account]); em.clear(); currentUser = mapUserToCurrentUser(user); @@ -216,19 +212,19 @@ describe(`${baseRouteName} (api)`, () => { }); it('should return status 200 for successful request', async () => { - await api.postUploadFile(`/file/upload/${validId}/schools/${validId}`, 'test1.txt'); - await api.delete(`/${validId}/schools/${validId}`); + await api.postUploadFile(`/file/upload/school/${validId}/schools/${validId}`, 'test1.txt'); + await api.delete(`/school/${validId}/schools/${validId}`); - const response = await api.restore(`/${validId}/schools/${validId}`); + const response = await api.restore(`/school/${validId}/schools/${validId}`); expect(response.status).toEqual(201); }); it('should return right type of data', async () => { - await api.postUploadFile(`/file/upload/${validId}/schools/${validId}`, 'test1.txt'); - await api.delete(`/${validId}/schools/${validId}`); + await api.postUploadFile(`/file/upload/school/${validId}/schools/${validId}`, 'test1.txt'); + await api.delete(`/school/${validId}/schools/${validId}`); - const { result } = await api.restore(`/${validId}/schools/${validId}`); + const { result } = await api.restore(`/school/${validId}/schools/${validId}`); expect(Array.isArray(result.data)).toBe(true); expect(result.data[0]).toBeDefined(); @@ -251,18 +247,18 @@ describe(`${baseRouteName} (api)`, () => { it('should return elements of requested scope', async () => { const otherParentId = new ObjectId().toHexString(); const uploadResponse = await Promise.all([ - api.postUploadFile(`/file/upload/${validId}/schools/${validId}`, 'test1.txt'), - api.postUploadFile(`/file/upload/${validId}/schools/${validId}`, 'test2.txt'), - api.postUploadFile(`/file/upload/${validId}/schools/${validId}`, 'test3.txt'), - api.postUploadFile(`/file/upload/${validId}/schools/${otherParentId}`, 'other1.txt'), - api.postUploadFile(`/file/upload/${validId}/schools/${otherParentId}`, 'other2.txt'), - api.postUploadFile(`/file/upload/${validId}/schools/${otherParentId}`, 'other3.txt'), + api.postUploadFile(`/file/upload/school/${validId}/schools/${validId}`, 'test1.txt'), + api.postUploadFile(`/file/upload/school/${validId}/schools/${validId}`, 'test2.txt'), + api.postUploadFile(`/file/upload/school/${validId}/schools/${validId}`, 'test3.txt'), + api.postUploadFile(`/file/upload/school/${validId}/schools/${otherParentId}`, 'other1.txt'), + api.postUploadFile(`/file/upload/school/${validId}/schools/${otherParentId}`, 'other2.txt'), + api.postUploadFile(`/file/upload/school/${validId}/schools/${otherParentId}`, 'other3.txt'), ]); const fileRecords = uploadResponse.map(({ result }) => result); - await api.delete(`/${validId}/schools/${validId}`); + await api.delete(`/school/${validId}/schools/${validId}`); - const { result } = await api.restore(`/${validId}/schools/${validId}`); + const { result } = await api.restore(`/school/${validId}/schools/${validId}`); const resultData: FileRecordResponse[] = result.data; const ids: EntityId[] = resultData.map((o) => o.id); @@ -303,17 +299,17 @@ describe(`${baseRouteName} (api)`, () => { beforeEach(async () => { await cleanupCollections(em); const school = schoolEntityFactory.build(); - const roles = roleFactory.buildList(1, { - permissions: [Permission.FILESTORAGE_CREATE, Permission.FILESTORAGE_VIEW, Permission.FILESTORAGE_REMOVE], - }); - const user = userFactory.build({ school, roles }); + const { studentUser: user, studentAccount: account } = UserAndAccountTestFactory.buildStudent({ school }); - await em.persistAndFlush([user]); + await em.persistAndFlush([user, account]); em.clear(); currentUser = mapUserToCurrentUser(user); - const { result } = await api.postUploadFile(`/file/upload/${school.id}/schools/${school.id}`, 'test1.txt'); + const { result } = await api.postUploadFile( + `/file/upload/school/${school.id}/schools/${school.id}`, + 'test1.txt' + ); fileRecordId = result.id; @@ -349,7 +345,6 @@ describe(`${baseRouteName} (api)`, () => { it('should return elements of requested scope', async () => { const otherFileRecords = fileRecordFactory.buildList(3, { - schoolId: new ObjectId().toHexString(), parentType: FileRecordParentType.School, }); diff --git a/apps/server/src/modules/files-storage/controller/api-test/mocks.ts b/apps/server/src/modules/files-storage/controller/api-test/mocks.ts index 4290f5ee620..c0ad93c37a0 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/mocks.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/mocks.ts @@ -1 +1,2 @@ -export const availableParentTypes = 'users, schools, courses, tasks, lessons, submissions, gradings, boardnodes'; +export const availableParentTypes = + 'users, schools, courses, tasks, lessons, submissions, gradings, boardnodes, externaltools'; diff --git a/apps/server/src/modules/files-storage/controller/dto/file-storage.params.ts b/apps/server/src/modules/files-storage/controller/dto/file-storage.params.ts index a294a37d180..137e91b5054 100644 --- a/apps/server/src/modules/files-storage/controller/dto/file-storage.params.ts +++ b/apps/server/src/modules/files-storage/controller/dto/file-storage.params.ts @@ -3,13 +3,17 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { StringToBoolean } from '@shared/controller'; import { EntityId } from '@shared/domain/types'; import { Allow, IsBoolean, IsEnum, IsMongoId, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; -import { FileRecordParentType } from '../../entity'; +import { FileRecordParentType, StorageLocation } from '../../entity'; import { PreviewOutputMimeTypes, PreviewWidth } from '../../interface'; export class FileRecordParams { @ApiProperty() @IsMongoId() - schoolId!: EntityId; + storageLocationId!: EntityId; + + @ApiProperty({ enum: StorageLocation, enumName: 'StorageLocation' }) + @IsEnum(StorageLocation) + storageLocation!: StorageLocation; @ApiProperty() @IsMongoId() diff --git a/apps/server/src/modules/files-storage/controller/files-storage.consumer.spec.ts b/apps/server/src/modules/files-storage/controller/files-storage.consumer.spec.ts index c8291131252..7cd58a1df38 100644 --- a/apps/server/src/modules/files-storage/controller/files-storage.consumer.spec.ts +++ b/apps/server/src/modules/files-storage/controller/files-storage.consumer.spec.ts @@ -6,10 +6,10 @@ import { ALL_ENTITIES } from '@shared/domain/entity'; import { EntityId } from '@shared/domain/types'; import { courseFactory, fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { FileRecord, FileRecordParentType } from '../entity'; +import { FileRecord, FileRecordParentType, StorageLocation } from '../entity'; import { FilesStorageService } from '../service/files-storage.service'; import { PreviewService } from '../service/preview.service'; -import { FileRecordResponse } from './dto'; +import { CopyFilesOfParentPayload, FileRecordResponse } from './dto'; import { FilesStorageConsumer } from './files-storage.consumer'; describe('FilesStorageConsumer', () => { @@ -55,20 +55,22 @@ describe('FilesStorageConsumer', () => { }); describe('copyFilesOfParent()', () => { - const schoolId: EntityId = new ObjectId().toHexString(); + const storageLocationId: EntityId = new ObjectId().toHexString(); describe('WHEN valid file exists', () => { it('should call filesStorageService.copyFilesOfParent with params', async () => { - const payload = { + const payload: CopyFilesOfParentPayload = { userId: new ObjectId().toHexString(), source: { parentId: new ObjectId().toHexString(), parentType: FileRecordParentType.Course, - schoolId, + storageLocationId, + storageLocation: StorageLocation.SCHOOL, }, target: { parentId: new ObjectId().toHexString(), parentType: FileRecordParentType.Course, - schoolId, + storageLocationId, + storageLocation: StorageLocation.SCHOOL, }, }; await service.copyFilesOfParent(payload); @@ -79,17 +81,19 @@ describe('FilesStorageConsumer', () => { it('regular RPC handler should receive a valid RPC response', async () => { const sourceCourse = courseFactory.buildWithId(); const targetCourse = courseFactory.buildWithId(); - const payload = { + const payload: CopyFilesOfParentPayload = { userId: new ObjectId().toHexString(), source: { parentId: sourceCourse.id, parentType: FileRecordParentType.Course, - schoolId: sourceCourse.school.id, + storageLocationId, + storageLocation: StorageLocation.SCHOOL, }, target: { parentId: targetCourse.id, parentType: FileRecordParentType.Course, - schoolId: targetCourse.school.id, + storageLocationId, + storageLocation: StorageLocation.SCHOOL, }, }; const responseData = [{ id: '1', sourceId: '2', name: 'test.txt' }]; @@ -107,12 +111,14 @@ describe('FilesStorageConsumer', () => { source: { parentId: sourceCourse.id, parentType: FileRecordParentType.Course, - schoolId: sourceCourse.school.id, + storageLocationId, + storageLocation: StorageLocation.SCHOOL, }, target: { parentId: targetCourse.id, parentType: FileRecordParentType.Course, - schoolId: targetCourse.school.id, + storageLocationId, + storageLocation: StorageLocation.SCHOOL, }, }; filesStorageService.copyFilesOfParent.mockResolvedValue([[], 0]); diff --git a/apps/server/src/modules/files-storage/controller/files-storage.controller.ts b/apps/server/src/modules/files-storage/controller/files-storage.controller.ts index 9773394d261..b64196c3fe6 100644 --- a/apps/server/src/modules/files-storage/controller/files-storage.controller.ts +++ b/apps/server/src/modules/files-storage/controller/files-storage.controller.ts @@ -57,7 +57,7 @@ export class FilesStorageController { @ApiResponse({ status: 400, type: BadRequestException }) @ApiResponse({ status: 403, type: ForbiddenException }) @ApiResponse({ status: 500, type: InternalServerErrorException }) - @Post('/upload-from-url/:schoolId/:parentType/:parentId') + @Post('/upload-from-url/:storageLocation/:storageLocationId/:parentType/:parentId') async uploadFromUrl( @Body() body: FileUrlParams, @Param() params: FileRecordParams, @@ -77,7 +77,7 @@ export class FilesStorageController { @ApiResponse({ status: 403, type: ForbiddenException }) @ApiResponse({ status: 500, type: InternalServerErrorException }) @ApiConsumes('multipart/form-data') - @Post('/upload/:schoolId/:parentType/:parentId') + @Post('/upload/:storageLocation/:storageLocationId/:parentType/:parentId') async upload( @Body() _: FileParams, @Param() params: FileRecordParams, @@ -187,7 +187,7 @@ export class FilesStorageController { @ApiResponse({ status: 200, type: FileRecordListResponse }) @ApiResponse({ status: 400, type: ApiValidationError }) @ApiResponse({ status: 403, type: ForbiddenException }) - @Get('/list/:schoolId/:parentType/:parentId') + @Get('/list/:storageLocation/:storageLocationId/:parentType/:parentId') async list( @Param() params: FileRecordParams, @CurrentUser() currentUser: ICurrentUser, @@ -232,7 +232,7 @@ export class FilesStorageController { @ApiResponse({ status: 400, type: ApiValidationError }) @ApiResponse({ status: 403, type: ForbiddenException }) @ApiResponse({ status: 500, type: InternalServerErrorException }) - @Delete('/delete/:schoolId/:parentType/:parentId') + @Delete('/delete/:storageLocation/:storageLocationId/:parentType/:parentId') @UseInterceptors(RequestLoggingInterceptor) async deleteByParent( @Param() params: FileRecordParams, @@ -266,7 +266,7 @@ export class FilesStorageController { @ApiResponse({ status: 201, type: FileRecordListResponse }) @ApiResponse({ status: 400, type: ApiValidationError }) @ApiResponse({ status: 403, type: ForbiddenException }) - @Post('/restore/:schoolId/:parentType/:parentId') + @Post('/restore/:storageLocation/:storageLocationId/:parentType/:parentId') async restore( @Param() params: FileRecordParams, @CurrentUser() currentUser: ICurrentUser @@ -299,7 +299,7 @@ export class FilesStorageController { @ApiResponse({ status: 400, type: ApiValidationError }) @ApiResponse({ status: 403, type: ForbiddenException }) @ApiResponse({ status: 500, type: InternalServerErrorException }) - @Post('/copy/:schoolId/:parentType/:parentId') + @Post('/copy/:storageLocation/:storageLocationId/:parentType/:parentId') async copy( @Param() params: FileRecordParams, @Body() copyFilesParam: CopyFilesOfParentParams, diff --git a/apps/server/src/modules/files-storage/entity/filerecord.entity.spec.ts b/apps/server/src/modules/files-storage/entity/filerecord.entity.spec.ts index 4caac8fafc6..6f0a036956a 100644 --- a/apps/server/src/modules/files-storage/entity/filerecord.entity.spec.ts +++ b/apps/server/src/modules/files-storage/entity/filerecord.entity.spec.ts @@ -10,6 +10,7 @@ import { FileRecordSecurityCheck, PreviewStatus, ScanStatus, + StorageLocation, } from './filerecord.entity'; describe('FileRecord Entity', () => { @@ -28,7 +29,8 @@ describe('FileRecord Entity', () => { parentType: FileRecordParentType.Course, parentId: new ObjectId().toHexString(), creatorId: new ObjectId().toHexString(), - schoolId: new ObjectId().toHexString(), + storageLocationId: new ObjectId().toHexString(), + storageLocation: StorageLocation.SCHOOL, }; }); @@ -50,13 +52,13 @@ describe('FileRecord Entity', () => { expect(fileRecord.creatorId).toEqual(creatorId); }); - it('should provide school id', () => { - const schoolId = new ObjectId().toHexString(); + it('should provide storageLocationId', () => { + const storageLocationId = new ObjectId().toHexString(); const fileRecord = new FileRecord({ ...props, - schoolId, + storageLocationId, }); - expect(fileRecord.schoolId).toEqual(schoolId); + expect(fileRecord.storageLocationId).toEqual(storageLocationId); }); it('should provide isCopyFrom', () => { @@ -218,25 +220,6 @@ describe('FileRecord Entity', () => { }); }); - describe('getSchoolId is called', () => { - describe('WHEN schoolId exists', () => { - const setup = () => { - const schoolId = new ObjectId().toHexString(); - const fileRecord = fileRecordFactory.build({ schoolId }); - - return { fileRecord, schoolId }; - }; - - it('should return the correct schoolId', () => { - const { fileRecord, schoolId } = setup(); - - const result = fileRecord.getSchoolId(); - - expect(result).toEqual(schoolId); - }); - }); - }); - describe('getSecurityToken is called', () => { describe('WHEN security token exists', () => { const setup = () => { @@ -260,18 +243,19 @@ describe('FileRecord Entity', () => { const setup = () => { const parentType = FileRecordParentType.School; const parentId = new ObjectId().toHexString(); - const schoolId = new ObjectId().toHexString(); - const fileRecord = fileRecordFactory.build({ parentType, parentId, schoolId }); + const storageLocationId = new ObjectId().toHexString(); + const storageLocation = StorageLocation.INSTANCE; + const fileRecord = fileRecordFactory.build({ parentType, parentId, storageLocationId, storageLocation }); - return { fileRecord, parentId, parentType, schoolId }; + return { fileRecord, parentId, parentType, storageLocationId, storageLocation }; }; it('should return an object that include parentId and parentType', () => { - const { fileRecord, parentId, parentType, schoolId } = setup(); + const { fileRecord, parentId, parentType, storageLocationId, storageLocation } = setup(); const result = fileRecord.getParentInfo(); - expect(result).toEqual({ parentId, parentType, schoolId }); + expect(result).toEqual({ parentId, parentType, storageLocationId, storageLocation }); }); }); }); @@ -471,9 +455,10 @@ describe('FileRecord Entity', () => { const fileRecord = fileRecordFactory.buildWithId(); const userId = new ObjectId().toHexString(); const parentId = new ObjectId().toHexString(); - const schoolId = new ObjectId().toHexString(); + const storageLocationId = new ObjectId().toHexString(); + const storageLocation = StorageLocation.INSTANCE; const parentType = FileRecordParentType.School; - const targetParentInfo = { parentId, schoolId, parentType }; + const targetParentInfo = { parentId, parentType, storageLocationId, storageLocation }; return { fileRecord, @@ -528,7 +513,8 @@ describe('FileRecord Entity', () => { expect(result.creatorId).toEqual(userId); expect(result.parentType).toEqual(targetParentInfo.parentType); expect(result.parentId).toEqual(targetParentInfo.parentId); - expect(result.schoolId).toEqual(targetParentInfo.schoolId); + expect(result.storageLocationId).toEqual(targetParentInfo.storageLocationId); + expect(result.storageLocation).toEqual(targetParentInfo.storageLocation); }); }); diff --git a/apps/server/src/modules/files-storage/entity/filerecord.entity.ts b/apps/server/src/modules/files-storage/entity/filerecord.entity.ts index 58195356de6..870fbbad61a 100644 --- a/apps/server/src/modules/files-storage/entity/filerecord.entity.ts +++ b/apps/server/src/modules/files-storage/entity/filerecord.entity.ts @@ -25,6 +25,12 @@ export enum FileRecordParentType { 'Submission' = 'submissions', 'Grading' = 'gradings', 'BoardNode' = 'boardnodes', + 'ExternalTool' = 'externaltools', +} + +export enum StorageLocation { + SCHOOL = 'school', + INSTANCE = 'instance', } export enum PreviewStatus { @@ -78,14 +84,16 @@ export interface FileRecordProperties { parentType: FileRecordParentType; parentId: EntityId; creatorId?: EntityId; - schoolId: EntityId; + storageLocation: StorageLocation; + storageLocationId: EntityId; deletedSince?: Date; isCopyFrom?: EntityId; isUploading?: boolean; } interface ParentInfo { - schoolId: EntityId; + storageLocationId: EntityId; + storageLocation: StorageLocation; parentId: EntityId; parentType: FileRecordParentType; } @@ -98,7 +106,7 @@ interface ParentInfo { * and instead just use the plain object ids. */ @Entity({ tableName: 'filerecords' }) -@Index({ properties: ['_schoolId', '_parentId'], options: { background: true } }) +@Index({ properties: ['storageLocation', '_storageLocationId', '_parentId'], options: { background: true } }) // https://github.com/mikro-orm/mikro-orm/issues/1230 @Index({ options: { 'securityCheck.requestToken': 1 } }) export class FileRecord extends BaseEntityWithTimestamps { @@ -145,13 +153,16 @@ export class FileRecord extends BaseEntityWithTimestamps { this._creatorId = userId !== undefined ? new ObjectId(userId) : undefined; } - @Property({ fieldName: 'school' }) - _schoolId: ObjectId; + @Property({ fieldName: 'storageLocationId' }) + _storageLocationId: ObjectId; - get schoolId(): EntityId { - return this._schoolId.toHexString(); + get storageLocationId(): EntityId { + return this._storageLocationId.toHexString(); } + @Property() + storageLocation: StorageLocation; + @Property({ fieldName: 'isCopyFrom', nullable: true }) _isCopyFrom?: ObjectId; @@ -172,7 +183,8 @@ export class FileRecord extends BaseEntityWithTimestamps { if (props.creatorId !== undefined) { this._creatorId = new ObjectId(props.creatorId); } - this._schoolId = new ObjectId(props.schoolId); + this._storageLocationId = new ObjectId(props.storageLocationId); + this.storageLocation = props.storageLocation; if (props.isCopyFrom) { this._isCopyFrom = new ObjectId(props.isCopyFrom); } @@ -193,7 +205,7 @@ export class FileRecord extends BaseEntityWithTimestamps { public copy(userId: EntityId, targetParentInfo: ParentInfo): FileRecord { const { size, name, mimeType, id } = this; - const { parentType, parentId, schoolId } = targetParentInfo; + const { parentType, parentId, storageLocation, storageLocationId } = targetParentInfo; const fileRecordCopy = new FileRecord({ size, @@ -202,7 +214,8 @@ export class FileRecord extends BaseEntityWithTimestamps { parentType, parentId, creatorId: userId, - schoolId, + storageLocationId, + storageLocation, isCopyFrom: id, }); @@ -276,13 +289,9 @@ export class FileRecord extends BaseEntityWithTimestamps { } public getParentInfo(): ParentInfo { - const { parentId, parentType, schoolId } = this; - - return { parentId, parentType, schoolId }; - } + const { parentId, parentType, storageLocation, storageLocationId } = this; - public getSchoolId(): EntityId { - return this.schoolId; + return { parentId, parentType, storageLocation, storageLocationId }; } public getPreviewStatus(): PreviewStatus { diff --git a/apps/server/src/modules/files-storage/helper/file-name.spec.ts b/apps/server/src/modules/files-storage/helper/file-name.spec.ts index 43227c3fc1a..6c1e776ec44 100644 --- a/apps/server/src/modules/files-storage/helper/file-name.spec.ts +++ b/apps/server/src/modules/files-storage/helper/file-name.spec.ts @@ -1,6 +1,6 @@ +import { ObjectId } from '@mikro-orm/mongodb'; import { EntityId } from '@shared/domain/types'; import { fileRecordFactory, setupEntities } from '@shared/testing'; -import { ObjectId } from '@mikro-orm/mongodb'; import crypto from 'crypto'; import { createPreviewNameHash, hasDuplicateName, resolveFileNameDuplicates } from '.'; import { FileRecord } from '../entity'; @@ -9,12 +9,12 @@ import { PreviewOutputMimeTypes } from '../interface/preview-output-mime-types.e describe('File Name Helper', () => { const setupFileRecords = () => { const userId: EntityId = new ObjectId().toHexString(); - const schoolId: EntityId = new ObjectId().toHexString(); + const storageLocationId: EntityId = new ObjectId().toHexString(); const fileRecords = [ - fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text.txt' }), - fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text-two.txt' }), - fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text-tree.txt' }), + fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text.txt' }), + fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text-two.txt' }), + fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text-tree.txt' }), ]; return fileRecords; diff --git a/apps/server/src/modules/files-storage/helper/file-record.spec.ts b/apps/server/src/modules/files-storage/helper/file-record.spec.ts index f50f7edd409..9636bf41303 100644 --- a/apps/server/src/modules/files-storage/helper/file-record.spec.ts +++ b/apps/server/src/modules/files-storage/helper/file-record.spec.ts @@ -1,19 +1,20 @@ +import { ObjectId } from '@mikro-orm/mongodb'; import { EntityId } from '@shared/domain/types'; import { fileRecordFactory, setupEntities } from '@shared/testing'; -import { ObjectId } from '@mikro-orm/mongodb'; import { createFileRecord, getFormat, getPreviewName, markForDelete, unmarkForDelete } from '.'; +import { FileRecordParams } from '../controller/dto'; import { FileRecord } from '../entity'; import { PreviewOutputMimeTypes } from '../interface'; describe('File Record Helper', () => { const setupFileRecords = () => { const userId: EntityId = new ObjectId().toHexString(); - const schoolId: EntityId = new ObjectId().toHexString(); + const storageLocationId: EntityId = new ObjectId().toHexString(); const fileRecords = [ - fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text.txt' }), - fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text-two.txt' }), - fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text-tree.txt' }), + fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text.txt' }), + fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text-two.txt' }), + fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text-tree.txt' }), ]; return { fileRecords, userId }; @@ -62,8 +63,9 @@ describe('File Record Helper', () => { const size = 256; const mimeType = 'image/jpeg'; const fileRecord = fileRecords[0]; - const fileRecordParams = { - schoolId: fileRecord.schoolId, + const fileRecordParams: FileRecordParams = { + storageLocation: fileRecord.storageLocation, + storageLocationId: fileRecord.storageLocationId, parentId: fileRecord.parentId, parentType: fileRecord.parentType, }; @@ -82,7 +84,8 @@ describe('File Record Helper', () => { parentType: fileRecord.parentType, parentId: fileRecord.parentId, creatorId: userId, - schoolId: fileRecord.schoolId, + storageLocation: fileRecord.storageLocation, + storageLocationId: fileRecord.storageLocationId, }; expect(newFileRecord).toEqual(expect.objectContaining({ ...expectedObject })); diff --git a/apps/server/src/modules/files-storage/helper/file-record.ts b/apps/server/src/modules/files-storage/helper/file-record.ts index 80a73249441..972c4b70b7e 100644 --- a/apps/server/src/modules/files-storage/helper/file-record.ts +++ b/apps/server/src/modules/files-storage/helper/file-record.ts @@ -35,7 +35,8 @@ export function createFileRecord( parentType: params.parentType, parentId: params.parentId, creatorId: userId, - schoolId: params.schoolId, + storageLocationId: params.storageLocationId, + storageLocation: params.storageLocation, isUploading: true, }); diff --git a/apps/server/src/modules/files-storage/helper/path.spec.ts b/apps/server/src/modules/files-storage/helper/path.spec.ts index 3b5fab49de4..862cef3ed24 100644 --- a/apps/server/src/modules/files-storage/helper/path.spec.ts +++ b/apps/server/src/modules/files-storage/helper/path.spec.ts @@ -1,6 +1,6 @@ +import { ObjectId } from '@mikro-orm/mongodb'; import { EntityId } from '@shared/domain/types'; import { fileRecordFactory, setupEntities } from '@shared/testing'; -import { ObjectId } from '@mikro-orm/mongodb'; import { createCopyFiles, createPath, createPreviewDirectoryPath, createPreviewFilePath, getPaths } from '.'; import { FileRecord } from '../entity'; import { ErrorType } from '../error'; @@ -12,12 +12,12 @@ describe('Path Helper', () => { const setupFileRecords = () => { const userId: EntityId = new ObjectId().toHexString(); - const schoolId: EntityId = new ObjectId().toHexString(); + const storageLocationId: EntityId = new ObjectId().toHexString(); const fileRecords = [ - fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text.txt' }), - fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text-two.txt' }), - fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text-tree.txt' }), + fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text.txt' }), + fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text-two.txt' }), + fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text-tree.txt' }), ]; return fileRecords; @@ -73,10 +73,12 @@ describe('Path Helper', () => { const fileRecordId1 = fileRecords[0].id; const fileRecordId2 = fileRecords[1].id; - const schoolId1 = fileRecords[0].schoolId; - const schoolId2 = fileRecords[1].schoolId; + const storageLocationId1 = fileRecords[0].storageLocationId; + const storageLocationId2 = fileRecords[1].storageLocationId; - expect(paths).toEqual(expect.arrayContaining([`${schoolId1}/${fileRecordId1}`, `${schoolId2}/${fileRecordId2}`])); + expect(paths).toEqual( + expect.arrayContaining([`${storageLocationId1}/${fileRecordId1}`, `${storageLocationId2}/${fileRecordId2}`]) + ); }); it('should return empty array on empty fileRecordsArray', () => { @@ -97,8 +99,8 @@ describe('Path Helper', () => { const targetFile = fileRecords[1]; const expectedICopyFiles = { - sourcePath: createPath(sourceFile.schoolId, sourceFile.id), - targetPath: createPath(targetFile.schoolId, targetFile.id), + sourcePath: createPath(sourceFile.storageLocationId, sourceFile.id), + targetPath: createPath(targetFile.storageLocationId, targetFile.id), }; const result = createCopyFiles(sourceFile, targetFile); diff --git a/apps/server/src/modules/files-storage/helper/path.ts b/apps/server/src/modules/files-storage/helper/path.ts index e1909ff4966..a9431fc495a 100644 --- a/apps/server/src/modules/files-storage/helper/path.ts +++ b/apps/server/src/modules/files-storage/helper/path.ts @@ -3,39 +3,39 @@ import { EntityId } from '@shared/domain/types'; import { FileRecord } from '../entity'; import { ErrorType } from '../error'; -export function createPath(schoolId: EntityId, fileRecordId: EntityId): string { - if (!schoolId || !fileRecordId) { +export function createPath(storageLocationId: EntityId, fileRecordId: EntityId): string { + if (!storageLocationId || !fileRecordId) { throw new Error(ErrorType.COULD_NOT_CREATE_PATH); } - const path = [schoolId, fileRecordId].join('/'); + const path = [storageLocationId, fileRecordId].join('/'); return path; } -export function createPreviewDirectoryPath(schoolId: EntityId, sourceFileRecordId: EntityId): string { - const path = ['previews', schoolId, sourceFileRecordId].join('/'); +export function createPreviewDirectoryPath(storageLocationId: EntityId, sourceFileRecordId: EntityId): string { + const path = ['previews', storageLocationId, sourceFileRecordId].join('/'); return path; } -export function createPreviewFilePath(schoolId: EntityId, hash: string, sourceFileRecordId: EntityId): string { - const folderPath = createPreviewDirectoryPath(schoolId, sourceFileRecordId); +export function createPreviewFilePath(storageLocationId: EntityId, hash: string, sourceFileRecordId: EntityId): string { + const folderPath = createPreviewDirectoryPath(storageLocationId, sourceFileRecordId); const filePath = [folderPath, hash].join('/'); return filePath; } export function getPaths(fileRecords: FileRecord[]): string[] { - const paths = fileRecords.map((fileRecord) => createPath(fileRecord.getSchoolId(), fileRecord.id)); + const paths = fileRecords.map((fileRecord) => createPath(fileRecord.storageLocationId, fileRecord.id)); return paths; } export function createCopyFiles(sourceFile: FileRecord, targetFile: FileRecord): CopyFiles { const copyFiles = { - sourcePath: createPath(sourceFile.getSchoolId(), sourceFile.id), - targetPath: createPath(targetFile.getSchoolId(), targetFile.id), + sourcePath: createPath(sourceFile.storageLocationId, sourceFile.id), + targetPath: createPath(targetFile.storageLocationId, targetFile.id), }; return copyFiles; diff --git a/apps/server/src/modules/files-storage/mapper/files-storage.mapper.spec.ts b/apps/server/src/modules/files-storage/mapper/files-storage.mapper.spec.ts index d6b62add4f0..72cc6be1afb 100644 --- a/apps/server/src/modules/files-storage/mapper/files-storage.mapper.spec.ts +++ b/apps/server/src/modules/files-storage/mapper/files-storage.mapper.spec.ts @@ -1,6 +1,6 @@ +import { AuthorizableReferenceType } from '@modules/authorization/domain'; import { NotImplementedException } from '@nestjs/common'; import { fileRecordFactory, setupEntities } from '@shared/testing'; -import { AuthorizableReferenceType } from '@modules/authorization/domain'; import { DownloadFileParams, FileRecordListResponse, @@ -42,6 +42,11 @@ describe('FilesStorageMapper', () => { expect(result).toBe(AuthorizableReferenceType.Submission); }); + it('should return allowed type equal ExternalTool', () => { + const result = FilesStorageMapper.mapToAllowedAuthorizationEntityType(FileRecordParentType.ExternalTool); + expect(result).toBe(AuthorizableReferenceType.ExternalTool); + }); + it('should throw Error', () => { const exec = () => { FilesStorageMapper.mapToAllowedAuthorizationEntityType('' as FileRecordParentType); @@ -91,7 +96,8 @@ describe('FilesStorageMapper', () => { const result = FilesStorageMapper.mapFileRecordToFileRecordParams(fileRecord); expect(result).toEqual({ - schoolId: fileRecord.schoolId, + storageLocationId: fileRecord.storageLocationId, + storageLocation: fileRecord.storageLocation, parentId: fileRecord.parentId, parentType: fileRecord.parentType, }); diff --git a/apps/server/src/modules/files-storage/mapper/files-storage.mapper.ts b/apps/server/src/modules/files-storage/mapper/files-storage.mapper.ts index 90439fdf9f2..d79cf00200e 100644 --- a/apps/server/src/modules/files-storage/mapper/files-storage.mapper.ts +++ b/apps/server/src/modules/files-storage/mapper/files-storage.mapper.ts @@ -12,34 +12,38 @@ import { FileRecord, FileRecordParentType } from '../entity'; import { GetFileResponse } from '../interface'; export class FilesStorageMapper { - static mapToAllowedAuthorizationEntityType(type: FileRecordParentType): AuthorizableReferenceType { - const types: Map = new Map(); - types.set(FileRecordParentType.Task, AuthorizableReferenceType.Task); - types.set(FileRecordParentType.Course, AuthorizableReferenceType.Course); - types.set(FileRecordParentType.User, AuthorizableReferenceType.User); - types.set(FileRecordParentType.School, AuthorizableReferenceType.School); - types.set(FileRecordParentType.Lesson, AuthorizableReferenceType.Lesson); - types.set(FileRecordParentType.Submission, AuthorizableReferenceType.Submission); - types.set(FileRecordParentType.Grading, AuthorizableReferenceType.Submission); - types.set(FileRecordParentType.BoardNode, AuthorizableReferenceType.BoardNode); - - const res = types.get(type); + private static authorizationEntityMap: Map = new Map([ + [FileRecordParentType.Task, AuthorizableReferenceType.Task], + [FileRecordParentType.Course, AuthorizableReferenceType.Course], + [FileRecordParentType.User, AuthorizableReferenceType.User], + [FileRecordParentType.School, AuthorizableReferenceType.School], + [FileRecordParentType.Lesson, AuthorizableReferenceType.Lesson], + [FileRecordParentType.Submission, AuthorizableReferenceType.Submission], + [FileRecordParentType.Grading, AuthorizableReferenceType.Submission], + [FileRecordParentType.BoardNode, AuthorizableReferenceType.BoardNode], + [FileRecordParentType.ExternalTool, AuthorizableReferenceType.ExternalTool], + ]); + + public static mapToAllowedAuthorizationEntityType(type: FileRecordParentType): AuthorizableReferenceType { + const res: AuthorizableReferenceType | undefined = this.authorizationEntityMap.get(type); if (!res) { throw new NotImplementedException(); } + return res; } - static mapToSingleFileParams(params: DownloadFileParams): SingleFileParams { + public static mapToSingleFileParams(params: DownloadFileParams): SingleFileParams { const singleFileParams = { fileRecordId: params.fileRecordId }; return singleFileParams; } - static mapFileRecordToFileRecordParams(fileRecord: FileRecord): FileRecordParams { + public static mapFileRecordToFileRecordParams(fileRecord: FileRecord): FileRecordParams { const fileRecordParams = plainToClass(FileRecordParams, { - schoolId: fileRecord.schoolId, + storageLocationId: fileRecord.storageLocationId, + storageLocation: fileRecord.storageLocation, parentId: fileRecord.parentId, parentType: fileRecord.parentType, }); @@ -47,23 +51,26 @@ export class FilesStorageMapper { return fileRecordParams; } - static mapToFileRecordResponse(fileRecord: FileRecord): FileRecordResponse { + public static mapToFileRecordResponse(fileRecord: FileRecord): FileRecordResponse { return new FileRecordResponse(fileRecord); } - static mapToFileRecordListResponse( + public static mapToFileRecordListResponse( fileRecords: FileRecord[], total: number, skip?: number, limit?: number ): FileRecordListResponse { - const responseFileRecords = fileRecords.map((fileRecord) => FilesStorageMapper.mapToFileRecordResponse(fileRecord)); + const responseFileRecords: FileRecordResponse[] = fileRecords.map((fileRecord) => + FilesStorageMapper.mapToFileRecordResponse(fileRecord) + ); const response = new FileRecordListResponse(responseFileRecords, total, skip, limit); + return response; } - static mapToStreamableFile(fileResponse: GetFileResponse): StreamableFile { + public static mapToStreamableFile(fileResponse: GetFileResponse): StreamableFile { let disposition: string; if (fileResponse.contentType === 'application/pdf') { diff --git a/apps/server/src/modules/files-storage/mapper/preview.builder.ts b/apps/server/src/modules/files-storage/mapper/preview.builder.ts index aea85be53d8..0af832e64b9 100644 --- a/apps/server/src/modules/files-storage/mapper/preview.builder.ts +++ b/apps/server/src/modules/files-storage/mapper/preview.builder.ts @@ -10,12 +10,12 @@ export class PreviewBuilder { previewParams: PreviewParams, bytesRange: string | undefined ): PreviewFileParams { - const { schoolId, id, mimeType } = fileRecord; - const originFilePath = createPath(schoolId, id); + const { storageLocationId, id, mimeType } = fileRecord; + const originFilePath = createPath(storageLocationId, id); const format = getFormat(previewParams.outputFormat ?? mimeType); const hash = createPreviewNameHash(id, previewParams); - const previewFilePath = createPreviewFilePath(schoolId, hash, id); + const previewFilePath = createPreviewFilePath(storageLocationId, hash, id); const previewFileParams = { fileRecord, diff --git a/apps/server/src/modules/files-storage/repo/filerecord-scope.ts b/apps/server/src/modules/files-storage/repo/filerecord-scope.ts index fddcfcb4ec0..f1804c199bd 100644 --- a/apps/server/src/modules/files-storage/repo/filerecord-scope.ts +++ b/apps/server/src/modules/files-storage/repo/filerecord-scope.ts @@ -1,7 +1,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { EntityId } from '@shared/domain/types'; import { Scope } from '@shared/repo'; -import { FileRecord } from '../entity'; +import { FileRecord, StorageLocation } from '../entity'; export class FileRecordScope extends Scope { byParentId(parentId: EntityId): FileRecordScope { @@ -16,8 +16,14 @@ export class FileRecordScope extends Scope { return this; } - bySchoolId(schoolId: EntityId): FileRecordScope { - this.addQuery({ _schoolId: new ObjectId(schoolId) }); + byStorageType(storageLocation: StorageLocation): FileRecordScope { + this.addQuery({ storageLocation }); + + return this; + } + + byStorageLocationId(storageLocationId: EntityId): FileRecordScope { + this.addQuery({ _storageLocationId: new ObjectId(storageLocationId) }); return this; } diff --git a/apps/server/src/modules/files-storage/repo/filerecord.repo.integration.spec.ts b/apps/server/src/modules/files-storage/repo/filerecord.repo.integration.spec.ts index 7fa1853bc05..dca53740869 100644 --- a/apps/server/src/modules/files-storage/repo/filerecord.repo.integration.spec.ts +++ b/apps/server/src/modules/files-storage/repo/filerecord.repo.integration.spec.ts @@ -1,10 +1,9 @@ +import { MongoMemoryDatabaseModule } from '@infra/database'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { cleanupCollections, fileRecordFactory } from '@shared/testing'; -import { MongoMemoryDatabaseModule } from '@infra/database'; - -import { FileRecord, FileRecordParentType } from '../entity'; +import { FileRecord, FileRecordParentType, StorageLocation } from '../entity'; import { FileRecordRepo } from './filerecord.repo'; const sortFunction = (a: string, b: string) => a.localeCompare(b); @@ -174,14 +173,14 @@ describe('FileRecordRepo', () => { }); }); - describe('findBySchoolIdAndParentId', () => { + describe('findByStorageLocationIdAndParentId', () => { const parentId1 = new ObjectId().toHexString(); - const schoolId1 = new ObjectId().toHexString(); + const storageLocationId1 = new ObjectId().toHexString(); let fileRecords1: FileRecord[]; beforeEach(() => { fileRecords1 = fileRecordFactory.buildList(3, { - schoolId: schoolId1, + storageLocationId: storageLocationId1, parentType: FileRecordParentType.Task, parentId: parentId1, }); @@ -193,7 +192,14 @@ describe('FileRecordRepo', () => { em.clear(); const pagination = { limit: 1 }; - const [fileRecords, count] = await repo.findBySchoolIdAndParentId(schoolId1, parentId1, { pagination }); + const [fileRecords, count] = await repo.findByStorageLocationIdAndParentId( + StorageLocation.SCHOOL, + storageLocationId1, + parentId1, + { + pagination, + } + ); expect(count).toEqual(3); expect(fileRecords.length).toEqual(1); @@ -204,7 +210,14 @@ describe('FileRecordRepo', () => { em.clear(); const pagination = { skip: 1 }; - const [fileRecords, count] = await repo.findBySchoolIdAndParentId(schoolId1, parentId1, { pagination }); + const [fileRecords, count] = await repo.findByStorageLocationIdAndParentId( + StorageLocation.SCHOOL, + storageLocationId1, + parentId1, + { + pagination, + } + ); expect(count).toEqual(3); expect(fileRecords.length).toEqual(2); @@ -213,7 +226,7 @@ describe('FileRecordRepo', () => { it('should only find searched parent', async () => { const parentId2 = new ObjectId().toHexString(); const fileRecords2 = fileRecordFactory.buildList(3, { - schoolId: schoolId1, + storageLocationId: storageLocationId1, parentType: FileRecordParentType.Task, parentId: parentId2, }); @@ -221,7 +234,11 @@ describe('FileRecordRepo', () => { await em.persistAndFlush([...fileRecords1, ...fileRecords2]); em.clear(); - const [results, count] = await repo.findBySchoolIdAndParentId(schoolId1, parentId1); + const [results, count] = await repo.findByStorageLocationIdAndParentId( + StorageLocation.SCHOOL, + storageLocationId1, + parentId1 + ); expect(count).toEqual(3); expect(results).toHaveLength(3); @@ -229,9 +246,9 @@ describe('FileRecordRepo', () => { }); it('should only find searched school', async () => { - const schoolId2 = new ObjectId().toHexString(); + const storageLocationId2 = new ObjectId().toHexString(); const fileRecords2 = fileRecordFactory.buildList(3, { - schoolId: schoolId2, + storageLocationId: storageLocationId2, parentType: FileRecordParentType.Task, parentId: parentId1, }); @@ -239,16 +256,24 @@ describe('FileRecordRepo', () => { await em.persistAndFlush([...fileRecords1, ...fileRecords2]); em.clear(); - const [results, count] = await repo.findBySchoolIdAndParentId(schoolId1, parentId1); + const [results, count] = await repo.findByStorageLocationIdAndParentId( + StorageLocation.SCHOOL, + storageLocationId1, + parentId1 + ); expect(count).toEqual(3); expect(results).toHaveLength(3); - expect(results.map((o) => o.schoolId)).toEqual([schoolId1, schoolId1, schoolId1]); + expect(results.map((o) => o.storageLocationId)).toEqual([ + storageLocationId1, + storageLocationId1, + storageLocationId1, + ]); }); it('should ignore deletedSince', async () => { const fileRecordsExpired = fileRecordFactory.markedForDelete().buildList(3, { - schoolId: schoolId1, + storageLocationId: storageLocationId1, parentType: FileRecordParentType.Task, parentId: parentId1, }); @@ -256,7 +281,11 @@ describe('FileRecordRepo', () => { await em.persistAndFlush([...fileRecords1, ...fileRecordsExpired]); em.clear(); - const [results, count] = await repo.findBySchoolIdAndParentId(schoolId1, parentId1); + const [results, count] = await repo.findByStorageLocationIdAndParentId( + StorageLocation.SCHOOL, + storageLocationId1, + parentId1 + ); expect(count).toEqual(3); expect(results).toHaveLength(3); @@ -268,12 +297,12 @@ describe('FileRecordRepo', () => { describe('findBySchoolIdAndParentIdAndMarkedForDelete', () => { const parentId1 = new ObjectId().toHexString(); - const schoolId1 = new ObjectId().toHexString(); + const storageLocationId1 = new ObjectId().toHexString(); let fileRecords1: FileRecord[]; beforeEach(() => { fileRecords1 = fileRecordFactory.markedForDelete().buildList(3, { - schoolId: schoolId1, + storageLocationId: storageLocationId1, parentType: FileRecordParentType.Task, parentId: parentId1, }); @@ -283,7 +312,7 @@ describe('FileRecordRepo', () => { const parentId2 = new ObjectId().toHexString(); const fileRecords2 = fileRecordFactory.markedForDelete().buildList(3, { - schoolId: schoolId1, + storageLocationId: storageLocationId1, parentType: FileRecordParentType.Task, parentId: parentId2, }); @@ -291,7 +320,11 @@ describe('FileRecordRepo', () => { await em.persistAndFlush([...fileRecords1, ...fileRecords2]); em.clear(); - const [results, count] = await repo.findBySchoolIdAndParentIdAndMarkedForDelete(schoolId1, parentId1); + const [results, count] = await repo.findByStorageLocationIdAndParentIdAndMarkedForDelete( + StorageLocation.SCHOOL, + storageLocationId1, + parentId1 + ); expect(count).toEqual(3); expect(results).toHaveLength(3); @@ -299,10 +332,10 @@ describe('FileRecordRepo', () => { }); it('should only find searched school', async () => { - const schoolId2 = new ObjectId().toHexString(); + const storageLocationId2 = new ObjectId().toHexString(); const fileRecords2 = fileRecordFactory.markedForDelete().buildList(3, { - schoolId: schoolId2, + storageLocationId: storageLocationId2, parentType: FileRecordParentType.Task, parentId: parentId1, }); @@ -310,16 +343,24 @@ describe('FileRecordRepo', () => { await em.persistAndFlush([...fileRecords1, ...fileRecords2]); em.clear(); - const [results, count] = await repo.findBySchoolIdAndParentIdAndMarkedForDelete(schoolId1, parentId1); + const [results, count] = await repo.findByStorageLocationIdAndParentIdAndMarkedForDelete( + StorageLocation.SCHOOL, + storageLocationId1, + parentId1 + ); expect(count).toEqual(3); expect(results).toHaveLength(3); - expect(results.map((o) => o.schoolId)).toEqual([schoolId1, schoolId1, schoolId1]); + expect(results.map((o) => o.storageLocationId)).toEqual([ + storageLocationId1, + storageLocationId1, + storageLocationId1, + ]); }); it('should ingnore if deletedSince is undefined', async () => { const fileRecordsExpired = fileRecordFactory.buildList(3, { - schoolId: schoolId1, + storageLocationId: storageLocationId1, parentType: FileRecordParentType.Task, parentId: parentId1, }); @@ -327,7 +368,11 @@ describe('FileRecordRepo', () => { await em.persistAndFlush([...fileRecords1, ...fileRecordsExpired]); em.clear(); - const [results, count] = await repo.findBySchoolIdAndParentIdAndMarkedForDelete(schoolId1, parentId1); + const [results, count] = await repo.findByStorageLocationIdAndParentIdAndMarkedForDelete( + StorageLocation.SCHOOL, + storageLocationId1, + parentId1 + ); expect(count).toEqual(3); expect(results).toHaveLength(3); @@ -341,11 +386,11 @@ describe('FileRecordRepo', () => { let fileRecord: FileRecord; beforeEach(() => { - const schoolId = new ObjectId().toHexString(); + const storageLocationId = new ObjectId().toHexString(); const parentId = new ObjectId().toHexString(); fileRecord = fileRecordFactory.build({ - schoolId, + storageLocationId, parentType: FileRecordParentType.Task, parentId, }); diff --git a/apps/server/src/modules/files-storage/repo/filerecord.repo.ts b/apps/server/src/modules/files-storage/repo/filerecord.repo.ts index 8333c5b7021..6fa0ebdd05f 100644 --- a/apps/server/src/modules/files-storage/repo/filerecord.repo.ts +++ b/apps/server/src/modules/files-storage/repo/filerecord.repo.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { IFindOptions, SortOrder } from '@shared/domain/interface'; import { Counted, EntityId } from '@shared/domain/types'; import { BaseRepo } from '@shared/repo'; -import { FileRecord } from '../entity'; +import { FileRecord, StorageLocation } from '../entity'; import { FileRecordScope } from './filerecord-scope'; @Injectable() @@ -32,23 +32,33 @@ export class FileRecordRepo extends BaseRepo { return result; } - async findBySchoolIdAndParentId( - schoolId: EntityId, + async findByStorageLocationIdAndParentId( + storageLocation: StorageLocation, + storageLocationId: EntityId, parentId: EntityId, options?: IFindOptions ): Promise> { - const scope = new FileRecordScope().bySchoolId(schoolId).byParentId(parentId).byMarkedForDelete(false); + const scope = new FileRecordScope() + .byStorageType(storageLocation) + .byStorageLocationId(storageLocationId) + .byParentId(parentId) + .byMarkedForDelete(false); const result = await this.findAndCount(scope, options); return result; } - async findBySchoolIdAndParentIdAndMarkedForDelete( - schoolId: EntityId, + async findByStorageLocationIdAndParentIdAndMarkedForDelete( + storageLocation: StorageLocation, + storageLocationId: EntityId, parentId: EntityId, options?: IFindOptions ): Promise> { - const scope = new FileRecordScope().bySchoolId(schoolId).byParentId(parentId).byMarkedForDelete(true); + const scope = new FileRecordScope() + .byStorageType(storageLocation) + .byStorageLocationId(storageLocationId) + .byParentId(parentId) + .byMarkedForDelete(true); const result = await this.findAndCount(scope, options); return result; diff --git a/apps/server/src/modules/files-storage/service/files-storage-copy.service.spec.ts b/apps/server/src/modules/files-storage/service/files-storage-copy.service.spec.ts index 51e2535e557..c67c3cf4498 100644 --- a/apps/server/src/modules/files-storage/service/files-storage-copy.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/files-storage-copy.service.spec.ts @@ -1,13 +1,13 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { ObjectId } from '@mikro-orm/mongodb'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@infra/antivirus'; -import { S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { FileRecordParams } from '../controller/dto'; -import { FileRecord, FileRecordParentType, ScanStatus } from '../entity'; +import { FileRecord, FileRecordParentType, ScanStatus, StorageLocation } from '../entity'; import { FILES_STORAGE_S3_CONNECTION } from '../files-storage.config'; import { createCopyFiles } from '../helper'; import { CopyFileResponseBuilder } from '../mapper'; @@ -16,16 +16,17 @@ import { FilesStorageService } from './files-storage.service'; const buildFileRecordsWithParams = () => { const parentId = new ObjectId().toHexString(); - const parentSchoolId = new ObjectId().toHexString(); + const storageLocationId = new ObjectId().toHexString(); const fileRecords = [ - fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text.txt' }), - fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text-two.txt' }), - fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text-tree.txt' }), + fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text.txt' }), + fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text-two.txt' }), + fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text-tree.txt' }), ]; const params: FileRecordParams = { - schoolId: parentSchoolId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId, parentId, parentType: FileRecordParentType.User, }; @@ -100,7 +101,10 @@ describe('FilesStorageService copy methods', () => { const { fileRecords: targetFileRecords, params } = buildFileRecordsWithParams(); const copyFilesOfParentParams = { target: params }; - fileRecordRepo.findBySchoolIdAndParentId.mockResolvedValueOnce([sourceFileRecords, sourceFileRecords.length]); + fileRecordRepo.findByStorageLocationIdAndParentId.mockResolvedValueOnce([ + sourceFileRecords, + sourceFileRecords.length, + ]); spy = jest.spyOn(service, 'copy'); spy.mockResolvedValueOnce(targetFileRecords); @@ -112,9 +116,10 @@ describe('FilesStorageService copy methods', () => { await service.copyFilesOfParent(userId, sourceParams, copyFilesOfParentParams); - expect(fileRecordRepo.findBySchoolIdAndParentId).toHaveBeenNthCalledWith( + expect(fileRecordRepo.findByStorageLocationIdAndParentId).toHaveBeenNthCalledWith( 1, - sourceParams.schoolId, + sourceParams.storageLocation, + sourceParams.storageLocationId, sourceParams.parentId ); }); @@ -143,7 +148,7 @@ describe('FilesStorageService copy methods', () => { const { params } = buildFileRecordsWithParams(); const copyFilesOfParentParams = { target: params }; - fileRecordRepo.findBySchoolIdAndParentId.mockResolvedValueOnce([[], 0]); + fileRecordRepo.findByStorageLocationIdAndParentId.mockResolvedValueOnce([[], 0]); return { sourceParams, copyFilesOfParentParams, fileRecords, userId }; }; @@ -170,7 +175,10 @@ describe('FilesStorageService copy methods', () => { const copyFilesOfParentParams = { target: params }; const error = new Error('test'); - fileRecordRepo.findBySchoolIdAndParentId.mockResolvedValueOnce([sourceFileRecords, sourceFileRecords.length]); + fileRecordRepo.findByStorageLocationIdAndParentId.mockResolvedValueOnce([ + sourceFileRecords, + sourceFileRecords.length, + ]); spy = jest.spyOn(service, 'copy'); spy.mockRejectedValueOnce(error); diff --git a/apps/server/src/modules/files-storage/service/files-storage-delete.service.spec.ts b/apps/server/src/modules/files-storage/service/files-storage-delete.service.spec.ts index 353b77837d9..3e03048d29c 100644 --- a/apps/server/src/modules/files-storage/service/files-storage-delete.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/files-storage-delete.service.spec.ts @@ -1,14 +1,14 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { ObjectId } from '@mikro-orm/mongodb'; import { InternalServerErrorException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@infra/antivirus'; -import { S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { FileRecordParams } from '../controller/dto'; -import { FileRecord, FileRecordParentType } from '../entity'; +import { FileRecord, FileRecordParentType, StorageLocation } from '../entity'; import { FILES_STORAGE_S3_CONNECTION } from '../files-storage.config'; import { getPaths } from '../helper'; import { FileRecordRepo } from '../repo'; @@ -16,16 +16,17 @@ import { FilesStorageService } from './files-storage.service'; const buildFileRecordsWithParams = () => { const parentId = new ObjectId().toHexString(); - const parentSchoolId = new ObjectId().toHexString(); + const storageLocationId = new ObjectId().toHexString(); const fileRecords = [ - fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text.txt' }), - fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text-two.txt' }), - fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text-tree.txt' }), + fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text.txt' }), + fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text-two.txt' }), + fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text-tree.txt' }), ]; const params: FileRecordParams = { - schoolId: parentSchoolId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId, parentId, parentType: FileRecordParentType.User, }; diff --git a/apps/server/src/modules/files-storage/service/files-storage-download.service.spec.ts b/apps/server/src/modules/files-storage/service/files-storage-download.service.spec.ts index 4c5f08e39ef..772e8a62b49 100644 --- a/apps/server/src/modules/files-storage/service/files-storage-download.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/files-storage-download.service.spec.ts @@ -1,14 +1,14 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { AntivirusService } from '@infra/antivirus'; +import { GetFile, S3ClientAdapter } from '@infra/s3-client'; import { ObjectId } from '@mikro-orm/mongodb'; import { NotAcceptableException, NotFoundException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@infra/antivirus'; -import { GetFile, S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { FileRecordParams } from '../controller/dto'; -import { FileRecord, FileRecordParentType, ScanStatus } from '../entity'; +import { FileRecord, FileRecordParentType, ScanStatus, StorageLocation } from '../entity'; import { ErrorType } from '../error'; import { FILES_STORAGE_S3_CONNECTION } from '../files-storage.config'; import { createPath } from '../helper'; @@ -18,16 +18,17 @@ import { FilesStorageService } from './files-storage.service'; const buildFileRecordsWithParams = () => { const parentId = new ObjectId().toHexString(); - const parentSchoolId = new ObjectId().toHexString(); + const storageLocationId = new ObjectId().toHexString(); const fileRecords = [ - fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text.txt' }), - fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text-two.txt' }), - fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text-tree.txt' }), + fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text.txt' }), + fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text-two.txt' }), + fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text-tree.txt' }), ]; const params: FileRecordParams = { - schoolId: parentSchoolId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId, parentId, parentType: FileRecordParentType.User, }; @@ -215,7 +216,7 @@ describe('FilesStorageService download methods', () => { it('calls get with correct params', async () => { const { fileRecord } = setup(); - const path = createPath(fileRecord.schoolId, fileRecord.id); + const path = createPath(fileRecord.storageLocationId, fileRecord.id); await service.downloadFile(fileRecord); diff --git a/apps/server/src/modules/files-storage/service/files-storage-get.service.spec.ts b/apps/server/src/modules/files-storage/service/files-storage-get.service.spec.ts index 50f3e28f184..751183ac263 100644 --- a/apps/server/src/modules/files-storage/service/files-storage-get.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/files-storage-get.service.spec.ts @@ -1,13 +1,13 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { ObjectId } from '@mikro-orm/mongodb'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@infra/antivirus'; -import { S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { FileRecordParams, SingleFileParams } from '../controller/dto'; -import { FileRecord, FileRecordParentType } from '../entity'; +import { FileRecord, FileRecordParentType, StorageLocation } from '../entity'; import { FILES_STORAGE_S3_CONNECTION } from '../files-storage.config'; import { FileRecordRepo } from '../repo'; import { FilesStorageService } from './files-storage.service'; @@ -15,16 +15,17 @@ import { FilesStorageService } from './files-storage.service'; const buildFileRecordsWithParams = () => { const creatorId = new ObjectId().toHexString(); const parentId = new ObjectId().toHexString(); - const parentSchoolId = new ObjectId().toHexString(); + const storageLocationId = new ObjectId().toHexString(); const fileRecords = [ - fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text.txt', creatorId }), - fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text-two.txt', creatorId }), - fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text-tree.txt', creatorId }), + fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text.txt', creatorId }), + fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text-two.txt', creatorId }), + fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text-tree.txt', creatorId }), ]; const params: FileRecordParams = { - schoolId: parentSchoolId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId, parentId, parentType: FileRecordParentType.User, }; @@ -34,9 +35,9 @@ const buildFileRecordsWithParams = () => { const buildFileRecordWithParams = () => { const parentId = new ObjectId().toHexString(); - const parentSchoolId = new ObjectId().toHexString(); + const storageLocationId = new ObjectId().toHexString(); - const fileRecord = fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text.txt' }); + const fileRecord = fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text.txt' }); const params: SingleFileParams = { fileRecordId: fileRecord.id, }; diff --git a/apps/server/src/modules/files-storage/service/files-storage-remove-creator.service.spec.ts b/apps/server/src/modules/files-storage/service/files-storage-remove-creator.service.spec.ts index cd3cd1e7473..527be94de12 100644 --- a/apps/server/src/modules/files-storage/service/files-storage-remove-creator.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/files-storage-remove-creator.service.spec.ts @@ -1,30 +1,31 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { ObjectId } from '@mikro-orm/mongodb'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@infra/antivirus'; -import { S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { FileRecordParams } from '../controller/dto'; -import { FileRecord, FileRecordParentType } from '../entity'; +import { FileRecord, FileRecordParentType, StorageLocation } from '../entity'; import { FILES_STORAGE_S3_CONNECTION } from '../files-storage.config'; import { FileRecordRepo } from '../repo'; import { FilesStorageService } from './files-storage.service'; const buildFileRecordsWithParams = () => { const parentId = new ObjectId().toHexString(); - const parentSchoolId = new ObjectId().toHexString(); + const storageLocationId = new ObjectId().toHexString(); const creatorId = new ObjectId().toHexString(); const fileRecords = [ - fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text.txt', creatorId }), - fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text-two.txt', creatorId }), - fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text-tree.txt', creatorId }), + fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text.txt', creatorId }), + fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text-two.txt', creatorId }), + fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text-tree.txt', creatorId }), ]; const params: FileRecordParams = { - schoolId: parentSchoolId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId, parentId, parentType: FileRecordParentType.User, }; diff --git a/apps/server/src/modules/files-storage/service/files-storage-restore.service.spec.ts b/apps/server/src/modules/files-storage/service/files-storage-restore.service.spec.ts index 3b6dec255fa..54855e3fcb0 100644 --- a/apps/server/src/modules/files-storage/service/files-storage-restore.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/files-storage-restore.service.spec.ts @@ -1,13 +1,13 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { ObjectId } from '@mikro-orm/mongodb'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@infra/antivirus'; -import { S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { FileRecordParams } from '../controller/dto'; -import { FileRecord, FileRecordParentType } from '../entity'; +import { FileRecord, FileRecordParentType, StorageLocation } from '../entity'; import { FILES_STORAGE_S3_CONNECTION } from '../files-storage.config'; import { getPaths, unmarkForDelete } from '../helper'; import { FileRecordRepo } from '../repo'; @@ -18,13 +18,18 @@ const buildFileRecordsWithParams = () => { const parentSchoolId = new ObjectId().toHexString(); const fileRecords = [ - fileRecordFactory.markedForDelete().buildWithId({ parentId, schoolId: parentSchoolId, name: 'text.txt' }), - fileRecordFactory.markedForDelete().buildWithId({ parentId, schoolId: parentSchoolId, name: 'text-two.txt' }), - fileRecordFactory.markedForDelete().buildWithId({ parentId, schoolId: parentSchoolId, name: 'text-tree.txt' }), + fileRecordFactory.markedForDelete().buildWithId({ parentId, storageLocationId: parentSchoolId, name: 'text.txt' }), + fileRecordFactory + .markedForDelete() + .buildWithId({ parentId, storageLocationId: parentSchoolId, name: 'text-two.txt' }), + fileRecordFactory + .markedForDelete() + .buildWithId({ parentId, storageLocationId: parentSchoolId, name: 'text-tree.txt' }), ]; const params: FileRecordParams = { - schoolId: parentSchoolId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId: parentSchoolId, parentId, parentType: FileRecordParentType.User, }; @@ -95,7 +100,7 @@ describe('FilesStorageService restore methods', () => { const setup = () => { const { params, fileRecords } = buildFileRecordsWithParams(); - fileRecordRepo.findBySchoolIdAndParentIdAndMarkedForDelete.mockResolvedValueOnce([ + fileRecordRepo.findByStorageLocationIdAndParentIdAndMarkedForDelete.mockResolvedValueOnce([ fileRecords, fileRecords.length, ]); @@ -109,8 +114,9 @@ describe('FilesStorageService restore methods', () => { await service.restoreFilesOfParent(params); - expect(fileRecordRepo.findBySchoolIdAndParentIdAndMarkedForDelete).toHaveBeenCalledWith( - params.schoolId, + expect(fileRecordRepo.findByStorageLocationIdAndParentIdAndMarkedForDelete).toHaveBeenCalledWith( + params.storageLocation, + params.storageLocationId, params.parentId ); }); @@ -142,7 +148,7 @@ describe('FilesStorageService restore methods', () => { const setup = () => { const { params } = buildFileRecordsWithParams(); - fileRecordRepo.findBySchoolIdAndParentIdAndMarkedForDelete.mockResolvedValueOnce([[], 0]); + fileRecordRepo.findByStorageLocationIdAndParentIdAndMarkedForDelete.mockResolvedValueOnce([[], 0]); spy = jest.spyOn(service, 'restore').mockResolvedValueOnce(); return { params }; @@ -168,7 +174,7 @@ describe('FilesStorageService restore methods', () => { const { params } = buildFileRecordsWithParams(); const error = new Error('bla'); - fileRecordRepo.findBySchoolIdAndParentIdAndMarkedForDelete.mockRejectedValueOnce(error); + fileRecordRepo.findByStorageLocationIdAndParentIdAndMarkedForDelete.mockRejectedValueOnce(error); spy = jest.spyOn(service, 'restore').mockResolvedValueOnce(); return { params, error }; @@ -192,7 +198,7 @@ describe('FilesStorageService restore methods', () => { const { params, fileRecords } = buildFileRecordsWithParams(); const error = new Error('bla'); - fileRecordRepo.findBySchoolIdAndParentIdAndMarkedForDelete.mockResolvedValueOnce([fileRecords, 3]); + fileRecordRepo.findByStorageLocationIdAndParentIdAndMarkedForDelete.mockResolvedValueOnce([fileRecords, 3]); spy = jest.spyOn(service, 'restore').mockRejectedValueOnce(error); return { params, error }; diff --git a/apps/server/src/modules/files-storage/service/files-storage-update.service.spec.ts b/apps/server/src/modules/files-storage/service/files-storage-update.service.spec.ts index eefd8176169..397caf6cc26 100644 --- a/apps/server/src/modules/files-storage/service/files-storage-update.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/files-storage-update.service.spec.ts @@ -1,15 +1,15 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { AntivirusService } from '@infra/antivirus'; +import { S3ClientAdapter } from '@infra/s3-client'; import { ObjectId } from '@mikro-orm/mongodb'; import { ConflictException, NotFoundException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@infra/antivirus'; -import { S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import _ from 'lodash'; import { FileRecordParams, RenameFileParams, ScanResultParams, SingleFileParams } from '../controller/dto'; -import { FileRecord, FileRecordParentType } from '../entity'; +import { FileRecord, FileRecordParentType, StorageLocation } from '../entity'; import { ErrorType } from '../error'; import { FILES_STORAGE_S3_CONNECTION } from '../files-storage.config'; import { FileRecordMapper, FilesStorageMapper } from '../mapper'; @@ -18,16 +18,17 @@ import { FilesStorageService } from './files-storage.service'; const buildFileRecordsWithParams = () => { const parentId = new ObjectId().toHexString(); - const parentSchoolId = new ObjectId().toHexString(); + const storageLocationId = new ObjectId().toHexString(); const fileRecords = [ - fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text.txt' }), - fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text-two.txt' }), - fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text-tree.txt' }), + fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text.txt' }), + fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text-two.txt' }), + fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text-tree.txt' }), ]; const params: FileRecordParams = { - schoolId: parentSchoolId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId, parentId, parentType: FileRecordParentType.User, }; @@ -37,9 +38,9 @@ const buildFileRecordsWithParams = () => { const buildFileRecordWithParams = () => { const parentId = new ObjectId().toHexString(); - const parentSchoolId = new ObjectId().toHexString(); + const storageLocationId = new ObjectId().toHexString(); - const fileRecord = fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text.txt' }); + const fileRecord = fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text.txt' }); const params: SingleFileParams = { fileRecordId: fileRecord.id, }; diff --git a/apps/server/src/modules/files-storage/service/files-storage-upload.service.spec.ts b/apps/server/src/modules/files-storage/service/files-storage-upload.service.spec.ts index 63049e0c587..0a8b60e780c 100644 --- a/apps/server/src/modules/files-storage/service/files-storage-upload.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/files-storage-upload.service.spec.ts @@ -13,7 +13,7 @@ import FileType from 'file-type-cjs/file-type-cjs-index'; import { PassThrough, Readable } from 'stream'; import { FileRecordParams } from '../controller/dto'; import { FileDto } from '../dto'; -import { FileRecord, FileRecordParentType } from '../entity'; +import { FileRecord, FileRecordParentType, StorageLocation } from '../entity'; import { ErrorType } from '../error'; import { FILES_STORAGE_S3_CONNECTION } from '../files-storage.config'; import { createFileRecord, resolveFileNameDuplicates } from '../helper'; @@ -28,16 +28,17 @@ jest.mock('file-type-cjs/file-type-cjs-index', () => { const buildFileRecordsWithParams = () => { const parentId = new ObjectId().toHexString(); - const parentSchoolId = new ObjectId().toHexString(); + const storageLocationId = new ObjectId().toHexString(); const fileRecords = [ - fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text.txt' }), - fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text-two.txt' }), - fileRecordFactory.buildWithId({ parentId, schoolId: parentSchoolId, name: 'text-tree.txt' }), + fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text.txt' }), + fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text-two.txt' }), + fileRecordFactory.buildWithId({ parentId, storageLocationId, name: 'text-tree.txt' }), ]; const params: FileRecordParams = { - schoolId: parentSchoolId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId, parentId, parentType: FileRecordParentType.User, }; @@ -244,7 +245,7 @@ describe('FilesStorageService upload methods', () => { const fileRecord = await service.uploadFile(userId, params, file); - const filePath = [fileRecord.schoolId, fileRecord.id].join('/'); + const filePath = [fileRecord.storageLocationId, fileRecord.id].join('/'); expect(storageClient.create).toHaveBeenCalledWith(filePath, file); }); diff --git a/apps/server/src/modules/files-storage/service/files-storage.service.ts b/apps/server/src/modules/files-storage/service/files-storage.service.ts index 656c6362baf..a7fe6a9018b 100644 --- a/apps/server/src/modules/files-storage/service/files-storage.service.ts +++ b/apps/server/src/modules/files-storage/service/files-storage.service.ts @@ -158,7 +158,7 @@ export class FilesStorageService { params: FileRecordParams, file: FileDto ): Promise { - const filePath = createPath(params.schoolId, fileRecord.id); + const filePath = createPath(params.storageLocationId, fileRecord.id); const useStreamToAntivirus = this.configService.get('USE_STREAM_TO_ANTIVIRUS'); try { @@ -275,7 +275,7 @@ export class FilesStorageService { } public async downloadFile(fileRecord: FileRecord, bytesRange?: string): Promise { - const pathToFile = createPath(fileRecord.schoolId, fileRecord.id); + const pathToFile = createPath(fileRecord.storageLocationId, fileRecord.id); const file = await this.storageClient.get(pathToFile, bytesRange); const response = FileResponseBuilder.build(file, fileRecord.getName()); @@ -351,8 +351,9 @@ export class FilesStorageService { } public async restoreFilesOfParent(params: FileRecordParams): Promise> { - const [fileRecords, count] = await this.fileRecordRepo.findBySchoolIdAndParentIdAndMarkedForDelete( - params.schoolId, + const [fileRecords, count] = await this.fileRecordRepo.findByStorageLocationIdAndParentIdAndMarkedForDelete( + params.storageLocation, + params.storageLocationId, params.parentId ); @@ -377,7 +378,11 @@ export class FilesStorageService { params: FileRecordParams, copyFilesParams: CopyFilesOfParentParams ): Promise> { - const [fileRecords, count] = await this.fileRecordRepo.findBySchoolIdAndParentId(params.schoolId, params.parentId); + const [fileRecords, count] = await this.fileRecordRepo.findByStorageLocationIdAndParentId( + params.storageLocation, + params.storageLocationId, + params.parentId + ); if (count === 0) { return [[], 0]; diff --git a/apps/server/src/modules/files-storage/service/preview.service.spec.ts b/apps/server/src/modules/files-storage/service/preview.service.spec.ts index a5fed69ef51..137c04e373d 100644 --- a/apps/server/src/modules/files-storage/service/preview.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/preview.service.spec.ts @@ -1,13 +1,13 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { PreviewProducer } from '@infra/preview-generator'; +import { S3ClientAdapter } from '@infra/s3-client'; import { ObjectId } from '@mikro-orm/mongodb'; import { NotFoundException, UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { PreviewProducer } from '@infra/preview-generator'; -import { S3ClientAdapter } from '@infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { FileRecordParams } from '../controller/dto'; -import { FileRecord, FileRecordParentType, ScanStatus } from '../entity'; +import { FileRecord, FileRecordParentType, ScanStatus, StorageLocation } from '../entity'; import { ErrorType } from '../error'; import { FILES_STORAGE_S3_CONNECTION } from '../files-storage.config'; import { createPath, createPreviewDirectoryPath, createPreviewFilePath, createPreviewNameHash } from '../helper'; @@ -20,17 +20,18 @@ import { PreviewService } from './preview.service'; const buildFileRecordWithParams = (mimeType: string, scanStatus?: ScanStatus) => { const parentId = new ObjectId().toHexString(); - const parentSchoolId = new ObjectId().toHexString(); + const parentStorageLocationId = new ObjectId().toHexString(); const fileRecord = fileRecordFactory.buildWithId({ parentId, - schoolId: parentSchoolId, + storageLocationId: parentStorageLocationId, name: 'text.png', mimeType, }); fileRecord.securityCheck.status = scanStatus ?? ScanStatus.VERIFIED; const params: FileRecordParams = { - schoolId: parentSchoolId, + storageLocationId: parentStorageLocationId, + storageLocation: StorageLocation.SCHOOL, parentId, parentType: FileRecordParentType.User, }; @@ -90,9 +91,9 @@ describe('PreviewService', () => { }); describe('download is called', () => { - describe('WHEN preview is possbile', () => { + describe('WHEN preview is possible', () => { describe('WHEN forceUpdate is true', () => { - describe('WHEN first get of preview file is successfull', () => { + describe('WHEN first get of preview file is successfully', () => { const setup = () => { const bytesRange = 'bytes=0-100'; const mimeType = 'image/png'; @@ -111,8 +112,8 @@ describe('PreviewService', () => { const previewFileResponse = FileResponseBuilder.build(previewFile, name); const hash = createPreviewNameHash(fileRecord.id, previewParams); - const previewPath = createPreviewFilePath(fileRecord.getSchoolId(), hash, fileRecord.id); - const originPath = createPath(fileRecord.getSchoolId(), fileRecord.id); + const previewPath = createPreviewFilePath(fileRecord.storageLocationId, hash, fileRecord.id); + const originPath = createPath(fileRecord.storageLocationId, fileRecord.id); return { bytesRange, @@ -174,8 +175,8 @@ describe('PreviewService', () => { const previewFileResponse = FileResponseBuilder.build(previewFile, name); const hash = createPreviewNameHash(fileRecord.id, previewParams); - const previewPath = createPreviewFilePath(fileRecord.getSchoolId(), hash, fileRecord.id); - const originPath = createPath(fileRecord.getSchoolId(), fileRecord.id); + const previewPath = createPreviewFilePath(fileRecord.storageLocationId, hash, fileRecord.id); + const originPath = createPath(fileRecord.storageLocationId, fileRecord.id); return { bytesRange, @@ -245,8 +246,8 @@ describe('PreviewService', () => { const previewFileResponse = FileResponseBuilder.build(previewFile, name); const hash = createPreviewNameHash(fileRecord.id, previewParams); - const previewPath = createPreviewFilePath(fileRecord.getSchoolId(), hash, fileRecord.id); - const originPath = createPath(fileRecord.getSchoolId(), fileRecord.id); + const previewPath = createPreviewFilePath(fileRecord.storageLocationId, hash, fileRecord.id); + const originPath = createPath(fileRecord.storageLocationId, fileRecord.id); return { bytesRange, @@ -287,8 +288,8 @@ describe('PreviewService', () => { const previewFileResponse = FileResponseBuilder.build(previewFile, name); const hash = createPreviewNameHash(fileRecord.id, previewParams); - const previewPath = createPreviewFilePath(fileRecord.getSchoolId(), hash, fileRecord.id); - const originPath = createPath(fileRecord.getSchoolId(), fileRecord.id); + const previewPath = createPreviewFilePath(fileRecord.storageLocationId, hash, fileRecord.id); + const originPath = createPath(fileRecord.storageLocationId, fileRecord.id); return { bytesRange, @@ -346,8 +347,8 @@ describe('PreviewService', () => { const previewFileResponse = FileResponseBuilder.build(previewFile, name); const hash = createPreviewNameHash(fileRecord.id, previewParams); - const previewPath = createPreviewFilePath(fileRecord.getSchoolId(), hash, fileRecord.id); - const originPath = createPath(fileRecord.getSchoolId(), fileRecord.id); + const previewPath = createPreviewFilePath(fileRecord.storageLocationId, hash, fileRecord.id); + const originPath = createPath(fileRecord.storageLocationId, fileRecord.id); return { bytesRange, @@ -417,8 +418,8 @@ describe('PreviewService', () => { const previewFileResponse = FileResponseBuilder.build(previewFile, name); const hash = createPreviewNameHash(fileRecord.id, previewParams); - const previewPath = createPreviewFilePath(fileRecord.getSchoolId(), hash, fileRecord.id); - const originPath = createPath(fileRecord.getSchoolId(), fileRecord.id); + const previewPath = createPreviewFilePath(fileRecord.storageLocationId, hash, fileRecord.id); + const originPath = createPath(fileRecord.storageLocationId, fileRecord.id); return { bytesRange, @@ -556,7 +557,7 @@ describe('PreviewService', () => { ...defaultPreviewParams, }; const format = previewParams.outputFormat.split('/')[1]; - const directoryPath = createPreviewDirectoryPath(fileRecord.schoolId, fileRecord.id); + const directoryPath = createPreviewDirectoryPath(fileRecord.storageLocationId, fileRecord.id); return { fileRecord, diff --git a/apps/server/src/modules/files-storage/service/preview.service.ts b/apps/server/src/modules/files-storage/service/preview.service.ts index 0a9ba63e8e1..fd00e578a9c 100644 --- a/apps/server/src/modules/files-storage/service/preview.service.ts +++ b/apps/server/src/modules/files-storage/service/preview.service.ts @@ -1,6 +1,6 @@ -import { Inject, Injectable, NotFoundException, UnprocessableEntityException } from '@nestjs/common'; import { PreviewProducer } from '@infra/preview-generator'; import { S3ClientAdapter } from '@infra/s3-client'; +import { Inject, Injectable, NotFoundException, UnprocessableEntityException } from '@nestjs/common'; import { LegacyLogger } from '@src/core/logger'; import { PreviewParams } from '../controller/dto'; import { FileRecord, PreviewStatus } from '../entity'; @@ -35,7 +35,9 @@ export class PreviewService { } public async deletePreviews(fileRecords: FileRecord[]): Promise { - const paths = fileRecords.map((fileRecord) => createPreviewDirectoryPath(fileRecord.getSchoolId(), fileRecord.id)); + const paths = fileRecords.map((fileRecord) => + createPreviewDirectoryPath(fileRecord.storageLocationId, fileRecord.id) + ); const promises = paths.map((path) => this.storageClient.deleteDirectory(path)); diff --git a/apps/server/src/modules/files-storage/uc/files-storage-copy.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-copy.uc.spec.ts index c737f641c3d..43b535bdddb 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-copy.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-copy.uc.spec.ts @@ -3,18 +3,17 @@ import { AntivirusService } from '@infra/antivirus'; import { S3ClientAdapter } from '@infra/s3-client'; import { EntityManager } from '@mikro-orm/core'; import { ObjectId } from '@mikro-orm/mongodb'; -import { Action } from '@modules/authorization'; import { AuthorizationReferenceService } from '@modules/authorization/domain'; import { HttpService } from '@nestjs/axios'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { fileRecordFactory, setupEntities } from '@shared/testing'; -import { LegacyLogger } from '@src/core/logger'; import { DomainErrorHandler } from '@src/core'; -import { FileRecordParams } from '../controller/dto'; -import { FileRecord, FileRecordParentType } from '../entity'; +import { LegacyLogger } from '@src/core/logger'; +import { CopyFilesOfParentParams, FileRecordParams } from '../controller/dto'; +import { FileRecord, FileRecordParentType, StorageLocation } from '../entity'; +import { FileStorageAuthorizationContext } from '../files-storage.const'; import { CopyFileResponseBuilder } from '../mapper'; import { FilesStorageService } from '../service/files-storage.service'; import { PreviewService } from '../service/preview.service'; @@ -22,16 +21,17 @@ import { FilesStorageUC } from './files-storage.uc'; const buildFileRecordsWithParams = () => { const userId = new ObjectId().toHexString(); - const schoolId = new ObjectId().toHexString(); + const storageLocationId = new ObjectId().toHexString(); const fileRecords = [ - fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text.txt' }), - fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text-two.txt' }), - fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text-tree.txt' }), + fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text.txt' }), + fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text-two.txt' }), + fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text-tree.txt' }), ]; const params: FileRecordParams = { - schoolId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId, parentId: userId, parentType: FileRecordParentType.User, }; @@ -39,9 +39,10 @@ const buildFileRecordsWithParams = () => { return { params, fileRecords, userId }; }; -const createRequestParams = (schoolId: EntityId, userId: EntityId) => { +const createRequestParams = (storageLocationId: EntityId, userId: EntityId): FileRecordParams => { return { - schoolId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId, parentId: userId, parentType: FileRecordParentType.User, }; @@ -55,13 +56,14 @@ const createParams = () => { return { userId, schoolId, requestParams }; }; -const createTargetParams = () => { +const createTargetParams = (): CopyFilesOfParentParams => { const targetParentId: EntityId = new ObjectId().toHexString(); - const schoolId: EntityId = new ObjectId().toHexString(); + const storageLocationId: EntityId = new ObjectId().toHexString(); return { target: { - schoolId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId, parentId: targetParentId, parentType: FileRecordParentType.Task, }, @@ -162,7 +164,7 @@ describe('FilesStorageUC', () => { userId, sourceParams.parentType, sourceParams.parentId, - { action: Action.write, requiredPermissions: [Permission.FILESTORAGE_CREATE] } + FileStorageAuthorizationContext.create ); }); @@ -176,7 +178,7 @@ describe('FilesStorageUC', () => { userId, targetParams.target.parentType, targetParams.target.parentId, - { action: Action.write, requiredPermissions: [Permission.FILESTORAGE_CREATE] } + FileStorageAuthorizationContext.create ); }); @@ -327,7 +329,7 @@ describe('FilesStorageUC', () => { userId, fileRecord.parentType, fileRecord.parentId, - { action: Action.write, requiredPermissions: [Permission.FILESTORAGE_CREATE] } + FileStorageAuthorizationContext.create ); }); @@ -341,7 +343,7 @@ describe('FilesStorageUC', () => { userId, copyFileParams.target.parentType, copyFileParams.target.parentId, - { action: Action.write, requiredPermissions: [Permission.FILESTORAGE_CREATE] } + FileStorageAuthorizationContext.create ); }); diff --git a/apps/server/src/modules/files-storage/uc/files-storage-delete.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-delete.uc.spec.ts index 471e6888334..fe2dd2ca371 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-delete.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-delete.uc.spec.ts @@ -9,10 +9,10 @@ import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Counted, EntityId } from '@shared/domain/types'; import { fileRecordFactory, setupEntities } from '@shared/testing'; -import { LegacyLogger } from '@src/core/logger'; import { DomainErrorHandler } from '@src/core'; +import { LegacyLogger } from '@src/core/logger'; import { FileRecordParams } from '../controller/dto'; -import { FileRecord, FileRecordParentType } from '../entity'; +import { FileRecord, FileRecordParentType, StorageLocation } from '../entity'; import { FileStorageAuthorizationContext } from '../files-storage.const'; import { FilesStorageMapper } from '../mapper'; import { FilesStorageService } from '../service/files-storage.service'; @@ -21,16 +21,17 @@ import { FilesStorageUC } from './files-storage.uc'; const buildFileRecordsWithParams = () => { const userId = new ObjectId().toHexString(); - const schoolId = new ObjectId().toHexString(); + const storageLocationId = new ObjectId().toHexString(); const fileRecords = [ - fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text.txt' }), - fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text-two.txt' }), - fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text-tree.txt' }), + fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text.txt' }), + fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text-two.txt' }), + fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text-tree.txt' }), ]; const params: FileRecordParams = { - schoolId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId, parentId: userId, parentType: FileRecordParentType.User, }; @@ -38,9 +39,10 @@ const buildFileRecordsWithParams = () => { return { params, fileRecords, userId }; }; -const createRequestParams = (schoolId: EntityId, userId: EntityId) => { +const createRequestParams = (storageLocationId: EntityId, userId: EntityId): FileRecordParams => { return { - schoolId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId, parentId: userId, parentType: FileRecordParentType.User, }; diff --git a/apps/server/src/modules/files-storage/uc/files-storage-download-preview.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-download-preview.uc.spec.ts index a32e9170c60..22f0d437801 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-download-preview.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-download-preview.uc.spec.ts @@ -8,8 +8,8 @@ import { HttpService } from '@nestjs/axios'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { fileRecordFactory, setupEntities } from '@shared/testing'; -import { LegacyLogger } from '@src/core/logger'; import { DomainErrorHandler } from '@src/core'; +import { LegacyLogger } from '@src/core/logger'; import { SingleFileParams } from '../controller/dto'; import { FileRecord } from '../entity'; import { FileStorageAuthorizationContext } from '../files-storage.const'; @@ -22,9 +22,9 @@ import { FilesStorageUC } from './files-storage.uc'; const buildFileRecordWithParams = () => { const userId = new ObjectId().toHexString(); - const schoolId = new ObjectId().toHexString(); + const storageLocationId = new ObjectId().toHexString(); - const fileRecord = fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text.txt' }); + const fileRecord = fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text.txt' }); const params: SingleFileParams = { fileRecordId: fileRecord.id, diff --git a/apps/server/src/modules/files-storage/uc/files-storage-download.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-download.uc.spec.ts index cc81964b8f7..7f7795d8c90 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-download.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-download.uc.spec.ts @@ -8,8 +8,8 @@ import { HttpService } from '@nestjs/axios'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { fileRecordFactory, setupEntities } from '@shared/testing'; -import { LegacyLogger } from '@src/core/logger'; import { DomainErrorHandler } from '@src/core'; +import { LegacyLogger } from '@src/core/logger'; import { SingleFileParams } from '../controller/dto'; import { FileRecord } from '../entity'; import { FileStorageAuthorizationContext } from '../files-storage.const'; @@ -21,9 +21,9 @@ import { FilesStorageUC } from './files-storage.uc'; const buildFileRecordWithParams = () => { const userId = new ObjectId().toHexString(); - const schoolId = new ObjectId().toHexString(); + const storageLocationId = new ObjectId().toHexString(); - const fileRecord = fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text.txt' }); + const fileRecord = fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text.txt' }); const params: SingleFileParams = { fileRecordId: fileRecord.id, diff --git a/apps/server/src/modules/files-storage/uc/files-storage-get.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-get.uc.spec.ts index 78ec33cd4c5..a650d21f897 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-get.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-get.uc.spec.ts @@ -7,10 +7,10 @@ import { AuthorizationReferenceService } from '@modules/authorization/domain'; import { HttpService } from '@nestjs/axios'; import { Test, TestingModule } from '@nestjs/testing'; import { fileRecordFactory, setupEntities } from '@shared/testing'; -import { LegacyLogger } from '@src/core/logger'; import { DomainErrorHandler } from '@src/core'; +import { LegacyLogger } from '@src/core/logger'; import { FileRecordParams } from '../controller/dto'; -import { FileRecord, FileRecordParentType } from '../entity'; +import { FileRecord, FileRecordParentType, StorageLocation } from '../entity'; import { FileStorageAuthorizationContext } from '../files-storage.const'; import { FilesStorageService } from '../service/files-storage.service'; import { PreviewService } from '../service/preview.service'; @@ -18,16 +18,17 @@ import { FilesStorageUC } from './files-storage.uc'; const buildFileRecordsWithParams = () => { const userId = new ObjectId().toHexString(); - const schoolId = new ObjectId().toHexString(); + const storageLocationId = new ObjectId().toHexString(); const fileRecords = [ - fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text.txt' }), - fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text-two.txt' }), - fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text-tree.txt' }), + fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text.txt' }), + fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text-two.txt' }), + fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text-tree.txt' }), ]; const params: FileRecordParams = { - schoolId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId, parentId: userId, parentType: FileRecordParentType.User, }; diff --git a/apps/server/src/modules/files-storage/uc/files-storage-restore.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-restore.uc.spec.ts index f8a375b3180..6a0d50c0772 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-restore.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-restore.uc.spec.ts @@ -8,10 +8,10 @@ import { HttpService } from '@nestjs/axios'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { fileRecordFactory, setupEntities } from '@shared/testing'; -import { LegacyLogger } from '@src/core/logger'; import { DomainErrorHandler } from '@src/core'; +import { LegacyLogger } from '@src/core/logger'; import { FileRecordParams, SingleFileParams } from '../controller/dto'; -import { FileRecord, FileRecordParentType } from '../entity'; +import { FileRecord, FileRecordParentType, StorageLocation } from '../entity'; import { FileStorageAuthorizationContext } from '../files-storage.const'; import { FilesStorageMapper } from '../mapper'; import { FilesStorageService } from '../service/files-storage.service'; @@ -20,16 +20,17 @@ import { FilesStorageUC } from './files-storage.uc'; const buildFileRecordsWithParams = () => { const userId = new ObjectId().toHexString(); - const schoolId = new ObjectId().toHexString(); + const storageLocationId = new ObjectId().toHexString(); const fileRecords = [ - fileRecordFactory.markedForDelete().buildWithId({ parentId: userId, schoolId, name: 'text.txt' }), - fileRecordFactory.markedForDelete().buildWithId({ parentId: userId, schoolId, name: 'text-two.txt' }), - fileRecordFactory.markedForDelete().buildWithId({ parentId: userId, schoolId, name: 'text-tree.txt' }), + fileRecordFactory.markedForDelete().buildWithId({ parentId: userId, storageLocationId, name: 'text.txt' }), + fileRecordFactory.markedForDelete().buildWithId({ parentId: userId, storageLocationId, name: 'text-two.txt' }), + fileRecordFactory.markedForDelete().buildWithId({ parentId: userId, storageLocationId, name: 'text-tree.txt' }), ]; const params: FileRecordParams = { - schoolId, + storageLocation: StorageLocation.SCHOOL, + storageLocationId, parentId: userId, parentType: FileRecordParentType.User, }; @@ -39,9 +40,11 @@ const buildFileRecordsWithParams = () => { const buildFileRecordWithParams = () => { const userId = new ObjectId().toHexString(); - const schoolId = new ObjectId().toHexString(); + const storageLocationId = new ObjectId().toHexString(); - const fileRecord = fileRecordFactory.markedForDelete().buildWithId({ parentId: userId, schoolId, name: 'text.txt' }); + const fileRecord = fileRecordFactory + .markedForDelete() + .buildWithId({ parentId: userId, storageLocationId, name: 'text.txt' }); const params: SingleFileParams = { fileRecordId: fileRecord.id, diff --git a/apps/server/src/modules/files-storage/uc/files-storage-update.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-update.uc.spec.ts index 576715c2f2d..cdb579c3d2f 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-update.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-update.uc.spec.ts @@ -7,8 +7,8 @@ import { AuthorizationReferenceService } from '@modules/authorization/domain'; import { HttpService } from '@nestjs/axios'; import { Test, TestingModule } from '@nestjs/testing'; import { fileRecordFactory, setupEntities } from '@shared/testing'; -import { LegacyLogger } from '@src/core/logger'; import { DomainErrorHandler } from '@src/core'; +import { LegacyLogger } from '@src/core/logger'; import { RenameFileParams, ScanResultParams, SingleFileParams } from '../controller/dto'; import { FileRecord } from '../entity'; import { FileStorageAuthorizationContext } from '../files-storage.const'; @@ -18,9 +18,9 @@ import { FilesStorageUC } from './files-storage.uc'; const buildFileRecordWithParams = () => { const userId = new ObjectId().toHexString(); - const schoolId = new ObjectId().toHexString(); + const storageLocationId = new ObjectId().toHexString(); - const fileRecord = fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text.txt' }); + const fileRecord = fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text.txt' }); const params: SingleFileParams = { fileRecordId: fileRecord.id, diff --git a/apps/server/src/modules/files-storage/uc/files-storage-upload.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-upload.uc.spec.ts index 03eb4ab2188..4c833930b61 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-upload.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-upload.uc.spec.ts @@ -3,21 +3,25 @@ import { AntivirusService } from '@infra/antivirus'; import { S3ClientAdapter } from '@infra/s3-client'; import { EntityManager } from '@mikro-orm/core'; import { ObjectId } from '@mikro-orm/mongodb'; -import { Action } from '@modules/authorization'; -import { AuthorizationReferenceService } from '@modules/authorization/domain'; +import { + Action, + AuthorizableReferenceType, + AuthorizationContextBuilder, + AuthorizationReferenceService, +} from '@modules/authorization/domain'; import { HttpService } from '@nestjs/axios'; import { ForbiddenException, NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain/interface'; import { AxiosHeadersKeyValue, axiosResponseFactory, fileRecordFactory, setupEntities } from '@shared/testing'; +import { DomainErrorHandler } from '@src/core'; import { LegacyLogger } from '@src/core/logger'; import { AxiosRequestConfig, AxiosResponse } from 'axios'; import { Request } from 'express'; import { of } from 'rxjs'; import { Readable } from 'stream'; -import { DomainErrorHandler } from '@src/core'; import { FileRecordParams } from '../controller/dto'; -import { FileRecord, FileRecordParentType } from '../entity'; +import { FileRecord, FileRecordParentType, StorageLocation } from '../entity'; import { ErrorType } from '../error'; import { FileStorageAuthorizationContext } from '../files-storage.const'; import { FileDtoBuilder, FilesStorageMapper } from '../mapper'; @@ -39,18 +43,19 @@ const createAxiosErrorResponse = (): AxiosResponse => { return errorResponse; }; -const buildFileRecordsWithParams = () => { +const buildFileRecordsWithParams = (storageLocation: StorageLocation = StorageLocation.SCHOOL) => { const userId = new ObjectId().toHexString(); - const schoolId = new ObjectId().toHexString(); + const storageLocationId = new ObjectId().toHexString(); const fileRecords = [ - fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text.txt' }), - fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text-two.txt' }), - fileRecordFactory.buildWithId({ parentId: userId, schoolId, name: 'text-tree.txt' }), + fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text.txt' }), + fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text-two.txt' }), + fileRecordFactory.buildWithId({ parentId: userId, storageLocationId, name: 'text-tree.txt' }), ]; const params: FileRecordParams = { - schoolId, + storageLocation, + storageLocationId, parentId: userId, parentType: FileRecordParentType.User, }; @@ -142,8 +147,8 @@ describe('FilesStorageUC upload methods', () => { }); describe('uploadFromUrl is called', () => { - const createUploadFromUrlParams = () => { - const { params, userId, fileRecords } = buildFileRecordsWithParams(); + const createUploadFromUrlParams = (storageLocation: StorageLocation = StorageLocation.SCHOOL) => { + const { params, userId, fileRecords } = buildFileRecordsWithParams(storageLocation); const fileRecord = fileRecords[0]; const uploadFromUrlParams = { @@ -190,6 +195,19 @@ describe('FilesStorageUC upload methods', () => { ); }); + it('should call authorizationService for storage location', async () => { + const { uploadFromUrlParams, userId } = setup(); + + await filesStorageUC.uploadFromUrl(userId, uploadFromUrlParams); + + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenCalledWith( + userId, + AuthorizableReferenceType.School, + uploadFromUrlParams.storageLocationId, + AuthorizationContextBuilder.write([]) + ); + }); + it('should call httpService get with correct params', async () => { const { uploadFromUrlParams, userId } = setup(); @@ -265,7 +283,7 @@ describe('FilesStorageUC upload methods', () => { describe('WHEN uploadFile throws error', () => { const setup = () => { - const { userId, uploadFromUrlParams, response } = createUploadFromUrlParams(); + const { userId, uploadFromUrlParams, response } = createUploadFromUrlParams(StorageLocation.SCHOOL); const error = new Error('test'); httpService.get.mockReturnValueOnce(of(response)); @@ -285,7 +303,7 @@ describe('FilesStorageUC upload methods', () => { describe('upload is called', () => { describe('WHEN user is authorized, busboy emits event and file is uploaded successfully', () => { const setup = () => { - const { params, userId, fileRecords } = buildFileRecordsWithParams(); + const { params, userId, fileRecords } = buildFileRecordsWithParams(StorageLocation.INSTANCE); const fileRecord = fileRecords[0]; const request = createRequest(); const readable = Readable.from('abc'); @@ -327,6 +345,19 @@ describe('FilesStorageUC upload methods', () => { ); }); + it('should call checkPermissionByReferences for storage location instance', async () => { + const { params, userId, request } = setup(); + + await filesStorageUC.upload(userId, params, request); + + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenCalledWith( + userId, + AuthorizableReferenceType.Instance, + params.storageLocationId, + AuthorizationContextBuilder.write([Permission.INSTANCE_VIEW]) + ); + }); + it('should call uploadFile with correct params', async () => { const { params, userId, request, readable, fileInfo } = setup(); const file = FileDtoBuilder.buildFromRequest(fileInfo, readable); diff --git a/apps/server/src/modules/files-storage/uc/files-storage.uc.ts b/apps/server/src/modules/files-storage/uc/files-storage.uc.ts index 90ca595e9f1..6c3ef984e59 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage.uc.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage.uc.ts @@ -1,16 +1,17 @@ import { EntityManager, RequestContext } from '@mikro-orm/core'; -import { AuthorizationContext } from '@modules/authorization'; +import { AuthorizableReferenceType, AuthorizationContext, AuthorizationContextBuilder } from '@modules/authorization'; import { AuthorizationReferenceService } from '@modules/authorization/domain'; import { HttpService } from '@nestjs/axios'; import { Injectable, NotFoundException } from '@nestjs/common'; +import { Permission } from '@shared/domain/interface'; import { Counted, EntityId } from '@shared/domain/types'; +import { DomainErrorHandler } from '@src/core'; import { LegacyLogger } from '@src/core/logger'; import { AxiosRequestConfig, AxiosResponse } from 'axios'; import busboy from 'busboy'; import { Request } from 'express'; import { firstValueFrom } from 'rxjs'; import internal from 'stream'; -import { DomainErrorHandler } from '@src/core'; import { CopyFileParams, CopyFileResponse, @@ -23,14 +24,13 @@ import { ScanResultParams, SingleFileParams, } from '../controller/dto'; -import { FileRecord, FileRecordParentType } from '../entity'; +import { FilesStorageConfigResponse } from '../dto/files-storage-config.response'; +import { FileRecord, FileRecordParentType, StorageLocation } from '../entity'; import { ErrorType } from '../error'; import { FileStorageAuthorizationContext } from '../files-storage.const'; import { GetFileResponse } from '../interface'; import { ConfigResponseMapper, FileDtoBuilder, FilesStorageMapper } from '../mapper'; -import { FilesStorageService } from '../service/files-storage.service'; -import { PreviewService } from '../service/preview.service'; -import { FilesStorageConfigResponse } from '../dto/files-storage-config.response'; +import { FilesStorageService, PreviewService } from '../service'; @Injectable() export class FilesStorageUC { @@ -52,8 +52,9 @@ export class FilesStorageUC { parentType: FileRecordParentType, parentId: EntityId, context: AuthorizationContext - ) { - const allowedType = FilesStorageMapper.mapToAllowedAuthorizationEntityType(parentType); + ): Promise { + const allowedType: AuthorizableReferenceType = FilesStorageMapper.mapToAllowedAuthorizationEntityType(parentType); + await this.authorizationReferenceService.checkPermissionByReferences(userId, allowedType, parentId, context); } @@ -69,11 +70,37 @@ export class FilesStorageUC { public async upload(userId: EntityId, params: FileRecordParams, req: Request): Promise { await this.checkPermission(userId, params.parentType, params.parentId, FileStorageAuthorizationContext.create); + await this.checkStorageLocation(userId, params.storageLocation, params.storageLocationId); + const fileRecord = await this.uploadFileWithBusboy(userId, params, req); return fileRecord; } + private async checkStorageLocation( + userId: EntityId, + storageLocation: StorageLocation, + storageLocationId: EntityId + ): Promise { + if (storageLocation === StorageLocation.INSTANCE) { + await this.authorizationReferenceService.checkPermissionByReferences( + userId, + AuthorizableReferenceType.Instance, + storageLocationId, + AuthorizationContextBuilder.write([Permission.INSTANCE_VIEW]) + ); + } + + if (storageLocation === StorageLocation.SCHOOL) { + await this.authorizationReferenceService.checkPermissionByReferences( + userId, + AuthorizableReferenceType.School, + storageLocationId, + AuthorizationContextBuilder.write([]) + ); + } + } + private async uploadFileWithBusboy(userId: EntityId, params: FileRecordParams, req: Request): Promise { const promise = new Promise((resolve, reject) => { const bb = busboy({ headers: req.headers, defParamCharset: 'utf8' }); @@ -107,6 +134,8 @@ export class FilesStorageUC { public async uploadFromUrl(userId: EntityId, params: FileRecordParams & FileUrlParams) { await this.checkPermission(userId, params.parentType, params.parentId, FileStorageAuthorizationContext.create); + await this.checkStorageLocation(userId, params.storageLocation, params.storageLocationId); + const response = await this.getResponse(params); const fileDto = FileDtoBuilder.buildFromAxiosResponse(params.fileName, response); diff --git a/apps/server/src/modules/instance/domain/index.ts b/apps/server/src/modules/instance/domain/index.ts new file mode 100644 index 00000000000..17dfb3c69e7 --- /dev/null +++ b/apps/server/src/modules/instance/domain/index.ts @@ -0,0 +1 @@ +export { Instance, InstanceProps } from './instance'; diff --git a/apps/server/src/modules/instance/domain/instance.ts b/apps/server/src/modules/instance/domain/instance.ts new file mode 100644 index 00000000000..b7338812388 --- /dev/null +++ b/apps/server/src/modules/instance/domain/instance.ts @@ -0,0 +1,11 @@ +import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; + +export interface InstanceProps extends AuthorizableObject { + name: string; +} + +export class Instance extends DomainObject { + public get name(): string { + return this.props.name; + } +} diff --git a/apps/server/src/modules/instance/entity/index.ts b/apps/server/src/modules/instance/entity/index.ts new file mode 100644 index 00000000000..8d23d733145 --- /dev/null +++ b/apps/server/src/modules/instance/entity/index.ts @@ -0,0 +1 @@ +export { InstanceEntity, InstanceEntityProps } from './instance.entity'; diff --git a/apps/server/src/modules/instance/entity/instance.entity.ts b/apps/server/src/modules/instance/entity/instance.entity.ts new file mode 100644 index 00000000000..93e1c1d93cc --- /dev/null +++ b/apps/server/src/modules/instance/entity/instance.entity.ts @@ -0,0 +1,23 @@ +import { Entity, Property } from '@mikro-orm/core'; +import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; // directly imported because of circular dependencies through ALL_ENTITIES +import { EntityId } from '@shared/domain/types'; + +export interface InstanceEntityProps { + id?: EntityId; + + name: string; +} + +@Entity({ tableName: 'instances' }) +export class InstanceEntity extends BaseEntityWithTimestamps { + @Property() + name: string; + + constructor(props: InstanceEntityProps) { + super(); + if (props.id) { + this.id = props.id; + } + this.name = props.name; + } +} diff --git a/apps/server/src/modules/instance/index.ts b/apps/server/src/modules/instance/index.ts new file mode 100644 index 00000000000..0f103700034 --- /dev/null +++ b/apps/server/src/modules/instance/index.ts @@ -0,0 +1,4 @@ +export { InstanceEntity } from './entity'; +export { Instance, InstanceProps } from './domain'; +export { InstanceService } from './service'; +export { InstanceModule } from './instance.module'; diff --git a/apps/server/src/modules/instance/instance.module.ts b/apps/server/src/modules/instance/instance.module.ts new file mode 100644 index 00000000000..f6bd69990a7 --- /dev/null +++ b/apps/server/src/modules/instance/instance.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { InstanceRepo } from './repo'; +import { InstanceService } from './service'; + +@Module({ + providers: [InstanceRepo, InstanceService], + exports: [InstanceService], +}) +export class InstanceModule {} diff --git a/apps/server/src/modules/instance/repo/index.ts b/apps/server/src/modules/instance/repo/index.ts new file mode 100644 index 00000000000..7895e173549 --- /dev/null +++ b/apps/server/src/modules/instance/repo/index.ts @@ -0,0 +1 @@ +export { InstanceRepo } from './instance-repo.service'; diff --git a/apps/server/src/modules/instance/repo/instance-repo.service.ts b/apps/server/src/modules/instance/repo/instance-repo.service.ts new file mode 100644 index 00000000000..d6e6d4b1854 --- /dev/null +++ b/apps/server/src/modules/instance/repo/instance-repo.service.ts @@ -0,0 +1,38 @@ +import { EntityData, EntityName } from '@mikro-orm/core'; +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain/types'; +import { BaseDomainObjectRepo } from '@shared/repo/base-domain-object.repo'; +import { Instance, InstanceProps } from '../domain'; +import { InstanceEntity } from '../entity'; + +@Injectable() +export class InstanceRepo extends BaseDomainObjectRepo { + protected override get entityName(): EntityName { + return InstanceEntity; + } + + protected override mapDOToEntityProperties(entityDO: Instance): EntityData { + const entityProps: EntityData = { + name: entityDO.name, + }; + + return entityProps; + } + + protected mapEntityToDoProperties(entity: InstanceEntity): InstanceProps { + const doProps: InstanceProps = { + id: entity.id, + name: entity.name, + }; + + return doProps; + } + + public async findById(id: EntityId): Promise { + const entity: InstanceEntity = await super.findEntityById(id); + + const course: Instance = new Instance(this.mapEntityToDoProperties(entity)); + + return course; + } +} diff --git a/apps/server/src/modules/instance/repo/instance.repo.spec.ts b/apps/server/src/modules/instance/repo/instance.repo.spec.ts new file mode 100644 index 00000000000..2ab73513c82 --- /dev/null +++ b/apps/server/src/modules/instance/repo/instance.repo.spec.ts @@ -0,0 +1,119 @@ +import { MongoMemoryDatabaseModule } from '@infra/database'; +import { EntityManager } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; +import { InstanceProps } from '../domain'; +import { InstanceEntity } from '../entity'; +import { instanceEntityFactory, instanceFactory } from '../testing'; +import { InstanceRepo } from './instance-repo.service'; + +describe(InstanceRepo.name, () => { + let module: TestingModule; + let em: EntityManager; + let repo: InstanceRepo; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [MongoMemoryDatabaseModule.forRoot()], + providers: [InstanceRepo], + }).compile(); + + repo = module.get(InstanceRepo); + em = module.get(EntityManager); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('findById', () => { + describe('when an entity with the id exists', () => { + const setup = async () => { + const instanceEntity = instanceEntityFactory.buildWithId(); + + await em.persistAndFlush(instanceEntity); + em.clear(); + + return { + instanceEntity, + }; + }; + + it('should return the instance', async () => { + const { instanceEntity } = await setup(); + + const result = await repo.findById(instanceEntity.id); + + expect(result.getProps()).toEqual({ + id: instanceEntity.id, + name: instanceEntity.name, + }); + }); + }); + }); + + describe('save', () => { + describe('when creating a new entity', () => { + const setup = () => { + const instance = instanceFactory.build(); + + return { + instance, + }; + }; + + it('should create a new entity', async () => { + const { instance } = setup(); + + await repo.save(instance); + + await expect(em.findOneOrFail(InstanceEntity, instance.id)).resolves.toBeDefined(); + }); + + it('should return the saved object', async () => { + const { instance } = setup(); + + const result = await repo.save(instance); + + expect(result).toEqual(instance); + }); + }); + + describe('when the entity exists', () => { + const setup = async () => { + const instanceEntity = instanceEntityFactory.buildWithId(); + + await em.persistAndFlush(instanceEntity); + em.clear(); + + const instance = instanceFactory.build({ id: instanceEntity.id, name: 'not_nbc' }); + + return { + instance, + instanceEntity, + }; + }; + + it('should update the entity', async () => { + const { instance, instanceEntity } = await setup(); + + await repo.save(instance); + + await expect(em.findOneOrFail(InstanceEntity, instanceEntity.id)).resolves.toEqual( + expect.objectContaining({ name: 'not_nbc' }) + ); + }); + + it('should return the object', async () => { + const { instance } = await setup(); + + const result = await repo.save(instance); + + expect(result).toEqual(instance); + }); + }); + }); +}); diff --git a/apps/server/src/modules/instance/service/index.ts b/apps/server/src/modules/instance/service/index.ts new file mode 100644 index 00000000000..32e148f530d --- /dev/null +++ b/apps/server/src/modules/instance/service/index.ts @@ -0,0 +1 @@ +export { InstanceService } from './instance.service'; diff --git a/apps/server/src/modules/instance/service/instance.service.spec.ts b/apps/server/src/modules/instance/service/instance.service.spec.ts new file mode 100644 index 00000000000..4727cb90394 --- /dev/null +++ b/apps/server/src/modules/instance/service/instance.service.spec.ts @@ -0,0 +1,57 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { InstanceRepo } from '../repo'; +import { instanceFactory } from '../testing'; +import { InstanceService } from './instance.service'; + +describe(InstanceService.name, () => { + let module: TestingModule; + let service: InstanceService; + + let instanceRepo: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + InstanceService, + { + provide: InstanceRepo, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(InstanceService); + instanceRepo = module.get(InstanceRepo); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('findById', () => { + describe('when a instance with the id exists', () => { + const setup = () => { + const instance = instanceFactory.build(); + + instanceRepo.findById.mockResolvedValue(instance); + + return { + instance, + }; + }; + + it('should return the instance', async () => { + const { instance } = setup(); + + const result = await service.findById(instance.id); + + expect(result).toEqual(instance); + }); + }); + }); +}); diff --git a/apps/server/src/modules/instance/service/instance.service.ts b/apps/server/src/modules/instance/service/instance.service.ts new file mode 100644 index 00000000000..07e17db3548 --- /dev/null +++ b/apps/server/src/modules/instance/service/instance.service.ts @@ -0,0 +1,16 @@ +import { AuthorizationLoaderServiceGeneric } from '@modules/authorization'; +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain/types'; +import { Instance } from '../domain'; +import { InstanceRepo } from '../repo'; + +@Injectable() +export class InstanceService implements AuthorizationLoaderServiceGeneric { + constructor(private readonly instanceRepo: InstanceRepo) {} + + public async findById(id: EntityId): Promise { + const instance: Instance = await this.instanceRepo.findById(id); + + return instance; + } +} diff --git a/apps/server/src/modules/instance/testing/index.ts b/apps/server/src/modules/instance/testing/index.ts new file mode 100644 index 00000000000..6fc31835fef --- /dev/null +++ b/apps/server/src/modules/instance/testing/index.ts @@ -0,0 +1,2 @@ +export { instanceFactory } from './instance.factory'; +export { instanceEntityFactory } from './instance-entity.factory'; diff --git a/apps/server/src/modules/instance/testing/instance-entity.factory.ts b/apps/server/src/modules/instance/testing/instance-entity.factory.ts new file mode 100644 index 00000000000..5e28b2e1b48 --- /dev/null +++ b/apps/server/src/modules/instance/testing/instance-entity.factory.ts @@ -0,0 +1,10 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseFactory } from '@shared/testing'; +import { InstanceEntity, InstanceEntityProps } from '../entity'; + +export const instanceEntityFactory = BaseFactory.define(InstanceEntity, () => { + return { + id: new ObjectId().toHexString(), + name: 'dbc', + }; +}); diff --git a/apps/server/src/modules/instance/testing/instance.factory.ts b/apps/server/src/modules/instance/testing/instance.factory.ts new file mode 100644 index 00000000000..b377ad78092 --- /dev/null +++ b/apps/server/src/modules/instance/testing/instance.factory.ts @@ -0,0 +1,10 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseFactory } from '@shared/testing'; +import { Instance, InstanceProps } from '../domain'; + +export const instanceFactory = BaseFactory.define(Instance, () => { + return { + id: new ObjectId().toHexString(), + name: 'dbc', + }; +}); diff --git a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts index 8b3a15a5c19..f4efdf13bfb 100644 --- a/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts +++ b/apps/server/src/modules/learnroom/repo/mikro-orm/course.repo.ts @@ -18,7 +18,7 @@ export class CourseMikroOrmRepo extends BaseDomainObjectRepo { - const entity: CourseEntity = await super.findById(id); + const entity: CourseEntity = await super.findEntityById(id); if (!entity.courseGroups.isInitialized()) { await entity.courseGroups.init(); diff --git a/apps/server/src/modules/tool/external-tool/external-tool.module.ts b/apps/server/src/modules/tool/external-tool/external-tool.module.ts index b329dbc2af4..c7273f73f0e 100644 --- a/apps/server/src/modules/tool/external-tool/external-tool.module.ts +++ b/apps/server/src/modules/tool/external-tool/external-tool.module.ts @@ -10,6 +10,7 @@ import { ToolConfigModule } from '../tool-config.module'; import { ExternalToolMetadataMapper } from './mapper'; import { DatasheetPdfService, + ExternalToolAuthorizableService, ExternalToolConfigurationService, ExternalToolLogoService, ExternalToolParameterValidationService, @@ -31,6 +32,7 @@ import { ExternalToolMetadataMapper, ToolContextMapper, DatasheetPdfService, + ExternalToolAuthorizableService, ], exports: [ ExternalToolService, @@ -38,6 +40,7 @@ import { ExternalToolConfigurationService, ExternalToolLogoService, DatasheetPdfService, + ExternalToolAuthorizableService, ], }) export class ExternalToolModule {} diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-authorizable.service.spec.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-authorizable.service.spec.ts new file mode 100644 index 00000000000..cb684f9caf4 --- /dev/null +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-authorizable.service.spec.ts @@ -0,0 +1,57 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ExternalToolRepo } from '@shared/repo'; +import { externalToolFactory } from '../testing'; +import { ExternalToolAuthorizableService } from './external-tool-authorizable.service'; + +describe(ExternalToolAuthorizableService.name, () => { + let module: TestingModule; + let service: ExternalToolAuthorizableService; + + let externalToolRepo: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + ExternalToolAuthorizableService, + { + provide: ExternalToolRepo, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(ExternalToolAuthorizableService); + externalToolRepo = module.get(ExternalToolRepo); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('findById', () => { + describe('when there is an external tool', () => { + const setup = () => { + const externalTool = externalToolFactory.build(); + + externalToolRepo.findById.mockResolvedValueOnce(externalTool); + + return { + externalTool, + }; + }; + + it('should return the external tool', async () => { + const { externalTool } = setup(); + + const result = await service.findById(externalTool.id); + + expect(result).toEqual(externalTool); + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-authorizable.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-authorizable.service.ts new file mode 100644 index 00000000000..9d5c89b51ab --- /dev/null +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-authorizable.service.ts @@ -0,0 +1,16 @@ +import { AuthorizationLoaderService } from '@modules/authorization'; +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain/types'; +import { ExternalToolRepo } from '@shared/repo'; +import { ExternalTool } from '../domain'; + +@Injectable() +export class ExternalToolAuthorizableService implements AuthorizationLoaderService { + constructor(private readonly externalToolRepo: ExternalToolRepo) {} + + async findById(id: EntityId): Promise { + const externalTool: ExternalTool = await this.externalToolRepo.findById(id); + + return externalTool; + } +} diff --git a/apps/server/src/modules/tool/external-tool/service/index.ts b/apps/server/src/modules/tool/external-tool/service/index.ts index 42d2c89786c..ddf55a9bea5 100644 --- a/apps/server/src/modules/tool/external-tool/service/index.ts +++ b/apps/server/src/modules/tool/external-tool/service/index.ts @@ -5,3 +5,4 @@ export * from './external-tool-parameter-validation.service'; export * from './external-tool-configuration.service'; export * from './external-tool-logo.service'; export { DatasheetPdfService } from './datasheet-pdf.service'; +export { ExternalToolAuthorizableService } from './external-tool-authorizable.service'; diff --git a/apps/server/src/modules/tool/index.ts b/apps/server/src/modules/tool/index.ts index 808813349b9..f6ba7329778 100644 --- a/apps/server/src/modules/tool/index.ts +++ b/apps/server/src/modules/tool/index.ts @@ -3,3 +3,4 @@ export * from './context-external-tool/service/context-external-tool-authorizabl export * from './external-tool'; export * from './tool.module'; export { default as ToolConfiguration, IToolFeatures } from './tool-config'; +export { ExternalToolAuthorizableService } from './external-tool/service/external-tool-authorizable.service'; diff --git a/apps/server/src/shared/domain/entity/all-entities.ts b/apps/server/src/shared/domain/entity/all-entities.ts index ef667da7c99..00e7784ccde 100644 --- a/apps/server/src/shared/domain/entity/all-entities.ts +++ b/apps/server/src/shared/domain/entity/all-entities.ts @@ -1,6 +1,6 @@ -import { AccountEntity } from '@src/modules/account/domain/entity/account.entity'; import { ClassEntity } from '@modules/class/entity'; import { GroupEntity } from '@modules/group/entity'; +import { InstanceEntity } from '@modules/instance'; import { SchoolSystemOptionsEntity } from '@modules/legacy-school/entity'; import { ExternalToolPseudonymEntity, PseudonymEntity } from '@modules/pseudonym/entity'; import { RegistrationPinEntity } from '@modules/registration-pin/entity'; @@ -10,6 +10,7 @@ import { ContextExternalToolEntity } from '@modules/tool/context-external-tool/e import { ExternalToolEntity } from '@modules/tool/external-tool/entity'; import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; import { MediaUserLicenseEntity, UserLicenseEntity } from '@modules/user-license/entity'; +import { AccountEntity } from '@src/modules/account/domain/entity/account.entity'; import { DeletionLogEntity } from '@src/modules/deletion/repo/entity/deletion-log.entity'; import { DeletionRequestEntity } from '@src/modules/deletion/repo/entity/deletion-request.entity'; import { RocketChatUserEntity } from '@src/modules/rocketchat-user/entity'; @@ -124,4 +125,5 @@ export const ALL_ENTITIES = [ TldrawDrawing, UserLicenseEntity, MediaUserLicenseEntity, + InstanceEntity, ]; diff --git a/apps/server/src/shared/domain/interface/permission.enum.ts b/apps/server/src/shared/domain/interface/permission.enum.ts index f971a1b05ea..2159ec1b820 100644 --- a/apps/server/src/shared/domain/interface/permission.enum.ts +++ b/apps/server/src/shared/domain/interface/permission.enum.ts @@ -68,6 +68,7 @@ export enum Permission { IMPORT_USER_MIGRATE = 'IMPORT_USER_MIGRATE', IMPORT_USER_UPDATE = 'IMPORT_USER_UPDATE', IMPORT_USER_VIEW = 'IMPORT_USER_VIEW', + INSTANCE_VIEW = 'INSTANCE_VIEW', INVITE_ADMINISTRATORS = 'INVITE_ADMINISTRATORS', INVITE_EXPERTS = 'INVITE_EXPERTS', JOIN_MEETING = 'JOIN_MEETING', diff --git a/apps/server/src/shared/repo/base-domain-object.repo.integration.spec.ts b/apps/server/src/shared/repo/base-domain-object.repo.integration.spec.ts index 17d8f51cfa1..56cb3ac905b 100644 --- a/apps/server/src/shared/repo/base-domain-object.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/base-domain-object.repo.integration.spec.ts @@ -350,7 +350,7 @@ describe('BaseDomainObjectRepo', () => { it('should find an entity by id', async () => { const { entity } = await setup(); - const foundEntity = await repo.findById(entity.id); + const foundEntity = await repo.findEntityById(entity.id); expect(foundEntity).toBeInstanceOf(TestEntity); expect(foundEntity.id).toEqual(entity.id); diff --git a/apps/server/src/shared/repo/base-domain-object.repo.ts b/apps/server/src/shared/repo/base-domain-object.repo.ts index 5fcdea0688a..75074a02534 100644 --- a/apps/server/src/shared/repo/base-domain-object.repo.ts +++ b/apps/server/src/shared/repo/base-domain-object.repo.ts @@ -2,7 +2,7 @@ import { EntityData, EntityName, FilterQuery, Primary, RequiredEntityData, Utils import { EntityManager } from '@mikro-orm/mongodb'; import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; -import { BaseEntity, baseEntityProperties } from '@shared/domain/entity'; +import { BaseEntity, baseEntityProperties } from '@shared/domain/entity/base.entity'; import { EntityId } from '@shared/domain/types'; import { BaseDomainObjectRepoInterface } from './base-domain-object.repo.interface'; @@ -57,7 +57,7 @@ export abstract class BaseDomainObjectRepo { + public async findEntityById(id: EntityId): Promise { const entity: E = await this.em.findOneOrFail(this.entityName, { id } as FilterQuery); return entity; diff --git a/apps/server/src/shared/testing/factory/filerecord.factory.ts b/apps/server/src/shared/testing/factory/filerecord.factory.ts index e4b316ec493..e84214c22bc 100644 --- a/apps/server/src/shared/testing/factory/filerecord.factory.ts +++ b/apps/server/src/shared/testing/factory/filerecord.factory.ts @@ -1,6 +1,11 @@ import { FileRecordParentType } from '@infra/rabbitmq'; -import { FileRecord, FileRecordProperties, FileRecordSecurityCheck } from '@modules/files-storage/entity'; import { ObjectId } from '@mikro-orm/mongodb'; +import { + FileRecord, + FileRecordProperties, + FileRecordSecurityCheck, + StorageLocation, +} from '@modules/files-storage/entity'; import { DeepPartial } from 'fishery'; import { BaseFactory } from './base.factory'; @@ -22,6 +27,7 @@ export const fileRecordFactory = FileRecordFactory.define(FileRecord, ({ sequenc parentType: FileRecordParentType.Course, parentId: new ObjectId().toHexString(), creatorId: new ObjectId().toHexString(), - schoolId: new ObjectId().toHexString(), + storageLocationId: new ObjectId().toHexString(), + storageLocation: StorageLocation.SCHOOL, }; }); diff --git a/apps/server/src/shared/testing/user-role-permissions.ts b/apps/server/src/shared/testing/user-role-permissions.ts index 47ab40c5741..ef368925d93 100644 --- a/apps/server/src/shared/testing/user-role-permissions.ts +++ b/apps/server/src/shared/testing/user-role-permissions.ts @@ -145,4 +145,4 @@ export const adminPermissions = [ Permission.USER_LOGIN_MIGRATION_ADMIN, ]; -export const superheroPermissions = [Permission.USER_LOGIN_MIGRATION_ROLLBACK]; +export const superheroPermissions = [Permission.USER_LOGIN_MIGRATION_ROLLBACK, Permission.INSTANCE_VIEW]; diff --git a/backup/setup/instances.json b/backup/setup/instances.json new file mode 100644 index 00000000000..00691641de8 --- /dev/null +++ b/backup/setup/instances.json @@ -0,0 +1,26 @@ +[ + { + "_id": { + "$oid": "666076ad83d1e69b5c692efd" + }, + "name": "nbc" + }, + { + "_id": { + "$oid": "666076b192e0f29a612935d1" + }, + "name": "thr" + }, + { + "_id": { + "$oid": "666076b4ccb83d0360de2802" + }, + "name": "brb" + }, + { + "_id": { + "$oid": "666076b859c84c275ec840c2" + }, + "name": "dbc" + } +] diff --git a/backup/setup/migrations.json b/backup/setup/migrations.json index 1e30d27791b..e3afad7d409 100644 --- a/backup/setup/migrations.json +++ b/backup/setup/migrations.json @@ -64,20 +64,20 @@ }, { "_id": { - "$oid": "6622341440e305c6e36dcd81" + "$oid": "66222c3267551d7ebd81c096" }, - "name": "Migration20240419075957", + "name": "Migration20240415124640", "created_at": { - "$date": "2024-04-19T09:06:28.592Z" + "$date": "2024-04-19T08:32:50.668Z" } }, { "_id": { - "$oid": "66222c3267551d7ebd81c096" + "$oid": "6622341440e305c6e36dcd81" }, - "name": "Migration20240415124640", + "name": "Migration20240419075957", "created_at": { - "$date": "2024-04-19T08:32:50.668Z" + "$date": "2024-04-19T09:06:28.592Z" } }, { @@ -115,5 +115,32 @@ "created_at": { "$date": "2024-05-29T09:25:23.454Z" } + }, + { + "_id": { + "$oid": "66684c3db14698848e23c0c2" + }, + "name": "Migration20240604131554", + "created_at": { + "$date": "2024-06-11T13:08:13.024Z" + } + }, + { + "_id": { + "$oid": "66684c3db14698848e23c0c3" + }, + "name": "Migration20240605065231", + "created_at": { + "$date": "2024-06-11T13:08:13.042Z" + } + }, + { + "_id": { + "$oid": "66684c3db14698848e23c0c4" + }, + "name": "Migration20240606142059", + "created_at": { + "$date": "2024-06-11T13:08:13.069Z" + } } ] diff --git a/backup/setup/roles.json b/backup/setup/roles.json index 9a3db6e128b..0dc34c41f22 100644 --- a/backup/setup/roles.json +++ b/backup/setup/roles.json @@ -200,7 +200,8 @@ "USER_CHANGE_OWN_NAME", "ACCOUNT_VIEW", "ACCOUNT_DELETE", - "USER_LOGIN_MIGRATION_ROLLBACK" + "USER_LOGIN_MIGRATION_ROLLBACK", + "INSTANCE_VIEW" ], "__v": 2 },