From dc47095ffb7ffa3139476fab48a409637c92ded4 Mon Sep 17 00:00:00 2001 From: sszafGCA <116172610+sszafGCA@users.noreply.github.com> Date: Thu, 30 Nov 2023 12:23:04 +0100 Subject: [PATCH] BC-5522-Implementation of an API for deleting data (#4533) * endpoints preparation * auth startegy impl * api impl * some changes * some fixes need in KNL module * changes in uc and register entities * Pr fixes and minor changes * rename file * some fixes * add exposing admin API port from the api-svc * add server test module + delete FileEntity from allEntities * change module * admin api server module test impl * add some tests * add some test for API * add some test * x-api-key.strategy tests * x-api-key-strategy-tests impl * remove test for api-setup-helper, remove unused imports * remove test file * add info for the Admin API port * add test for controller * add test for deletionRequestResponse * remove not needed parameter from constructor * add test for deletionRequestLogResponse * add test for executionParams and requestbodyParams * replace hard-coded Admin API server port value with the one taken from the var * change creation of API method location * change default json * fix import in server module * fixes soma issues * fixes after review * add admin api object * default json chnages * Revert "default json chnages" This reverts commit 6764f956c22cb1ae92b6cbdfd813729c0a28243b. * Revert "add admin api object" This reverts commit aba6e1aad058c834bf905ee874bc738e9b10858c. * Revert "fixes after review" This reverts commit 4fc4c70b0750f258d805b0d1a7d6f542d251a326. * Revert "fixes soma issues" This reverts commit 3b58d132f77e9f05a5fafeeece1c7cc5535e80f7. * fixes after review * Revert "small fixes" This reverts commit ccf0aef7579c3903dee99302b45e244ffc046018, reversing changes made to 011b0f787b70cab2362ec7e75cdebdf20bac1348. * Revert "Revert "small fixes"" This reverts commit 30ab3f4eeb6a44fe6263612b651b04a27b817dc7. * default ADMIN_API_KEY * fix some imports * small change in x-api-key.strategy * Update apps/server/src/modules/deletion/uc/builder/deletion-log-statistic.builder.spec.ts Co-authored-by: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> * change default.schema.json * new changes to default.schema.json * fix after review * add logger for error in deeltionRequestUC * fix problem with fileentity * fix imports * add logger to test in uc of deletionModule * change module config * hard-coded Admin API server port after the discussion with Paul * fixes bug during deletion uders data from lessons * fix pipeline * fix pipeline #2 * fix pipeline * fix lint * fix modules imports * fix providers in exports in deletion module * split deletion module and deletion-api.module * move setup sessions to server config * try to fix test coverage * Revert "try to fix test coverage" This reverts commit e3361965ad98c01f5343912086d14d5d88494b42. * Revert "move setup sessions to server config" This reverts commit 86b176dc7ee96386b1812a94d74bc92b106fea3f. * remove api key from default.json * changes to server module * use timers in tests * revert last commit * add testXApiKeyClient * deployment impl * add newlines * add test * change character length * fix imports * fix deployment in PR * PR fixes part 1 * fix build and push * fix prettier * default schema changes * small fixes * fix lint * add registration pins module * change sorting of code lines * small fixes after review * add enabled in admiApiServer * add process.exit * test revert enabled (testing purposes) * Revert "test revert enabled (testing purposes)" This reverts commit b2fa12f9f2ff1dfb20ac209a51db10e037b07999. * add default for testers * Revert "add default for testers" This reverts commit d58943538ddaf4c7f3acb1b73e3223d364cf09c4. * fix with elsson entity * fix lint * changes in main.yml * fix enabled --------- Co-authored-by: WojciechGrancow Co-authored-by: Bartosz Nowicki <116367402+bn-pass@users.noreply.github.com> Co-authored-by: WojciechGrancow <116577704+WojciechGrancow@users.noreply.github.com> Co-authored-by: Cedric Evers <12080057+CeEv@users.noreply.github.com> --- .../schulcloud-server-core/tasks/main.yml | 15 +++ .../admin-api-server-deployment.yml.j2 | 64 ++++++++++ .../templates/admin-api-server-svc.yml.j2 | 17 +++ apps/server/src/apps/admin-api-server.app.ts | 43 +++++++ .../authentication/authentication.module.ts | 2 + .../modules/authentication/config/index.ts | 1 + .../authentication/config/x-api-key.config.ts | 3 + .../src/modules/authentication/index.ts | 1 + .../strategy/x-api-key.strategy.spec.ts | 74 ++++++++++++ .../strategy/x-api-key.strategy.ts | 22 ++++ ...ch-deletion-summary-detail.builder.spec.ts | 4 +- .../batch-deletion-summary-detail.builder.ts | 2 +- .../batch-deletion-summary.builder.spec.ts | 0 .../builder/batch-deletion-summary.builder.ts | 0 .../deletion-log-statistic.builder.spec.ts | 4 +- .../builder/deletion-log-statistic.builder.ts | 2 +- ...eletion-request-body-props.builder.spec.ts | 27 +++++ .../deletion-request-body-props.builder.ts | 14 +++ ...tion-request-log-response.builder.spec.ts} | 10 +- .../deletion-request-log-response.builder.ts} | 7 +- .../deletion-target-ref.builder.spec.ts | 8 +- .../builder/deletion-target-ref.builder.ts | 11 ++ .../src/modules/deletion/builder/index.ts | 6 + .../api-test/deletion-executions.api.spec.ts | 47 ++++++++ .../deletion-request-create.api.spec.ts | 112 ++++++++++++++++++ .../deletion-request-delete.api.spec.ts | 72 +++++++++++ .../deletion-request-find.api.spec.ts | 72 +++++++++++ .../deletion-executions.controller.ts | 24 ++++ .../deletion-requests.controller.ts | 54 +++++++++ .../dto/deletion-execution.params.spec.ts | 44 +++++++ .../dto/deletion-execution.params.ts | 10 ++ .../dto/deletion-request-log.response.spec.ts | 32 +++++ .../dto/deletion-request-log.response.ts | 21 ++++ .../dto/deletion-request.body.params.spec.ts | 38 ++++++ .../dto/deletion-request.body.params.ts | 21 ++++ .../dto/deletion-request.response.spec.ts | 26 ++++ .../dto/deletion-request.response.ts | 14 +++ .../modules/deletion/controller/dto/index.ts | 4 + .../modules/deletion/deletion-api.module.ts | 47 ++++++++ .../src/modules/deletion/deletion.module.ts | 16 ++- .../deletion/domain/deletion-log.do.spec.ts | 5 +- .../deletion/domain/deletion-log.do.ts | 8 +- .../domain/deletion-request.do.spec.ts | 3 +- .../deletion/domain/deletion-request.do.ts | 3 +- .../testing/factory/deletion-log.factory.ts | 4 +- .../factory/deletion-request.factory.ts | 3 +- .../deletion/domain/testing/factory/index.ts | 2 + .../modules/deletion/domain/testing/index.ts | 1 + .../modules/deletion/domain/types/index.ts | 3 + .../entity/deletion-log.entity.spec.ts | 5 +- .../deletion/entity/deletion-log.entity.ts | 19 ++- .../entity/deletion-request.entity.spec.ts | 5 +- .../entity/deletion-request.entity.ts | 12 +- .../factory/deletion-log.entity.factory.ts | 3 +- .../deletion-request.entity.factory.ts | 3 +- .../deletion/entity/testing/factory/index.ts | 2 + .../modules/deletion/entity/testing/index.ts | 1 + ...batch-deletion-summary-detail.interface.ts | 2 +- ...ch-deletion-summary-overall-status.enum.ts | 0 .../batch-deletion-summary.interface.ts | 0 .../deletion/{uc => }/interface/index.ts | 0 .../modules/deletion/interface/interfaces.ts | 13 ++ .../deletion/repo/deletion-log.repo.spec.ts | 4 + .../deletion/repo/deletion-request-scope.ts | 2 +- .../repo/deletion-request.repo.spec.ts | 2 +- .../repo/mapper/deletion-log.mapper.spec.ts | 4 + .../repo/mapper/deletion-log.mapper.ts | 2 + .../services/deletion-log.service.spec.ts | 4 +- .../deletion/services/deletion-log.service.ts | 4 +- .../services/deletion-request.service.spec.ts | 3 +- .../services/deletion-request.service.ts | 3 +- .../deletion/uc/batch-deletion.uc.spec.ts | 4 +- .../modules/deletion/uc/batch-deletion.uc.ts | 4 +- .../uc/builder/deletion-target-ref.builder.ts | 11 -- .../src/modules/deletion/uc/builder/index.ts | 2 - .../deletion/uc/deletion-request.uc.spec.ts | 109 ++++++++--------- .../deletion/uc/deletion-request.uc.ts | 71 +++++++---- apps/server/src/modules/deletion/uc/index.ts | 3 +- .../deletion/uc/interface/interfaces.ts | 29 ----- apps/server/src/modules/files/files.module.ts | 4 +- apps/server/src/modules/files/index.ts | 1 + apps/server/src/modules/learnroom/index.ts | 8 +- .../src/modules/learnroom/learnroom.module.ts | 7 +- apps/server/src/modules/lesson/index.ts | 3 +- .../lesson.repo.integration.spec.ts | 6 +- .../modules/lesson/repository/lesson.repo.ts | 3 +- .../modules/lesson/service/lesson.service.ts | 2 +- .../modules/rocketchat-user/domain/index.ts | 1 + .../entity/rocket-chat-user.entity.ts | 2 +- .../rocketchat-user/rocketchat-user.module.ts | 5 +- .../modules/server/admin-api.server.module.ts | 53 +++++++++ .../src/modules/server/controller/index.ts | 1 + .../src/modules/server/server.config.ts | 7 +- .../src/shared/domain/entity/all-entities.ts | 5 + apps/server/src/shared/testing/index.ts | 1 + .../testing/test-xApiKey-client.spec.ts | 93 +++++++++++++++ .../src/shared/testing/test-xApiKey-client.ts | 69 +++++++++++ config/default.schema.json | 35 ++++-- nest-cli.json | 9 ++ package-lock.json | 19 +++ package.json | 4 + 101 files changed, 1469 insertions(+), 218 deletions(-) create mode 100644 ansible/roles/schulcloud-server-core/templates/admin-api-server-deployment.yml.j2 create mode 100644 ansible/roles/schulcloud-server-core/templates/admin-api-server-svc.yml.j2 create mode 100644 apps/server/src/apps/admin-api-server.app.ts create mode 100644 apps/server/src/modules/authentication/config/index.ts create mode 100644 apps/server/src/modules/authentication/config/x-api-key.config.ts create mode 100644 apps/server/src/modules/authentication/strategy/x-api-key.strategy.spec.ts create mode 100644 apps/server/src/modules/authentication/strategy/x-api-key.strategy.ts rename apps/server/src/modules/deletion/{uc => }/builder/batch-deletion-summary-detail.builder.spec.ts (96%) rename apps/server/src/modules/deletion/{uc => }/builder/batch-deletion-summary-detail.builder.ts (93%) rename apps/server/src/modules/deletion/{uc => }/builder/batch-deletion-summary.builder.spec.ts (100%) rename apps/server/src/modules/deletion/{uc => }/builder/batch-deletion-summary.builder.ts (100%) rename apps/server/src/modules/deletion/{uc => }/builder/deletion-log-statistic.builder.spec.ts (77%) rename apps/server/src/modules/deletion/{uc => }/builder/deletion-log-statistic.builder.ts (79%) create mode 100644 apps/server/src/modules/deletion/builder/deletion-request-body-props.builder.spec.ts create mode 100644 apps/server/src/modules/deletion/builder/deletion-request-body-props.builder.ts rename apps/server/src/modules/deletion/{uc/builder/deletion-request-log.builder.spec.ts => builder/deletion-request-log-response.builder.spec.ts} (60%) rename apps/server/src/modules/deletion/{uc/builder/deletion-request-log.builder.ts => builder/deletion-request-log-response.builder.ts} (50%) rename apps/server/src/modules/deletion/{uc => }/builder/deletion-target-ref.builder.spec.ts (58%) create mode 100644 apps/server/src/modules/deletion/builder/deletion-target-ref.builder.ts create mode 100644 apps/server/src/modules/deletion/builder/index.ts create mode 100644 apps/server/src/modules/deletion/controller/api-test/deletion-executions.api.spec.ts create mode 100644 apps/server/src/modules/deletion/controller/api-test/deletion-request-create.api.spec.ts create mode 100644 apps/server/src/modules/deletion/controller/api-test/deletion-request-delete.api.spec.ts create mode 100644 apps/server/src/modules/deletion/controller/api-test/deletion-request-find.api.spec.ts create mode 100644 apps/server/src/modules/deletion/controller/deletion-executions.controller.ts create mode 100644 apps/server/src/modules/deletion/controller/deletion-requests.controller.ts create mode 100644 apps/server/src/modules/deletion/controller/dto/deletion-execution.params.spec.ts create mode 100644 apps/server/src/modules/deletion/controller/dto/deletion-execution.params.ts create mode 100644 apps/server/src/modules/deletion/controller/dto/deletion-request-log.response.spec.ts create mode 100644 apps/server/src/modules/deletion/controller/dto/deletion-request-log.response.ts create mode 100644 apps/server/src/modules/deletion/controller/dto/deletion-request.body.params.spec.ts create mode 100644 apps/server/src/modules/deletion/controller/dto/deletion-request.body.params.ts create mode 100644 apps/server/src/modules/deletion/controller/dto/deletion-request.response.spec.ts create mode 100644 apps/server/src/modules/deletion/controller/dto/deletion-request.response.ts create mode 100644 apps/server/src/modules/deletion/controller/dto/index.ts create mode 100644 apps/server/src/modules/deletion/deletion-api.module.ts create mode 100644 apps/server/src/modules/deletion/domain/testing/factory/index.ts create mode 100644 apps/server/src/modules/deletion/domain/testing/index.ts create mode 100644 apps/server/src/modules/deletion/domain/types/index.ts create mode 100644 apps/server/src/modules/deletion/entity/testing/factory/index.ts create mode 100644 apps/server/src/modules/deletion/entity/testing/index.ts rename apps/server/src/modules/deletion/{uc => }/interface/batch-deletion-summary-detail.interface.ts (88%) rename apps/server/src/modules/deletion/{uc => }/interface/batch-deletion-summary-overall-status.enum.ts (100%) rename apps/server/src/modules/deletion/{uc => }/interface/batch-deletion-summary.interface.ts (100%) rename apps/server/src/modules/deletion/{uc => }/interface/index.ts (100%) create mode 100644 apps/server/src/modules/deletion/interface/interfaces.ts delete mode 100644 apps/server/src/modules/deletion/uc/builder/deletion-target-ref.builder.ts delete mode 100644 apps/server/src/modules/deletion/uc/builder/index.ts delete mode 100644 apps/server/src/modules/deletion/uc/interface/interfaces.ts create mode 100644 apps/server/src/modules/server/admin-api.server.module.ts create mode 100644 apps/server/src/modules/server/controller/index.ts create mode 100644 apps/server/src/shared/testing/test-xApiKey-client.spec.ts create mode 100644 apps/server/src/shared/testing/test-xApiKey-client.ts diff --git a/ansible/roles/schulcloud-server-core/tasks/main.yml b/ansible/roles/schulcloud-server-core/tasks/main.yml index 4840ec78474..6321fffae36 100644 --- a/ansible/roles/schulcloud-server-core/tasks/main.yml +++ b/ansible/roles/schulcloud-server-core/tasks/main.yml @@ -156,6 +156,21 @@ - KEDA_ENABLED is defined and KEDA_ENABLED|bool - SCALED_PREVIEW_GENERATOR_ENABLED is defined and SCALED_PREVIEW_GENERATOR_ENABLED|bool + + - name: admin api server deployment + kubernetes.core.k8s: + kubeconfig: ~/.kube/config + namespace: "{{ NAMESPACE }}" + template: admin-api-server-deployment.yml.j2 + when: WITH_API_ADMIN is defined and WITH_API_ADMIN|bool + + - name: admin api server service + kubernetes.core.k8s: + kubeconfig: ~/.kube/config + namespace: "{{ NAMESPACE }}" + template: admin-api-server-svc.yml.j2 + when: WITH_API_ADMIN is defined and WITH_API_ADMIN|bool + - name: TlDraw server deployment kubernetes.core.k8s: kubeconfig: ~/.kube/config diff --git a/ansible/roles/schulcloud-server-core/templates/admin-api-server-deployment.yml.j2 b/ansible/roles/schulcloud-server-core/templates/admin-api-server-deployment.yml.j2 new file mode 100644 index 00000000000..c191916fe2d --- /dev/null +++ b/ansible/roles/schulcloud-server-core/templates/admin-api-server-deployment.yml.j2 @@ -0,0 +1,64 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: admin-api-deployment + namespace: {{ NAMESPACE }} + labels: + app: api-admin + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-admin + app.kubernetes.io/component: server + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} +spec: + replicas: {{ ADMIN_API_SERVER_REPLICAS|default("1", true) }} + strategy: + type: RollingUpdate + rollingUpdate: + maxSurge: 1 + #maxUnavailable: 1 + revisionHistoryLimit: 4 + paused: false + selector: + matchLabels: + app: api-admin + template: + metadata: + labels: + app: api-admin + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-admin + app.kubernetes.io/component: server + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} + spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + runAsNonRoot: true + containers: + - name: api-admin + image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }} + imagePullPolicy: IfNotPresent + ports: + - containerPort: 4030 + name: admin + protocol: TCP + envFrom: + - configMapRef: + name: api-configmap + - secretRef: + name: api-secret + command: ['npm', 'run', 'nest:start:admin-api-server:prod'] + resources: + limits: + cpu: {{ ADMIN_API_SERVER_CPU_LIMITS|default("2000m", true) }} + memory: {{ ADMIN_API_SERVER_MEMORY_LIMITS|default("4Gi", true) }} + requests: + cpu: {{ ADMIN_API_SERVER_CPU_REQUESTS|default("100m", true) }} + memory: {{ ADMIN_API_SERVER_MEMORY_REQUESTS|default("150Mi", true) }} \ No newline at end of file diff --git a/ansible/roles/schulcloud-server-core/templates/admin-api-server-svc.yml.j2 b/ansible/roles/schulcloud-server-core/templates/admin-api-server-svc.yml.j2 new file mode 100644 index 00000000000..cde6dcef4cd --- /dev/null +++ b/ansible/roles/schulcloud-server-core/templates/admin-api-server-svc.yml.j2 @@ -0,0 +1,17 @@ +apiVersion: v1 +kind: Service +metadata: + name: api-admin-svc + namespace: {{ NAMESPACE }} + labels: + app: api-admin +spec: + type: ClusterIP + ports: + # port for http managing drawing data + - port: 4030 + targetPort: 4030 + protocol: TCP + name: admin + selector: + app: api-admin \ No newline at end of file diff --git a/apps/server/src/apps/admin-api-server.app.ts b/apps/server/src/apps/admin-api-server.app.ts new file mode 100644 index 00000000000..4bcbf9681b6 --- /dev/null +++ b/apps/server/src/apps/admin-api-server.app.ts @@ -0,0 +1,43 @@ +/* istanbul ignore file */ +import { NestFactory } from '@nestjs/core'; +import { install as sourceMapInstall } from 'source-map-support'; +import { LegacyLogger, Logger } from '@src/core/logger'; +import { enableOpenApiDocs } from '@shared/controller/swagger'; +import { AppStartLoggable } from '@src/apps/helpers/app-start-loggable'; +import { ExpressAdapter } from '@nestjs/platform-express'; +import express from 'express'; +import { AdminApiServerModule } from '@src/modules/server/admin-api.server.module'; +import { Configuration } from '@hpi-schul-cloud/commons/lib'; + +async function bootstrap() { + sourceMapInstall(); + + const nestAdminServerExpress = express(); + const nestAdminServerExpressAdapter = new ExpressAdapter(nestAdminServerExpress); + nestAdminServerExpressAdapter.disable('x-powered-by'); + + const nestAdminServerApp = await NestFactory.create(AdminApiServerModule, nestAdminServerExpressAdapter); + const logger = await nestAdminServerApp.resolve(Logger); + const legacyLogger = await nestAdminServerApp.resolve(LegacyLogger); + nestAdminServerApp.useLogger(legacyLogger); + nestAdminServerApp.enableCors(); + + enableOpenApiDocs(nestAdminServerApp, 'docs'); + nestAdminServerApp.setGlobalPrefix('/admin/api/v1'); + + await nestAdminServerApp.init(); + + const adminApiServerPort = Configuration.get('ADMIN_API__PORT') as number; + + nestAdminServerExpress.listen(adminApiServerPort, () => { + logger.info( + new AppStartLoggable({ + appName: 'Admin API server app', + port: adminApiServerPort, + mountsDescription: `/admin/api/v1 --> Admin API Server`, + }) + ); + }); +} + +void bootstrap(); diff --git a/apps/server/src/modules/authentication/authentication.module.ts b/apps/server/src/modules/authentication/authentication.module.ts index ca9872f75e0..e28c4d563b2 100644 --- a/apps/server/src/modules/authentication/authentication.module.ts +++ b/apps/server/src/modules/authentication/authentication.module.ts @@ -18,6 +18,7 @@ import { JwtStrategy } from './strategy/jwt.strategy'; import { LdapStrategy } from './strategy/ldap.strategy'; import { LocalStrategy } from './strategy/local.strategy'; import { Oauth2Strategy } from './strategy/oauth2.strategy'; +import { XApiKeyStrategy } from './strategy/x-api-key.strategy'; // values copied from Algorithm definition. Type does not exist at runtime and can't be checked anymore otherwise const algorithms = [ @@ -76,6 +77,7 @@ const jwtModuleOptions: JwtModuleOptions = { LdapService, LdapStrategy, Oauth2Strategy, + XApiKeyStrategy, ], exports: [AuthenticationService], }) diff --git a/apps/server/src/modules/authentication/config/index.ts b/apps/server/src/modules/authentication/config/index.ts new file mode 100644 index 00000000000..cd9c0d28b96 --- /dev/null +++ b/apps/server/src/modules/authentication/config/index.ts @@ -0,0 +1 @@ +export * from './x-api-key.config'; diff --git a/apps/server/src/modules/authentication/config/x-api-key.config.ts b/apps/server/src/modules/authentication/config/x-api-key.config.ts new file mode 100644 index 00000000000..97c189e2853 --- /dev/null +++ b/apps/server/src/modules/authentication/config/x-api-key.config.ts @@ -0,0 +1,3 @@ +export interface XApiKeyConfig { + ADMIN_API__ALLOWED_API_KEYS: string[]; +} diff --git a/apps/server/src/modules/authentication/index.ts b/apps/server/src/modules/authentication/index.ts index 80e9d64ab69..fba2d4332d2 100644 --- a/apps/server/src/modules/authentication/index.ts +++ b/apps/server/src/modules/authentication/index.ts @@ -2,3 +2,4 @@ export { AuthenticationModule } from './authentication.module'; export { Authenticate, CurrentUser, JWT } from './decorator'; export { ICurrentUser } from './interface'; export { AuthenticationService } from './services'; +export { XApiKeyConfig } from './config'; diff --git a/apps/server/src/modules/authentication/strategy/x-api-key.strategy.spec.ts b/apps/server/src/modules/authentication/strategy/x-api-key.strategy.spec.ts new file mode 100644 index 00000000000..f71394f8b9d --- /dev/null +++ b/apps/server/src/modules/authentication/strategy/x-api-key.strategy.spec.ts @@ -0,0 +1,74 @@ +import { UnauthorizedException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigService } from '@nestjs/config'; +import { createMock } from '@golevelup/ts-jest'; +import { XApiKeyStrategy } from './x-api-key.strategy'; +import { XApiKeyConfig } from '../config/x-api-key.config'; + +describe('XApiKeyStrategy', () => { + let module: TestingModule; + let strategy: XApiKeyStrategy; + let configService: ConfigService; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [], + providers: [ + XApiKeyStrategy, + { + provide: ConfigService, + useValue: createMock>({ get: () => ['1ab2c3d4e5f61ab2c3d4e5f6'] }), + }, + ], + }).compile(); + + strategy = module.get(XApiKeyStrategy); + configService = module.get(ConfigService); + }); + + afterAll(async () => { + await module.close(); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('validate', () => { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const done = jest.fn((error: Error | null, data: boolean | null) => {}); + describe('when a valid api key is provided', () => { + const setup = () => { + const CORRECT_API_KEY = '1ab2c3d4e5f61ab2c3d4e5f6'; + + return { CORRECT_API_KEY, done }; + }; + it('should do nothing', () => { + const { CORRECT_API_KEY } = setup(); + strategy.validate(CORRECT_API_KEY, done); + expect(done).toBeCalledWith(null, true); + }); + }); + + describe('when a invalid api key is provided', () => { + const setup = () => { + const INVALID_API_KEY = '1ab2c3d4e5f61ab2c3d4e5f6778173'; + + return { INVALID_API_KEY, done }; + }; + it('should throw error', () => { + const { INVALID_API_KEY } = setup(); + strategy.validate(INVALID_API_KEY, done); + expect(done).toBeCalledWith(new UnauthorizedException(), null); + }); + }); + }); + + describe('constructor', () => { + it('should create strategy', () => { + const ApiKeyStrategy = new XApiKeyStrategy(configService); + expect(ApiKeyStrategy).toBeDefined(); + expect(ApiKeyStrategy).toBeInstanceOf(XApiKeyStrategy); + }); + }); +}); diff --git a/apps/server/src/modules/authentication/strategy/x-api-key.strategy.ts b/apps/server/src/modules/authentication/strategy/x-api-key.strategy.ts new file mode 100644 index 00000000000..0becc2f8c86 --- /dev/null +++ b/apps/server/src/modules/authentication/strategy/x-api-key.strategy.ts @@ -0,0 +1,22 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { PassportStrategy } from '@nestjs/passport'; +import { ConfigService } from '@nestjs/config'; +import Strategy from 'passport-headerapikey'; +import { XApiKeyConfig } from '../config/x-api-key.config'; + +@Injectable() +export class XApiKeyStrategy extends PassportStrategy(Strategy, 'api-key') { + private readonly allowedApiKeys: string[]; + + constructor(private readonly configService: ConfigService) { + super({ header: 'X-API-KEY' }, false); + this.allowedApiKeys = this.configService.get('ADMIN_API__ALLOWED_API_KEYS'); + } + + public validate = (apiKey: string, done: (error: Error | null, data: boolean | null) => void) => { + if (this.allowedApiKeys.includes(apiKey)) { + done(null, true); + } + done(new UnauthorizedException(), null); + }; +} diff --git a/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.spec.ts b/apps/server/src/modules/deletion/builder/batch-deletion-summary-detail.builder.spec.ts similarity index 96% rename from apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.spec.ts rename to apps/server/src/modules/deletion/builder/batch-deletion-summary-detail.builder.spec.ts index 43ea82e86d5..864563e97dc 100644 --- a/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.spec.ts +++ b/apps/server/src/modules/deletion/builder/batch-deletion-summary-detail.builder.spec.ts @@ -1,7 +1,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; -import { BatchDeletionSummaryDetail } from '..'; -import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from '../../services'; +import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from '../services'; import { BatchDeletionSummaryDetailBuilder } from './batch-deletion-summary-detail.builder'; +import { BatchDeletionSummaryDetail } from '../interface'; describe(BatchDeletionSummaryDetailBuilder.name, () => { describe(BatchDeletionSummaryDetailBuilder.build.name, () => { diff --git a/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.ts b/apps/server/src/modules/deletion/builder/batch-deletion-summary-detail.builder.ts similarity index 93% rename from apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.ts rename to apps/server/src/modules/deletion/builder/batch-deletion-summary-detail.builder.ts index 9ebbce66171..3184a6f9028 100644 --- a/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary-detail.builder.ts +++ b/apps/server/src/modules/deletion/builder/batch-deletion-summary-detail.builder.ts @@ -1,4 +1,4 @@ -import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from '../../services'; +import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from '../services'; import { BatchDeletionSummaryDetail } from '../interface'; export class BatchDeletionSummaryDetailBuilder { diff --git a/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.spec.ts b/apps/server/src/modules/deletion/builder/batch-deletion-summary.builder.spec.ts similarity index 100% rename from apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.spec.ts rename to apps/server/src/modules/deletion/builder/batch-deletion-summary.builder.spec.ts diff --git a/apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.ts b/apps/server/src/modules/deletion/builder/batch-deletion-summary.builder.ts similarity index 100% rename from apps/server/src/modules/deletion/uc/builder/batch-deletion-summary.builder.ts rename to apps/server/src/modules/deletion/builder/batch-deletion-summary.builder.ts diff --git a/apps/server/src/modules/deletion/uc/builder/deletion-log-statistic.builder.spec.ts b/apps/server/src/modules/deletion/builder/deletion-log-statistic.builder.spec.ts similarity index 77% rename from apps/server/src/modules/deletion/uc/builder/deletion-log-statistic.builder.spec.ts rename to apps/server/src/modules/deletion/builder/deletion-log-statistic.builder.spec.ts index c2952f40f59..babef08f8d9 100644 --- a/apps/server/src/modules/deletion/uc/builder/deletion-log-statistic.builder.spec.ts +++ b/apps/server/src/modules/deletion/builder/deletion-log-statistic.builder.spec.ts @@ -1,5 +1,5 @@ -import { DeletionDomainModel } from '../../domain/types/deletion-domain-model.enum'; -import { DeletionLogStatisticBuilder } from './deletion-log-statistic.builder'; +import { DeletionDomainModel } from '../domain/types'; +import { DeletionLogStatisticBuilder } from '.'; describe(DeletionLogStatisticBuilder.name, () => { afterAll(() => { diff --git a/apps/server/src/modules/deletion/uc/builder/deletion-log-statistic.builder.ts b/apps/server/src/modules/deletion/builder/deletion-log-statistic.builder.ts similarity index 79% rename from apps/server/src/modules/deletion/uc/builder/deletion-log-statistic.builder.ts rename to apps/server/src/modules/deletion/builder/deletion-log-statistic.builder.ts index a562505b885..2e467eed310 100644 --- a/apps/server/src/modules/deletion/uc/builder/deletion-log-statistic.builder.ts +++ b/apps/server/src/modules/deletion/builder/deletion-log-statistic.builder.ts @@ -1,4 +1,4 @@ -import { DeletionDomainModel } from '../../domain/types/deletion-domain-model.enum'; +import { DeletionDomainModel } from '../domain/types'; import { DeletionLogStatistic } from '../interface'; export class DeletionLogStatisticBuilder { diff --git a/apps/server/src/modules/deletion/builder/deletion-request-body-props.builder.spec.ts b/apps/server/src/modules/deletion/builder/deletion-request-body-props.builder.spec.ts new file mode 100644 index 00000000000..4a363d86a40 --- /dev/null +++ b/apps/server/src/modules/deletion/builder/deletion-request-body-props.builder.spec.ts @@ -0,0 +1,27 @@ +import { ObjectId } from 'bson'; +import { DeletionDomainModel } from '../domain/types'; +import { DeletionRequestBodyPropsBuilder } from './deletion-request-body-props.builder'; + +describe(DeletionRequestBodyPropsBuilder.name, () => { + afterAll(() => { + jest.clearAllMocks(); + }); + describe('when create deletionRequestBodyParams', () => { + const setup = () => { + const domain = DeletionDomainModel.PSEUDONYMS; + const refId = new ObjectId().toHexString(); + const deleteInMinutes = 1000; + return { domain, refId, deleteInMinutes }; + }; + it('should build deletionRequestBodyParams with all attributes', () => { + const { domain, refId, deleteInMinutes } = setup(); + + const result = DeletionRequestBodyPropsBuilder.build(domain, refId, deleteInMinutes); + + // Assert + expect(result.targetRef.domain).toEqual(domain); + expect(result.targetRef.id).toEqual(refId); + expect(result.deleteInMinutes).toEqual(deleteInMinutes); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/builder/deletion-request-body-props.builder.ts b/apps/server/src/modules/deletion/builder/deletion-request-body-props.builder.ts new file mode 100644 index 00000000000..b7dcd72b441 --- /dev/null +++ b/apps/server/src/modules/deletion/builder/deletion-request-body-props.builder.ts @@ -0,0 +1,14 @@ +import { EntityId } from '@shared/domain'; +import { DeletionDomainModel } from '../domain/types'; +import { DeletionRequestBodyProps } from '../controller/dto'; + +export class DeletionRequestBodyPropsBuilder { + static build(domain: DeletionDomainModel, id: EntityId, deleteInMinutes?: number): DeletionRequestBodyProps { + const deletionRequestItem = { + targetRef: { domain, id }, + deleteInMinutes, + }; + + return deletionRequestItem; + } +} diff --git a/apps/server/src/modules/deletion/uc/builder/deletion-request-log.builder.spec.ts b/apps/server/src/modules/deletion/builder/deletion-request-log-response.builder.spec.ts similarity index 60% rename from apps/server/src/modules/deletion/uc/builder/deletion-request-log.builder.spec.ts rename to apps/server/src/modules/deletion/builder/deletion-request-log-response.builder.spec.ts index b317a4b2221..eb1adeee7cd 100644 --- a/apps/server/src/modules/deletion/uc/builder/deletion-request-log.builder.spec.ts +++ b/apps/server/src/modules/deletion/builder/deletion-request-log-response.builder.spec.ts @@ -1,9 +1,7 @@ -import { DeletionDomainModel } from '../../domain/types/deletion-domain-model.enum'; -import { DeletionLogStatisticBuilder } from './deletion-log-statistic.builder'; -import { DeletionRequestLogBuilder } from './deletion-request-log.builder'; -import { DeletionTargetRefBuilder } from './deletion-target-ref.builder'; +import { DeletionDomainModel } from '../domain/types'; +import { DeletionLogStatisticBuilder, DeletionRequestLogResponseBuilder, DeletionTargetRefBuilder } from './index'; -describe(DeletionRequestLogBuilder.name, () => { +describe(DeletionRequestLogResponseBuilder.name, () => { afterAll(() => { jest.clearAllMocks(); }); @@ -18,7 +16,7 @@ describe(DeletionRequestLogBuilder.name, () => { const deletedCount = 2; const statistics = [DeletionLogStatisticBuilder.build(targetRefDomain, modifiedCount, deletedCount)]; - const result = DeletionRequestLogBuilder.build(targetRef, deletionPlannedAt, statistics); + const result = DeletionRequestLogResponseBuilder.build(targetRef, deletionPlannedAt, statistics); // Assert expect(result.targetRef).toEqual(targetRef); diff --git a/apps/server/src/modules/deletion/uc/builder/deletion-request-log.builder.ts b/apps/server/src/modules/deletion/builder/deletion-request-log-response.builder.ts similarity index 50% rename from apps/server/src/modules/deletion/uc/builder/deletion-request-log.builder.ts rename to apps/server/src/modules/deletion/builder/deletion-request-log-response.builder.ts index 8247acf6776..be4b0ba5a96 100644 --- a/apps/server/src/modules/deletion/uc/builder/deletion-request-log.builder.ts +++ b/apps/server/src/modules/deletion/builder/deletion-request-log-response.builder.ts @@ -1,11 +1,12 @@ -import { DeletionLogStatistic, DeletionRequestLog, DeletionTargetRef } from '../interface'; +import { DeletionRequestLogResponse } from '../controller/dto'; +import { DeletionLogStatistic, DeletionTargetRef } from '../interface'; -export class DeletionRequestLogBuilder { +export class DeletionRequestLogResponseBuilder { static build( targetRef: DeletionTargetRef, deletionPlannedAt: Date, statistics?: DeletionLogStatistic[] - ): DeletionRequestLog { + ): DeletionRequestLogResponse { const deletionRequestLog = { targetRef, deletionPlannedAt, statistics }; return deletionRequestLog; diff --git a/apps/server/src/modules/deletion/uc/builder/deletion-target-ref.builder.spec.ts b/apps/server/src/modules/deletion/builder/deletion-target-ref.builder.spec.ts similarity index 58% rename from apps/server/src/modules/deletion/uc/builder/deletion-target-ref.builder.spec.ts rename to apps/server/src/modules/deletion/builder/deletion-target-ref.builder.spec.ts index 2fb4ae440a7..4667f290b80 100644 --- a/apps/server/src/modules/deletion/uc/builder/deletion-target-ref.builder.spec.ts +++ b/apps/server/src/modules/deletion/builder/deletion-target-ref.builder.spec.ts @@ -1,5 +1,5 @@ -import { DeletionDomainModel } from '../../domain/types/deletion-domain-model.enum'; -import { DeletionTargetRefBuilder } from './deletion-target-ref.builder'; +import { DeletionDomainModel } from '../domain/types'; +import { DeletionTargetRefBuilder } from './index'; describe(DeletionTargetRefBuilder.name, () => { afterAll(() => { @@ -14,7 +14,7 @@ describe(DeletionTargetRefBuilder.name, () => { const result = DeletionTargetRefBuilder.build(domain, refId); // Assert - expect(result.targetRefDomain).toEqual(domain); - expect(result.targetRefId).toEqual(refId); + expect(result.domain).toEqual(domain); + expect(result.id).toEqual(refId); }); }); diff --git a/apps/server/src/modules/deletion/builder/deletion-target-ref.builder.ts b/apps/server/src/modules/deletion/builder/deletion-target-ref.builder.ts new file mode 100644 index 00000000000..f98271ff317 --- /dev/null +++ b/apps/server/src/modules/deletion/builder/deletion-target-ref.builder.ts @@ -0,0 +1,11 @@ +import { EntityId } from '@shared/domain'; +import { DeletionDomainModel } from '../domain/types'; +import { DeletionTargetRef } from '../interface'; + +export class DeletionTargetRefBuilder { + static build(domain: DeletionDomainModel, id: EntityId): DeletionTargetRef { + const deletionTargetRef = { domain, id }; + + return deletionTargetRef; + } +} diff --git a/apps/server/src/modules/deletion/builder/index.ts b/apps/server/src/modules/deletion/builder/index.ts new file mode 100644 index 00000000000..c7ac430fa63 --- /dev/null +++ b/apps/server/src/modules/deletion/builder/index.ts @@ -0,0 +1,6 @@ +export * from './deletion-log-statistic.builder'; +export * from './deletion-request-body-props.builder'; +export * from './deletion-request-log-response.builder'; +export * from './deletion-target-ref.builder'; +export * from './batch-deletion-summary-detail.builder'; +export * from './batch-deletion-summary.builder'; diff --git a/apps/server/src/modules/deletion/controller/api-test/deletion-executions.api.spec.ts b/apps/server/src/modules/deletion/controller/api-test/deletion-executions.api.spec.ts new file mode 100644 index 00000000000..fdf1a30bf7a --- /dev/null +++ b/apps/server/src/modules/deletion/controller/api-test/deletion-executions.api.spec.ts @@ -0,0 +1,47 @@ +import { ExecutionContext, INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Request } from 'express'; +import { AuthGuard } from '@nestjs/passport'; +import { TestXApiKeyClient } from '@shared/testing'; +import { AdminApiServerTestModule } from '@modules/server/admin-api.server.module'; + +const baseRouteName = '/deletionExecutions'; + +describe(`deletionExecution (api)`, () => { + let app: INestApplication; + let testXApiKeyClient: TestXApiKeyClient; + const API_KEY = '1ab2c3d4e5f61ab2c3d4e5f6'; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AdminApiServerTestModule], + }) + .overrideGuard(AuthGuard('api-key')) + .useValue({ + canActivate(context: ExecutionContext) { + const req: Request = context.switchToHttp().getRequest(); + req.headers['X-API-KEY'] = API_KEY; + return true; + }, + }) + .compile(); + + app = module.createNestApplication(); + await app.init(); + testXApiKeyClient = new TestXApiKeyClient(app, baseRouteName); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('executeDeletions', () => { + describe('when execute deletionRequests with default limit', () => { + it('should return status 204', async () => { + const response = await testXApiKeyClient.post(''); + + expect(response.status).toEqual(204); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/controller/api-test/deletion-request-create.api.spec.ts b/apps/server/src/modules/deletion/controller/api-test/deletion-request-create.api.spec.ts new file mode 100644 index 00000000000..d389b997944 --- /dev/null +++ b/apps/server/src/modules/deletion/controller/api-test/deletion-request-create.api.spec.ts @@ -0,0 +1,112 @@ +import { ExecutionContext, INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Request } from 'express'; +import { AuthGuard } from '@nestjs/passport'; +import { EntityManager } from '@mikro-orm/mongodb'; +import { TestXApiKeyClient } from '@shared/testing'; +import { AdminApiServerTestModule } from '@modules/server/admin-api.server.module'; +import { DeletionRequestBodyProps, DeletionRequestResponse } from '../dto'; +import { DeletionDomainModel } from '../../domain/types'; +import { DeletionRequestEntity } from '../../entity'; + +const baseRouteName = '/deletionRequests'; + +describe(`deletionRequest create (api)`, () => { + let app: INestApplication; + let em: EntityManager; + let testXApiKeyClient: TestXApiKeyClient; + const API_KEY = '1ab2c3d4e5f61ab2c3d4e5f6'; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AdminApiServerTestModule], + }) + .overrideGuard(AuthGuard('api-key')) + .useValue({ + canActivate(context: ExecutionContext) { + const req: Request = context.switchToHttp().getRequest(); + req.headers['X-API-KEY'] = API_KEY; + return true; + }, + }) + .compile(); + + app = module.createNestApplication(); + await app.init(); + em = module.get(EntityManager); + testXApiKeyClient = new TestXApiKeyClient(app, baseRouteName); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('createDeletionRequests', () => { + describe('when create deletionRequest', () => { + const setup = () => { + const deletionRequestToCreate: DeletionRequestBodyProps = { + targetRef: { + domain: DeletionDomainModel.USER, + id: '653e4833cc39e5907a1e18d2', + }, + }; + + const deletionRequestToImmediateRemoval: DeletionRequestBodyProps = { + targetRef: { + domain: DeletionDomainModel.USER, + id: '653e4833cc39e5907a1e18d2', + }, + deleteInMinutes: 0, + }; + + return { deletionRequestToCreate, deletionRequestToImmediateRemoval }; + }; + + it('should return status 202', async () => { + const { deletionRequestToCreate } = setup(); + + const response = await testXApiKeyClient.post('', deletionRequestToCreate); + + expect(response.status).toEqual(202); + }); + + it('should return the created deletionRequest', async () => { + const { deletionRequestToCreate } = setup(); + + const response = await testXApiKeyClient.post('', deletionRequestToCreate); + + const result = response.body as DeletionRequestResponse; + expect(result.requestId).toBeDefined(); + }); + + it('should create deletionRequest with default deletion time (add 43200 minutes to current time) ', async () => { + const { deletionRequestToCreate } = setup(); + + const response = await testXApiKeyClient.post('', deletionRequestToCreate); + + const result = response.body as DeletionRequestResponse; + const createdDeletionRequestId = result.requestId; + + const createdItem = await em.findOneOrFail(DeletionRequestEntity, createdDeletionRequestId); + + const deletionPlannedAt = createdItem.createdAt; + deletionPlannedAt.setMinutes(deletionPlannedAt.getMinutes() + 43200); + + expect(createdItem.deleteAfter).toEqual(deletionPlannedAt); + }); + + it('should create deletionRequest with deletion time (0 minutes to current time) ', async () => { + const { deletionRequestToImmediateRemoval } = setup(); + + const response = await testXApiKeyClient.post('', deletionRequestToImmediateRemoval); + + const result = response.body as DeletionRequestResponse; + const createdDeletionRequestId = result.requestId; + + const createdItem = await em.findOneOrFail(DeletionRequestEntity, createdDeletionRequestId); + + expect(createdItem.createdAt).toEqual(createdItem.deleteAfter); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/controller/api-test/deletion-request-delete.api.spec.ts b/apps/server/src/modules/deletion/controller/api-test/deletion-request-delete.api.spec.ts new file mode 100644 index 00000000000..1c049cba0bc --- /dev/null +++ b/apps/server/src/modules/deletion/controller/api-test/deletion-request-delete.api.spec.ts @@ -0,0 +1,72 @@ +import { ExecutionContext, INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Request } from 'express'; +import { AuthGuard } from '@nestjs/passport'; +import { TestXApiKeyClient, cleanupCollections } from '@shared/testing'; +import { EntityManager } from '@mikro-orm/mongodb'; +import { AdminApiServerTestModule } from '@modules/server/admin-api.server.module'; +import { deletionRequestEntityFactory } from '../../entity/testing'; +import { DeletionRequestEntity } from '../../entity'; + +const baseRouteName = '/deletionRequests'; + +describe(`deletionRequest delete (api)`, () => { + let app: INestApplication; + let em: EntityManager; + let testXApiKeyClient: TestXApiKeyClient; + const API_KEY = '1ab2c3d4e5f61ab2c3d4e5f6'; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AdminApiServerTestModule], + }) + .overrideGuard(AuthGuard('api-key')) + .useValue({ + canActivate(context: ExecutionContext) { + const req: Request = context.switchToHttp().getRequest(); + req.headers['X-API-KEY'] = API_KEY; + return true; + }, + }) + .compile(); + + app = module.createNestApplication(); + await app.init(); + em = module.get(EntityManager); + testXApiKeyClient = new TestXApiKeyClient(app, baseRouteName); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('cancelDeletionRequest', () => { + describe('when deletiong deletionRequest', () => { + const setup = async () => { + await cleanupCollections(em); + const deletionRequest = deletionRequestEntityFactory.build(); + + await em.persistAndFlush(deletionRequest); + em.clear(); + + return { deletionRequest }; + }; + + it('should return status 204', async () => { + const { deletionRequest } = await setup(); + + const response = await testXApiKeyClient.delete(`${deletionRequest.id}`); + + expect(response.status).toEqual(204); + }); + + it('should actually delete deletionRequest', async () => { + const { deletionRequest } = await setup(); + + await testXApiKeyClient.delete(`${deletionRequest.id}`); + + await expect(em.findOneOrFail(DeletionRequestEntity, deletionRequest.id)).rejects.toThrow(); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/controller/api-test/deletion-request-find.api.spec.ts b/apps/server/src/modules/deletion/controller/api-test/deletion-request-find.api.spec.ts new file mode 100644 index 00000000000..437063e9651 --- /dev/null +++ b/apps/server/src/modules/deletion/controller/api-test/deletion-request-find.api.spec.ts @@ -0,0 +1,72 @@ +import { ExecutionContext, INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Request } from 'express'; +import { AuthGuard } from '@nestjs/passport'; +import { EntityManager } from '@mikro-orm/mongodb'; +import { TestXApiKeyClient, cleanupCollections } from '@shared/testing'; +import { AdminApiServerTestModule } from '@modules/server/admin-api.server.module'; +import { deletionRequestEntityFactory } from '../../entity/testing'; +import { DeletionRequestLogResponse } from '../dto'; + +const baseRouteName = '/deletionRequests'; + +describe(`deletionRequest find (api)`, () => { + let app: INestApplication; + let em: EntityManager; + let testXApiKeyClient: TestXApiKeyClient; + const API_KEY = '1ab2c3d4e5f61ab2c3d4e5f6'; + + beforeAll(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [AdminApiServerTestModule], + }) + .overrideGuard(AuthGuard('api-key')) + .useValue({ + canActivate(context: ExecutionContext) { + const req: Request = context.switchToHttp().getRequest(); + req.headers['X-API-KEY'] = API_KEY; + return true; + }, + }) + .compile(); + + app = module.createNestApplication(); + await app.init(); + em = module.get(EntityManager); + testXApiKeyClient = new TestXApiKeyClient(app, baseRouteName); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('getPerformedDeletionDetails', () => { + describe('when searching for deletionRequest', () => { + const setup = async () => { + await cleanupCollections(em); + const deletionRequest = deletionRequestEntityFactory.build(); + + await em.persistAndFlush(deletionRequest); + em.clear(); + + return { deletionRequest }; + }; + it('should return status 202', async () => { + const { deletionRequest } = await setup(); + + const response = await testXApiKeyClient.get(`${deletionRequest.id}`); + + expect(response.status).toEqual(200); + }); + + it('should return the found deletionRequest', async () => { + const { deletionRequest } = await setup(); + + const response = await testXApiKeyClient.get(`${deletionRequest.id}`); + const result = response.body as DeletionRequestLogResponse; + + expect(result.targetRef.id).toEqual(deletionRequest.targetRefId); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/controller/deletion-executions.controller.ts b/apps/server/src/modules/deletion/controller/deletion-executions.controller.ts new file mode 100644 index 00000000000..143e69735dd --- /dev/null +++ b/apps/server/src/modules/deletion/controller/deletion-executions.controller.ts @@ -0,0 +1,24 @@ +import { Controller, HttpCode, Post, Query, UseGuards } from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { AuthGuard } from '@nestjs/passport'; +import { DeletionRequestUc } from '../uc'; +import { DeletionExecutionParams } from './dto'; + +@ApiTags('DeletionExecutions') +@UseGuards(AuthGuard('api-key')) +@Controller('deletionExecutions') +export class DeletionExecutionsController { + constructor(private readonly deletionRequestUc: DeletionRequestUc) {} + + @Post() + @HttpCode(204) + @ApiOperation({ + summary: 'Execute the deletion process', + }) + @ApiResponse({ + status: 204, + }) + async executeDeletions(@Query() deletionExecutionQuery: DeletionExecutionParams) { + return this.deletionRequestUc.executeDeletionRequests(deletionExecutionQuery.limit); + } +} diff --git a/apps/server/src/modules/deletion/controller/deletion-requests.controller.ts b/apps/server/src/modules/deletion/controller/deletion-requests.controller.ts new file mode 100644 index 00000000000..159678f2042 --- /dev/null +++ b/apps/server/src/modules/deletion/controller/deletion-requests.controller.ts @@ -0,0 +1,54 @@ +import { Body, Controller, Delete, Get, HttpCode, Param, Post, UseGuards } from '@nestjs/common'; +import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { AuthGuard } from '@nestjs/passport'; +import { DeletionRequestUc } from '../uc'; +import { DeletionRequestLogResponse, DeletionRequestBodyProps, DeletionRequestResponse } from './dto'; + +@ApiTags('DeletionRequests') +@UseGuards(AuthGuard('api-key')) +@Controller('deletionRequests') +export class DeletionRequestsController { + constructor(private readonly deletionRequestUc: DeletionRequestUc) {} + + @Post() + @HttpCode(202) + @ApiOperation({ + summary: '"Queueing" a deletion request', + }) + @ApiResponse({ + status: 202, + type: DeletionRequestResponse, + description: 'Returns identifier of the deletion request and when deletion is planned at', + }) + async createDeletionRequests( + @Body() deletionRequestBody: DeletionRequestBodyProps + ): Promise { + return this.deletionRequestUc.createDeletionRequest(deletionRequestBody); + } + + @Get(':requestId') + @HttpCode(200) + @ApiOperation({ + summary: 'Retrieving details of performed or planned deletion', + }) + @ApiResponse({ + status: 200, + type: DeletionRequestLogResponse, + description: 'Return details of performed or planned deletion', + }) + async getPerformedDeletionDetails(@Param('requestId') requestId: string): Promise { + return this.deletionRequestUc.findById(requestId); + } + + @Delete(':requestId') + @HttpCode(204) + @ApiOperation({ + summary: 'Canceling a deletion request', + }) + @ApiResponse({ + status: 204, + }) + async cancelDeletionRequest(@Param('requestId') requestId: string) { + return this.deletionRequestUc.deleteDeletionRequestById(requestId); + } +} diff --git a/apps/server/src/modules/deletion/controller/dto/deletion-execution.params.spec.ts b/apps/server/src/modules/deletion/controller/dto/deletion-execution.params.spec.ts new file mode 100644 index 00000000000..45051c94d29 --- /dev/null +++ b/apps/server/src/modules/deletion/controller/dto/deletion-execution.params.spec.ts @@ -0,0 +1,44 @@ +import { DeletionExecutionParams } from './deletion-execution.params'; + +describe(DeletionExecutionParams.name, () => { + describe('constructor', () => { + describe('when passed properties', () => { + const setup = () => { + const deletionExecutionParams = new DeletionExecutionParams(); + + return { deletionExecutionParams }; + }; + + it('should be defined', () => { + const { deletionExecutionParams } = setup(); + + expect(deletionExecutionParams).toBeDefined(); + }); + + it('should have a limit property', () => { + const { deletionExecutionParams } = setup(); + expect(deletionExecutionParams.limit).toBeDefined(); + }); + + it('limit should be a number with a default value of 100', () => { + const { deletionExecutionParams } = setup(); + expect(deletionExecutionParams.limit).toBeDefined(); + expect(typeof deletionExecutionParams.limit).toBe('number'); + expect(deletionExecutionParams.limit).toBe(100); + }); + + it('limit should be optional', () => { + const { deletionExecutionParams } = setup(); + deletionExecutionParams.limit = undefined; + expect(deletionExecutionParams.limit).toBeUndefined(); + }); + + it('limit should be a number when provided', () => { + const { deletionExecutionParams } = setup(); + const customLimit = 50; + deletionExecutionParams.limit = customLimit; + expect(deletionExecutionParams.limit).toBe(customLimit); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/controller/dto/deletion-execution.params.ts b/apps/server/src/modules/deletion/controller/dto/deletion-execution.params.ts new file mode 100644 index 00000000000..9ca4a18bd71 --- /dev/null +++ b/apps/server/src/modules/deletion/controller/dto/deletion-execution.params.ts @@ -0,0 +1,10 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { IsInt, IsOptional, Min } from 'class-validator'; + +export class DeletionExecutionParams { + @IsInt() + @Min(1) + @IsOptional() + @ApiPropertyOptional({ description: 'Page limit, defaults to 100.', minimum: 1 }) + limit?: number = 100; +} diff --git a/apps/server/src/modules/deletion/controller/dto/deletion-request-log.response.spec.ts b/apps/server/src/modules/deletion/controller/dto/deletion-request-log.response.spec.ts new file mode 100644 index 00000000000..5036a0d39e0 --- /dev/null +++ b/apps/server/src/modules/deletion/controller/dto/deletion-request-log.response.spec.ts @@ -0,0 +1,32 @@ +import { ObjectId } from 'bson'; +import { DeletionDomainModel } from '../../domain/types'; +import { DeletionLogStatisticBuilder, DeletionTargetRefBuilder } from '../../builder'; +import { DeletionRequestLogResponse } from './index'; + +describe(DeletionRequestLogResponse.name, () => { + describe('constructor', () => { + describe('when passed properties', () => { + const setup = () => { + const targetRefDomain = DeletionDomainModel.PSEUDONYMS; + const targetRefId = new ObjectId().toHexString(); + const targetRef = DeletionTargetRefBuilder.build(targetRefDomain, targetRefId); + const deletionPlannedAt = new Date(); + const modifiedCount = 0; + const deletedCount = 2; + const statistics = [DeletionLogStatisticBuilder.build(targetRefDomain, modifiedCount, deletedCount)]; + + return { targetRef, deletionPlannedAt, statistics }; + }; + + it('should set the id', () => { + const { targetRef, deletionPlannedAt, statistics } = setup(); + + const deletionRequestLog = new DeletionRequestLogResponse({ targetRef, deletionPlannedAt, statistics }); + + expect(deletionRequestLog.targetRef).toEqual(targetRef); + expect(deletionRequestLog.deletionPlannedAt).toEqual(deletionPlannedAt); + expect(deletionRequestLog.statistics).toEqual(statistics); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/controller/dto/deletion-request-log.response.ts b/apps/server/src/modules/deletion/controller/dto/deletion-request-log.response.ts new file mode 100644 index 00000000000..3619bebace8 --- /dev/null +++ b/apps/server/src/modules/deletion/controller/dto/deletion-request-log.response.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsOptional } from 'class-validator'; +import { DeletionLogStatistic, DeletionTargetRef } from '../../interface'; + +export class DeletionRequestLogResponse { + @ApiProperty() + targetRef: DeletionTargetRef; + + @ApiProperty() + deletionPlannedAt: Date; + + @ApiProperty() + @IsOptional() + statistics?: DeletionLogStatistic[]; + + constructor(response: DeletionRequestLogResponse) { + this.targetRef = response.targetRef; + this.deletionPlannedAt = response.deletionPlannedAt; + this.statistics = response.statistics; + } +} diff --git a/apps/server/src/modules/deletion/controller/dto/deletion-request.body.params.spec.ts b/apps/server/src/modules/deletion/controller/dto/deletion-request.body.params.spec.ts new file mode 100644 index 00000000000..787e44ec22a --- /dev/null +++ b/apps/server/src/modules/deletion/controller/dto/deletion-request.body.params.spec.ts @@ -0,0 +1,38 @@ +import { DeletionRequestBodyProps } from './deletion-request.body.params'; + +describe(DeletionRequestBodyProps.name, () => { + describe('constructor', () => { + describe('when passed properties', () => { + const setup = () => { + const deletionRequestBodyProps = new DeletionRequestBodyProps(); + + return { deletionRequestBodyProps }; + }; + + it('should be defined', () => { + const { deletionRequestBodyProps } = setup(); + expect(deletionRequestBodyProps).toBeDefined(); + }); + + it('deleteInMinutes should be a number with default value 43200', () => { + const { deletionRequestBodyProps } = setup(); + expect(deletionRequestBodyProps.deleteInMinutes).toBeDefined(); + expect(typeof deletionRequestBodyProps.deleteInMinutes).toBe('number'); + expect(deletionRequestBodyProps.deleteInMinutes).toBe(43200); + }); + + it('deleteInMinutes should be optional', () => { + const { deletionRequestBodyProps } = setup(); + deletionRequestBodyProps.deleteInMinutes = undefined; + expect(deletionRequestBodyProps.deleteInMinutes).toBeUndefined(); + }); + + it('deleteInMinutes should be a number when provided', () => { + const { deletionRequestBodyProps } = setup(); + const customDeleteInMinutes = 60; + deletionRequestBodyProps.deleteInMinutes = customDeleteInMinutes; + expect(deletionRequestBodyProps.deleteInMinutes).toBe(customDeleteInMinutes); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/controller/dto/deletion-request.body.params.ts b/apps/server/src/modules/deletion/controller/dto/deletion-request.body.params.ts new file mode 100644 index 00000000000..d772ba7bbb6 --- /dev/null +++ b/apps/server/src/modules/deletion/controller/dto/deletion-request.body.params.ts @@ -0,0 +1,21 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsNumber, IsOptional, Min } from 'class-validator'; +import { DeletionTargetRef } from '../../interface'; + +const MINUTES_OF_30_DAYS = 30 * 24 * 60; +export class DeletionRequestBodyProps { + @ApiProperty({ + required: true, + nullable: false, + }) + targetRef!: DeletionTargetRef; + + @IsNumber() + @Min(0) + @IsOptional() + @ApiPropertyOptional({ + required: true, + nullable: false, + }) + deleteInMinutes?: number = MINUTES_OF_30_DAYS; +} diff --git a/apps/server/src/modules/deletion/controller/dto/deletion-request.response.spec.ts b/apps/server/src/modules/deletion/controller/dto/deletion-request.response.spec.ts new file mode 100644 index 00000000000..d99dbf07c13 --- /dev/null +++ b/apps/server/src/modules/deletion/controller/dto/deletion-request.response.spec.ts @@ -0,0 +1,26 @@ +import { deletionRequestFactory } from '../../domain/testing'; +import { DeletionRequestResponse } from './deletion-request.response'; + +describe(DeletionRequestResponse.name, () => { + describe('constructor', () => { + describe('when passed properties', () => { + const setup = () => { + const deletionRequest = deletionRequestFactory.build(); + const deletionRequestResponse: DeletionRequestResponse = { + requestId: deletionRequest.id, + deletionPlannedAt: deletionRequest.deleteAfter, + }; + + return { deletionRequestResponse }; + }; + + it('should set the id', () => { + const { deletionRequestResponse } = setup(); + + const deletionRequest = new DeletionRequestResponse(deletionRequestResponse); + + expect(deletionRequest.requestId).toEqual(deletionRequestResponse.requestId); + }); + }); + }); +}); diff --git a/apps/server/src/modules/deletion/controller/dto/deletion-request.response.ts b/apps/server/src/modules/deletion/controller/dto/deletion-request.response.ts new file mode 100644 index 00000000000..8de61f52552 --- /dev/null +++ b/apps/server/src/modules/deletion/controller/dto/deletion-request.response.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class DeletionRequestResponse { + @ApiProperty() + requestId: string; + + @ApiProperty() + deletionPlannedAt: Date; + + constructor(response: DeletionRequestResponse) { + this.requestId = response.requestId; + this.deletionPlannedAt = response.deletionPlannedAt; + } +} diff --git a/apps/server/src/modules/deletion/controller/dto/index.ts b/apps/server/src/modules/deletion/controller/dto/index.ts new file mode 100644 index 00000000000..5f8cd514d35 --- /dev/null +++ b/apps/server/src/modules/deletion/controller/dto/index.ts @@ -0,0 +1,4 @@ +export * from './deletion-request.response'; +export * from './deletion-request.body.params'; +export * from './deletion-request-log.response'; +export * from './deletion-execution.params'; diff --git a/apps/server/src/modules/deletion/deletion-api.module.ts b/apps/server/src/modules/deletion/deletion-api.module.ts new file mode 100644 index 00000000000..08a64e343c1 --- /dev/null +++ b/apps/server/src/modules/deletion/deletion-api.module.ts @@ -0,0 +1,47 @@ +import { Module } from '@nestjs/common'; +import { DeletionModule } from '@modules/deletion'; +import { AccountModule } from '@modules/account'; +import { ClassModule } from '@modules/class'; +import { LearnroomModule } from '@modules/learnroom'; +import { FilesModule } from '@modules/files'; +import { PseudonymModule } from '@modules/pseudonym'; +import { LessonModule } from '@modules/lesson'; +import { TeamsModule } from '@modules/teams'; +import { UserModule } from '@modules/user'; +import { LoggerModule } from '@src/core/logger'; +import { AuthenticationModule } from '@modules/authentication'; +import { RocketChatUserModule } from '@modules/rocketchat-user'; +import { Configuration } from '@hpi-schul-cloud/commons'; +import { RocketChatModule } from '@modules/rocketchat'; +import { RegistrationPinModule } from '@modules/registration-pin'; +import { DeletionRequestsController } from './controller/deletion-requests.controller'; +import { DeletionExecutionsController } from './controller/deletion-executions.controller'; +import { DeletionRequestUc } from './uc'; + +@Module({ + imports: [ + DeletionModule, + AccountModule, + ClassModule, + LearnroomModule, + FilesModule, + LessonModule, + PseudonymModule, + TeamsModule, + UserModule, + LoggerModule, + AuthenticationModule, + RocketChatUserModule, + RegistrationPinModule, + RocketChatModule.forRoot({ + uri: Configuration.get('ROCKET_CHAT_URI') as string, + adminId: Configuration.get('ROCKET_CHAT_ADMIN_ID') as string, + adminToken: Configuration.get('ROCKET_CHAT_ADMIN_TOKEN') as string, + adminUser: Configuration.get('ROCKET_CHAT_ADMIN_USER') as string, + adminPassword: Configuration.get('ROCKET_CHAT_ADMIN_PASSWORD') as string, + }), + ], + controllers: [DeletionRequestsController, DeletionExecutionsController], + providers: [DeletionRequestUc], +}) +export class DeletionApiModule {} diff --git a/apps/server/src/modules/deletion/deletion.module.ts b/apps/server/src/modules/deletion/deletion.module.ts index 440a9418d70..3be026b827c 100644 --- a/apps/server/src/modules/deletion/deletion.module.ts +++ b/apps/server/src/modules/deletion/deletion.module.ts @@ -1,11 +1,19 @@ import { Module } from '@nestjs/common'; -import { LoggerModule } from '@src/core/logger'; +import { ConfigService } from '@nestjs/config'; import { DeletionRequestService } from './services/deletion-request.service'; import { DeletionRequestRepo } from './repo/deletion-request.repo'; +import { XApiKeyConfig } from '../authentication/config/x-api-key.config'; +import { DeletionLogService } from './services/deletion-log.service'; +import { DeletionLogRepo } from './repo'; @Module({ - imports: [LoggerModule], - providers: [DeletionRequestService, DeletionRequestRepo], - exports: [DeletionRequestService], + providers: [ + DeletionRequestRepo, + DeletionLogRepo, + ConfigService, + DeletionLogService, + DeletionRequestService, + ], + exports: [DeletionRequestService, DeletionLogService], }) export class DeletionModule {} diff --git a/apps/server/src/modules/deletion/domain/deletion-log.do.spec.ts b/apps/server/src/modules/deletion/domain/deletion-log.do.spec.ts index 9117ded29c5..fd320ff79cb 100644 --- a/apps/server/src/modules/deletion/domain/deletion-log.do.spec.ts +++ b/apps/server/src/modules/deletion/domain/deletion-log.do.spec.ts @@ -1,8 +1,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { deletionLogFactory } from './testing/factory/deletion-log.factory'; import { DeletionLog } from './deletion-log.do'; -import { DeletionOperationModel } from './types/deletion-operation-model.enum'; -import { DeletionDomainModel } from './types/deletion-domain-model.enum'; +import { DeletionOperationModel, DeletionDomainModel } from './types'; describe(DeletionLog.name, () => { describe('constructor', () => { @@ -41,6 +40,7 @@ describe(DeletionLog.name, () => { modifiedCount: 0, deletedCount: 1, deletionRequestId: new ObjectId().toHexString(), + performedAt: new Date(), createdAt: new Date(), updatedAt: new Date(), }; @@ -59,6 +59,7 @@ describe(DeletionLog.name, () => { modifiedCount: deletionLogDo.modifiedCount, deletedCount: deletionLogDo.deletedCount, deletionRequestId: deletionLogDo.deletionRequestId, + performedAt: deletionLogDo.performedAt, createdAt: deletionLogDo.createdAt, updatedAt: deletionLogDo.updatedAt, }; diff --git a/apps/server/src/modules/deletion/domain/deletion-log.do.ts b/apps/server/src/modules/deletion/domain/deletion-log.do.ts index 73e62b46055..c5ca2b652d1 100644 --- a/apps/server/src/modules/deletion/domain/deletion-log.do.ts +++ b/apps/server/src/modules/deletion/domain/deletion-log.do.ts @@ -1,7 +1,6 @@ import { EntityId } from '@shared/domain/types'; import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; -import { DeletionDomainModel } from './types/deletion-domain-model.enum'; -import { DeletionOperationModel } from './types/deletion-operation-model.enum'; +import { DeletionDomainModel, DeletionOperationModel } from './types'; export interface DeletionLogProps extends AuthorizableObject { createdAt?: Date; @@ -11,6 +10,7 @@ export interface DeletionLogProps extends AuthorizableObject { modifiedCount?: number; deletedCount?: number; deletionRequestId?: EntityId; + performedAt?: Date; } export class DeletionLog extends DomainObject { @@ -41,4 +41,8 @@ export class DeletionLog extends DomainObject { get deletionRequestId(): EntityId | undefined { return this.props.deletionRequestId; } + + get performedAt(): Date | undefined { + return this.props.performedAt; + } } diff --git a/apps/server/src/modules/deletion/domain/deletion-request.do.spec.ts b/apps/server/src/modules/deletion/domain/deletion-request.do.spec.ts index 3c0eb608c87..1ffb7d3f906 100644 --- a/apps/server/src/modules/deletion/domain/deletion-request.do.spec.ts +++ b/apps/server/src/modules/deletion/domain/deletion-request.do.spec.ts @@ -1,8 +1,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { DeletionRequest } from './deletion-request.do'; -import { DeletionDomainModel } from './types/deletion-domain-model.enum'; +import { DeletionDomainModel, DeletionStatusModel } from './types'; import { deletionRequestFactory } from './testing/factory/deletion-request.factory'; -import { DeletionStatusModel } from './types/deletion-status-model.enum'; describe(DeletionRequest.name, () => { describe('constructor', () => { diff --git a/apps/server/src/modules/deletion/domain/deletion-request.do.ts b/apps/server/src/modules/deletion/domain/deletion-request.do.ts index e1a8b289ef0..76b7f0371ba 100644 --- a/apps/server/src/modules/deletion/domain/deletion-request.do.ts +++ b/apps/server/src/modules/deletion/domain/deletion-request.do.ts @@ -1,7 +1,6 @@ import { EntityId } from '@shared/domain/types'; import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; -import { DeletionDomainModel } from './types/deletion-domain-model.enum'; -import { DeletionStatusModel } from './types/deletion-status-model.enum'; +import { DeletionDomainModel, DeletionStatusModel } from './types'; export interface DeletionRequestProps extends AuthorizableObject { createdAt?: Date; diff --git a/apps/server/src/modules/deletion/domain/testing/factory/deletion-log.factory.ts b/apps/server/src/modules/deletion/domain/testing/factory/deletion-log.factory.ts index d83b2f44c8a..2a3d0529866 100644 --- a/apps/server/src/modules/deletion/domain/testing/factory/deletion-log.factory.ts +++ b/apps/server/src/modules/deletion/domain/testing/factory/deletion-log.factory.ts @@ -1,8 +1,7 @@ import { DoBaseFactory } from '@shared/testing'; import { ObjectId } from '@mikro-orm/mongodb'; import { DeletionLog, DeletionLogProps } from '../../deletion-log.do'; -import { DeletionOperationModel } from '../../types/deletion-operation-model.enum'; -import { DeletionDomainModel } from '../../types/deletion-domain-model.enum'; +import { DeletionOperationModel, DeletionDomainModel } from '../../types'; export const deletionLogFactory = DoBaseFactory.define(DeletionLog, () => { return { @@ -12,6 +11,7 @@ export const deletionLogFactory = DoBaseFactory.define { withUserIds(id: string): this { diff --git a/apps/server/src/modules/deletion/domain/testing/factory/index.ts b/apps/server/src/modules/deletion/domain/testing/factory/index.ts new file mode 100644 index 00000000000..9bd93ea6254 --- /dev/null +++ b/apps/server/src/modules/deletion/domain/testing/factory/index.ts @@ -0,0 +1,2 @@ +export * from './deletion-log.factory'; +export * from './deletion-request.factory'; diff --git a/apps/server/src/modules/deletion/domain/testing/index.ts b/apps/server/src/modules/deletion/domain/testing/index.ts new file mode 100644 index 00000000000..d847d7abce6 --- /dev/null +++ b/apps/server/src/modules/deletion/domain/testing/index.ts @@ -0,0 +1 @@ +export * from './factory'; diff --git a/apps/server/src/modules/deletion/domain/types/index.ts b/apps/server/src/modules/deletion/domain/types/index.ts new file mode 100644 index 00000000000..d1f4de8eb6b --- /dev/null +++ b/apps/server/src/modules/deletion/domain/types/index.ts @@ -0,0 +1,3 @@ +export * from './deletion-domain-model.enum'; +export * from './deletion-operation-model.enum'; +export * from './deletion-status-model.enum'; diff --git a/apps/server/src/modules/deletion/entity/deletion-log.entity.spec.ts b/apps/server/src/modules/deletion/entity/deletion-log.entity.spec.ts index 4f9f098cbb3..c1b5f5f7184 100644 --- a/apps/server/src/modules/deletion/entity/deletion-log.entity.spec.ts +++ b/apps/server/src/modules/deletion/entity/deletion-log.entity.spec.ts @@ -1,8 +1,7 @@ import { setupEntities } from '@shared/testing'; import { ObjectId } from '@mikro-orm/mongodb'; import { DeletionLogEntity } from './deletion-log.entity'; -import { DeletionOperationModel } from '../domain/types/deletion-operation-model.enum'; -import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; +import { DeletionOperationModel, DeletionDomainModel } from '../domain/types'; describe(DeletionLogEntity.name, () => { beforeAll(async () => { @@ -19,6 +18,7 @@ describe(DeletionLogEntity.name, () => { modifiedCount: 0, deletedCount: 1, deletionRequestId: new ObjectId(), + performedAt: new Date(), createdAt: new Date(), updatedAt: new Date(), }; @@ -49,6 +49,7 @@ describe(DeletionLogEntity.name, () => { modifiedCount: entity.modifiedCount, deletedCount: entity.deletedCount, deletionRequestId: entity.deletionRequestId, + performedAt: entity.performedAt, createdAt: entity.createdAt, updatedAt: entity.updatedAt, }; diff --git a/apps/server/src/modules/deletion/entity/deletion-log.entity.ts b/apps/server/src/modules/deletion/entity/deletion-log.entity.ts index 8a9d2bab025..38c13aaf78d 100644 --- a/apps/server/src/modules/deletion/entity/deletion-log.entity.ts +++ b/apps/server/src/modules/deletion/entity/deletion-log.entity.ts @@ -1,8 +1,8 @@ -import { Entity, Property } from '@mikro-orm/core'; -import { BaseEntityWithTimestamps, EntityId } from '@shared/domain'; -import { ObjectId } from '@mikro-orm/mongodb'; -import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; -import { DeletionOperationModel } from '../domain/types/deletion-operation-model.enum'; +import { Entity, Index, Property } from '@mikro-orm/core'; +import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; +import { EntityId } from '@shared/domain'; +import { ObjectId } from 'bson'; +import { DeletionDomainModel, DeletionOperationModel } from '../domain/types'; export interface DeletionLogEntityProps { id?: EntityId; @@ -11,6 +11,7 @@ export interface DeletionLogEntityProps { modifiedCount?: number; deletedCount?: number; deletionRequestId?: ObjectId; + performedAt?: Date; createdAt?: Date; updatedAt?: Date; } @@ -32,6 +33,10 @@ export class DeletionLogEntity extends BaseEntityWithTimestamps { @Property({ nullable: true }) deletionRequestId?: ObjectId; + @Property({ nullable: true }) + @Index({ options: { expireAfterSeconds: 7776000 } }) + performedAt?: Date; + constructor(props: DeletionLogEntityProps) { super(); if (props.id !== undefined) { @@ -63,5 +68,9 @@ export class DeletionLogEntity extends BaseEntityWithTimestamps { if (props.updatedAt !== undefined) { this.updatedAt = props.updatedAt; } + + if (props.performedAt !== undefined) { + this.performedAt = props.performedAt; + } } } diff --git a/apps/server/src/modules/deletion/entity/deletion-request.entity.spec.ts b/apps/server/src/modules/deletion/entity/deletion-request.entity.spec.ts index 6a0e416d580..d4e8440bfa0 100644 --- a/apps/server/src/modules/deletion/entity/deletion-request.entity.spec.ts +++ b/apps/server/src/modules/deletion/entity/deletion-request.entity.spec.ts @@ -1,8 +1,7 @@ import { setupEntities } from '@shared/testing'; import { ObjectId } from '@mikro-orm/mongodb'; -import { DeletionRequestEntity } from '@src/modules/deletion/entity/deletion-request.entity'; -import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; -import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; +import { DeletionDomainModel, DeletionStatusModel } from '../domain/types'; +import { DeletionRequestEntity } from '.'; describe(DeletionRequestEntity.name, () => { beforeAll(async () => { diff --git a/apps/server/src/modules/deletion/entity/deletion-request.entity.ts b/apps/server/src/modules/deletion/entity/deletion-request.entity.ts index 150fed4d91e..56567155f26 100644 --- a/apps/server/src/modules/deletion/entity/deletion-request.entity.ts +++ b/apps/server/src/modules/deletion/entity/deletion-request.entity.ts @@ -1,9 +1,9 @@ -import { Entity, Index, Property } from '@mikro-orm/core'; +import { Entity, Index, Property, Unique } from '@mikro-orm/core'; import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; import { EntityId } from '@shared/domain'; -import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; -import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; +import { DeletionDomainModel, DeletionStatusModel } from '../domain/types'; +const SECONDS_OF_90_DAYS = 90 * 24 * 60 * 60; export interface DeletionRequestEntityProps { id?: EntityId; targetRefDomain: DeletionDomainModel; @@ -15,19 +15,19 @@ export interface DeletionRequestEntityProps { } @Entity({ tableName: 'deletionrequests' }) -@Index({ properties: ['targetRefId', 'targetRefDomain'] }) +@Unique({ properties: ['targetRefId', 'targetRefDomain'] }) export class DeletionRequestEntity extends BaseEntityWithTimestamps { @Property() + @Index({ options: { expireAfterSeconds: SECONDS_OF_90_DAYS } }) deleteAfter: Date; @Property() - targetRefId: EntityId; + targetRefId!: EntityId; @Property() targetRefDomain: DeletionDomainModel; @Property() - @Index() status: DeletionStatusModel; constructor(props: DeletionRequestEntityProps) { diff --git a/apps/server/src/modules/deletion/entity/testing/factory/deletion-log.entity.factory.ts b/apps/server/src/modules/deletion/entity/testing/factory/deletion-log.entity.factory.ts index 897fba6820a..93ed4198f15 100644 --- a/apps/server/src/modules/deletion/entity/testing/factory/deletion-log.entity.factory.ts +++ b/apps/server/src/modules/deletion/entity/testing/factory/deletion-log.entity.factory.ts @@ -1,8 +1,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { BaseFactory } from '@shared/testing'; import { DeletionLogEntity, DeletionLogEntityProps } from '../../deletion-log.entity'; -import { DeletionOperationModel } from '../../../domain/types/deletion-operation-model.enum'; -import { DeletionDomainModel } from '../../../domain/types/deletion-domain-model.enum'; +import { DeletionOperationModel, DeletionDomainModel } from '../../../domain/types'; export const deletionLogEntityFactory = BaseFactory.define( DeletionLogEntity, diff --git a/apps/server/src/modules/deletion/entity/testing/factory/deletion-request.entity.factory.ts b/apps/server/src/modules/deletion/entity/testing/factory/deletion-request.entity.factory.ts index 3ccba779e3e..cc65bb5f4dc 100644 --- a/apps/server/src/modules/deletion/entity/testing/factory/deletion-request.entity.factory.ts +++ b/apps/server/src/modules/deletion/entity/testing/factory/deletion-request.entity.factory.ts @@ -1,8 +1,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { BaseFactory } from '@shared/testing'; -import { DeletionStatusModel } from '../../../domain/types/deletion-status-model.enum'; +import { DeletionStatusModel, DeletionDomainModel } from '../../../domain/types'; import { DeletionRequestEntity, DeletionRequestEntityProps } from '../../deletion-request.entity'; -import { DeletionDomainModel } from '../../../domain/types/deletion-domain-model.enum'; export const deletionRequestEntityFactory = BaseFactory.define( DeletionRequestEntity, diff --git a/apps/server/src/modules/deletion/entity/testing/factory/index.ts b/apps/server/src/modules/deletion/entity/testing/factory/index.ts new file mode 100644 index 00000000000..6d9e8115468 --- /dev/null +++ b/apps/server/src/modules/deletion/entity/testing/factory/index.ts @@ -0,0 +1,2 @@ +export * from './deletion-log.entity.factory'; +export * from './deletion-request.entity.factory'; diff --git a/apps/server/src/modules/deletion/entity/testing/index.ts b/apps/server/src/modules/deletion/entity/testing/index.ts new file mode 100644 index 00000000000..d847d7abce6 --- /dev/null +++ b/apps/server/src/modules/deletion/entity/testing/index.ts @@ -0,0 +1 @@ +export * from './factory'; diff --git a/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-detail.interface.ts b/apps/server/src/modules/deletion/interface/batch-deletion-summary-detail.interface.ts similarity index 88% rename from apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-detail.interface.ts rename to apps/server/src/modules/deletion/interface/batch-deletion-summary-detail.interface.ts index 4fe99c13fad..1ded1a0a263 100644 --- a/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-detail.interface.ts +++ b/apps/server/src/modules/deletion/interface/batch-deletion-summary-detail.interface.ts @@ -1,4 +1,4 @@ -import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from '../../services'; +import { QueueDeletionRequestInput, QueueDeletionRequestOutput } from '../services'; export interface BatchDeletionSummaryDetail { input: QueueDeletionRequestInput; diff --git a/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-overall-status.enum.ts b/apps/server/src/modules/deletion/interface/batch-deletion-summary-overall-status.enum.ts similarity index 100% rename from apps/server/src/modules/deletion/uc/interface/batch-deletion-summary-overall-status.enum.ts rename to apps/server/src/modules/deletion/interface/batch-deletion-summary-overall-status.enum.ts diff --git a/apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts b/apps/server/src/modules/deletion/interface/batch-deletion-summary.interface.ts similarity index 100% rename from apps/server/src/modules/deletion/uc/interface/batch-deletion-summary.interface.ts rename to apps/server/src/modules/deletion/interface/batch-deletion-summary.interface.ts diff --git a/apps/server/src/modules/deletion/uc/interface/index.ts b/apps/server/src/modules/deletion/interface/index.ts similarity index 100% rename from apps/server/src/modules/deletion/uc/interface/index.ts rename to apps/server/src/modules/deletion/interface/index.ts diff --git a/apps/server/src/modules/deletion/interface/interfaces.ts b/apps/server/src/modules/deletion/interface/interfaces.ts new file mode 100644 index 00000000000..b615254e608 --- /dev/null +++ b/apps/server/src/modules/deletion/interface/interfaces.ts @@ -0,0 +1,13 @@ +import { EntityId } from '@shared/domain'; +import { DeletionDomainModel } from '../domain/types'; + +export interface DeletionTargetRef { + domain: DeletionDomainModel; + id: EntityId; +} + +export interface DeletionLogStatistic { + domain: DeletionDomainModel; + modifiedCount?: number; + deletedCount?: number; +} diff --git a/apps/server/src/modules/deletion/repo/deletion-log.repo.spec.ts b/apps/server/src/modules/deletion/repo/deletion-log.repo.spec.ts index bba32408e84..1ab1e515acc 100644 --- a/apps/server/src/modules/deletion/repo/deletion-log.repo.spec.ts +++ b/apps/server/src/modules/deletion/repo/deletion-log.repo.spec.ts @@ -64,6 +64,7 @@ describe(DeletionLogRepo.name, () => { modifiedCount: domainObject.modifiedCount, deletedCount: domainObject.deletedCount, deletionRequestId: domainObject.deletionRequestId, + performedAt: domainObject.performedAt, createdAt: domainObject.createdAt, updatedAt: domainObject.updatedAt, }; @@ -95,6 +96,7 @@ describe(DeletionLogRepo.name, () => { modifiedCount: entity.modifiedCount, deletedCount: entity.deletedCount, deletionRequestId: entity.deletionRequestId?.toHexString(), + performedAt: entity.performedAt, createdAt: entity.createdAt, updatedAt: entity.updatedAt, }; @@ -149,6 +151,7 @@ describe(DeletionLogRepo.name, () => { domain: deletionLogEntity1.domain, operation: deletionLogEntity1.operation, deletionRequestId: deletionLogEntity1.deletionRequestId?.toHexString(), + performedAt: deletionLogEntity1.performedAt, modifiedCount: deletionLogEntity1.modifiedCount, deletedCount: deletionLogEntity1.deletedCount, createdAt: deletionLogEntity1.createdAt, @@ -159,6 +162,7 @@ describe(DeletionLogRepo.name, () => { domain: deletionLogEntity2.domain, operation: deletionLogEntity2.operation, deletionRequestId: deletionLogEntity2.deletionRequestId?.toHexString(), + performedAt: deletionLogEntity2.performedAt, modifiedCount: deletionLogEntity2.modifiedCount, deletedCount: deletionLogEntity2.deletedCount, createdAt: deletionLogEntity2.createdAt, diff --git a/apps/server/src/modules/deletion/repo/deletion-request-scope.ts b/apps/server/src/modules/deletion/repo/deletion-request-scope.ts index 202bc09a887..6ac8351ce13 100644 --- a/apps/server/src/modules/deletion/repo/deletion-request-scope.ts +++ b/apps/server/src/modules/deletion/repo/deletion-request-scope.ts @@ -1,6 +1,6 @@ import { Scope } from '@shared/repo'; import { DeletionRequestEntity } from '../entity'; -import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; +import { DeletionStatusModel } from '../domain/types'; export class DeletionRequestScope extends Scope { byDeleteAfter(currentDate: Date): DeletionRequestScope { diff --git a/apps/server/src/modules/deletion/repo/deletion-request.repo.spec.ts b/apps/server/src/modules/deletion/repo/deletion-request.repo.spec.ts index c3018180218..4bd0c86c5cc 100644 --- a/apps/server/src/modules/deletion/repo/deletion-request.repo.spec.ts +++ b/apps/server/src/modules/deletion/repo/deletion-request.repo.spec.ts @@ -9,7 +9,7 @@ import { DeletionRequestEntity } from '../entity'; import { DeletionRequest } from '../domain/deletion-request.do'; import { deletionRequestEntityFactory } from '../entity/testing/factory/deletion-request.entity.factory'; import { deletionRequestFactory } from '../domain/testing/factory/deletion-request.factory'; -import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; +import { DeletionStatusModel } from '../domain/types'; describe(DeletionRequestRepo.name, () => { let module: TestingModule; diff --git a/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.spec.ts b/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.spec.ts index a5823f5ce32..3937710336a 100644 --- a/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.spec.ts +++ b/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.spec.ts @@ -16,6 +16,7 @@ describe(DeletionLogMapper.name, () => { domain: entity.domain, operation: entity.operation, deletionRequestId: entity.deletionRequestId?.toHexString(), + performedAt: entity.performedAt, modifiedCount: entity.modifiedCount, deletedCount: entity.deletedCount, createdAt: entity.createdAt, @@ -54,6 +55,7 @@ describe(DeletionLogMapper.name, () => { domain: entity.domain, operation: entity.operation, deletionRequestId: entity.deletionRequestId?.toHexString(), + performedAt: entity.performedAt, modifiedCount: entity.modifiedCount, deletedCount: entity.deletedCount, createdAt: entity.createdAt, @@ -92,6 +94,7 @@ describe(DeletionLogMapper.name, () => { domain: domainObject.domain, operation: domainObject.operation, deletionRequestId: new ObjectId(domainObject.deletionRequestId), + performedAt: domainObject.performedAt, modifiedCount: domainObject.modifiedCount, deletedCount: domainObject.deletedCount, createdAt: domainObject.createdAt, @@ -140,6 +143,7 @@ describe(DeletionLogMapper.name, () => { domain: domainObject.domain, operation: domainObject.operation, deletionRequestId: new ObjectId(domainObject.deletionRequestId), + performedAt: domainObject.performedAt, modifiedCount: domainObject.modifiedCount, deletedCount: domainObject.deletedCount, createdAt: domainObject.createdAt, diff --git a/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.ts b/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.ts index 820cd9d87c0..bb10fe0ac93 100644 --- a/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.ts +++ b/apps/server/src/modules/deletion/repo/mapper/deletion-log.mapper.ts @@ -13,6 +13,7 @@ export class DeletionLogMapper { modifiedCount: entity.modifiedCount, deletedCount: entity.deletedCount, deletionRequestId: entity.deletionRequestId?.toHexString(), + performedAt: entity.performedAt, }); } @@ -26,6 +27,7 @@ export class DeletionLogMapper { modifiedCount: domainObject.modifiedCount, deletedCount: domainObject.deletedCount, deletionRequestId: new ObjectId(domainObject.deletionRequestId), + performedAt: domainObject.performedAt, }); } diff --git a/apps/server/src/modules/deletion/services/deletion-log.service.spec.ts b/apps/server/src/modules/deletion/services/deletion-log.service.spec.ts index 21522e5e924..7b63e866b14 100644 --- a/apps/server/src/modules/deletion/services/deletion-log.service.spec.ts +++ b/apps/server/src/modules/deletion/services/deletion-log.service.spec.ts @@ -3,9 +3,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { setupEntities } from '@shared/testing'; import { ObjectId } from '@mikro-orm/mongodb'; import { DeletionLogRepo } from '../repo'; -import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; +import { DeletionDomainModel, DeletionOperationModel } from '../domain/types'; import { DeletionLogService } from './deletion-log.service'; -import { DeletionOperationModel } from '../domain/types/deletion-operation-model.enum'; import { deletionLogFactory } from '../domain/testing/factory/deletion-log.factory'; describe(DeletionLogService.name, () => { @@ -64,6 +63,7 @@ describe(DeletionLogService.name, () => { expect(deletionLogRepo.create).toHaveBeenCalledWith( expect.objectContaining({ id: expect.any(String), + performedAt: expect.any(Date), deletionRequestId, domain, operation, diff --git a/apps/server/src/modules/deletion/services/deletion-log.service.ts b/apps/server/src/modules/deletion/services/deletion-log.service.ts index 937d422ebb3..1fce142decb 100644 --- a/apps/server/src/modules/deletion/services/deletion-log.service.ts +++ b/apps/server/src/modules/deletion/services/deletion-log.service.ts @@ -3,8 +3,7 @@ import { EntityId } from '@shared/domain'; import { ObjectId } from '@mikro-orm/mongodb'; import { DeletionLogRepo } from '../repo'; import { DeletionLog } from '../domain/deletion-log.do'; -import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; -import { DeletionOperationModel } from '../domain/types/deletion-operation-model.enum'; +import { DeletionDomainModel, DeletionOperationModel } from '../domain/types'; @Injectable() export class DeletionLogService { @@ -19,6 +18,7 @@ export class DeletionLogService { ): Promise { const newDeletionLog = new DeletionLog({ id: new ObjectId().toHexString(), + performedAt: new Date(), domain, deletionRequestId, operation, diff --git a/apps/server/src/modules/deletion/services/deletion-request.service.spec.ts b/apps/server/src/modules/deletion/services/deletion-request.service.spec.ts index fcccfc433db..99763882064 100644 --- a/apps/server/src/modules/deletion/services/deletion-request.service.spec.ts +++ b/apps/server/src/modules/deletion/services/deletion-request.service.spec.ts @@ -5,8 +5,7 @@ import { setupEntities } from '@shared/testing'; import { DeletionRequestService } from './deletion-request.service'; import { DeletionRequestRepo } from '../repo'; import { deletionRequestFactory } from '../domain/testing/factory/deletion-request.factory'; -import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; -import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; +import { DeletionDomainModel, DeletionStatusModel } from '../domain/types'; describe(DeletionRequestService.name, () => { let module: TestingModule; diff --git a/apps/server/src/modules/deletion/services/deletion-request.service.ts b/apps/server/src/modules/deletion/services/deletion-request.service.ts index 82b65521d68..206cb01a7a1 100644 --- a/apps/server/src/modules/deletion/services/deletion-request.service.ts +++ b/apps/server/src/modules/deletion/services/deletion-request.service.ts @@ -3,8 +3,7 @@ import { EntityId } from '@shared/domain'; import { ObjectId } from '@mikro-orm/mongodb'; import { DeletionRequestRepo } from '../repo/deletion-request.repo'; import { DeletionRequest } from '../domain/deletion-request.do'; -import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; -import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; +import { DeletionDomainModel, DeletionStatusModel } from '../domain/types'; @Injectable() export class DeletionRequestService { diff --git a/apps/server/src/modules/deletion/uc/batch-deletion.uc.spec.ts b/apps/server/src/modules/deletion/uc/batch-deletion.uc.spec.ts index 7292f36efb0..c3a391034fe 100644 --- a/apps/server/src/modules/deletion/uc/batch-deletion.uc.spec.ts +++ b/apps/server/src/modules/deletion/uc/batch-deletion.uc.spec.ts @@ -8,8 +8,8 @@ import { QueueDeletionRequestOutputBuilder, ReferencesService, } from '../services'; -import { BatchDeletionSummaryDetail, BatchDeletionSummaryOverallStatus } from './interface'; -import { BatchDeletionSummaryDetailBuilder } from './builder'; +import { BatchDeletionSummaryDetail, BatchDeletionSummaryOverallStatus } from '../interface'; +import { BatchDeletionSummaryDetailBuilder } from '../builder'; import { BatchDeletionUc } from './batch-deletion.uc'; describe(BatchDeletionUc.name, () => { diff --git a/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts index 258b1b53f65..1b3c757a130 100644 --- a/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts +++ b/apps/server/src/modules/deletion/uc/batch-deletion.uc.ts @@ -1,12 +1,12 @@ import { Injectable } from '@nestjs/common'; -import { BatchDeletionSummaryBuilder, BatchDeletionSummaryDetailBuilder } from './builder'; +import { BatchDeletionSummaryBuilder, BatchDeletionSummaryDetailBuilder } from '../builder'; import { ReferencesService, BatchDeletionService, QueueDeletionRequestInput, QueueDeletionRequestInputBuilder, } from '../services'; -import { BatchDeletionSummary, BatchDeletionSummaryOverallStatus } from './interface'; +import { BatchDeletionSummary, BatchDeletionSummaryOverallStatus } from '../interface'; @Injectable() export class BatchDeletionUc { diff --git a/apps/server/src/modules/deletion/uc/builder/deletion-target-ref.builder.ts b/apps/server/src/modules/deletion/uc/builder/deletion-target-ref.builder.ts deleted file mode 100644 index 91f3385a9aa..00000000000 --- a/apps/server/src/modules/deletion/uc/builder/deletion-target-ref.builder.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { EntityId } from '@shared/domain'; -import { DeletionDomainModel } from '../../domain/types/deletion-domain-model.enum'; -import { DeletionTargetRef } from '../interface'; - -export class DeletionTargetRefBuilder { - static build(targetRefDomain: DeletionDomainModel, targetRefId: EntityId): DeletionTargetRef { - const deletionTargetRef = { targetRefDomain, targetRefId }; - - return deletionTargetRef; - } -} diff --git a/apps/server/src/modules/deletion/uc/builder/index.ts b/apps/server/src/modules/deletion/uc/builder/index.ts deleted file mode 100644 index 46733980f94..00000000000 --- a/apps/server/src/modules/deletion/uc/builder/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './batch-deletion-summary-detail.builder'; -export * from './batch-deletion-summary.builder'; diff --git a/apps/server/src/modules/deletion/uc/deletion-request.uc.spec.ts b/apps/server/src/modules/deletion/uc/deletion-request.uc.spec.ts index 69ec72a0db5..9e2f2604f16 100644 --- a/apps/server/src/modules/deletion/uc/deletion-request.uc.spec.ts +++ b/apps/server/src/modules/deletion/uc/deletion-request.uc.spec.ts @@ -1,26 +1,27 @@ import { Test, TestingModule } from '@nestjs/testing'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { setupEntities, userDoFactory } from '@shared/testing'; -import { AccountService } from '@modules/account/services'; +import { AccountService } from '@modules/account'; import { ClassService } from '@modules/class'; -import { CourseGroupService, CourseService } from '@modules/learnroom/service'; -import { FilesService } from '@modules/files/service'; -import { LessonService } from '@modules/lesson/service'; +import { CourseGroupService, CourseService } from '@modules/learnroom'; +import { FilesService } from '@modules/files'; +import { LessonService } from '@modules/lesson'; import { PseudonymService } from '@modules/pseudonym'; import { TeamService } from '@modules/teams'; import { UserService } from '@modules/user'; import { RocketChatService } from '@modules/rocketchat'; -import { rocketChatUserFactory } from '@modules/rocketchat-user/domain/testing'; -import { RocketChatUser, RocketChatUserService } from '@modules/rocketchat-user'; +import { RocketChatUser, RocketChatUserService, rocketChatUserFactory } from '@modules/rocketchat-user'; +import { LegacyLogger } from '@src/core/logger'; +import { ObjectId } from 'bson'; import { RegistrationPinService } from '@modules/registration-pin'; -import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; +import { DeletionDomainModel, DeletionStatusModel } from '../domain/types'; import { DeletionLogService } from '../services/deletion-log.service'; import { DeletionRequestService } from '../services'; import { DeletionRequestUc } from './deletion-request.uc'; import { deletionRequestFactory } from '../domain/testing/factory/deletion-request.factory'; -import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; -import { deletionLogFactory } from '../domain/testing/factory/deletion-log.factory'; -import { DeletionRequestLog, DeletionRequestProps } from './interface'; +import { deletionLogFactory } from '../domain/testing'; +import { DeletionRequestBodyProps } from '../controller/dto'; +import { DeletionRequestLogResponseBuilder, DeletionTargetRefBuilder, DeletionLogStatisticBuilder } from '../builder'; describe(DeletionRequestUc.name, () => { let module: TestingModule; @@ -96,6 +97,10 @@ describe(DeletionRequestUc.name, () => { provide: RocketChatService, useValue: createMock(), }, + { + provide: LegacyLogger, + useValue: createMock(), + }, { provide: RegistrationPinService, useValue: createMock(), @@ -121,14 +126,17 @@ describe(DeletionRequestUc.name, () => { await setupEntities(); }); + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('createDeletionRequest', () => { describe('when creating a deletionRequest', () => { const setup = () => { - jest.clearAllMocks(); - const deletionRequestToCreate: DeletionRequestProps = { + const deletionRequestToCreate: DeletionRequestBodyProps = { targetRef: { - targetRefDoamin: DeletionDomainModel.USER, - targetRefId: '653e4833cc39e5907a1e18d2', + domain: DeletionDomainModel.USER, + id: new ObjectId().toHexString(), }, deleteInMinutes: 1440, }; @@ -146,8 +154,8 @@ describe(DeletionRequestUc.name, () => { await uc.createDeletionRequest(deletionRequestToCreate); expect(deletionRequestService.createDeletionRequest).toHaveBeenCalledWith( - deletionRequestToCreate.targetRef.targetRefId, - deletionRequestToCreate.targetRef.targetRefDoamin, + deletionRequestToCreate.targetRef.id, + deletionRequestToCreate.targetRef.domain, deletionRequestToCreate.deleteInMinutes ); }); @@ -173,7 +181,6 @@ describe(DeletionRequestUc.name, () => { describe('executeDeletionRequests', () => { describe('when executing deletionRequests', () => { const setup = () => { - jest.clearAllMocks(); const deletionRequestToExecute = deletionRequestFactory.build({ deleteAfter: new Date('2023-01-01') }); const user = userDoFactory.buildWithId(); const rocketChatUser: RocketChatUser = rocketChatUserFactory.build({ @@ -381,13 +388,12 @@ describe(DeletionRequestUc.name, () => { await uc.executeDeletionRequests(); - expect(deletionLogService.createDeletionLog).toHaveBeenCalledTimes(9); + expect(deletionLogService.createDeletionLog).toHaveBeenCalledTimes(10); }); }); describe('when an error occurred', () => { const setup = () => { - jest.clearAllMocks(); const deletionRequestToExecute = deletionRequestFactory.build({ deleteAfter: new Date('2023-01-01') }); classService.deleteUserDataFromClasses.mockResolvedValueOnce(1); @@ -420,41 +426,29 @@ describe(DeletionRequestUc.name, () => { describe('findById', () => { describe('when searching for logs for deletionRequest which was executed', () => { const setup = () => { - jest.clearAllMocks(); const deletionRequestExecuted = deletionRequestFactory.build({ status: DeletionStatusModel.SUCCESS }); - const deletionLogExecuted1 = deletionLogFactory.build({ deletionRequestId: deletionRequestExecuted.id }); - const deletionLogExecuted2 = deletionLogFactory.build({ - deletionRequestId: deletionRequestExecuted.id, - domain: DeletionDomainModel.ACCOUNT, - modifiedCount: 0, - deletedCount: 1, - }); + const deletionLogExecuted = deletionLogFactory.build({ deletionRequestId: deletionRequestExecuted.id }); - const executedDeletionRequestSummary: DeletionRequestLog = { - targetRef: { - targetRefDomain: deletionRequestExecuted.targetRefDomain, - targetRefId: deletionRequestExecuted.targetRefId, - }, - deletionPlannedAt: deletionRequestExecuted.deleteAfter, - statistics: [ - { - domain: deletionLogExecuted1.domain, - modifiedCount: deletionLogExecuted1.modifiedCount, - deletedCount: deletionLogExecuted1.deletedCount, - }, - { - domain: deletionLogExecuted2.domain, - modifiedCount: deletionLogExecuted2.modifiedCount, - deletedCount: deletionLogExecuted2.deletedCount, - }, - ], - }; + const targetRef = DeletionTargetRefBuilder.build( + deletionRequestExecuted.targetRefDomain, + deletionRequestExecuted.targetRefId + ); + const statistics = DeletionLogStatisticBuilder.build( + deletionLogExecuted.domain, + deletionLogExecuted.modifiedCount, + deletionLogExecuted.deletedCount + ); + + const executedDeletionRequestSummary = DeletionRequestLogResponseBuilder.build( + targetRef, + deletionRequestExecuted.deleteAfter, + [statistics] + ); return { deletionRequestExecuted, executedDeletionRequestSummary, - deletionLogExecuted1, - deletionLogExecuted2, + deletionLogExecuted, }; }; @@ -470,11 +464,10 @@ describe(DeletionRequestUc.name, () => { }); it('should return object with summary of deletionRequest', async () => { - const { deletionRequestExecuted, deletionLogExecuted1, deletionLogExecuted2, executedDeletionRequestSummary } = - setup(); + const { deletionRequestExecuted, deletionLogExecuted, executedDeletionRequestSummary } = setup(); deletionRequestService.findById.mockResolvedValueOnce(deletionRequestExecuted); - deletionLogService.findByDeletionRequestId.mockResolvedValueOnce([deletionLogExecuted1, deletionLogExecuted2]); + deletionLogService.findByDeletionRequestId.mockResolvedValueOnce([deletionLogExecuted]); const result = await uc.findById(deletionRequestExecuted.id); @@ -484,15 +477,12 @@ describe(DeletionRequestUc.name, () => { describe('when searching for logs for deletionRequest which was not executed', () => { const setup = () => { - jest.clearAllMocks(); const deletionRequest = deletionRequestFactory.build(); - const notExecutedDeletionRequestSummary: DeletionRequestLog = { - targetRef: { - targetRefDomain: deletionRequest.targetRefDomain, - targetRefId: deletionRequest.targetRefId, - }, - deletionPlannedAt: deletionRequest.deleteAfter, - }; + const targetRef = DeletionTargetRefBuilder.build(deletionRequest.targetRefDomain, deletionRequest.targetRefId); + const notExecutedDeletionRequestSummary = DeletionRequestLogResponseBuilder.build( + targetRef, + deletionRequest.deleteAfter + ); return { deletionRequest, @@ -526,7 +516,6 @@ describe(DeletionRequestUc.name, () => { describe('deleteDeletionRequestById', () => { describe('when deleting a deletionRequestId', () => { const setup = () => { - jest.clearAllMocks(); const deletionRequest = deletionRequestFactory.build(); return { diff --git a/apps/server/src/modules/deletion/uc/deletion-request.uc.ts b/apps/server/src/modules/deletion/uc/deletion-request.uc.ts index 7bacc428310..0e5c3c10c85 100644 --- a/apps/server/src/modules/deletion/uc/deletion-request.uc.ts +++ b/apps/server/src/modules/deletion/uc/deletion-request.uc.ts @@ -10,23 +10,16 @@ import { FilesService } from '@modules/files/service'; import { AccountService } from '@modules/account/services'; import { RocketChatUserService } from '@modules/rocketchat-user'; import { RocketChatService } from '@modules/rocketchat'; +import { LegacyLogger } from '@src/core/logger'; import { RegistrationPinService } from '@modules/registration-pin'; import { DeletionRequestService } from '../services/deletion-request.service'; -import { DeletionDomainModel } from '../domain/types/deletion-domain-model.enum'; +import { DeletionDomainModel, DeletionOperationModel, DeletionStatusModel } from '../domain/types'; import { DeletionLogService } from '../services/deletion-log.service'; import { DeletionRequest } from '../domain/deletion-request.do'; -import { DeletionOperationModel } from '../domain/types/deletion-operation-model.enum'; -import { DeletionStatusModel } from '../domain/types/deletion-status-model.enum'; import { DeletionLog } from '../domain/deletion-log.do'; -import { - DeletionRequestProps, - DeletionRequestLog, - DeletionLogStatistic, - DeletionRequestCreateAnswer, -} from './interface/interfaces'; -import { DeletionLogStatisticBuilder } from './builder/deletion-log-statistic.builder'; -import { DeletionRequestLogBuilder } from './builder/deletion-request-log.builder'; -import { DeletionTargetRefBuilder } from './builder/deletion-target-ref.builder'; +import { DeletionLogStatistic } from '../interface/interfaces'; +import { DeletionLogStatisticBuilder, DeletionRequestLogResponseBuilder, DeletionTargetRefBuilder } from '../builder'; +import { DeletionRequestBodyProps, DeletionRequestLogResponse, DeletionRequestResponse } from '../controller/dto'; @Injectable() export class DeletionRequestUc { @@ -44,13 +37,17 @@ export class DeletionRequestUc { private readonly userService: UserService, private readonly rocketChatUserService: RocketChatUserService, private readonly rocketChatService: RocketChatService, + private readonly logger: LegacyLogger, private readonly registrationPinService: RegistrationPinService - ) {} + ) { + this.logger.setContext(DeletionRequestUc.name); + } - async createDeletionRequest(deletionRequest: DeletionRequestProps): Promise { + async createDeletionRequest(deletionRequest: DeletionRequestBodyProps): Promise { + this.logger.debug({ action: 'createDeletionRequest', deletionRequest }); const result = await this.deletionRequestService.createDeletionRequest( - deletionRequest.targetRef.targetRefId, - deletionRequest.targetRef.targetRefDoamin, + deletionRequest.targetRef.id, + deletionRequest.targetRef.domain, deletionRequest.deleteInMinutes ); @@ -58,6 +55,8 @@ export class DeletionRequestUc { } async executeDeletionRequests(limit?: number): Promise { + this.logger.debug({ action: 'executeDeletionRequests', limit }); + const deletionRequestToExecution: DeletionRequest[] = await this.deletionRequestService.findAllItemsToExecute( limit ); @@ -68,9 +67,11 @@ export class DeletionRequestUc { } } - async findById(deletionRequestId: EntityId): Promise { + async findById(deletionRequestId: EntityId): Promise { + this.logger.debug({ action: 'deletionRequestId', deletionRequestId }); + const deletionRequest: DeletionRequest = await this.deletionRequestService.findById(deletionRequestId); - let response: DeletionRequestLog = DeletionRequestLogBuilder.build( + let response: DeletionRequestLogResponse = DeletionRequestLogResponseBuilder.build( DeletionTargetRefBuilder.build(deletionRequest.targetRefDomain, deletionRequest.targetRefId), deletionRequest.deleteAfter ); @@ -87,6 +88,8 @@ export class DeletionRequestUc { } async deleteDeletionRequestById(deletionRequestId: EntityId): Promise { + this.logger.debug({ action: 'deleteDeletionRequestById', deletionRequestId }); + await this.deletionRequestService.deleteById(deletionRequestId); } @@ -107,6 +110,7 @@ export class DeletionRequestUc { ]); await this.deletionRequestService.markDeletionRequestAsExecuted(deletionRequest.id); } catch (error) { + this.logger.error(`execution of deletionRequest ${deletionRequest.id} was failed`, error); await this.deletionRequestService.markDeletionRequestAsFailed(deletionRequest.id); } } @@ -130,6 +134,8 @@ export class DeletionRequestUc { } private async removeAccount(deletionRequest: DeletionRequest) { + this.logger.debug({ action: 'removeAccount', deletionRequest }); + await this.accountService.deleteByUserId(deletionRequest.targetRefId); await this.logDeletion(deletionRequest, DeletionDomainModel.ACCOUNT, DeletionOperationModel.DELETE, 0, 1); } @@ -154,6 +160,8 @@ export class DeletionRequestUc { } private async removeUserFromClasses(deletionRequest: DeletionRequest) { + this.logger.debug({ action: 'removeUserFromClasses', deletionRequest }); + const classesUpdated: number = await this.classService.deleteUserDataFromClasses(deletionRequest.targetRefId); await this.logDeletion( deletionRequest, @@ -165,6 +173,8 @@ export class DeletionRequestUc { } private async removeUserFromCourseGroup(deletionRequest: DeletionRequest) { + this.logger.debug({ action: 'removeUserFromCourseGroup', deletionRequest }); + const courseGroupUpdated: number = await this.courseGroupService.deleteUserDataFromCourseGroup( deletionRequest.targetRefId ); @@ -178,6 +188,8 @@ export class DeletionRequestUc { } private async removeUserFromCourse(deletionRequest: DeletionRequest) { + this.logger.debug({ action: 'removeUserFromCourse', deletionRequest }); + const courseUpdated: number = await this.courseService.deleteUserDataFromCourse(deletionRequest.targetRefId); await this.logDeletion( deletionRequest, @@ -189,6 +201,8 @@ export class DeletionRequestUc { } private async removeUsersFilesAndPermissions(deletionRequest: DeletionRequest) { + this.logger.debug({ action: 'removeUsersFilesAndPermissions', deletionRequest }); + const filesDeleted: number = await this.filesService.markFilesOwnedByUserForDeletion(deletionRequest.targetRefId); const filePermissionsUpdated: number = await this.filesService.removeUserPermissionsToAnyFiles( deletionRequest.targetRefId @@ -203,6 +217,8 @@ export class DeletionRequestUc { } private async removeUserFromLessons(deletionRequest: DeletionRequest) { + this.logger.debug({ action: 'removeUserFromLessons', deletionRequest }); + const lessonsUpdated: number = await this.lessonService.deleteUserDataFromLessons(deletionRequest.targetRefId); await this.logDeletion( deletionRequest, @@ -214,6 +230,8 @@ export class DeletionRequestUc { } private async removeUsersPseudonyms(deletionRequest: DeletionRequest) { + this.logger.debug({ action: 'removeUsersPseudonyms', deletionRequest }); + const pseudonymDeleted: number = await this.pseudonymService.deleteByUserId(deletionRequest.targetRefId); await this.logDeletion( deletionRequest, @@ -225,23 +243,34 @@ export class DeletionRequestUc { } private async removeUserFromTeams(deletionRequest: DeletionRequest) { + this.logger.debug({ action: ' removeUserFromTeams', deletionRequest }); + const teamsUpdated: number = await this.teamService.deleteUserDataFromTeams(deletionRequest.targetRefId); await this.logDeletion(deletionRequest, DeletionDomainModel.TEAMS, DeletionOperationModel.UPDATE, teamsUpdated, 0); } private async removeUser(deletionRequest: DeletionRequest) { + this.logger.debug({ action: 'removeUser', deletionRequest }); + const userDeleted: number = await this.userService.deleteUser(deletionRequest.targetRefId); await this.logDeletion(deletionRequest, DeletionDomainModel.USER, DeletionOperationModel.DELETE, 0, userDeleted); } - private async removeUserFromRocketChat(deletionRequest: DeletionRequest): Promise { + private async removeUserFromRocketChat(deletionRequest: DeletionRequest) { + this.logger.debug({ action: 'removeUserFromRocketChat', deletionRequest }); + const rocketChatUser = await this.rocketChatUserService.findByUserId(deletionRequest.targetRefId); const [, rocketChatUserDeleted] = await Promise.all([ this.rocketChatService.deleteUser(rocketChatUser.username), this.rocketChatUserService.deleteByUserId(rocketChatUser.userId), ]); - - return rocketChatUserDeleted; + await this.logDeletion( + deletionRequest, + DeletionDomainModel.ROCKETCHATUSER, + DeletionOperationModel.DELETE, + 0, + rocketChatUserDeleted + ); } } diff --git a/apps/server/src/modules/deletion/uc/index.ts b/apps/server/src/modules/deletion/uc/index.ts index 4b1451b563d..964025bc80e 100644 --- a/apps/server/src/modules/deletion/uc/index.ts +++ b/apps/server/src/modules/deletion/uc/index.ts @@ -1,3 +1,4 @@ -export * from './interface'; +export * from './deletion-request.uc'; +export * from '../builder'; export * from './batch-deletion.uc'; export * from './deletion-execution.uc'; diff --git a/apps/server/src/modules/deletion/uc/interface/interfaces.ts b/apps/server/src/modules/deletion/uc/interface/interfaces.ts deleted file mode 100644 index 47f4d887735..00000000000 --- a/apps/server/src/modules/deletion/uc/interface/interfaces.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { EntityId } from '@shared/domain'; -import { DeletionDomainModel } from '../../domain/types/deletion-domain-model.enum'; - -export interface DeletionTargetRef { - targetRefDomain: DeletionDomainModel; - targetRefId: EntityId; -} - -export interface DeletionRequestLog { - targetRef: DeletionTargetRef; - deletionPlannedAt: Date; - statistics?: DeletionLogStatistic[]; -} - -export interface DeletionLogStatistic { - domain: DeletionDomainModel; - modifiedCount?: number; - deletedCount?: number; -} - -export interface DeletionRequestProps { - targetRef: { targetRefDoamin: DeletionDomainModel; targetRefId: EntityId }; - deleteInMinutes?: number; -} - -export interface DeletionRequestCreateAnswer { - requestId: EntityId; - deletionPlannedAt: Date; -} diff --git a/apps/server/src/modules/files/files.module.ts b/apps/server/src/modules/files/files.module.ts index a62815eb69f..d451381171d 100644 --- a/apps/server/src/modules/files/files.module.ts +++ b/apps/server/src/modules/files/files.module.ts @@ -4,9 +4,11 @@ import { LoggerModule } from '@src/core/logger'; import { DeleteFilesConsole } from './job'; import { DeleteFilesUc } from './uc'; import { FilesRepo } from './repo'; +import { FilesService } from './service'; @Module({ imports: [LoggerModule], - providers: [DeleteFilesConsole, DeleteFilesUc, FilesRepo, StorageProviderRepo], + providers: [DeleteFilesConsole, DeleteFilesUc, FilesRepo, StorageProviderRepo, FilesService], + exports: [FilesService], }) export class FilesModule {} diff --git a/apps/server/src/modules/files/index.ts b/apps/server/src/modules/files/index.ts index 51eaedc8548..158749cfd9d 100644 --- a/apps/server/src/modules/files/index.ts +++ b/apps/server/src/modules/files/index.ts @@ -1 +1,2 @@ export * from './files.module'; +export * from './service'; diff --git a/apps/server/src/modules/learnroom/index.ts b/apps/server/src/modules/learnroom/index.ts index 9fe9c100886..6c28dcb2e95 100644 --- a/apps/server/src/modules/learnroom/index.ts +++ b/apps/server/src/modules/learnroom/index.ts @@ -1,2 +1,8 @@ export * from './learnroom.module'; -export { CommonCartridgeExportService, CourseCopyService, CourseService, RoomsService } from './service'; +export { + CommonCartridgeExportService, + CourseCopyService, + CourseService, + RoomsService, + CourseGroupService, +} from './service'; diff --git a/apps/server/src/modules/learnroom/learnroom.module.ts b/apps/server/src/modules/learnroom/learnroom.module.ts index 02071369766..9c9d4d80036 100644 --- a/apps/server/src/modules/learnroom/learnroom.module.ts +++ b/apps/server/src/modules/learnroom/learnroom.module.ts @@ -3,13 +3,14 @@ import { CopyHelperModule } from '@modules/copy-helper'; import { LessonModule } from '@modules/lesson'; import { TaskModule } from '@modules/task'; import { Module } from '@nestjs/common'; -import { BoardRepo, CourseRepo, DashboardModelMapper, DashboardRepo, UserRepo } from '@shared/repo'; +import { BoardRepo, CourseGroupRepo, CourseRepo, DashboardModelMapper, DashboardRepo, UserRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; import { BoardCopyService, ColumnBoardTargetService, CommonCartridgeExportService, CourseCopyService, + CourseGroupService, CourseService, RoomsService, } from './service'; @@ -31,7 +32,9 @@ import { CourseService, CommonCartridgeExportService, ColumnBoardTargetService, + CourseGroupService, + CourseGroupRepo, ], - exports: [CourseCopyService, CourseService, RoomsService, CommonCartridgeExportService], + exports: [CourseCopyService, CourseService, RoomsService, CommonCartridgeExportService, CourseGroupService], }) export class LearnroomModule {} diff --git a/apps/server/src/modules/lesson/index.ts b/apps/server/src/modules/lesson/index.ts index b552bf9c988..b031de2b421 100644 --- a/apps/server/src/modules/lesson/index.ts +++ b/apps/server/src/modules/lesson/index.ts @@ -1,5 +1,4 @@ export * from './lesson.module'; -export * from './service/lesson-copy.service'; -export * from './service/lesson.service'; export * from './types/lesson-copy-parent.params'; export * from './types/lesson-copy.params'; +export { NexboardService, LessonService, LessonCopyService, EtherpadService } from './service'; diff --git a/apps/server/src/modules/lesson/repository/lesson.repo.integration.spec.ts b/apps/server/src/modules/lesson/repository/lesson.repo.integration.spec.ts index d6bdafd1971..3eb94f6fc5b 100644 --- a/apps/server/src/modules/lesson/repository/lesson.repo.integration.spec.ts +++ b/apps/server/src/modules/lesson/repository/lesson.repo.integration.spec.ts @@ -182,7 +182,7 @@ describe('LessonRepo', () => { const result = await repo.findByUserId(userId); // Assert - expect(result).toHaveLength(2); + expect(result).toHaveLength(0); expect(result.some((lesson: LessonEntity) => lesson.id === lesson3.id)).toBeFalsy(); const receivedContents = result.flatMap((o) => o.contents); receivedContents.forEach((content) => { @@ -207,7 +207,7 @@ describe('LessonRepo', () => { em.clear(); // Arrange expected Array after User deletion - lesson1.contents[0].user = ''; + lesson1.contents[0].user = undefined; // Act await repo.save([lesson1]); @@ -218,7 +218,7 @@ describe('LessonRepo', () => { const result2 = await repo.findById(lesson1.id); const receivedContents = result2.contents; receivedContents.forEach((content) => { - expect(content.user).toEqual(''); + expect(content.user).toBe(null); }); }); }); diff --git a/apps/server/src/modules/lesson/repository/lesson.repo.ts b/apps/server/src/modules/lesson/repository/lesson.repo.ts index c2fc2f0269e..76332a7cab5 100644 --- a/apps/server/src/modules/lesson/repository/lesson.repo.ts +++ b/apps/server/src/modules/lesson/repository/lesson.repo.ts @@ -1,6 +1,7 @@ import { EntityDictionary } from '@mikro-orm/core'; import { Injectable } from '@nestjs/common'; import { Counted, EntityId, LessonEntity, SortOrder } from '@shared/domain'; +import { ObjectId } from '@mikro-orm/mongodb'; import { BaseRepo } from '@shared/repo'; import { LessonScope } from './lesson-scope'; @@ -44,7 +45,7 @@ export class LessonRepo extends BaseRepo { $match: { contents: { $elemMatch: { - user: userId, + user: new ObjectId(userId), }, }, }, diff --git a/apps/server/src/modules/lesson/service/lesson.service.ts b/apps/server/src/modules/lesson/service/lesson.service.ts index 3ef2f44d9bf..7359d041f51 100644 --- a/apps/server/src/modules/lesson/service/lesson.service.ts +++ b/apps/server/src/modules/lesson/service/lesson.service.ts @@ -37,7 +37,7 @@ export class LessonService implements AuthorizationLoaderService { const updatedLessons = lessons.map((lesson: LessonEntity) => { lesson.contents.map((c: ComponentProperties) => { if (c.user === userId) { - c.user = ''; + c.user = undefined; } return c; }); diff --git a/apps/server/src/modules/rocketchat-user/domain/index.ts b/apps/server/src/modules/rocketchat-user/domain/index.ts index 0246dd0f0f9..441baa69c52 100644 --- a/apps/server/src/modules/rocketchat-user/domain/index.ts +++ b/apps/server/src/modules/rocketchat-user/domain/index.ts @@ -1 +1,2 @@ export * from './rocket-chat-user.do'; +export * from './testing'; diff --git a/apps/server/src/modules/rocketchat-user/entity/rocket-chat-user.entity.ts b/apps/server/src/modules/rocketchat-user/entity/rocket-chat-user.entity.ts index 6df469e0ddb..23a30f66d5e 100644 --- a/apps/server/src/modules/rocketchat-user/entity/rocket-chat-user.entity.ts +++ b/apps/server/src/modules/rocketchat-user/entity/rocket-chat-user.entity.ts @@ -13,7 +13,7 @@ export interface RocketChatUserEntityProps { updatedAt?: Date; } -@Entity({ tableName: 'rocketchatuser' }) +@Entity({ tableName: 'rocketchatusers' }) export class RocketChatUserEntity extends BaseEntityWithTimestamps { @Property() @Unique() diff --git a/apps/server/src/modules/rocketchat-user/rocketchat-user.module.ts b/apps/server/src/modules/rocketchat-user/rocketchat-user.module.ts index 798b2276a4d..f0d30d76c37 100644 --- a/apps/server/src/modules/rocketchat-user/rocketchat-user.module.ts +++ b/apps/server/src/modules/rocketchat-user/rocketchat-user.module.ts @@ -1,10 +1,9 @@ import { Module } from '@nestjs/common'; import { RocketChatUserRepo } from './repo'; import { RocketChatUserService } from './service/rocket-chat-user.service'; -import { RocketChatService } from '../rocketchat/rocket-chat.service'; @Module({ - providers: [RocketChatUserService, RocketChatUserRepo], - exports: [RocketChatService], + providers: [RocketChatUserRepo, RocketChatUserService], + exports: [RocketChatUserService], }) export class RocketChatUserModule {} diff --git a/apps/server/src/modules/server/admin-api.server.module.ts b/apps/server/src/modules/server/admin-api.server.module.ts new file mode 100644 index 00000000000..6d5ae3f3297 --- /dev/null +++ b/apps/server/src/modules/server/admin-api.server.module.ts @@ -0,0 +1,53 @@ +import { MikroOrmModule } from '@mikro-orm/nestjs'; +import { DynamicModule, Module } from '@nestjs/common'; +import { ALL_ENTITIES } from '@shared/domain'; +import { DB_PASSWORD, DB_URL, DB_USERNAME, createConfigModuleOptions } from '@src/config'; +import { LoggerModule } from '@src/core/logger'; +import { ConfigModule } from '@nestjs/config'; +import { RabbitMQWrapperModule, RabbitMQWrapperTestModule } from '@src/infra/rabbitmq'; +import { MongoDatabaseModuleOptions, MongoMemoryDatabaseModule } from '@src/infra/database'; +import { FileEntity } from '@modules/files/entity'; +import { defaultMikroOrmOptions } from './server.module'; +import { serverConfig } from './server.config'; +import { DeletionApiModule } from '../deletion/deletion-api.module'; + +const serverModules = [ConfigModule.forRoot(createConfigModuleOptions(serverConfig)), DeletionApiModule]; + +@Module({ + imports: [ + RabbitMQWrapperModule, + ...serverModules, + MikroOrmModule.forRoot({ + ...defaultMikroOrmOptions, + type: 'mongo', + clientUrl: DB_URL, + password: DB_PASSWORD, + user: DB_USERNAME, + entities: [...ALL_ENTITIES, FileEntity], + debug: true, + }), + LoggerModule, + ], +}) +export class AdminApiServerModule {} + +@Module({ + imports: [ + ...serverModules, + MongoMemoryDatabaseModule.forRoot({ ...defaultMikroOrmOptions }), + RabbitMQWrapperTestModule, + LoggerModule, + ], +}) +export class AdminApiServerTestModule { + static forRoot(options?: MongoDatabaseModuleOptions): DynamicModule { + return { + module: AdminApiServerTestModule, + imports: [ + ...serverModules, + MongoMemoryDatabaseModule.forRoot({ ...defaultMikroOrmOptions, ...options }), + RabbitMQWrapperTestModule, + ], + }; + } +} diff --git a/apps/server/src/modules/server/controller/index.ts b/apps/server/src/modules/server/controller/index.ts new file mode 100644 index 00000000000..27d99f9a2dc --- /dev/null +++ b/apps/server/src/modules/server/controller/index.ts @@ -0,0 +1 @@ +export * from './server.controller'; diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index c119d0fa25a..e5fb0e2188e 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -6,6 +6,7 @@ import type { CommonCartridgeConfig } from '@modules/learnroom/common-cartridge' import type { UserConfig } from '@modules/user'; import type { CoreModuleConfig } from '@src/core'; import { MailConfig } from '@src/infra/mail/interfaces/mail-config'; +import { XApiKeyConfig } from '@modules/authentication'; export enum NodeEnvType { TEST = 'test', @@ -21,7 +22,8 @@ export interface ServerConfig AccountConfig, IdentityManagementConfig, CommonCartridgeConfig, - MailConfig { + MailConfig, + XApiKeyConfig { NODE_ENV: string; SC_DOMAIN: string; } @@ -41,6 +43,9 @@ const config: ServerConfig = { FEATURE_IDENTITY_MANAGEMENT_ENABLED: Configuration.get('FEATURE_IDENTITY_MANAGEMENT_ENABLED') as boolean, FEATURE_IDENTITY_MANAGEMENT_STORE_ENABLED: Configuration.get('FEATURE_IDENTITY_MANAGEMENT_STORE_ENABLED') as boolean, FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED: Configuration.get('FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED') as boolean, + ADMIN_API__ALLOWED_API_KEYS: (Configuration.get('ADMIN_API__ALLOWED_API_KEYS') as string) + .split(',') + .map((apiKey) => apiKey.trim()), ADDITIONAL_BLACKLISTED_EMAIL_DOMAINS: (Configuration.get('ADDITIONAL_BLACKLISTED_EMAIL_DOMAINS') as string) .split(',') .map((domain) => domain.trim()), diff --git a/apps/server/src/shared/domain/entity/all-entities.ts b/apps/server/src/shared/domain/entity/all-entities.ts index eb4b2478379..a666b2850c2 100644 --- a/apps/server/src/shared/domain/entity/all-entities.ts +++ b/apps/server/src/shared/domain/entity/all-entities.ts @@ -5,6 +5,8 @@ import { ShareToken } from '@modules/sharing/entity/share-token.entity'; import { ContextExternalToolEntity } from '@modules/tool/context-external-tool/entity'; import { ExternalToolEntity } from '@modules/tool/external-tool/entity'; import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; +import { DeletionLogEntity, DeletionRequestEntity } from '@src/modules/deletion/entity'; +import { RocketChatUserEntity } from '@src/modules/rocketchat-user/entity'; import { RegistrationPinEntity } from '@modules/registration-pin/entity'; import { Account } from './account.entity'; import { @@ -60,6 +62,8 @@ export const ALL_ENTITIES = [ ColumnBoardTarget, ColumnNode, ClassEntity, + DeletionRequestEntity, + DeletionLogEntity, FileElementNode, LinkElementNode, RichTextElementNode, @@ -83,6 +87,7 @@ export const ALL_ENTITIES = [ News, PseudonymEntity, ExternalToolPseudonymEntity, + RocketChatUserEntity, Role, SchoolEntity, SchoolExternalToolEntity, diff --git a/apps/server/src/shared/testing/index.ts b/apps/server/src/shared/testing/index.ts index 7640045df95..afb9facad15 100644 --- a/apps/server/src/shared/testing/index.ts +++ b/apps/server/src/shared/testing/index.ts @@ -3,3 +3,4 @@ export * from './setup-entities'; export * from './cleanup-collections'; export * from './map-user-to-current-user'; export * from './test-api-client'; +export * from './test-xApiKey-client'; diff --git a/apps/server/src/shared/testing/test-xApiKey-client.spec.ts b/apps/server/src/shared/testing/test-xApiKey-client.spec.ts new file mode 100644 index 00000000000..83d9386b18d --- /dev/null +++ b/apps/server/src/shared/testing/test-xApiKey-client.spec.ts @@ -0,0 +1,93 @@ +import { Controller, Delete, ExecutionContext, Get, Headers, HttpStatus, INestApplication, Post } from '@nestjs/common'; +import { Test } from '@nestjs/testing'; +import { ObjectId } from 'bson'; +import { AuthGuard } from '@nestjs/passport'; +import { TestXApiKeyClient } from './test-xApiKey-client'; + +@Controller('') +class TestController { + @Delete(':id') + async delete(@Headers('X-API-KEY') authorization: string) { + return Promise.resolve({ method: 'delete', authorization }); + } + + @Post() + async post(@Headers('X-API-KEY') authorization: string) { + return Promise.resolve({ method: 'post', authorization }); + } + + @Get(':id') + async get(@Headers('X-API-KEY') authorization: string) { + return Promise.resolve({ method: 'get', authorization }); + } +} + +describe(TestXApiKeyClient.name, () => { + describe('when test request instance exists', () => { + let app: INestApplication; + const API_KEY = '1ab2c3d4e5f61ab2c3d4e5f6'; + + beforeAll(async () => { + const moduleFixture = await Test.createTestingModule({ + controllers: [TestController], + }) + .overrideGuard(AuthGuard('api-key')) + .useValue({ + canActivate(context: ExecutionContext) { + const req: Request = context.switchToHttp().getRequest(); + req.headers['X-API-KEY'] = API_KEY; + return true; + }, + }) + .compile(); + + app = moduleFixture.createNestApplication(); + + await app.init(); + }); + + afterAll(async () => { + await app.close(); + }); + + const setup = () => { + const testXApiKeyClient = new TestXApiKeyClient(app, ''); + const id = new ObjectId().toHexString(); + + return { testXApiKeyClient, id }; + }; + + describe('get', () => { + it('should resolve requests', async () => { + const { testXApiKeyClient, id } = setup(); + + const result = await testXApiKeyClient.get(id); + + expect(result.statusCode).toEqual(HttpStatus.OK); + expect(result.body).toEqual(expect.objectContaining({ method: 'get' })); + }); + }); + + describe('post', () => { + it('should resolve requests', async () => { + const { testXApiKeyClient } = setup(); + + const result = await testXApiKeyClient.post(); + + expect(result.statusCode).toEqual(HttpStatus.CREATED); + expect(result.body).toEqual(expect.objectContaining({ method: 'post' })); + }); + }); + + describe('delete', () => { + it('should resolve requests', async () => { + const { testXApiKeyClient, id } = setup(); + + const result = await testXApiKeyClient.delete(id); + + expect(result.statusCode).toEqual(HttpStatus.OK); + expect(result.body).toEqual(expect.objectContaining({ method: 'delete' })); + }); + }); + }); +}); diff --git a/apps/server/src/shared/testing/test-xApiKey-client.ts b/apps/server/src/shared/testing/test-xApiKey-client.ts new file mode 100644 index 00000000000..8be9e319bc7 --- /dev/null +++ b/apps/server/src/shared/testing/test-xApiKey-client.ts @@ -0,0 +1,69 @@ +import { INestApplication } from '@nestjs/common'; +import supertest from 'supertest'; + +export class TestXApiKeyClient { + private readonly app: INestApplication; + + private readonly baseRoute: string; + + constructor(app: INestApplication, baseRoute: string) { + this.app = app; + this.baseRoute = this.checkAndAddPrefix(baseRoute); + } + + public get(subPath?: string): supertest.Test { + const path = this.getPath(subPath); + const testRequestInstance = supertest(this.app.getHttpServer()).get(path).set('Accept', 'application/json'); + + return testRequestInstance; + } + + public delete(subPath?: string): supertest.Test { + const path = this.getPath(subPath); + const testRequestInstance = supertest(this.app.getHttpServer()).delete(path).set('Accept', 'application/json'); + + return testRequestInstance; + } + + public post(subPath?: string, data = {}): supertest.Test { + const path = this.getPath(subPath); + const testRequestInstance = supertest(this.app.getHttpServer()) + .post(path) + .set('Accept', 'application/json') + .send(data); + + return testRequestInstance; + } + + private isSlash(inputPath: string, pos: number): boolean { + const isSlash = inputPath.charAt(pos) === '/'; + + return isSlash; + } + + private checkAndAddPrefix(inputPath = '/'): string { + let path = ''; + if (!this.isSlash(inputPath, 0)) { + path = '/'; + } + path += inputPath; + + return path; + } + + private cleanupPath(inputPath: string): string { + let path = inputPath; + if (this.isSlash(path, 0) && this.isSlash(path, 1)) { + path = path.slice(1); + } + + return path; + } + + private getPath(routeNameInput = ''): string { + const routeName = this.checkAndAddPrefix(routeNameInput); + const path = this.cleanupPath(this.baseRoute + routeName); + + return path; + } +} diff --git a/config/default.schema.json b/config/default.schema.json index b27ea565dba..bcf9fe7de4e 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -177,7 +177,7 @@ }, "ADDITIONAL_BLACKLISTED_EMAIL_DOMAINS": { "type": "string", - "default":"", + "default": "", "description": "Add custom domain to the list of blocked domains (comma separated list)." }, "FEATURE_TSP_AUTO_CONSENT_ENABLED": { @@ -379,12 +379,7 @@ "H5P_Library": { "type": "object", "description": "Properties of the H5P server microservice", - "required": [ - "S3_ENDPOINT", - "S3_BUCKET_LIBRARIES", - "S3_ACCESS_KEY_ID", - "S3_SECRET_ACCESS_KEY" - ], + "required": ["S3_ENDPOINT", "S3_BUCKET_LIBRARIES", "S3_ACCESS_KEY_ID", "S3_SECRET_ACCESS_KEY"], "default": {}, "properties": { "S3_ENDPOINT": { @@ -1306,6 +1301,29 @@ "default": false, "description": "Enables feature that allows the insecure LDAP URL (with ldap:// protocol)." }, + "ADMIN_API": { + "type": "object", + "description": "Configuration of the schulcloud-server's admin API.", + "properties": { + "ENABLED": { + "type": "boolean", + "description": "Flag to turn on/off the Admin API." + }, + "PORT": { + "type": "number", + "description": "Port of the exposed Admin API server." + }, + "ALLOWED_API_KEYS": { + "type": "string", + "description": "Allowed Admin API keys (for accessing the Admin API)." + } + }, + "default": { + "ENABLED": true, + "PORT": 4030, + "ALLOWED_API_KEYS": "" + } + }, "FEATURE_NEW_SCHOOL_ADMINISTRATION_PAGE_AS_DEFAULT_ENABLED": { "type": "boolean", "default": false, @@ -1397,7 +1415,8 @@ } }, "default": { - "BASE_URL": "http://localhost:4030" + "BASE_URL": "http://localhost:4030", + "API_KEY": "" } }, "TLDRAW": { diff --git a/nest-cli.json b/nest-cli.json index a90498a7327..73598e6f9dc 100644 --- a/nest-cli.json +++ b/nest-cli.json @@ -108,6 +108,15 @@ "tsConfigPath": "apps/server/tsconfig.app.json" } }, + "admin-api-server": { + "type": "application", + "root": "apps/server", + "entryFile": "apps/admin-api-server.app", + "sourceRoot": "apps/server/src", + "compilerOptions": { + "tsConfigPath": "apps/server/tsconfig.app.json" + } + }, "tldraw": { "type": "application", "root": "apps/server", diff --git a/package-lock.json b/package-lock.json index 5fb17d023cc..78cb8ad507e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -109,6 +109,7 @@ "papaparse": "^5.1.1", "passport": "^0.6.0", "passport-custom": "^1.1.1", + "passport-headerapikey": "^1.2.2", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "prom-client": "^13.1.0", @@ -19481,6 +19482,15 @@ "node": ">= 0.10.0" } }, + "node_modules/passport-headerapikey": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/passport-headerapikey/-/passport-headerapikey-1.2.2.tgz", + "integrity": "sha512-4BvVJRrWsNJPrd3UoZfcnnl4zvUWYKEtfYkoDsaOKBsrWHYmzTApCjs7qUbncOLexE9ul0IRiYBFfBG0y9IVQA==", + "dependencies": { + "lodash": "^4.17.15", + "passport-strategy": "^1.0.0" + } + }, "node_modules/passport-jwt": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", @@ -40068,6 +40078,15 @@ "passport-strategy": "1.x.x" } }, + "passport-headerapikey": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/passport-headerapikey/-/passport-headerapikey-1.2.2.tgz", + "integrity": "sha512-4BvVJRrWsNJPrd3UoZfcnnl4zvUWYKEtfYkoDsaOKBsrWHYmzTApCjs7qUbncOLexE9ul0IRiYBFfBG0y9IVQA==", + "requires": { + "lodash": "^4.17.15", + "passport-strategy": "^1.0.0" + } + }, "passport-jwt": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/passport-jwt/-/passport-jwt-4.0.1.tgz", diff --git a/package.json b/package.json index 854cb849b0e..b047d5ada90 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,9 @@ "nest:start:debug": "nest start server --debug --watch", "nest:start:prod": "node dist/apps/server/apps/server.app", "nest:start:management": "nest start management", + "nest:start:admin-api-server": "nest start admin-api-server", + "nest:start:admin-api-server:debug": "nest start admin-api-server --debug --watch", + "nest:start:admin-api-server:prod": "node dist/apps/server/apps/admin-api-server.app", "nest:start:management:dev": "nest start management --watch", "nest:start:management:debug": "nest start management --debug --watch", "nest:start:management:prod": "node dist/apps/server/apps/management.app", @@ -201,6 +204,7 @@ "papaparse": "^5.1.1", "passport": "^0.6.0", "passport-custom": "^1.1.1", + "passport-headerapikey": "^1.2.2", "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "prom-client": "^13.1.0",