From a82c051a6f504980935a956f35d6c3dd9646e473 Mon Sep 17 00:00:00 2001 From: Arne Gnisa Date: Mon, 15 Jul 2024 10:19:31 +0200 Subject: [PATCH 1/2] N21-1895 refactors configs (#5083) * N21-1895 refactors tool config * N21-1895 refactors bbb config and videoconference config * N21-1895 refactors user import config * N21-1895 refactors provisioning config --- apps/server/src/modules/board/board.module.ts | 6 +-- .../board-node-copy-general.service.spec.ts | 7 +-- .../board-node-copy-specific.service.spec.ts | 29 +++++++----- .../internal/board-node-copy.service.ts | 9 ++-- .../src/modules/learnroom/learnroom.module.ts | 2 - .../service/course-copy.service.spec.ts | 17 ++++--- .../learnroom/service/course-copy.service.ts | 13 ++--- .../oauth-provider/oauth-provider.module.ts | 11 +---- .../oauth-provider.login-flow.service.spec.ts | 23 +++++---- .../oauth-provider.login-flow.service.ts | 8 ++-- .../src/modules/provisioning/config/index.ts | 1 - .../config/provisioning-config.ts | 17 ------- .../provisioning-config.module.ts | 13 ----- .../provisioning/provisioning.config.ts | 2 + .../provisioning/provisioning.module.ts | 2 - .../oidc/schulconnex.strategy.spec.ts | 42 ++++++++--------- .../strategy/oidc/schulconnex.strategy.ts | 10 ++-- .../strategy/sanis/sanis.strategy.spec.ts | 32 +++++-------- .../strategy/sanis/sanis.strategy.ts | 7 +-- .../sanis/schulconnex-response-mapper.spec.ts | 24 ++++------ .../sanis/schulconnex-response-mapper.ts | 9 ++-- .../modules/server/api/dto/config.response.ts | 12 ++--- .../src/modules/server/server.config.ts | 38 +++++++++++---- .../src/modules/server/server.module.ts | 7 ++- .../context-external-tool.module.ts | 2 - .../external-tool/external-tool.module.ts | 3 +- ...xternal-tool-configuration.service.spec.ts | 7 --- ....ts => external-tool-logo.service.spec.ts} | 22 ++++----- .../service/external-tool-logo.service.ts | 22 +++++---- .../external-tool-validation.service.spec.ts | 19 ++++---- apps/server/src/modules/tool/index.ts | 1 - .../school-external-tool.module.ts | 5 +- .../src/modules/tool/tool-api.module.ts | 2 - .../src/modules/tool/tool-config.module.ts | 13 ----- apps/server/src/modules/tool/tool-config.ts | 29 +++--------- apps/server/src/modules/tool/tool.module.ts | 8 ++-- .../src/modules/user-import/config/index.ts | 1 - .../user-import/config/user-import-config.ts | 17 ------- .../api-test/import-user-populate.api.spec.ts | 22 +++++---- .../api-test/import-user.api.spec.ts | 32 ++++++++----- apps/server/src/modules/user-import/index.ts | 3 +- .../service/user-import.service.spec.ts | 30 +++++++----- .../service/user-import.service.ts | 11 +++-- .../uc/user-import-fetch.uc.spec.ts | 32 ++++++++----- .../user-import/uc/user-import-fetch.uc.ts | 14 ++++-- .../user-import/uc/user-import.uc.spec.ts | 47 ++++++++++--------- .../modules/user-import/uc/user-import.uc.ts | 29 +++++++----- .../user-import/user-import-config.module.ts | 13 ----- .../modules/user-import/user-import-config.ts | 3 ++ .../modules/user-import/user-import.module.ts | 2 - .../video-conference/bbb/bbb-config.ts | 5 ++ .../bbb/bbb-settings.interface.ts | 7 --- .../video-conference/bbb/bbb.service.spec.ts | 31 +++++------- .../video-conference/bbb/bbb.service.ts | 14 +++--- .../src/modules/video-conference/bbb/index.ts | 2 +- .../src/modules/video-conference/index.ts | 3 +- .../video-conference/interface/index.ts | 1 - .../video-conference-settings.interface.ts | 9 ---- .../service/video-conference.service.spec.ts | 28 +++++++---- .../service/video-conference.service.ts | 32 ++++++++----- .../video-conference-config.ts | 19 ++------ .../video-conference.module.ts | 12 +---- 62 files changed, 392 insertions(+), 501 deletions(-) delete mode 100644 apps/server/src/modules/provisioning/config/index.ts delete mode 100644 apps/server/src/modules/provisioning/config/provisioning-config.ts delete mode 100644 apps/server/src/modules/provisioning/provisioning-config.module.ts rename apps/server/src/modules/tool/external-tool/service/{external-tool-logo-service.spec.ts => external-tool-logo.service.spec.ts} (95%) delete mode 100644 apps/server/src/modules/tool/tool-config.module.ts delete mode 100644 apps/server/src/modules/user-import/config/index.ts delete mode 100644 apps/server/src/modules/user-import/config/user-import-config.ts delete mode 100644 apps/server/src/modules/user-import/user-import-config.module.ts create mode 100644 apps/server/src/modules/video-conference/bbb/bbb-config.ts delete mode 100644 apps/server/src/modules/video-conference/bbb/bbb-settings.interface.ts delete mode 100644 apps/server/src/modules/video-conference/interface/video-conference-settings.interface.ts diff --git a/apps/server/src/modules/board/board.module.ts b/apps/server/src/modules/board/board.module.ts index 3e7d5cf3c49..884db4ee05a 100644 --- a/apps/server/src/modules/board/board.module.ts +++ b/apps/server/src/modules/board/board.module.ts @@ -3,7 +3,6 @@ import { CopyHelperModule } from '@modules/copy-helper'; import { FilesStorageClientModule } from '@modules/files-storage-client'; import { TldrawClientModule } from '@modules/tldraw-client'; import { ContextExternalToolModule } from '@modules/tool/context-external-tool'; -import { ToolConfigModule } from '@modules/tool/tool-config.module'; import { UserModule } from '@modules/user'; import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; @@ -21,14 +20,14 @@ import { UserDeletedEventHandlerService, } from './service'; import { + BoardContextService, BoardNodeCopyService, + BoardNodeDeleteHooksService, ColumnBoardCopyService, ColumnBoardLinkService, ColumnBoardReferenceService, ColumnBoardTitleService, ContentElementUpdateService, - BoardNodeDeleteHooksService, - BoardContextService, } from './service/internal'; @Module({ @@ -39,7 +38,6 @@ import { UserModule, ContextExternalToolModule, HttpModule, - ToolConfigModule, TldrawClientModule, CqrsModule, CollaborativeTextEditorModule, diff --git a/apps/server/src/modules/board/service/internal/board-node-copy-general.service.spec.ts b/apps/server/src/modules/board/service/internal/board-node-copy-general.service.spec.ts index 7402a8c1436..6c9201e4f1d 100644 --- a/apps/server/src/modules/board/service/internal/board-node-copy-general.service.spec.ts +++ b/apps/server/src/modules/board/service/internal/board-node-copy-general.service.spec.ts @@ -3,7 +3,8 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; import { StorageLocation } from '@modules/files-storage/entity'; import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; -import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; +import { ToolConfig } from '@modules/tool/tool-config'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { setupEntities } from '@shared/testing'; import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; @@ -35,8 +36,8 @@ describe(BoardNodeCopyService.name, () => { providers: [ BoardNodeCopyService, { - provide: ToolFeatures, - useValue: createMock(), + provide: ConfigService, + useValue: createMock>(), }, { provide: ContextExternalToolService, diff --git a/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts b/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts index 99299317481..d4c57e4d0e6 100644 --- a/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts +++ b/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts @@ -3,7 +3,8 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; import { StorageLocation } from '@modules/files-storage/entity'; import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; -import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; +import { ToolConfig } from '@modules/tool/tool-config'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { setupEntities } from '@shared/testing'; import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; @@ -43,13 +44,13 @@ import { BoardNodeCopyService } from './board-node-copy.service'; describe(BoardNodeCopyService.name, () => { let module: TestingModule; let service: BoardNodeCopyService; - const toolFeatures: IToolFeatures = { - ctlToolsTabEnabled: false, - ltiToolsTabEnabled: false, - maxExternalToolLogoSizeInBytes: 0, - backEndUrl: '', - ctlToolsCopyEnabled: false, - ctlToolsReloadTimeMs: 0, + const config: ToolConfig = { + FEATURE_CTL_TOOLS_TAB_ENABLED: false, + FEATURE_LTI_TOOLS_TAB_ENABLED: false, + CTL_TOOLS__EXTERNAL_TOOL_MAX_LOGO_SIZE_IN_BYTES: 0, + CTL_TOOLS_BACKEND_URL: '', + FEATURE_CTL_TOOLS_COPY_ENABLED: false, + CTL_TOOLS_RELOAD_TIME_MS: 0, }; let contextExternalToolService: DeepMocked; let copyHelperService: DeepMocked; @@ -59,8 +60,10 @@ describe(BoardNodeCopyService.name, () => { providers: [ BoardNodeCopyService, { - provide: ToolFeatures, - useValue: toolFeatures, + provide: ConfigService, + useValue: { + get: jest.fn().mockImplementation((key: keyof ToolConfig) => config[key]), + }, }, { provide: ContextExternalToolService, @@ -81,7 +84,7 @@ describe(BoardNodeCopyService.name, () => { }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); afterAll(async () => { @@ -404,7 +407,7 @@ describe(BoardNodeCopyService.name, () => { const setupCopyEnabled = () => { const { copyContext, externalToolElement } = setup(); - toolFeatures.ctlToolsCopyEnabled = true; + config.FEATURE_CTL_TOOLS_COPY_ENABLED = true; return { copyContext, externalToolElement }; }; @@ -474,7 +477,7 @@ describe(BoardNodeCopyService.name, () => { const setupCopyDisabled = () => { const { copyContext, externalToolElement } = setup(); - toolFeatures.ctlToolsCopyEnabled = false; + config.FEATURE_CTL_TOOLS_COPY_ENABLED = false; return { copyContext, externalToolElement }; }; diff --git a/apps/server/src/modules/board/service/internal/board-node-copy.service.ts b/apps/server/src/modules/board/service/internal/board-node-copy.service.ts index dd254390eb9..4686f6cfcae 100644 --- a/apps/server/src/modules/board/service/internal/board-node-copy.service.ts +++ b/apps/server/src/modules/board/service/internal/board-node-copy.service.ts @@ -3,8 +3,9 @@ import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from ' import { CopyFileDto } from '@modules/files-storage-client/dto'; import { ContextExternalToolService } from '@modules/tool/context-external-tool'; import { ContextExternalTool } from '@modules/tool/context-external-tool/domain'; -import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; -import { Inject, Injectable } from '@nestjs/common'; +import { ToolConfig } from '@modules/tool/tool-config'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { EntityId } from '@shared/domain/types'; import { AnyBoardNode, @@ -34,7 +35,7 @@ export interface CopyContext { @Injectable() export class BoardNodeCopyService { constructor( - @Inject(ToolFeatures) private readonly toolFeatures: IToolFeatures, + private readonly configService: ConfigService, private readonly contextExternalToolService: ContextExternalToolService, private readonly copyHelperService: CopyHelperService ) {} @@ -286,7 +287,7 @@ export class BoardNodeCopyService { }); let status: CopyStatusEnum; - if (this.toolFeatures.ctlToolsCopyEnabled && original.contextExternalToolId) { + if (this.configService.get('FEATURE_CTL_TOOLS_COPY_ENABLED') && original.contextExternalToolId) { const linkedTool = await this.contextExternalToolService.findById(original.contextExternalToolId); if (linkedTool) { diff --git a/apps/server/src/modules/learnroom/learnroom.module.ts b/apps/server/src/modules/learnroom/learnroom.module.ts index 5f112d07dff..4ed7eae9fef 100644 --- a/apps/server/src/modules/learnroom/learnroom.module.ts +++ b/apps/server/src/modules/learnroom/learnroom.module.ts @@ -3,7 +3,6 @@ import { CopyHelperModule } from '@modules/copy-helper'; import { LessonModule } from '@modules/lesson'; import { TaskModule } from '@modules/task'; import { ContextExternalToolModule } from '@modules/tool/context-external-tool'; -import { ToolConfigModule } from '@modules/tool/tool-config.module'; import { forwardRef, Module } from '@nestjs/common'; import { CqrsModule } from '@nestjs/cqrs'; import { @@ -44,7 +43,6 @@ import { CommonCartridgeFileValidatorPipe } from './utils'; LessonModule, LoggerModule, TaskModule, - ToolConfigModule, CqrsModule, ], providers: [ diff --git a/apps/server/src/modules/learnroom/service/course-copy.service.spec.ts b/apps/server/src/modules/learnroom/service/course-copy.service.spec.ts index 91f85457879..8d5242de5e2 100644 --- a/apps/server/src/modules/learnroom/service/course-copy.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/course-copy.service.spec.ts @@ -4,7 +4,8 @@ import { LessonCopyService } from '@modules/lesson/service'; import { ToolContextType } from '@modules/tool/common/enum'; import { ContextExternalTool } from '@modules/tool/context-external-tool/domain'; import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; -import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; +import { ToolConfig } from '@modules/tool/tool-config'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { Course } from '@shared/domain/entity'; import { CourseRepo, LegacyBoardRepo, UserRepo } from '@shared/repo'; @@ -32,7 +33,7 @@ describe('course copy service', () => { let copyHelperService: DeepMocked; let userRepo: DeepMocked; let contextExternalToolService: DeepMocked; - let toolFeatures: IToolFeatures; + let configService: DeepMocked>; afterAll(async () => { await module.close(); @@ -80,10 +81,8 @@ describe('course copy service', () => { useValue: createMock(), }, { - provide: ToolFeatures, - useValue: { - ctlToolsTabEnabled: false, - }, + provide: ConfigService, + useValue: createMock>(), }, ], }).compile(); @@ -97,7 +96,7 @@ describe('course copy service', () => { copyHelperService = module.get(CopyHelperService); userRepo = module.get(UserRepo); contextExternalToolService = module.get(ContextExternalToolService); - toolFeatures = module.get(ToolFeatures); + configService = module.get(ConfigService); }); beforeEach(() => { @@ -136,7 +135,7 @@ describe('course copy service', () => { lessonCopyService.updateCopiedEmbeddedTasks.mockReturnValue(boardCopyStatus); - toolFeatures.ctlToolsCopyEnabled = true; + configService.get.mockReturnValue(true); return { user, @@ -383,7 +382,7 @@ describe('course copy service', () => { lessonCopyService.updateCopiedEmbeddedTasks.mockReturnValue(boardCopyStatus); - toolFeatures.ctlToolsCopyEnabled = false; + configService.get.mockReturnValue(false); return { user, diff --git a/apps/server/src/modules/learnroom/service/course-copy.service.ts b/apps/server/src/modules/learnroom/service/course-copy.service.ts index e176ef3ea37..bf425d0c376 100644 --- a/apps/server/src/modules/learnroom/service/course-copy.service.ts +++ b/apps/server/src/modules/learnroom/service/course-copy.service.ts @@ -2,11 +2,12 @@ import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from ' import { ToolContextType } from '@modules/tool/common/enum'; import { ContextExternalTool, ContextRef } from '@modules/tool/context-external-tool/domain'; import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; -import { Inject, Injectable } from '@nestjs/common'; +import { ToolConfig } from '@modules/tool/tool-config'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { Course, User } from '@shared/domain/entity'; import { EntityId } from '@shared/domain/types'; -import { LegacyBoardRepo, CourseRepo, UserRepo } from '@shared/repo'; -import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; +import { CourseRepo, LegacyBoardRepo, UserRepo } from '@shared/repo'; import { BoardCopyService } from './board-copy.service'; import { RoomsService } from './rooms.service'; @@ -19,7 +20,7 @@ type CourseCopyParams = { @Injectable() export class CourseCopyService { constructor( - @Inject(ToolFeatures) private readonly toolFeatures: IToolFeatures, + private readonly configService: ConfigService, private readonly courseRepo: CourseRepo, private readonly legacyBoardRepo: LegacyBoardRepo, private readonly roomsService: RoomsService, @@ -52,7 +53,7 @@ export class CourseCopyService { // copy course and board const courseCopy = await this.copyCourseEntity({ user, originalCourse, copyName }); - if (this.toolFeatures.ctlToolsCopyEnabled) { + if (this.configService.get('FEATURE_CTL_TOOLS_COPY_ENABLED')) { const contextRef: ContextRef = { id: courseId, type: ToolContextType.COURSE }; const contextExternalToolsInContext: ContextExternalTool[] = await this.contextExternalToolService.findAllByContext(contextRef); @@ -120,7 +121,7 @@ export class CourseCopyService { boardStatus, ]; - if (this.toolFeatures.ctlToolsCopyEnabled) { + if (this.configService.get('FEATURE_CTL_TOOLS_COPY_ENABLED')) { elements.push({ type: CopyElementType.EXTERNAL_TOOL, status: CopyStatusEnum.SUCCESS, diff --git a/apps/server/src/modules/oauth-provider/oauth-provider.module.ts b/apps/server/src/modules/oauth-provider/oauth-provider.module.ts index 35a1b9ece82..cc9b95bdabf 100644 --- a/apps/server/src/modules/oauth-provider/oauth-provider.module.ts +++ b/apps/server/src/modules/oauth-provider/oauth-provider.module.ts @@ -2,7 +2,6 @@ import { OauthProviderServiceModule } from '@infra/oauth-provider'; import { LtiToolModule } from '@modules/lti-tool'; import { PseudonymModule } from '@modules/pseudonym'; import { ToolModule } from '@modules/tool'; -import { ToolConfigModule } from '@modules/tool/tool-config.module'; import { UserModule } from '@modules/user'; import { Module } from '@nestjs/common'; import { TeamsRepo } from '@shared/repo'; @@ -11,15 +10,7 @@ import { IdTokenService } from './service/id-token.service'; import { OauthProviderLoginFlowService } from './service/oauth-provider.login-flow.service'; @Module({ - imports: [ - OauthProviderServiceModule, - UserModule, - LoggerModule, - PseudonymModule, - LtiToolModule, - ToolModule, - ToolConfigModule, - ], + imports: [OauthProviderServiceModule, UserModule, LoggerModule, PseudonymModule, LtiToolModule, ToolModule], providers: [OauthProviderLoginFlowService, IdTokenService, TeamsRepo], exports: [OauthProviderLoginFlowService, IdTokenService], }) diff --git a/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.spec.ts b/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.spec.ts index df118628596..fb308ebe962 100644 --- a/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.spec.ts +++ b/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.spec.ts @@ -3,20 +3,21 @@ import { LtiToolService } from '@modules/lti-tool'; import { ExternalTool } from '@modules/tool/external-tool/domain'; import { ExternalToolService } from '@modules/tool/external-tool/service'; import { externalToolFactory } from '@modules/tool/external-tool/testing'; -import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; +import { ToolConfig } from '@modules/tool/tool-config'; import { NotFoundException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { LtiToolDO } from '@shared/domain/domainobject'; import { ltiToolDOFactory, setupEntities } from '@shared/testing'; import { OauthProviderLoginFlowService } from './oauth-provider.login-flow.service'; -describe('OauthProviderLoginFlowService', () => { +describe(OauthProviderLoginFlowService.name, () => { let module: TestingModule; let service: OauthProviderLoginFlowService; let ltiToolService: DeepMocked; let externalToolService: DeepMocked; - let toolFeatures: IToolFeatures; + let configService: DeepMocked>; beforeAll(async () => { module = await Test.createTestingModule({ @@ -31,10 +32,8 @@ describe('OauthProviderLoginFlowService', () => { useValue: createMock(), }, { - provide: ToolFeatures, - useValue: { - ctlToolsTabEnabled: false, - }, + provide: ConfigService, + useValue: createMock>(), }, ], }).compile(); @@ -42,7 +41,7 @@ describe('OauthProviderLoginFlowService', () => { service = module.get(OauthProviderLoginFlowService); ltiToolService = module.get(LtiToolService); externalToolService = module.get(ExternalToolService); - toolFeatures = module.get(ToolFeatures); + configService = module.get(ConfigService); await setupEntities(); }); @@ -58,7 +57,7 @@ describe('OauthProviderLoginFlowService', () => { describe('findToolByClientId', () => { describe('when it finds a ctl tool and the ctl feature is active', () => { const setup = () => { - toolFeatures.ctlToolsTabEnabled = true; + configService.get.mockReturnValue(true); const tool: ExternalTool = externalToolFactory.buildWithId({ name: 'SchulcloudNextcloud' }); @@ -81,7 +80,7 @@ describe('OauthProviderLoginFlowService', () => { describe('when a lti tool exists and the ctl feature is deactivated', () => { const setup = () => { - toolFeatures.ctlToolsTabEnabled = false; + configService.get.mockReturnValue(false); const tool: LtiToolDO = ltiToolDOFactory.buildWithId({ name: 'SchulcloudNextcloud' }); @@ -111,7 +110,7 @@ describe('OauthProviderLoginFlowService', () => { describe('when a lti tool exists and the ctl feature is active and no ctl tool exists', () => { const setup = () => { - toolFeatures.ctlToolsTabEnabled = true; + configService.get.mockReturnValue(true); const tool: LtiToolDO = ltiToolDOFactory.buildWithId({ name: 'SchulcloudNextcloud' }); @@ -142,7 +141,7 @@ describe('OauthProviderLoginFlowService', () => { describe('when no lti or ctl tool was found', () => { const setup = () => { - toolFeatures.ctlToolsTabEnabled = true; + configService.get.mockReturnValue(true); externalToolService.findExternalToolByOAuth2ConfigClientId.mockResolvedValue(null); ltiToolService.findByClientIdAndIsLocal.mockResolvedValue(null); diff --git a/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.ts b/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.ts index adf363415fd..75ed2aaa891 100644 --- a/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.ts +++ b/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.ts @@ -1,10 +1,10 @@ import { LtiToolService } from '@modules/lti-tool/service'; import { ExternalTool } from '@modules/tool/external-tool/domain'; import { ExternalToolService } from '@modules/tool/external-tool/service'; -import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; -import { Inject } from '@nestjs/common'; +import { ToolConfig } from '@modules/tool/tool-config'; import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; import { NotFoundException } from '@nestjs/common/exceptions/not-found.exception'; +import { ConfigService } from '@nestjs/config'; import { LtiToolDO } from '@shared/domain/domainobject/ltitool.do'; @Injectable() @@ -12,11 +12,11 @@ export class OauthProviderLoginFlowService { constructor( private readonly ltiToolService: LtiToolService, private readonly externalToolService: ExternalToolService, - @Inject(ToolFeatures) private readonly toolFeatures: IToolFeatures + private readonly configService: ConfigService ) {} public async findToolByClientId(clientId: string): Promise { - if (this.toolFeatures.ctlToolsTabEnabled) { + if (this.configService.get('FEATURE_CTL_TOOLS_TAB_ENABLED')) { const externalTool: ExternalTool | null = await this.externalToolService.findExternalToolByOAuth2ConfigClientId( clientId ); diff --git a/apps/server/src/modules/provisioning/config/index.ts b/apps/server/src/modules/provisioning/config/index.ts deleted file mode 100644 index dbbb1de579b..00000000000 --- a/apps/server/src/modules/provisioning/config/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { ProvisioningFeatures, ProvisioningConfiguration, IProvisioningFeatures } from './provisioning-config'; diff --git a/apps/server/src/modules/provisioning/config/provisioning-config.ts b/apps/server/src/modules/provisioning/config/provisioning-config.ts deleted file mode 100644 index 1fa9b078d66..00000000000 --- a/apps/server/src/modules/provisioning/config/provisioning-config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Configuration } from '@hpi-schul-cloud/commons/lib'; - -export const ProvisioningFeatures = Symbol('ProvisioningFeatures'); - -export interface IProvisioningFeatures { - schulconnexGroupProvisioningEnabled: boolean; - schulconnexCourseSyncEnabled: boolean; - schulconnexOtherGroupusersEnabled: boolean; -} - -export class ProvisioningConfiguration { - static provisioningFeatures: IProvisioningFeatures = { - schulconnexGroupProvisioningEnabled: Configuration.get('FEATURE_SANIS_GROUP_PROVISIONING_ENABLED') as boolean, - schulconnexCourseSyncEnabled: Configuration.get('FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED') as boolean, - schulconnexOtherGroupusersEnabled: Configuration.get('FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED') as boolean, - }; -} diff --git a/apps/server/src/modules/provisioning/provisioning-config.module.ts b/apps/server/src/modules/provisioning/provisioning-config.module.ts deleted file mode 100644 index 2e1aad944e9..00000000000 --- a/apps/server/src/modules/provisioning/provisioning-config.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; -import { ProvisioningConfiguration, ProvisioningFeatures } from './config'; - -@Module({ - providers: [ - { - provide: ProvisioningFeatures, - useValue: ProvisioningConfiguration.provisioningFeatures, - }, - ], - exports: [ProvisioningFeatures], -}) -export class ProvisioningConfigModule {} diff --git a/apps/server/src/modules/provisioning/provisioning.config.ts b/apps/server/src/modules/provisioning/provisioning.config.ts index 45586873b17..4d7906e59c6 100644 --- a/apps/server/src/modules/provisioning/provisioning.config.ts +++ b/apps/server/src/modules/provisioning/provisioning.config.ts @@ -2,4 +2,6 @@ export interface ProvisioningConfig { FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED: boolean; FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED: boolean; PROVISIONING_SCHULCONNEX_LIZENZ_INFO_URL: string; + FEATURE_SANIS_GROUP_PROVISIONING_ENABLED: boolean; + FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED: boolean; } diff --git a/apps/server/src/modules/provisioning/provisioning.module.ts b/apps/server/src/modules/provisioning/provisioning.module.ts index 152636a3c1c..6d65266a0a6 100644 --- a/apps/server/src/modules/provisioning/provisioning.module.ts +++ b/apps/server/src/modules/provisioning/provisioning.module.ts @@ -11,7 +11,6 @@ import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; import { SchulconnexClientModule } from '@src/infra/schulconnex-client'; import { UserLicenseModule } from '../user-license'; -import { ProvisioningConfigModule } from './provisioning-config.module'; import { ProvisioningService } from './service/provisioning.service'; import { IservProvisioningStrategy, @@ -30,7 +29,6 @@ import { @Module({ imports: [ - ProvisioningConfigModule, AccountModule, LegacySchoolModule, UserModule, diff --git a/apps/server/src/modules/provisioning/strategy/oidc/schulconnex.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/oidc/schulconnex.strategy.spec.ts index a74bf170322..30408619f57 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/schulconnex.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/schulconnex.strategy.spec.ts @@ -14,7 +14,6 @@ import { legacySchoolDoFactory, userDoFactory, } from '@shared/testing'; -import { IProvisioningFeatures, ProvisioningFeatures } from '../../config'; import { ExternalGroupDto, ExternalSchoolDto, @@ -50,7 +49,6 @@ describe(SchulconnexProvisioningStrategy.name, () => { let module: TestingModule; let strategy: TestSchulconnexStrategy; - let provisioningFeatures: IProvisioningFeatures; let schulconnexSchoolProvisioningService: DeepMocked; let schulconnexUserProvisioningService: DeepMocked; let schulconnexGroupProvisioningService: DeepMocked; @@ -60,14 +58,12 @@ describe(SchulconnexProvisioningStrategy.name, () => { let configService: DeepMocked>; let schulconnexToolProvisioningService: DeepMocked; + const config: Partial = {}; + beforeAll(async () => { module = await Test.createTestingModule({ providers: [ TestSchulconnexStrategy, - { - provide: ProvisioningFeatures, - useValue: {}, - }, { provide: SchulconnexSchoolProvisioningService, useValue: createMock(), @@ -97,14 +93,15 @@ describe(SchulconnexProvisioningStrategy.name, () => { useValue: createMock(), }, { - provide: ConfigService, - useValue: createMock>(), + provide: ConfigService, + useValue: { + get: jest.fn().mockImplementation((key: keyof ProvisioningConfig) => config[key]), + }, }, ], }).compile(); strategy = module.get(TestSchulconnexStrategy); - provisioningFeatures = module.get(ProvisioningFeatures); schulconnexSchoolProvisioningService = module.get(SchulconnexSchoolProvisioningService); schulconnexUserProvisioningService = module.get(SchulconnexUserProvisioningService); schulconnexGroupProvisioningService = module.get(SchulconnexGroupProvisioningService); @@ -116,10 +113,9 @@ describe(SchulconnexProvisioningStrategy.name, () => { }); beforeEach(() => { - Object.assign>(provisioningFeatures, { - schulconnexGroupProvisioningEnabled: false, - schulconnexCourseSyncEnabled: false, - }); + config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = false; + config.FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED = false; + config.FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED = true; }); afterAll(async () => { @@ -127,7 +123,7 @@ describe(SchulconnexProvisioningStrategy.name, () => { }); afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); }); describe('apply is called', () => { @@ -246,7 +242,7 @@ describe(SchulconnexProvisioningStrategy.name, () => { describe('when group data is provided and the feature is enabled', () => { const setup = () => { - provisioningFeatures.schulconnexGroupProvisioningEnabled = true; + config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = true; const externalUserId = 'externalUserId'; const externalGroups: ExternalGroupDto[] = externalGroupDtoFactory.buildList(2); @@ -306,7 +302,7 @@ describe(SchulconnexProvisioningStrategy.name, () => { describe('when group data is provided, but the feature is disabled', () => { const setup = () => { - provisioningFeatures.schulconnexGroupProvisioningEnabled = false; + config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = false; const externalUserId = 'externalUserId'; const oauthData: OauthDataDto = new OauthDataDto({ @@ -350,7 +346,7 @@ describe(SchulconnexProvisioningStrategy.name, () => { describe('when group data is not provided', () => { const setup = () => { - provisioningFeatures.schulconnexGroupProvisioningEnabled = true; + config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = true; const externalUserId = 'externalUserId'; const oauthData: OauthDataDto = new OauthDataDto({ @@ -391,8 +387,8 @@ describe(SchulconnexProvisioningStrategy.name, () => { describe('when an existing group gets provisioned', () => { const setup = () => { - provisioningFeatures.schulconnexGroupProvisioningEnabled = true; - provisioningFeatures.schulconnexCourseSyncEnabled = true; + config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = true; + config.FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED = true; const externalUserId = 'externalUserId'; const externalGroups: ExternalGroupDto[] = externalGroupDtoFactory.buildList(2); @@ -441,8 +437,8 @@ describe(SchulconnexProvisioningStrategy.name, () => { describe('when a new group is provisioned', () => { const setup = () => { - provisioningFeatures.schulconnexGroupProvisioningEnabled = true; - provisioningFeatures.schulconnexCourseSyncEnabled = true; + config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = true; + config.FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED = true; const externalUserId = 'externalUserId'; const externalGroups: ExternalGroupDto[] = externalGroupDtoFactory.buildList(2); @@ -486,8 +482,8 @@ describe(SchulconnexProvisioningStrategy.name, () => { describe('when a user was removed from a group', () => { const setup = () => { - provisioningFeatures.schulconnexGroupProvisioningEnabled = true; - provisioningFeatures.schulconnexCourseSyncEnabled = true; + config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = true; + config.FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED = true; const externalUserId = 'externalUserId'; const oauthData: OauthDataDto = new OauthDataDto({ diff --git a/apps/server/src/modules/provisioning/strategy/oidc/schulconnex.strategy.ts b/apps/server/src/modules/provisioning/strategy/oidc/schulconnex.strategy.ts index e976afb7bc5..007b70319bc 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/schulconnex.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/schulconnex.strategy.ts @@ -1,8 +1,7 @@ import { Group, GroupService } from '@modules/group'; -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { LegacySchoolDo, UserDO } from '@shared/domain/domainobject'; -import { IProvisioningFeatures, ProvisioningFeatures } from '../../config'; import { ExternalGroupDto, OauthDataDto, ProvisioningDto } from '../../dto'; import { ProvisioningConfig } from '../../provisioning.config'; import { ProvisioningStrategy } from '../base.strategy'; @@ -18,7 +17,6 @@ import { @Injectable() export abstract class SchulconnexProvisioningStrategy extends ProvisioningStrategy { constructor( - @Inject(ProvisioningFeatures) protected readonly provisioningFeatures: IProvisioningFeatures, protected readonly schulconnexSchoolProvisioningService: SchulconnexSchoolProvisioningService, protected readonly schulconnexUserProvisioningService: SchulconnexUserProvisioningService, protected readonly schulconnexGroupProvisioningService: SchulconnexGroupProvisioningService, @@ -46,7 +44,7 @@ export abstract class SchulconnexProvisioningStrategy extends ProvisioningStrate school?.id ); - if (this.provisioningFeatures.schulconnexGroupProvisioningEnabled) { + if (this.configService.get('FEATURE_SANIS_GROUP_PROVISIONING_ENABLED')) { await this.provisionGroups(data, school); } @@ -87,7 +85,7 @@ export abstract class SchulconnexProvisioningStrategy extends ProvisioningStrate data.system.systemId ); - if (this.provisioningFeatures.schulconnexCourseSyncEnabled && provisionedGroup) { + if (this.configService.get('FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED') && provisionedGroup) { await this.schulconnexCourseSyncService.synchronizeCourseWithGroup( provisionedGroup, existingGroup ?? undefined @@ -108,7 +106,7 @@ export abstract class SchulconnexProvisioningStrategy extends ProvisioningStrate data.system.systemId ); - if (this.provisioningFeatures.schulconnexCourseSyncEnabled) { + if (this.configService.get('FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED')) { const courseSyncPromises: Promise[] = removedFromGroups.map( async (removedFromGroup: Group): Promise => { await this.schulconnexCourseSyncService.synchronizeCourseWithGroup(removedFromGroup, removedFromGroup); diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts index 5b380116894..58d54a44495 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts @@ -17,7 +17,6 @@ import { ValidationErrorLoggableException } from '@shared/common/loggable-except import { RoleName } from '@shared/domain/interface'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import * as classValidator from 'class-validator'; -import { IProvisioningFeatures, ProvisioningFeatures } from '../../config'; import { ExternalGroupDto, ExternalLicenseDto, @@ -52,9 +51,8 @@ describe(SanisProvisioningStrategy.name, () => { ArgsType >; - let provisioningFeatures: IProvisioningFeatures; - let configService: DeepMocked>; let schulconnexRestClient: DeepMocked; + const config: Partial = {}; beforeAll(async () => { module = await Test.createTestingModule({ @@ -92,13 +90,11 @@ describe(SanisProvisioningStrategy.name, () => { provide: SchulconnexToolProvisioningService, useValue: createMock(), }, - { - provide: ProvisioningFeatures, - useValue: {}, - }, { provide: ConfigService, - useValue: createMock>(), + useValue: { + get: jest.fn().mockImplementation((key: keyof ProvisioningConfig) => config[key]), + }, }, { provide: SchulconnexRestClient, @@ -109,21 +105,16 @@ describe(SanisProvisioningStrategy.name, () => { strategy = module.get(SanisProvisioningStrategy); mapper = module.get(SchulconnexResponseMapper); - provisioningFeatures = module.get(ProvisioningFeatures); schulconnexRestClient = module.get(SchulconnexRestClient); - configService = module.get(ConfigService); validationFunction = jest.spyOn(classValidator, 'validate'); }); - beforeEach(() => { - Object.assign>(provisioningFeatures, { - schulconnexGroupProvisioningEnabled: true, - }); - }); - afterEach(() => { - jest.resetAllMocks(); + jest.clearAllMocks(); + validationFunction.mockReset(); + config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = false; + config.FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED = false; }); const setupSchulconnexResponse = (): SchulconnexResponse => schulconnexResponseFactory.build(); @@ -179,13 +170,14 @@ describe(SanisProvisioningStrategy.name, () => { schulconnexLizenzInfoResponse, ]); + config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = true; + config.FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED = true; schulconnexRestClient.getPersonInfo.mockResolvedValueOnce(schulconnexResponse); mapper.mapToExternalUserDto.mockReturnValue(user); mapper.mapToExternalSchoolDto.mockReturnValue(school); mapper.mapToExternalGroupDtos.mockReturnValue(groups); validationFunction.mockResolvedValueOnce([]); validationFunction.mockResolvedValueOnce([]); - configService.get.mockReturnValueOnce(true); schulconnexRestClient.getLizenzInfo.mockResolvedValueOnce(schulconnexLizenzInfoResponses); validationFunction.mockResolvedValueOnce([]); @@ -287,7 +279,7 @@ describe(SanisProvisioningStrategy.name, () => { mapper.mapToExternalSchoolDto.mockReturnValue(school); validationFunction.mockResolvedValueOnce([]); - provisioningFeatures.schulconnexGroupProvisioningEnabled = false; + config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = false; return { input, @@ -342,7 +334,7 @@ describe(SanisProvisioningStrategy.name, () => { validationFunction.mockResolvedValueOnce([]); validationFunction.mockResolvedValueOnce([]); - configService.get.mockReturnValueOnce(false); + config.FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED = false; return { input, diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts index 760f93218e1..e55305ade14 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts @@ -5,14 +5,13 @@ import { } from '@infra/schulconnex-client/response'; import { SchulconnexRestClient } from '@infra/schulconnex-client/schulconnex-rest-client'; import { GroupService } from '@modules/group/service/group.service'; -import { Inject, Injectable, InternalServerErrorException } from '@nestjs/common'; +import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { ValidationErrorLoggableException } from '@shared/common/loggable-exception'; import { RoleName } from '@shared/domain/interface'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { plainToClass } from 'class-transformer'; import { validate, ValidationError } from 'class-validator'; -import { IProvisioningFeatures, ProvisioningFeatures } from '../../config'; import { ExternalGroupDto, ExternalLicenseDto, @@ -36,7 +35,6 @@ import { SchulconnexResponseMapper } from './schulconnex-response-mapper'; @Injectable() export class SanisProvisioningStrategy extends SchulconnexProvisioningStrategy { constructor( - @Inject(ProvisioningFeatures) protected readonly provisioningFeatures: IProvisioningFeatures, protected readonly schulconnexSchoolProvisioningService: SchulconnexSchoolProvisioningService, protected readonly schulconnexUserProvisioningService: SchulconnexUserProvisioningService, protected readonly schulconnexGroupProvisioningService: SchulconnexGroupProvisioningService, @@ -49,7 +47,6 @@ export class SanisProvisioningStrategy extends SchulconnexProvisioningStrategy { private readonly schulconnexRestClient: SchulconnexRestClient ) { super( - provisioningFeatures, schulconnexSchoolProvisioningService, schulconnexUserProvisioningService, schulconnexGroupProvisioningService, @@ -92,7 +89,7 @@ export class SanisProvisioningStrategy extends SchulconnexProvisioningStrategy { const externalSchool: ExternalSchoolDto = this.responseMapper.mapToExternalSchoolDto(schulconnexResponse); let externalGroups: ExternalGroupDto[] | undefined; - if (this.provisioningFeatures.schulconnexGroupProvisioningEnabled) { + if (this.configService.get('FEATURE_SANIS_GROUP_PROVISIONING_ENABLED')) { await this.checkResponseValidation(schulconnexResponse, [SchulconnexResponseValidationGroups.GROUPS]); externalGroups = this.responseMapper.mapToExternalGroupDtos(schulconnexResponse); diff --git a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.spec.ts b/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.spec.ts index 5a8bc7d0a15..4173c8d2ed0 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.spec.ts @@ -11,19 +11,20 @@ import { SchulconnexSonstigeGruppenzugehoerigeResponse, } from '@infra/schulconnex-client'; import { GroupTypes } from '@modules/group'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { RoleName } from '@shared/domain/interface'; import { Logger } from '@src/core/logger'; -import { IProvisioningFeatures, ProvisioningFeatures } from '../../config'; import { InvalidLaufzeitResponseLoggableException, InvalidLernperiodeResponseLoggableException } from '../../domain'; import { ExternalGroupDto, ExternalLicenseDto, ExternalSchoolDto, ExternalUserDto } from '../../dto'; +import { ProvisioningConfig } from '../../provisioning.config'; import { SchulconnexResponseMapper } from './schulconnex-response-mapper'; describe(SchulconnexResponseMapper.name, () => { let module: TestingModule; let mapper: SchulconnexResponseMapper; - let provisioningFeatures: IProvisioningFeatures; + const config: Partial = {}; beforeAll(async () => { module = await Test.createTestingModule({ @@ -34,14 +35,15 @@ describe(SchulconnexResponseMapper.name, () => { useValue: createMock(), }, { - provide: ProvisioningFeatures, - useValue: {}, + provide: ConfigService, + useValue: { + get: jest.fn().mockImplementation((key: keyof ProvisioningConfig) => config[key]), + }, }, ], }).compile(); mapper = module.get(SchulconnexResponseMapper); - provisioningFeatures = module.get(ProvisioningFeatures); }); describe('mapToExternalSchoolDto', () => { @@ -143,9 +145,7 @@ describe(SchulconnexResponseMapper.name, () => { describe('when group type class is given', () => { const setup = () => { - Object.assign>(provisioningFeatures, { - schulconnexOtherGroupusersEnabled: true, - }); + config.FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED = true; const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); @@ -274,9 +274,7 @@ describe(SchulconnexResponseMapper.name, () => { describe('when no other participants are provided and FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED is false', () => { const setup = () => { - Object.assign>(provisioningFeatures, { - schulconnexOtherGroupusersEnabled: false, - }); + config.FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED = false; const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); schulconnexResponse.personenkontexte[0].gruppen![0].sonstige_gruppenzugehoerige = undefined; @@ -296,9 +294,7 @@ describe(SchulconnexResponseMapper.name, () => { describe('when no other participants are provided and FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED is true', () => { const setup = () => { - Object.assign>(provisioningFeatures, { - schulconnexOtherGroupusersEnabled: true, - }); + config.FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED = true; const schulconnexResponse: SchulconnexResponse = schulconnexResponseFactory.build(); schulconnexResponse.personenkontexte[0].gruppen![0].sonstige_gruppenzugehoerige = undefined; diff --git a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts b/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts index 7e66f6d2900..34a31f9a55e 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts @@ -12,10 +12,10 @@ import { SchulconnexSonstigeGruppenzugehoerigeResponse, } from '@infra/schulconnex-client/response'; import { GroupTypes } from '@modules/group'; -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { RoleName } from '@shared/domain/interface'; import { Logger } from '@src/core/logger'; -import { IProvisioningFeatures, ProvisioningFeatures } from '../../config'; import { InvalidLaufzeitResponseLoggableException, InvalidLernperiodeResponseLoggableException } from '../../domain'; import { ExternalGroupDto, @@ -25,6 +25,7 @@ import { ExternalUserDto, } from '../../dto'; import { GroupRoleUnknownLoggable } from '../../loggable'; +import { ProvisioningConfig } from '../../provisioning.config'; const RoleMapping: Record = { [SchulconnexRole.LEHR]: RoleName.TEACHER, @@ -54,7 +55,7 @@ export class SchulconnexResponseMapper { SCHOOLNUMBER_PREFIX_REGEX = /^NI_/; constructor( - @Inject(ProvisioningFeatures) protected readonly provisioningFeatures: IProvisioningFeatures, + private readonly configService: ConfigService, private readonly logger: Logger ) {} @@ -141,7 +142,7 @@ export class SchulconnexResponseMapper { } let otherUsers: ExternalGroupUserDto[] | undefined; - if (this.provisioningFeatures.schulconnexOtherGroupusersEnabled) { + if (this.configService.get('FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED')) { otherUsers = group.sonstige_gruppenzugehoerige ? (group.sonstige_gruppenzugehoerige .map((relation): ExternalGroupUserDto | null => this.mapToExternalGroupUser(relation)) diff --git a/apps/server/src/modules/server/api/dto/config.response.ts b/apps/server/src/modules/server/api/dto/config.response.ts index 3110d7761f4..9fe1276ab7f 100644 --- a/apps/server/src/modules/server/api/dto/config.response.ts +++ b/apps/server/src/modules/server/api/dto/config.response.ts @@ -244,7 +244,7 @@ export class ConfigResponse { this.FEATURE_LESSON_SHARE = config.FEATURE_LESSON_SHARE; this.FEATURE_TASK_SHARE = config.FEATURE_TASK_SHARE; this.FEATURE_BOARD_LAYOUT_ENABLED = config.FEATURE_BOARD_LAYOUT_ENABLED; - this.FEATURE_USER_MIGRATION_ENABLED = config.userMigrationEnabled; + this.FEATURE_USER_MIGRATION_ENABLED = config.FEATURE_USER_MIGRATION_ENABLED; this.FEATURE_COPY_SERVICE_ENABLED = config.FEATURE_COPY_SERVICE_ENABLED; this.FEATURE_CONSENT_NECESSARY = config.FEATURE_CONSENT_NECESSARY; this.FEATURE_COMMON_CARTRIDGE_COURSE_EXPORT_ENABLED = config.FEATURE_COMMON_CARTRIDGE_COURSE_EXPORT_ENABLED; @@ -266,20 +266,20 @@ export class ConfigResponse { this.FEATURE_NEW_SCHOOL_ADMINISTRATION_PAGE_AS_DEFAULT_ENABLED = config.FEATURE_NEW_SCHOOL_ADMINISTRATION_PAGE_AS_DEFAULT_ENABLED; this.MIGRATION_END_GRACE_PERIOD_MS = config.MIGRATION_END_GRACE_PERIOD_MS; - this.FEATURE_CTL_TOOLS_TAB_ENABLED = config.ctlToolsTabEnabled; - this.FEATURE_LTI_TOOLS_TAB_ENABLED = config.ltiToolsTabEnabled; + this.FEATURE_CTL_TOOLS_TAB_ENABLED = config.FEATURE_CTL_TOOLS_TAB_ENABLED; + this.FEATURE_LTI_TOOLS_TAB_ENABLED = config.FEATURE_LTI_TOOLS_TAB_ENABLED; this.FEATURE_SHOW_OUTDATED_USERS = config.FEATURE_SHOW_OUTDATED_USERS; this.FEATURE_ENABLE_LDAP_SYNC_DURING_MIGRATION = config.FEATURE_ENABLE_LDAP_SYNC_DURING_MIGRATION; - this.CTL_TOOLS_RELOAD_TIME_MS = config.ctlToolsReloadTimeMs; + this.CTL_TOOLS_RELOAD_TIME_MS = config.CTL_TOOLS_RELOAD_TIME_MS; this.FEATURE_SHOW_NEW_CLASS_VIEW_ENABLED = config.FEATURE_SHOW_NEW_CLASS_VIEW_ENABLED; - this.FEATURE_CTL_TOOLS_COPY_ENABLED = config.ctlToolsCopyEnabled; + this.FEATURE_CTL_TOOLS_COPY_ENABLED = config.FEATURE_CTL_TOOLS_COPY_ENABLED; this.FEATURE_SHOW_MIGRATION_WIZARD = config.FEATURE_SHOW_MIGRATION_WIZARD; this.MIGRATION_WIZARD_DOCUMENTATION_LINK = config.MIGRATION_WIZARD_DOCUMENTATION_LINK; this.FEATURE_TLDRAW_ENABLED = config.FEATURE_TLDRAW_ENABLED; this.TLDRAW__ASSETS_ENABLED = config.TLDRAW__ASSETS_ENABLED; this.TLDRAW__ASSETS_MAX_SIZE = config.TLDRAW__ASSETS_MAX_SIZE; this.TLDRAW__ASSETS_ALLOWED_MIME_TYPES_LIST = config.TLDRAW__ASSETS_ALLOWED_MIME_TYPES_LIST; - this.FEATURE_VIDEOCONFERENCE_ENABLED = config.enabled; + this.FEATURE_VIDEOCONFERENCE_ENABLED = config.FEATURE_VIDEOCONFERENCE_ENABLED; this.FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED = config.FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED; this.FEATURE_MEDIA_SHELF_ENABLED = config.FEATURE_MEDIA_SHELF_ENABLED; this.BOARD_COLLABORATION_URI = config.BOARD_COLLABORATION_URI; diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index a5d2b6ddf45..3b55a49ee4a 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -16,16 +16,16 @@ import { ProvisioningConfig } from '@modules/provisioning'; import type { SchoolConfig } from '@modules/school'; import type { SharingConfig } from '@modules/sharing'; import { getTldrawClientConfig, type TldrawClientConfig } from '@modules/tldraw-client'; -import { ToolConfiguration, type IToolFeatures } from '@modules/tool'; +import type { ToolConfig } from '@modules/tool/tool-config'; import type { UserConfig } from '@modules/user'; -import { UserImportConfiguration, type IUserImportFeatures } from '@modules/user-import'; +import type { UserImportConfig } from '@modules/user-import'; import type { UserLoginMigrationConfig } from '@modules/user-login-migration'; -import { VideoConferenceConfiguration, type IVideoConferenceSettings } from '@modules/video-conference'; +import { VideoConferenceConfig } from '@modules/video-conference'; import { LanguageType } from '@shared/domain/interface'; import { SchulcloudTheme } from '@shared/domain/types'; import type { CoreModuleConfig } from '@src/core'; import type { MailConfig } from '@src/infra/mail/interfaces/mail-config'; -import { UserImportConfig } from '../user-import/user-import-config'; +import { BbbConfig } from '../video-conference/bbb'; import { Timezone } from './types/timezone.enum'; export enum NodeEnvType { @@ -48,24 +48,26 @@ export interface ServerConfig XApiKeyConfig, LearnroomConfig, AuthenticationConfig, - IToolFeatures, + ToolConfig, TldrawClientConfig, UserLoginMigrationConfig, LessonConfig, - IVideoConferenceSettings, BoardConfig, MediaBoardConfig, SharingConfig, - IUserImportFeatures, + UserImportConfig, SchulconnexClientConfig, SynchronizationConfig, DeletionConfig, CollaborativeTextEditorConfig, ProvisioningConfig, UserImportConfig, + VideoConferenceConfig, + BbbConfig, AlertConfig { NODE_ENV: NodeEnvType; SC_DOMAIN: string; + HOST: string; ACCESSIBILITY_REPORT_EMAIL: string; ADMIN_TABLES_DISPLAY_CONSENT_COLUMN: boolean; ALERT_STATUS_URL: string | null; @@ -254,9 +256,6 @@ const config: ServerConfig = { ) as number, FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED: Configuration.get('FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED') as boolean, ...getTldrawClientConfig(), - ...ToolConfiguration.toolFeatures, - ...VideoConferenceConfiguration.videoConference, - ...UserImportConfiguration.userImportFeatures, FEATURE_MEDIA_SHELF_ENABLED: Configuration.get('FEATURE_MEDIA_SHELF_ENABLED') as boolean, FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED: Configuration.get( 'FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED' @@ -266,6 +265,25 @@ const config: ServerConfig = { PROVISIONING_SCHULCONNEX_LIZENZ_INFO_URL: Configuration.get('PROVISIONING_SCHULCONNEX_LIZENZ_INFO_URL') as string, BOARD_COLLABORATION_URI: Configuration.get('BOARD_COLLABORATION_URI') as string, FEATURE_NEW_LAYOUT_ENABLED: Configuration.get('FEATURE_NEW_LAYOUT_ENABLED') as boolean, + FEATURE_CTL_TOOLS_TAB_ENABLED: Configuration.get('FEATURE_CTL_TOOLS_TAB_ENABLED') as boolean, + FEATURE_LTI_TOOLS_TAB_ENABLED: Configuration.get('FEATURE_LTI_TOOLS_TAB_ENABLED') as boolean, + CTL_TOOLS__EXTERNAL_TOOL_MAX_LOGO_SIZE_IN_BYTES: Configuration.get( + 'CTL_TOOLS__EXTERNAL_TOOL_MAX_LOGO_SIZE_IN_BYTES' + ) as number, + CTL_TOOLS_BACKEND_URL: Configuration.get('PUBLIC_BACKEND_URL') as string, + FEATURE_CTL_TOOLS_COPY_ENABLED: Configuration.get('FEATURE_CTL_TOOLS_COPY_ENABLED') as boolean, + CTL_TOOLS_RELOAD_TIME_MS: Configuration.get('CTL_TOOLS_RELOAD_TIME_MS') as number, + HOST: Configuration.get('HOST') as string, + FEATURE_VIDEOCONFERENCE_ENABLED: Configuration.get('FEATURE_VIDEOCONFERENCE_ENABLED') as boolean, + VIDEOCONFERENCE_HOST: Configuration.get('VIDEOCONFERENCE_HOST') as string, + VIDEOCONFERENCE_SALT: Configuration.get('VIDEOCONFERENCE_SALT') as string, + VIDEOCONFERENCE_DEFAULT_PRESENTATION: Configuration.get('VIDEOCONFERENCE_DEFAULT_PRESENTATION') as string, + FEATURE_USER_MIGRATION_ENABLED: Configuration.get('FEATURE_USER_MIGRATION_ENABLED') as boolean, + FEATURE_USER_MIGRATION_SYSTEM_ID: Configuration.get('FEATURE_USER_MIGRATION_SYSTEM_ID') as string, + FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION: Configuration.get( + 'FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION' + ) as boolean, + FEATURE_SANIS_GROUP_PROVISIONING_ENABLED: Configuration.get('FEATURE_SANIS_GROUP_PROVISIONING_ENABLED') as boolean, }; export const serverConfig = () => config; diff --git a/apps/server/src/modules/server/server.module.ts b/apps/server/src/modules/server/server.module.ts index b28498f9b1e..ed1060ec737 100644 --- a/apps/server/src/modules/server/server.module.ts +++ b/apps/server/src/modules/server/server.module.ts @@ -8,6 +8,7 @@ import { MikroOrmModule, MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs'; import { AccountApiModule } from '@modules/account/account-api.module'; import { AlertModule } from '@modules/alert/alert.module'; import { AuthenticationApiModule } from '@modules/authentication/authentication-api.module'; +import { AuthorizationReferenceApiModule } from '@modules/authorization/authorization-reference.api.module'; import { BoardApiModule } from '@modules/board/board-api.module'; import { MediaBoardApiModule } from '@modules/board/media-board-api.module'; import { CollaborativeStorageModule } from '@modules/collaborative-storage'; @@ -30,7 +31,8 @@ import { SystemApiModule } from '@modules/system/system-api.module'; import { TaskApiModule } from '@modules/task/task-api.module'; import { TeamsApiModule } from '@modules/teams/teams-api.module'; import { ToolApiModule } from '@modules/tool/tool-api.module'; -import { ImportUserModule, UserImportConfigModule } from '@modules/user-import'; +import { ImportUserModule } from '@modules/user-import'; +import { UserLicenseModule } from '@modules/user-license'; import { UserLoginMigrationApiModule } from '@modules/user-login-migration/user-login-migration-api.module'; import { UsersAdminApiModule } from '@modules/user/legacy/users-admin-api.module'; import { UserApiModule } from '@modules/user/user-api.module'; @@ -41,8 +43,6 @@ import { ALL_ENTITIES } from '@shared/domain/entity'; import { createConfigModuleOptions, DB_PASSWORD, DB_URL, DB_USERNAME } from '@src/config'; import { CoreModule } from '@src/core'; import { LoggerModule } from '@src/core/logger'; -import { UserLicenseModule } from '@modules/user-license'; -import { AuthorizationReferenceApiModule } from '@modules/authorization/authorization-reference.api.module'; import { ServerConfigController, ServerController, ServerUc } from './api'; import { SERVER_CONFIG_TOKEN, serverConfig } from './server.config'; @@ -62,7 +62,6 @@ const serverModules = [ UsersAdminApiModule, SchulconnexClientModule.registerAsync(), ImportUserModule, - UserImportConfigModule, LearnroomApiModule, FilesStorageClientModule, SystemApiModule, diff --git a/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts b/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts index 563118a175d..35b04c2d3b4 100644 --- a/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts +++ b/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts @@ -4,7 +4,6 @@ import { LoggerModule } from '@src/core/logger'; import { CommonToolModule } from '../common'; import { ExternalToolModule } from '../external-tool'; import { SchoolExternalToolModule } from '../school-external-tool'; -import { ToolConfigModule } from '../tool-config.module'; import { ContextExternalToolAuthorizableService, ContextExternalToolService, ToolReferenceService } from './service'; import { ContextExternalToolValidationService } from './service/context-external-tool-validation.service'; import { ToolConfigurationStatusService } from './service/tool-configuration-status.service'; @@ -15,7 +14,6 @@ import { ToolConfigurationStatusService } from './service/tool-configuration-sta forwardRef(() => ExternalToolModule), SchoolExternalToolModule, LoggerModule, - ToolConfigModule, UserLicenseModule, ], providers: [ diff --git a/apps/server/src/modules/tool/external-tool/external-tool.module.ts b/apps/server/src/modules/tool/external-tool/external-tool.module.ts index c7273f73f0e..20d96485a9a 100644 --- a/apps/server/src/modules/tool/external-tool/external-tool.module.ts +++ b/apps/server/src/modules/tool/external-tool/external-tool.module.ts @@ -6,7 +6,6 @@ import { ExternalToolRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; import { CommonToolModule } from '../common'; import { ToolContextMapper } from '../common/mapper/tool-context.mapper'; -import { ToolConfigModule } from '../tool-config.module'; import { ExternalToolMetadataMapper } from './mapper'; import { DatasheetPdfService, @@ -20,7 +19,7 @@ import { } from './service'; @Module({ - imports: [CommonToolModule, ToolConfigModule, LoggerModule, OauthProviderServiceModule, EncryptionModule, HttpModule], + imports: [CommonToolModule, LoggerModule, OauthProviderServiceModule, EncryptionModule, HttpModule], providers: [ ExternalToolService, ExternalToolServiceMapper, diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-configuration.service.spec.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-configuration.service.spec.ts index 4afa83fe301..d6240b1c242 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-configuration.service.spec.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-configuration.service.spec.ts @@ -11,7 +11,6 @@ import { schoolExternalToolConfigurationStatusFactory, schoolExternalToolFactory, } from '../../school-external-tool/testing'; -import { ToolFeatures } from '../../tool-config'; import { ExternalTool } from '../domain'; import { customParameterFactory, externalToolFactory } from '../testing'; import { ContextExternalToolTemplateInfo } from '../uc'; @@ -28,12 +27,6 @@ describe('ExternalToolConfigurationService', () => { module = await Test.createTestingModule({ providers: [ ExternalToolConfigurationService, - { - provide: ToolFeatures, - useValue: { - contextConfigurationEnabled: false, - }, - }, { provide: CommonToolService, useValue: createMock(), diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-logo-service.spec.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-logo.service.spec.ts similarity index 95% rename from apps/server/src/modules/tool/external-tool/service/external-tool-logo-service.spec.ts rename to apps/server/src/modules/tool/external-tool/service/external-tool-logo.service.spec.ts index 40d7185ac98..c374e1362d0 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-logo-service.spec.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-logo.service.spec.ts @@ -1,11 +1,12 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { HttpService } from '@nestjs/axios'; import { HttpException, HttpStatus } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { axiosResponseFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; import { of, throwError } from 'rxjs'; -import { IToolFeatures, ToolFeatures } from '../../tool-config'; +import { ToolConfig } from '../../tool-config'; import { ExternalTool } from '../domain'; import { ExternalToolLogo } from '../domain/external-tool-logo'; import { @@ -19,13 +20,13 @@ import { externalToolFactory } from '../testing'; import { ExternalToolLogoService } from './external-tool-logo.service'; import { ExternalToolService } from './external-tool.service'; -describe('ExternalToolLogoService', () => { +describe(ExternalToolLogoService.name, () => { let module: TestingModule; let service: ExternalToolLogoService; let httpService: DeepMocked; let logger: DeepMocked; - let toolFeatures: IToolFeatures; + let configService: DeepMocked>; let externalToolService: DeepMocked; beforeAll(async () => { @@ -41,10 +42,8 @@ describe('ExternalToolLogoService', () => { useValue: createMock(), }, { - provide: ToolFeatures, - useValue: { - maxExternalToolLogoSizeInBytes: 30000, - }, + provide: ConfigService, + useValue: createMock>(), }, { provide: ExternalToolService, @@ -56,7 +55,7 @@ describe('ExternalToolLogoService', () => { service = module.get(ExternalToolLogoService); httpService = module.get(HttpService); logger = module.get(Logger); - toolFeatures = module.get(ToolFeatures); + configService = module.get(ConfigService); externalToolService = module.get(ExternalToolService); }); @@ -91,7 +90,8 @@ describe('ExternalToolLogoService', () => { const setup = () => { const externalTool: ExternalTool = externalToolFactory.withBase64Logo().buildWithId(); - const baseUrl = toolFeatures.backEndUrl; + const baseUrl = 'https://backend.com'; + configService.get.mockReturnValue(baseUrl); const { id } = externalTool; const expected = `${baseUrl}/v3/tools/external-tools/${id}/logo`; @@ -116,7 +116,7 @@ describe('ExternalToolLogoService', () => { describe('when size is exceeded', () => { const setup = () => { const externalTool: ExternalTool = externalToolFactory.withBase64Logo().build(); - toolFeatures.maxExternalToolLogoSizeInBytes = 1; + configService.get.mockReturnValue(1); return { externalTool }; }; @@ -133,7 +133,7 @@ describe('ExternalToolLogoService', () => { describe('when size is not exceeded', () => { const setup = () => { const externalTool: ExternalTool = externalToolFactory.withBase64Logo().build(); - toolFeatures.maxExternalToolLogoSizeInBytes = 30000; + configService.get.mockReturnValue(30000); return { externalTool }; }; diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-logo.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-logo.service.ts index 2a274f1a17d..01c4689ad53 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-logo.service.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-logo.service.ts @@ -1,10 +1,11 @@ import { HttpService } from '@nestjs/axios'; -import { HttpException, Inject } from '@nestjs/common'; +import { HttpException, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { EntityId } from '@shared/domain/types'; import { Logger } from '@src/core/logger'; import { AxiosResponse } from 'axios'; import { lastValueFrom } from 'rxjs'; -import { IToolFeatures, ToolFeatures } from '../../tool-config'; +import { ToolConfig } from '../../tool-config'; import { ExternalTool } from '../domain'; import { ExternalToolLogo } from '../domain/external-tool-logo'; import { @@ -23,17 +24,18 @@ const contentTypeDetector: Record = { '47494638': 'image/gif', }; +@Injectable() export class ExternalToolLogoService { constructor( - @Inject(ToolFeatures) private readonly toolFeatures: IToolFeatures, + private readonly configService: ConfigService, private readonly logger: Logger, private readonly httpService: HttpService, private readonly externalToolService: ExternalToolService ) {} - buildLogoUrl(externalTool: ExternalTool): string | undefined { + public buildLogoUrl(externalTool: ExternalTool): string | undefined { const { logo, id } = externalTool; - const backendUrl = this.toolFeatures.backEndUrl; + const backendUrl = this.configService.get('CTL_TOOLS_BACKEND_URL'); if (logo && id) { return `${backendUrl}/v3/tools/external-tools/${id}/logo`; @@ -42,22 +44,22 @@ export class ExternalToolLogoService { return undefined; } - validateLogoSize(externalTool: Partial): void { + public validateLogoSize(externalTool: Partial): void { if (!externalTool.logo) { return; } const buffer: Buffer = Buffer.from(externalTool.logo, 'base64'); - if (buffer.length > this.toolFeatures.maxExternalToolLogoSizeInBytes) { + if (buffer.length > this.configService.get('CTL_TOOLS__EXTERNAL_TOOL_MAX_LOGO_SIZE_IN_BYTES')) { throw new ExternalToolLogoSizeExceededLoggableException( externalTool.id, - this.toolFeatures.maxExternalToolLogoSizeInBytes + this.configService.get('CTL_TOOLS__EXTERNAL_TOOL_MAX_LOGO_SIZE_IN_BYTES') ); } } - async fetchLogo(externalTool: Partial): Promise { + public async fetchLogo(externalTool: Partial): Promise { if (externalTool.logoUrl) { const base64Logo: string = await this.fetchBase64Logo(externalTool.logoUrl); @@ -93,7 +95,7 @@ export class ExternalToolLogoService { } } - async getExternalToolBinaryLogo(toolId: EntityId): Promise { + public async getExternalToolBinaryLogo(toolId: EntityId): Promise { const tool: ExternalTool = await this.externalToolService.findById(toolId); if (!tool.logo) { diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.spec.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.spec.ts index 7216a57414e..ae618ae084a 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.spec.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.spec.ts @@ -1,7 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { ValidationError } from '@shared/common'; -import { IToolFeatures, ToolFeatures } from '../../tool-config'; +import { ToolConfig } from '../../tool-config'; import { ExternalTool } from '../domain'; import { externalToolFactory } from '../testing'; import { ExternalToolLogoService } from './external-tool-logo.service'; @@ -9,13 +10,13 @@ import { ExternalToolParameterValidationService } from './external-tool-paramete import { ExternalToolValidationService } from './external-tool-validation.service'; import { ExternalToolService } from './external-tool.service'; -describe('ExternalToolValidationService', () => { +describe(ExternalToolValidationService.name, () => { let module: TestingModule; let service: ExternalToolValidationService; let externalToolService: DeepMocked; let commonToolValidationService: DeepMocked; - let toolFeatures: IToolFeatures; + let configService: DeepMocked>; let logoService: DeepMocked; beforeAll(async () => { @@ -31,10 +32,8 @@ describe('ExternalToolValidationService', () => { useValue: createMock(), }, { - provide: ToolFeatures, - useValue: { - maxExternalToolLogoSizeInBytes: 30000, - }, + provide: ConfigService, + useValue: createMock>(), }, { provide: ExternalToolLogoService, @@ -46,7 +45,7 @@ describe('ExternalToolValidationService', () => { service = module.get(ExternalToolValidationService); externalToolService = module.get(ExternalToolService); commonToolValidationService = module.get(ExternalToolParameterValidationService); - toolFeatures = module.get(ToolFeatures); + configService = module.get(ConfigService); logoService = module.get(ExternalToolLogoService); }); @@ -189,7 +188,7 @@ describe('ExternalToolValidationService', () => { describe('when external tool has a given base64 logo', () => { const setup = () => { const externalTool: ExternalTool = externalToolFactory.withBase64Logo().build(); - toolFeatures.maxExternalToolLogoSizeInBytes = 30000; + configService.get.mockReturnValue(30000); return { externalTool }; }; @@ -362,7 +361,7 @@ describe('ExternalToolValidationService', () => { describe('when external tool has a given base64 logo', () => { const setup = () => { const externalTool: ExternalTool = externalToolFactory.withBase64Logo().build(); - toolFeatures.maxExternalToolLogoSizeInBytes = 30000; + configService.get.mockReturnValue(30000); return { externalTool }; }; diff --git a/apps/server/src/modules/tool/index.ts b/apps/server/src/modules/tool/index.ts index f6ba7329778..a8006029057 100644 --- a/apps/server/src/modules/tool/index.ts +++ b/apps/server/src/modules/tool/index.ts @@ -2,5 +2,4 @@ export * from './common/interface'; export * from './context-external-tool/service/context-external-tool-authorizable.service'; export * from './external-tool'; export * from './tool.module'; -export { default as ToolConfiguration, IToolFeatures } from './tool-config'; export { ExternalToolAuthorizableService } from './external-tool/service/external-tool-authorizable.service'; diff --git a/apps/server/src/modules/tool/school-external-tool/school-external-tool.module.ts b/apps/server/src/modules/tool/school-external-tool/school-external-tool.module.ts index 2ff671cb62b..8d183c1f88b 100644 --- a/apps/server/src/modules/tool/school-external-tool/school-external-tool.module.ts +++ b/apps/server/src/modules/tool/school-external-tool/school-external-tool.module.ts @@ -1,11 +1,10 @@ -import { Module, forwardRef } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { CommonToolModule } from '../common'; import { ExternalToolModule } from '../external-tool'; -import { ToolConfigModule } from '../tool-config.module'; import { SchoolExternalToolService, SchoolExternalToolValidationService } from './service'; @Module({ - imports: [forwardRef(() => CommonToolModule), forwardRef(() => ExternalToolModule), ToolConfigModule], + imports: [forwardRef(() => CommonToolModule), forwardRef(() => ExternalToolModule)], providers: [SchoolExternalToolService, SchoolExternalToolValidationService], exports: [SchoolExternalToolService, SchoolExternalToolValidationService], }) diff --git a/apps/server/src/modules/tool/tool-api.module.ts b/apps/server/src/modules/tool/tool-api.module.ts index 783c82ac3da..12d33bd3d90 100644 --- a/apps/server/src/modules/tool/tool-api.module.ts +++ b/apps/server/src/modules/tool/tool-api.module.ts @@ -19,7 +19,6 @@ import { ExternalToolConfigurationService } from './external-tool/service'; import { ExternalToolConfigurationUc, ExternalToolUc } from './external-tool/uc'; import { ToolSchoolController } from './school-external-tool/controller'; import { SchoolExternalToolUc } from './school-external-tool/uc'; -import { ToolConfigModule } from './tool-config.module'; import { ToolLaunchController } from './tool-launch/controller/tool-launch.controller'; import { ToolLaunchUc } from './tool-launch/uc'; import { ToolModule } from './tool.module'; @@ -32,7 +31,6 @@ import { ToolModule } from './tool.module'; AuthorizationModule, LoggerModule, LegacySchoolModule, - ToolConfigModule, LearnroomModule, BoardModule, SchoolModule, diff --git a/apps/server/src/modules/tool/tool-config.module.ts b/apps/server/src/modules/tool/tool-config.module.ts deleted file mode 100644 index f30458519f5..00000000000 --- a/apps/server/src/modules/tool/tool-config.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; -import ToolConfiguration, { ToolFeatures } from './tool-config'; - -@Module({ - providers: [ - { - provide: ToolFeatures, - useValue: ToolConfiguration.toolFeatures, - }, - ], - exports: [ToolFeatures], -}) -export class ToolConfigModule {} diff --git a/apps/server/src/modules/tool/tool-config.ts b/apps/server/src/modules/tool/tool-config.ts index ccd33a579ce..98ec436a322 100644 --- a/apps/server/src/modules/tool/tool-config.ts +++ b/apps/server/src/modules/tool/tool-config.ts @@ -1,23 +1,8 @@ -import { Configuration } from '@hpi-schul-cloud/commons/lib'; - -export const ToolFeatures = Symbol('ToolFeatures'); - -export interface IToolFeatures { - ctlToolsTabEnabled: boolean; - ltiToolsTabEnabled: boolean; - maxExternalToolLogoSizeInBytes: number; - backEndUrl: string; - ctlToolsCopyEnabled: boolean; - ctlToolsReloadTimeMs: number; -} - -export default class ToolConfiguration { - static toolFeatures: IToolFeatures = { - ctlToolsTabEnabled: Configuration.get('FEATURE_CTL_TOOLS_TAB_ENABLED') as boolean, - ltiToolsTabEnabled: Configuration.get('FEATURE_LTI_TOOLS_TAB_ENABLED') as boolean, - maxExternalToolLogoSizeInBytes: Configuration.get('CTL_TOOLS__EXTERNAL_TOOL_MAX_LOGO_SIZE_IN_BYTES') as number, - backEndUrl: Configuration.get('PUBLIC_BACKEND_URL') as string, - ctlToolsCopyEnabled: Configuration.get('FEATURE_CTL_TOOLS_COPY_ENABLED') as boolean, - ctlToolsReloadTimeMs: Configuration.get('CTL_TOOLS_RELOAD_TIME_MS') as number, - }; +export interface ToolConfig { + FEATURE_CTL_TOOLS_TAB_ENABLED: boolean; + FEATURE_LTI_TOOLS_TAB_ENABLED: boolean; + CTL_TOOLS__EXTERNAL_TOOL_MAX_LOGO_SIZE_IN_BYTES: number; + CTL_TOOLS_BACKEND_URL: string; + FEATURE_CTL_TOOLS_COPY_ENABLED: boolean; + CTL_TOOLS_RELOAD_TIME_MS: number; } diff --git a/apps/server/src/modules/tool/tool.module.ts b/apps/server/src/modules/tool/tool.module.ts index 91a19c5c995..b0ba0d429fe 100644 --- a/apps/server/src/modules/tool/tool.module.ts +++ b/apps/server/src/modules/tool/tool.module.ts @@ -1,15 +1,13 @@ import { forwardRef, Module } from '@nestjs/common'; +import { CommonToolModule } from './common'; +import { CommonToolService } from './common/service'; import { ContextExternalToolModule } from './context-external-tool'; -import { SchoolExternalToolModule } from './school-external-tool'; import { ExternalToolModule } from './external-tool'; -import { CommonToolModule } from './common'; +import { SchoolExternalToolModule } from './school-external-tool'; import { ToolLaunchModule } from './tool-launch'; -import { CommonToolService } from './common/service'; -import { ToolConfigModule } from './tool-config.module'; @Module({ imports: [ - ToolConfigModule, forwardRef(() => CommonToolModule), ExternalToolModule, SchoolExternalToolModule, diff --git a/apps/server/src/modules/user-import/config/index.ts b/apps/server/src/modules/user-import/config/index.ts deleted file mode 100644 index 3267752a160..00000000000 --- a/apps/server/src/modules/user-import/config/index.ts +++ /dev/null @@ -1 +0,0 @@ -export { UserImportFeatures, UserImportConfiguration, IUserImportFeatures } from './user-import-config'; diff --git a/apps/server/src/modules/user-import/config/user-import-config.ts b/apps/server/src/modules/user-import/config/user-import-config.ts deleted file mode 100644 index 05cd6265a87..00000000000 --- a/apps/server/src/modules/user-import/config/user-import-config.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { Configuration } from '@hpi-schul-cloud/commons/lib'; - -export const UserImportFeatures = Symbol('UserImportFeatures'); - -export interface IUserImportFeatures { - userMigrationEnabled: boolean; - userMigrationSystemId: string; - useWithUserLoginMigration: boolean; -} - -export class UserImportConfiguration { - static userImportFeatures: IUserImportFeatures = { - userMigrationEnabled: Configuration.get('FEATURE_USER_MIGRATION_ENABLED') as boolean, - userMigrationSystemId: Configuration.get('FEATURE_USER_MIGRATION_SYSTEM_ID') as string, - useWithUserLoginMigration: Configuration.get('FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION') as boolean, - }; -} diff --git a/apps/server/src/modules/user-import/controller/api-test/import-user-populate.api.spec.ts b/apps/server/src/modules/user-import/controller/api-test/import-user-populate.api.spec.ts index 2da305760de..21b9b6b005a 100644 --- a/apps/server/src/modules/user-import/controller/api-test/import-user-populate.api.spec.ts +++ b/apps/server/src/modules/user-import/controller/api-test/import-user-populate.api.spec.ts @@ -1,6 +1,7 @@ import { SchulconnexResponse, schulconnexResponseFactory } from '@infra/schulconnex-client'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { OauthTokenResponse } from '@modules/oauth/service/dto'; +import { serverConfig, ServerConfig } from '@modules/server'; import { ServerTestModule } from '@modules/server/server.module'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; @@ -10,14 +11,12 @@ import { roleFactory, schoolEntityFactory, systemEntityFactory, TestApiClient, u import { accountFactory } from '@src/modules/account/testing'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; -import { IUserImportFeatures, UserImportFeatures } from '../../config'; describe('ImportUser Controller Populate (API)', () => { let app: INestApplication; let em: EntityManager; let testApiClient: TestApiClient; - let userImportFeatures: IUserImportFeatures; let axiosMock: MockAdapter; const authenticatedUser = async ( @@ -42,9 +41,10 @@ describe('ImportUser Controller Populate (API)', () => { }; const setConfig = (systemId?: string) => { - userImportFeatures.userMigrationEnabled = true; - userImportFeatures.userMigrationSystemId = systemId || new ObjectId().toString(); - userImportFeatures.useWithUserLoginMigration = false; + const config: ServerConfig = serverConfig(); + config.FEATURE_USER_MIGRATION_ENABLED = true; + config.FEATURE_USER_MIGRATION_SYSTEM_ID = systemId || new ObjectId().toString(); + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = false; }; beforeAll(async () => { @@ -58,7 +58,6 @@ describe('ImportUser Controller Populate (API)', () => { em = app.get(EntityManager); testApiClient = new TestApiClient(app, 'user/import'); - userImportFeatures = app.get(UserImportFeatures); axiosMock = new MockAdapter(axios); }); @@ -90,7 +89,8 @@ describe('ImportUser Controller Populate (API)', () => { const { account } = await authenticatedUser([Permission.IMPORT_USER_MIGRATE]); const loggedInClient = await testApiClient.login(account); - userImportFeatures.userMigrationEnabled = false; + const config: ServerConfig = serverConfig(); + config.FEATURE_USER_MIGRATION_ENABLED = false; return { loggedInClient }; }; @@ -113,7 +113,8 @@ describe('ImportUser Controller Populate (API)', () => { const setup = async () => { const { account, school, system } = await authenticatedUser([Permission.IMPORT_USER_MIGRATE], [], false); const loggedInClient = await testApiClient.login(account); - userImportFeatures.userMigrationSystemId = system.id; + const config: ServerConfig = serverConfig(); + config.FEATURE_USER_MIGRATION_SYSTEM_ID = system.id; school.externalId = undefined; @@ -139,8 +140,9 @@ describe('ImportUser Controller Populate (API)', () => { const { account, school, system } = await authenticatedUser([Permission.IMPORT_USER_MIGRATE]); const loggedInClient = await testApiClient.login(account); - userImportFeatures.userMigrationEnabled = true; - userImportFeatures.userMigrationSystemId = system.id; + const config: ServerConfig = serverConfig(); + config.FEATURE_USER_MIGRATION_ENABLED = true; + config.FEATURE_USER_MIGRATION_SYSTEM_ID = system.id; axiosMock.onPost(/(.*)\/token/).reply(HttpStatus.OK, { id_token: 'idToken', diff --git a/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts b/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts index 65cb8f82969..c0aa9b06e36 100644 --- a/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts +++ b/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts @@ -1,4 +1,5 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { serverConfig, ServerConfig } from '@modules/server'; import { ServerTestModule } from '@modules/server/server.module'; import { SystemEntity } from '@modules/system/entity'; import { @@ -35,14 +36,12 @@ import { } from '@shared/testing'; import { AccountEntity } from '@src/modules/account/domain/entity/account.entity'; import { accountFactory } from '@src/modules/account/testing'; -import { IUserImportFeatures, UserImportFeatures } from '../../config'; describe('ImportUser Controller (API)', () => { let app: INestApplication; let em: EntityManager; let testApiClient: TestApiClient; - let userImportFeatures: IUserImportFeatures; const authenticatedUser = async ( permissions: Permission[] = [], @@ -67,9 +66,10 @@ describe('ImportUser Controller (API)', () => { }; const setConfig = (systemId?: string) => { - userImportFeatures.userMigrationEnabled = true; - userImportFeatures.userMigrationSystemId = systemId || new ObjectId().toString(); - userImportFeatures.useWithUserLoginMigration = false; + const config: ServerConfig = serverConfig(); + config.FEATURE_USER_MIGRATION_ENABLED = true; + config.FEATURE_USER_MIGRATION_SYSTEM_ID = systemId || new ObjectId().toString(); + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = false; }; beforeAll(async () => { @@ -83,7 +83,6 @@ describe('ImportUser Controller (API)', () => { em = app.get(EntityManager); testApiClient = new TestApiClient(app, 'user/import'); - userImportFeatures = app.get(UserImportFeatures); }); afterAll(async () => { @@ -116,8 +115,11 @@ describe('ImportUser Controller (API)', () => { Permission.IMPORT_USER_VIEW, ])); testApiClient = await testApiClient.login(account); - userImportFeatures.userMigrationEnabled = false; - userImportFeatures.userMigrationSystemId = ''; + + const config: ServerConfig = serverConfig(); + config.FEATURE_USER_MIGRATION_ENABLED = false; + config.FEATURE_USER_MIGRATION_SYSTEM_ID = ''; + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = false; }); afterEach(() => { @@ -173,7 +175,8 @@ describe('ImportUser Controller (API)', () => { beforeEach(async () => { ({ account, system } = await authenticatedUser()); testApiClient = await testApiClient.login(account); - userImportFeatures.userMigrationSystemId = system._id.toString(); + const config: ServerConfig = serverConfig(); + config.FEATURE_USER_MIGRATION_SYSTEM_ID = system._id.toString(); }); it('GET /user/import is UNAUTHORIZED', async () => { @@ -225,8 +228,9 @@ describe('ImportUser Controller (API)', () => { [SchoolFeature.LDAP_UNIVENTION_MIGRATION] )); testApiClient = await testApiClient.login(account); - userImportFeatures.userMigrationSystemId = system._id.toString(); - userImportFeatures.userMigrationEnabled = false; + const config: ServerConfig = serverConfig(); + config.FEATURE_USER_MIGRATION_ENABLED = false; + config.FEATURE_USER_MIGRATION_SYSTEM_ID = system._id.toString(); }); it('GET user/import is authorized, despite feature not enabled', async () => { @@ -244,7 +248,8 @@ describe('ImportUser Controller (API)', () => { beforeEach(async () => { ({ school, system, account } = await authenticatedUser([Permission.IMPORT_USER_VIEW])); testApiClient = await testApiClient.login(account); - userImportFeatures.userMigrationSystemId = system._id.toString(); + const config: ServerConfig = serverConfig(); + config.FEATURE_USER_MIGRATION_SYSTEM_ID = system._id.toString(); }); it('GET /user/import responds with importusers', async () => { @@ -1082,7 +1087,8 @@ describe('ImportUser Controller (API)', () => { it('should set in user migration mode', async () => { ({ account, system } = await authenticatedUser([Permission.IMPORT_USER_MIGRATE])); testApiClient = await testApiClient.login(account); - userImportFeatures.userMigrationSystemId = system._id.toString(); + const config: ServerConfig = serverConfig(); + config.FEATURE_USER_MIGRATION_SYSTEM_ID = system._id.toString(); await testApiClient.post('startUserMigration').expect(HttpStatus.CREATED); }); diff --git a/apps/server/src/modules/user-import/index.ts b/apps/server/src/modules/user-import/index.ts index 0d666df6a20..e149f9d70bf 100644 --- a/apps/server/src/modules/user-import/index.ts +++ b/apps/server/src/modules/user-import/index.ts @@ -1,4 +1,3 @@ export { ImportUserModule } from './user-import.module'; -export { UserImportConfigModule } from './user-import-config.module'; -export { IUserImportFeatures, UserImportConfiguration } from './config'; +export { UserImportConfig } from './user-import-config'; export { UserImportService } from './service'; diff --git a/apps/server/src/modules/user-import/service/user-import.service.spec.ts b/apps/server/src/modules/user-import/service/user-import.service.spec.ts index 5575c423610..87d683f2868 100644 --- a/apps/server/src/modules/user-import/service/user-import.service.spec.ts +++ b/apps/server/src/modules/user-import/service/user-import.service.spec.ts @@ -5,6 +5,7 @@ import { LegacySchoolService } from '@modules/legacy-school'; import { System, SystemService } from '@modules/system'; import { UserService } from '@modules/user'; import { InternalServerErrorException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { LegacySchoolDo } from '@shared/domain/domainobject'; import { ImportUser, MatchCreator, SchoolEntity, User } from '@shared/domain/entity'; @@ -20,8 +21,8 @@ import { userFactory, } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { IUserImportFeatures, UserImportFeatures } from '../config'; import { UserMigrationCanceledLoggable, UserMigrationIsNotEnabled } from '../loggable'; +import { UserImportConfig } from '../user-import-config'; import { UserImportService } from './user-import.service'; describe(UserImportService.name, () => { @@ -35,10 +36,11 @@ describe(UserImportService.name, () => { let logger: DeepMocked; let schoolService: DeepMocked; - const features: IUserImportFeatures = { - userMigrationSystemId: new ObjectId().toHexString(), - userMigrationEnabled: true, - useWithUserLoginMigration: true, + const config: UserImportConfig = { + FEATURE_USER_MIGRATION_SYSTEM_ID: new ObjectId().toHexString(), + FEATURE_USER_MIGRATION_ENABLED: true, + FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION: true, + IMPORTUSER_SAVE_ALL_MATCHES_REQUEST_TIMEOUT_MS: 8000, }; beforeAll(async () => { @@ -48,6 +50,12 @@ describe(UserImportService.name, () => { imports: [MongoMemoryDatabaseModule.forRoot()], providers: [ UserImportService, + { + provide: ConfigService, + useValue: { + get: jest.fn().mockImplementation((key: keyof UserImportConfig) => config[key]), + }, + }, { provide: ImportUserRepo, useValue: createMock(), @@ -60,10 +68,6 @@ describe(UserImportService.name, () => { provide: UserService, useValue: createMock(), }, - { - provide: UserImportFeatures, - useValue: features, - }, { provide: Logger, useValue: createMock(), @@ -138,7 +142,7 @@ describe(UserImportService.name, () => { describe('checkFeatureEnabled', () => { describe('when the global feature is enabled', () => { const setup = () => { - features.userMigrationEnabled = true; + config.FEATURE_USER_MIGRATION_ENABLED = true; const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ features: undefined }); @@ -156,7 +160,7 @@ describe(UserImportService.name, () => { describe('when the school feature is enabled', () => { const setup = () => { - features.userMigrationEnabled = false; + config.FEATURE_USER_MIGRATION_ENABLED = false; const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ features: [SchoolFeature.LDAP_UNIVENTION_MIGRATION], @@ -174,9 +178,9 @@ describe(UserImportService.name, () => { }); }); - describe('when the features are disabled', () => { + describe('when the config are disabled', () => { const setup = () => { - features.userMigrationEnabled = false; + config.FEATURE_USER_MIGRATION_ENABLED = false; const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ features: [], diff --git a/apps/server/src/modules/user-import/service/user-import.service.ts b/apps/server/src/modules/user-import/service/user-import.service.ts index 4b47451f74c..db3fbeb5c70 100644 --- a/apps/server/src/modules/user-import/service/user-import.service.ts +++ b/apps/server/src/modules/user-import/service/user-import.service.ts @@ -1,22 +1,23 @@ import { LegacySchoolService } from '@modules/legacy-school'; import { System, SystemService } from '@modules/system'; import { UserService } from '@modules/user'; -import { Inject, Injectable, InternalServerErrorException } from '@nestjs/common'; +import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { LegacySchoolDo } from '@shared/domain/domainobject'; import { ImportUser, MatchCreator, SchoolEntity, User } from '@shared/domain/entity'; import { SchoolFeature } from '@shared/domain/types'; import { ImportUserRepo } from '@shared/repo'; import { Logger } from '@src/core/logger'; -import { IUserImportFeatures, UserImportFeatures } from '../config'; import { UserMigrationCanceledLoggable, UserMigrationIsNotEnabled } from '../loggable'; +import { UserImportConfig } from '../user-import-config'; @Injectable() export class UserImportService { constructor( + private readonly configService: ConfigService, private readonly userImportRepo: ImportUserRepo, private readonly systemService: SystemService, private readonly userService: UserService, - @Inject(UserImportFeatures) private readonly userImportFeatures: IUserImportFeatures, private readonly logger: Logger, private readonly schoolService: LegacySchoolService ) {} @@ -26,7 +27,7 @@ export class UserImportService { } public async getMigrationSystem(): Promise { - const systemId: string = this.userImportFeatures.userMigrationSystemId; + const systemId: string = this.configService.get('FEATURE_USER_MIGRATION_SYSTEM_ID'); const system: System = await this.systemService.findByIdOrFail(systemId); @@ -34,7 +35,7 @@ export class UserImportService { } public checkFeatureEnabled(school: LegacySchoolDo): void { - const enabled = this.userImportFeatures.userMigrationEnabled; + const enabled = this.configService.get('FEATURE_USER_MIGRATION_ENABLED'); const isLdapPilotSchool = school.features && school.features.includes(SchoolFeature.LDAP_UNIVENTION_MIGRATION); if (!enabled && !isLdapPilotSchool) { diff --git a/apps/server/src/modules/user-import/uc/user-import-fetch.uc.spec.ts b/apps/server/src/modules/user-import/uc/user-import-fetch.uc.spec.ts index f9ca795e3e3..8394306979f 100644 --- a/apps/server/src/modules/user-import/uc/user-import-fetch.uc.spec.ts +++ b/apps/server/src/modules/user-import/uc/user-import-fetch.uc.spec.ts @@ -1,15 +1,16 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { AuthorizationService } from '@modules/authorization'; +import { ConfigService } from '@nestjs/config'; import { System } from '@modules/system'; import { SystemEntity } from '@modules/system/entity'; import { Test, TestingModule } from '@nestjs/testing'; import { ImportUser, User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { importUserFactory, setupEntities, systemEntityFactory, systemFactory, userFactory } from '@shared/testing'; -import { IUserImportFeatures, UserImportFeatures } from '../config'; import { UserMigrationIsNotEnabledLoggableException } from '../loggable'; import { SchulconnexFetchImportUsersService, UserImportService } from '../service'; +import { UserImportConfig } from '../user-import-config'; import { UserImportFetchUc } from './user-import-fetch.uc'; describe(UserImportFetchUc.name, () => { @@ -19,7 +20,13 @@ describe(UserImportFetchUc.name, () => { let schulconnexFetchImportUsersService: DeepMocked; let authorizationService: DeepMocked; let userImportService: DeepMocked; - let userImportFeatures: IUserImportFeatures; + + const config: UserImportConfig = { + FEATURE_USER_MIGRATION_ENABLED: true, + FEATURE_USER_MIGRATION_SYSTEM_ID: new ObjectId().toHexString(), + FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION: true, + IMPORTUSER_SAVE_ALL_MATCHES_REQUEST_TIMEOUT_MS: 0, + }; beforeAll(async () => { await setupEntities(); @@ -28,8 +35,10 @@ describe(UserImportFetchUc.name, () => { providers: [ UserImportFetchUc, { - provide: UserImportFeatures, - useValue: {}, + provide: ConfigService, + useValue: { + get: jest.fn().mockImplementation((key: keyof UserImportConfig) => config[key]), + }, }, { provide: SchulconnexFetchImportUsersService, @@ -50,15 +59,12 @@ describe(UserImportFetchUc.name, () => { schulconnexFetchImportUsersService = module.get(SchulconnexFetchImportUsersService); authorizationService = module.get(AuthorizationService); userImportService = module.get(UserImportService); - userImportFeatures = module.get(UserImportFeatures); }); beforeEach(() => { - Object.assign(userImportFeatures, { - userMigrationEnabled: true, - userMigrationSystemId: new ObjectId().toHexString(), - useWithUserLoginMigration: true, - }); + config.FEATURE_USER_MIGRATION_ENABLED = true; + config.FEATURE_USER_MIGRATION_SYSTEM_ID = new ObjectId().toHexString(); + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = true; }); afterAll(async () => { @@ -74,7 +80,7 @@ describe(UserImportFetchUc.name, () => { const setup = () => { const system: SystemEntity = systemEntityFactory.buildWithId( undefined, - userImportFeatures.userMigrationSystemId + config.FEATURE_USER_MIGRATION_SYSTEM_ID ); const systemDo: System = systemFactory.build({ id: system.id }); const user: User = userFactory.buildWithId(); @@ -142,7 +148,7 @@ describe(UserImportFetchUc.name, () => { describe('when the migration feature is not enabled', () => { const setup = () => { - userImportFeatures.userMigrationEnabled = false; + config.FEATURE_USER_MIGRATION_ENABLED = false; const user: User = userFactory.buildWithId(); @@ -160,7 +166,7 @@ describe(UserImportFetchUc.name, () => { describe('when the target system id is not defined', () => { const setup = () => { - userImportFeatures.userMigrationSystemId = ''; + config.FEATURE_USER_MIGRATION_SYSTEM_ID = ''; const user: User = userFactory.buildWithId(); diff --git a/apps/server/src/modules/user-import/uc/user-import-fetch.uc.ts b/apps/server/src/modules/user-import/uc/user-import-fetch.uc.ts index 1e08f64f3b2..743e942449d 100644 --- a/apps/server/src/modules/user-import/uc/user-import-fetch.uc.ts +++ b/apps/server/src/modules/user-import/uc/user-import-fetch.uc.ts @@ -1,17 +1,18 @@ import { AuthorizationService } from '@modules/authorization'; import { System } from '@modules/system'; -import { Inject, Injectable } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { ImportUser, User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; -import { IUserImportFeatures, UserImportFeatures } from '../config'; import { UserMigrationIsNotEnabledLoggableException } from '../loggable'; import { SchulconnexFetchImportUsersService, UserImportService } from '../service'; +import { UserImportConfig } from '../user-import-config'; @Injectable() export class UserImportFetchUc { constructor( - @Inject(UserImportFeatures) private readonly userImportFeatures: IUserImportFeatures, + private readonly configService: ConfigService, private readonly schulconnexFetchImportUsersService: SchulconnexFetchImportUsersService, private readonly authorizationService: AuthorizationService, private readonly userImportService: UserImportService @@ -28,7 +29,7 @@ export class UserImportFetchUc { const filteredFetchedData: ImportUser[] = await this.schulconnexFetchImportUsersService.filterAlreadyMigratedUser( fetchedData, - this.userImportFeatures.userMigrationSystemId + this.configService.get('FEATURE_USER_MIGRATION_SYSTEM_ID') ); const matchedImportUsers: ImportUser[] = await this.userImportService.matchUsers(filteredFetchedData); @@ -39,7 +40,10 @@ export class UserImportFetchUc { } private checkMigrationEnabled(userId: EntityId): void { - if (!this.userImportFeatures.userMigrationEnabled || !this.userImportFeatures.userMigrationSystemId) { + if ( + !this.configService.get('FEATURE_USER_MIGRATION_ENABLED') || + !this.configService.get('FEATURE_USER_MIGRATION_SYSTEM_ID') + ) { throw new UserMigrationIsNotEnabledLoggableException(userId); } } diff --git a/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts b/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts index 147dba2887e..5b22368f4f3 100644 --- a/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts +++ b/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts @@ -9,6 +9,7 @@ import { UserService } from '@modules/user'; import { UserLoginMigrationNotActiveLoggableException } from '@modules/user-import/loggable/user-login-migration-not-active.loggable-exception'; import { UserLoginMigrationService, UserMigrationService } from '@modules/user-login-migration'; import { BadRequestException, ForbiddenException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { UserAlreadyAssignedToImportUserError } from '@shared/common'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; @@ -30,9 +31,9 @@ import { userLoginMigrationDOFactory, } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { IUserImportFeatures, UserImportFeatures } from '../config'; import { SchoolNotMigratedLoggableException, UserAlreadyMigratedLoggable } from '../loggable'; import { UserImportService } from '../service'; +import { UserImportConfig } from '../user-import-config'; import { LdapAlreadyPersistedException, MigrationAlreadyActivatedException, @@ -56,7 +57,12 @@ describe('[ImportUserModule]', () => { let userMigrationService: DeepMocked; let logger: DeepMocked; - let userImportFeatures: IUserImportFeatures; + const config: UserImportConfig = { + FEATURE_USER_MIGRATION_ENABLED: true, + FEATURE_USER_MIGRATION_SYSTEM_ID: new ObjectId().toHexString(), + FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION: false, + IMPORTUSER_SAVE_ALL_MATCHES_REQUEST_TIMEOUT_MS: 80000, + }; beforeAll(async () => { await setupEntities(); @@ -64,6 +70,12 @@ describe('[ImportUserModule]', () => { module = await Test.createTestingModule({ providers: [ UserImportUc, + { + provide: ConfigService, + useValue: { + get: jest.fn().mockImplementation((key: keyof UserImportConfig) => config[key]), + }, + }, { provide: AccountService, useValue: createMock(), @@ -100,10 +112,6 @@ describe('[ImportUserModule]', () => { provide: UserLoginMigrationService, useValue: createMock(), }, - { - provide: UserImportFeatures, - useValue: {}, - }, { provide: UserMigrationService, useValue: createMock(), @@ -124,18 +132,15 @@ describe('[ImportUserModule]', () => { userService = module.get(UserService); authorizationService = module.get(AuthorizationService); userImportService = module.get(UserImportService); - userImportFeatures = module.get(UserImportFeatures); userLoginMigrationService = module.get(UserLoginMigrationService); userMigrationService = module.get(UserMigrationService); logger = module.get(Logger); }); beforeEach(() => { - Object.assign(userImportFeatures, { - userMigrationEnabled: true, - userMigrationSystemId: new ObjectId().toHexString(), - useWithUserLoginMigration: false, - }); + config.FEATURE_USER_MIGRATION_ENABLED = true; + config.FEATURE_USER_MIGRATION_SYSTEM_ID = new ObjectId().toHexString(); + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = false; }); afterAll(async () => { @@ -658,7 +663,7 @@ describe('[ImportUserModule]', () => { userService.findByExternalId.mockResolvedValueOnce(null); schoolService.getSchoolById.mockResolvedValueOnce(school); importUserRepo.findImportUsers.mockResolvedValueOnce([[importUser, importUserWithoutUser], 2]); - userImportFeatures.useWithUserLoginMigration = true; + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = true; return { user, @@ -726,7 +731,7 @@ describe('[ImportUserModule]', () => { userDoFactory.buildWithId({ id: user.id, externalId: user.externalId }) ); importUserRepo.findImportUsers.mockResolvedValueOnce([[importUser, importUserWithoutUser], 2]); - userImportFeatures.useWithUserLoginMigration = true; + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = true; return { user, @@ -820,7 +825,7 @@ describe('[ImportUserModule]', () => { schoolServiceSaveSpy = schoolService.save.mockReturnValueOnce(Promise.resolve(createMockSchoolDo(school))); schoolServiceSpy = schoolService.getSchoolById.mockResolvedValue(createMockSchoolDo(school)); systemRepoSpy = systemService.findById.mockReturnValueOnce(Promise.resolve(systemDo)); - userImportFeatures.userMigrationSystemId = system.id; + config.FEATURE_USER_MIGRATION_SYSTEM_ID = system.id; dateSpy = jest.spyOn(global, 'Date').mockReturnValue(currentDate as unknown as string); }); @@ -932,7 +937,7 @@ describe('[ImportUserModule]', () => { targetSystemId, }); - userImportFeatures.useWithUserLoginMigration = true; + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = true; userRepo.findById.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); userLoginMigrationService.findMigrationBySchool.mockResolvedValue(userLoginMigration); @@ -982,7 +987,7 @@ describe('[ImportUserModule]', () => { closedAt: new Date(), }); - userImportFeatures.useWithUserLoginMigration = true; + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = true; userRepo.findById.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); userLoginMigrationService.findMigrationBySchool.mockResolvedValue(userLoginMigration); @@ -1012,7 +1017,7 @@ describe('[ImportUserModule]', () => { systems: [targetSystemId], }); - userImportFeatures.useWithUserLoginMigration = true; + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = true; userRepo.findById.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); userLoginMigrationService.findMigrationBySchool.mockResolvedValue(null); @@ -1045,7 +1050,7 @@ describe('[ImportUserModule]', () => { targetSystemId, }); - userImportFeatures.useWithUserLoginMigration = true; + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = true; userRepo.findById.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); userLoginMigrationService.findMigrationBySchool.mockResolvedValue(userLoginMigration); @@ -1142,7 +1147,7 @@ describe('[ImportUserModule]', () => { userRepo.findById.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); - userImportFeatures.useWithUserLoginMigration = true; + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = true; return { user, @@ -1171,7 +1176,7 @@ describe('[ImportUserModule]', () => { userRepo.findById.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); - userImportFeatures.useWithUserLoginMigration = true; + config.FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = true; return { user, diff --git a/apps/server/src/modules/user-import/uc/user-import.uc.ts b/apps/server/src/modules/user-import/uc/user-import.uc.ts index be63375ad4e..6a8547fc4a3 100644 --- a/apps/server/src/modules/user-import/uc/user-import.uc.ts +++ b/apps/server/src/modules/user-import/uc/user-import.uc.ts @@ -5,7 +5,8 @@ import { System, SystemService } from '@modules/system'; import { UserService } from '@modules/user'; import { UserLoginMigrationNotActiveLoggableException } from '@modules/user-import/loggable/user-login-migration-not-active.loggable-exception'; import { UserLoginMigrationService, UserMigrationService } from '@modules/user-login-migration'; -import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { UserAlreadyAssignedToImportUserError } from '@shared/common'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { LegacySchoolDo, UserDO, UserLoginMigrationDO } from '@shared/domain/domainobject'; @@ -14,7 +15,6 @@ import { IFindOptions, Permission } from '@shared/domain/interface'; import { Counted, EntityId, IImportUserScope, MatchCreatorScope, NameMatch } from '@shared/domain/types'; import { ImportUserRepo, UserRepo } from '@shared/repo'; import { Logger } from '@src/core/logger'; -import { IUserImportFeatures, UserImportFeatures } from '../config'; import { MigrationMayBeCompleted, MigrationMayNotBeCompleted, @@ -26,6 +26,7 @@ import { } from '../loggable'; import { UserImportService } from '../service'; +import { UserImportConfig } from '../user-import-config'; import { LdapAlreadyPersistedException, MigrationAlreadyActivatedException, @@ -40,6 +41,7 @@ export type UserImportPermissions = @Injectable() export class UserImportUc { constructor( + private readonly configService: ConfigService, private readonly accountService: AccountService, private readonly importUserRepo: ImportUserRepo, private readonly authorizationService: AuthorizationService, @@ -49,7 +51,6 @@ export class UserImportUc { private readonly userService: UserService, private readonly logger: Logger, private readonly userImportService: UserImportService, - @Inject(UserImportFeatures) private readonly userImportFeatures: IUserImportFeatures, private readonly userLoginMigrationService: UserLoginMigrationService, private readonly userMigrationService: UserMigrationService ) { @@ -238,9 +239,11 @@ export class UserImportUc { } public async startSchoolInUserMigration(currentUserId: EntityId, useCentralLdap = true): Promise { - const { useWithUserLoginMigration } = this.userImportFeatures; + const FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = this.configService.get( + 'FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION' + ); - if (useWithUserLoginMigration) { + if (FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION) { useCentralLdap = false; } @@ -248,11 +251,11 @@ export class UserImportUc { const school: LegacySchoolDo = await this.schoolService.getSchoolById(currentUser.school.id); this.userImportService.checkFeatureEnabled(school); - if (useCentralLdap || useWithUserLoginMigration) { + if (useCentralLdap || FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION) { this.checkSchoolNumber(school); } this.checkSchoolNotInMigration(school); - if (useWithUserLoginMigration) { + if (FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION) { await this.checkSchoolMigrated(currentUser.school.id, school); await this.checkMigrationActive(currentUser.school.id); } else { @@ -261,7 +264,7 @@ export class UserImportUc { this.logger.notice(new SchoolInUserMigrationStartLoggable(currentUserId, school.name, useCentralLdap)); - if (!useWithUserLoginMigration) { + if (!FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION) { school.externalId = school.officialSchoolNumber; } @@ -316,7 +319,9 @@ export class UserImportUc { school.inMaintenanceSince = undefined; - const isMigrationRestartable: boolean = this.userImportFeatures.useWithUserLoginMigration; + const isMigrationRestartable: boolean = this.configService.get( + 'FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION' + ); if (isMigrationRestartable) { school.inUserMigration = undefined; } @@ -343,9 +348,11 @@ export class UserImportUc { } private async updateUserAndAccount(importUser: ImportUser, school: LegacySchoolDo): Promise { - const { useWithUserLoginMigration } = this.userImportFeatures; + const FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION = this.configService.get( + 'FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION' + ); - if (useWithUserLoginMigration) { + if (FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION) { await this.updateUserAndAccountWithUserLoginMigration(importUser); } else { await this.updateUserAndAccountWithLdap(importUser, school); diff --git a/apps/server/src/modules/user-import/user-import-config.module.ts b/apps/server/src/modules/user-import/user-import-config.module.ts deleted file mode 100644 index 34ba1e5f994..00000000000 --- a/apps/server/src/modules/user-import/user-import-config.module.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { Module } from '@nestjs/common'; -import { UserImportConfiguration, UserImportFeatures } from './config'; - -@Module({ - providers: [ - { - provide: UserImportFeatures, - useValue: UserImportConfiguration.userImportFeatures, - }, - ], - exports: [UserImportFeatures], -}) -export class UserImportConfigModule {} diff --git a/apps/server/src/modules/user-import/user-import-config.ts b/apps/server/src/modules/user-import/user-import-config.ts index a7145a56d1b..ce005c20ce5 100644 --- a/apps/server/src/modules/user-import/user-import-config.ts +++ b/apps/server/src/modules/user-import/user-import-config.ts @@ -1,3 +1,6 @@ export interface UserImportConfig { + FEATURE_USER_MIGRATION_ENABLED: boolean; + FEATURE_USER_MIGRATION_SYSTEM_ID: string; + FEATURE_MIGRATION_WIZARD_WITH_USER_LOGIN_MIGRATION: boolean; IMPORTUSER_SAVE_ALL_MATCHES_REQUEST_TIMEOUT_MS: number; } diff --git a/apps/server/src/modules/user-import/user-import.module.ts b/apps/server/src/modules/user-import/user-import.module.ts index b73db2aebda..b69b80b9fd1 100644 --- a/apps/server/src/modules/user-import/user-import.module.ts +++ b/apps/server/src/modules/user-import/user-import.module.ts @@ -13,7 +13,6 @@ import { LoggerModule } from '@src/core/logger'; import { ImportUserController } from './controller/import-user.controller'; import { SchulconnexFetchImportUsersService, UserImportService } from './service'; import { UserImportFetchUc, UserImportUc } from './uc'; -import { UserImportConfigModule } from './user-import-config.module'; @Module({ imports: [ @@ -21,7 +20,6 @@ import { UserImportConfigModule } from './user-import-config.module'; AccountModule, LegacySchoolModule, AuthorizationModule, - UserImportConfigModule, HttpModule, UserModule, OauthModule, diff --git a/apps/server/src/modules/video-conference/bbb/bbb-config.ts b/apps/server/src/modules/video-conference/bbb/bbb-config.ts new file mode 100644 index 00000000000..61488050134 --- /dev/null +++ b/apps/server/src/modules/video-conference/bbb/bbb-config.ts @@ -0,0 +1,5 @@ +export interface BbbConfig { + VIDEOCONFERENCE_HOST: string; + VIDEOCONFERENCE_SALT: string; + VIDEOCONFERENCE_DEFAULT_PRESENTATION: string; +} diff --git a/apps/server/src/modules/video-conference/bbb/bbb-settings.interface.ts b/apps/server/src/modules/video-conference/bbb/bbb-settings.interface.ts deleted file mode 100644 index c250bebb929..00000000000 --- a/apps/server/src/modules/video-conference/bbb/bbb-settings.interface.ts +++ /dev/null @@ -1,7 +0,0 @@ -export const BbbSettings = Symbol('BbbSettings'); - -export interface IBbbSettings { - host: string; - salt: string; - presentationUrl: string; -} diff --git a/apps/server/src/modules/video-conference/bbb/bbb.service.spec.ts b/apps/server/src/modules/video-conference/bbb/bbb.service.spec.ts index 1731d10ff8e..9c643f80698 100644 --- a/apps/server/src/modules/video-conference/bbb/bbb.service.spec.ts +++ b/apps/server/src/modules/video-conference/bbb/bbb.service.spec.ts @@ -1,6 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { HttpService } from '@nestjs/axios'; import { InternalServerErrorException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { ConverterUtil } from '@shared/common'; import { axiosResponseFactory } from '@shared/testing'; @@ -9,7 +10,8 @@ import { AxiosResponse } from 'axios'; import crypto, { Hash } from 'crypto'; import { of } from 'rxjs'; import { URLSearchParams } from 'url'; -import { BbbSettings, IBbbSettings } from './bbb-settings.interface'; +import { VideoConferenceConfig } from '../video-conference-config'; +import { BbbConfig } from './bbb-config'; import { BBBService } from './bbb.service'; import { BBBBaseMeetingConfig, BBBCreateConfig, BBBJoinConfig, BBBRole, GuestPolicy } from './request'; import { BBBBaseResponse, BBBCreateResponse, BBBMeetingInfoResponse, BBBResponse, BBBStatus } from './response'; @@ -106,23 +108,20 @@ class BBBServiceTest extends BBBService { } } -describe('BBB Service', () => { +describe(BBBService.name, () => { let module: TestingModule; let service: BBBServiceTest; let httpService: DeepMocked; let converterUtil: DeepMocked; + let configService: DeepMocked>; beforeAll(async () => { module = await Test.createTestingModule({ providers: [ BBBServiceTest, { - provide: BbbSettings, - useValue: createMock({ - host: 'https://bbb.de', - salt: 'salt12345', - presentationUrl: '', - }), + provide: ConfigService, + useValue: createMock>(), }, { provide: HttpService, @@ -137,12 +136,17 @@ describe('BBB Service', () => { service = module.get(BBBServiceTest); httpService = module.get(HttpService); converterUtil = module.get(ConverterUtil); + configService = module.get(ConfigService); }); afterAll(async () => { await module.close(); }); + beforeEach(() => { + configService.get.mockReturnValue('https://mocked'); + }); + describe('create', () => { describe('when valid parameter passed and the BBB response well', () => { const setup = () => { @@ -200,13 +204,10 @@ describe('BBB Service', () => { }); it('should return a xml configuration with provided presentation url', () => { - // Arrange const presentationUrl = 'https://s3.hidrive.strato.com/cloud-instances/bbb/presentation.pdf'; - // Act const result = service.getBbbRequestConfig(presentationUrl); - // Assert expect(result).toBe( "" ); @@ -377,13 +378,10 @@ describe('BBB Service', () => { }); it('toParams: should return params based on bbb configs', () => { - // Arrange const createConfig: BBBCreateConfig = createBBBCreateConfig(); - // Act const params: URLSearchParams = service.superToParams(createConfig); - // Assert expect(params.get('name')).toEqual(createConfig.name); expect(params.get('meetingID')).toEqual(createConfig.meetingID); expect(params.get('logoutURL')).toEqual(createConfig.logoutURL); @@ -408,24 +406,19 @@ describe('BBB Service', () => { const sha = crypto.createHash('sha1'); const expectedChecksum: string = sha.update(callName + queryString + service.getSalt()).digest('hex'); - // Act const checksum: string = service.superGenerateChecksum(callName, urlSearchParams); - // Assert expect(checksum).toEqual(expectedChecksum); expect(createHashMock).toBeCalledWith('sha1'); }); it('getUrl: should return composed url', () => { - // Arrange const createConfig = createBBBCreateConfig(); const callName = 'create'; const params: URLSearchParams = service.superToParams(createConfig); - // Act const url: string = service.superGetUrl(callName, params); - // Assert expect(url.toString()).toContain(`${service.getBaseUrl()}/bigbluebutton/api/${callName}`); expect(url.includes('checksum')).toBeTruthy(); }); diff --git a/apps/server/src/modules/video-conference/bbb/bbb.service.ts b/apps/server/src/modules/video-conference/bbb/bbb.service.ts index 2fcc3db9981..b40e47053b4 100644 --- a/apps/server/src/modules/video-conference/bbb/bbb.service.ts +++ b/apps/server/src/modules/video-conference/bbb/bbb.service.ts @@ -1,33 +1,35 @@ import { HttpService } from '@nestjs/axios'; -import { Inject, Injectable, InternalServerErrorException } from '@nestjs/common'; +import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { ConverterUtil } from '@shared/common/utils'; import { ErrorUtils } from '@src/core/error/utils'; import { AxiosResponse } from 'axios'; import crypto from 'crypto'; import { firstValueFrom, Observable } from 'rxjs'; import { URL, URLSearchParams } from 'url'; -import { BbbSettings, IBbbSettings } from './bbb-settings.interface'; +import { VideoConferenceConfig } from '../video-conference-config'; +import { BbbConfig } from './bbb-config'; import { BBBBaseMeetingConfig, BBBCreateConfig, BBBJoinConfig } from './request'; import { BBBBaseResponse, BBBCreateResponse, BBBMeetingInfoResponse, BBBResponse, BBBStatus } from './response'; @Injectable() export class BBBService { constructor( - @Inject(BbbSettings) private readonly bbbSettings: IBbbSettings, + private readonly configService: ConfigService, private readonly httpService: HttpService, private readonly converterUtil: ConverterUtil ) {} protected get baseUrl(): string { - return this.bbbSettings.host; + return this.configService.get('VIDEOCONFERENCE_HOST'); } protected get salt(): string { - return this.bbbSettings.salt; + return this.configService.get('VIDEOCONFERENCE_SALT'); } protected get presentationUrl(): string { - return this.bbbSettings.presentationUrl; + return this.configService.get('VIDEOCONFERENCE_DEFAULT_PRESENTATION'); } /** diff --git a/apps/server/src/modules/video-conference/bbb/index.ts b/apps/server/src/modules/video-conference/bbb/index.ts index 66f3703a950..cc35a1e2d07 100644 --- a/apps/server/src/modules/video-conference/bbb/index.ts +++ b/apps/server/src/modules/video-conference/bbb/index.ts @@ -1,5 +1,5 @@ -export * from './bbb-settings.interface'; export * from './request'; export * from './builder'; export * from './response'; export * from './bbb.service'; +export { BbbConfig } from './bbb-config'; diff --git a/apps/server/src/modules/video-conference/index.ts b/apps/server/src/modules/video-conference/index.ts index 16071ac6627..7564a72653f 100644 --- a/apps/server/src/modules/video-conference/index.ts +++ b/apps/server/src/modules/video-conference/index.ts @@ -1,3 +1,2 @@ export { VideoConferenceModule } from './video-conference.module'; -export { IVideoConferenceSettings } from './interface'; -export { default as VideoConferenceConfiguration } from './video-conference-config'; +export { VideoConferenceConfig } from './video-conference-config'; diff --git a/apps/server/src/modules/video-conference/interface/index.ts b/apps/server/src/modules/video-conference/interface/index.ts index 5df41ce67c3..6c797da8dc5 100644 --- a/apps/server/src/modules/video-conference/interface/index.ts +++ b/apps/server/src/modules/video-conference/interface/index.ts @@ -1,2 +1 @@ -export * from './video-conference-settings.interface'; export * from './video-conference-options.interface'; diff --git a/apps/server/src/modules/video-conference/interface/video-conference-settings.interface.ts b/apps/server/src/modules/video-conference/interface/video-conference-settings.interface.ts deleted file mode 100644 index a1d07fb516a..00000000000 --- a/apps/server/src/modules/video-conference/interface/video-conference-settings.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { IBbbSettings } from '../bbb'; - -export const VideoConferenceSettings = Symbol('VideoConferenceSettings'); - -export interface IVideoConferenceSettings { - enabled: boolean; - hostUrl: string; - bbb: IBbbSettings; -} diff --git a/apps/server/src/modules/video-conference/service/video-conference.service.spec.ts b/apps/server/src/modules/video-conference/service/video-conference.service.spec.ts index d0daef948aa..3bc50b9f20f 100644 --- a/apps/server/src/modules/video-conference/service/video-conference.service.spec.ts +++ b/apps/server/src/modules/video-conference/service/video-conference.service.spec.ts @@ -1,11 +1,13 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { CalendarEventDto, CalendarService } from '@infra/calendar'; +import { ObjectId } from '@mikro-orm/mongodb'; import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { CourseService } from '@modules/learnroom/service'; import { LegacySchoolService } from '@modules/legacy-school'; import { UserService } from '@modules/user'; import { BadRequestException, ForbiddenException } from '@nestjs/common'; import { NotFoundException } from '@nestjs/common/exceptions/not-found.exception'; +import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { UserDO, VideoConferenceDO } from '@shared/domain/domainobject'; import { Course, TeamUserEntity } from '@shared/domain/entity'; @@ -16,14 +18,14 @@ import { courseFactory, roleFactory, setupEntities, userDoFactory, userFactory } import { teamFactory } from '@shared/testing/factory/team.factory'; import { teamUserFactory } from '@shared/testing/factory/teamuser.factory'; import { videoConferenceDOFactory } from '@shared/testing/factory/video-conference.do.factory'; -import { ObjectId } from '@mikro-orm/mongodb'; import { BBBRole } from '../bbb'; import { ErrorStatus } from '../error'; -import { IVideoConferenceSettings, VideoConferenceOptions, VideoConferenceSettings } from '../interface'; +import { VideoConferenceOptions } from '../interface'; import { ScopeInfo, ScopeRef, VideoConferenceState } from '../uc/dto'; +import { VideoConferenceConfig } from '../video-conference-config'; import { VideoConferenceService } from './video-conference.service'; -describe('VideoConferenceService', () => { +describe(VideoConferenceService.name, () => { let service: DeepMocked; let courseService: DeepMocked; let calendarService: DeepMocked; @@ -32,17 +34,15 @@ describe('VideoConferenceService', () => { let teamsRepo: DeepMocked; let userService: DeepMocked; let videoConferenceRepo: DeepMocked; - let videoConferenceSettings: DeepMocked; + let configService: DeepMocked>; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ VideoConferenceService, { - provide: VideoConferenceSettings, - useValue: createMock({ - hostUrl: 'https://api.example.com', - }), + provide: ConfigService, + useValue: createMock>(), }, { provide: CourseService, @@ -83,13 +83,15 @@ describe('VideoConferenceService', () => { teamsRepo = module.get(TeamsRepo); userService = module.get(UserService); videoConferenceRepo = module.get(VideoConferenceRepo); - videoConferenceSettings = module.get(VideoConferenceSettings); + configService = module.get(ConfigService); await setupEntities(); }); describe('canGuestJoin', () => { const setup = (isGuest: boolean, state: VideoConferenceState, waitingRoomEnabled: boolean) => { + configService.get.mockReturnValue('https://api.example.com'); + return { isGuest, state, @@ -139,6 +141,7 @@ describe('VideoConferenceService', () => { const userId = user.id as EntityId; const scopeId = new ObjectId().toHexString(); + configService.get.mockReturnValue('https://api.example.com'); userService.findById.mockResolvedValue(user); return { @@ -492,7 +495,8 @@ describe('VideoConferenceService', () => { describe('when video conference feature is globally disabled', () => { it('should throw a ForbiddenException', async () => { const { schoolId } = setup(false); - videoConferenceSettings.enabled = false; + + configService.get.mockReturnValue(false); const func = () => service.throwOnFeaturesDisabled(schoolId); @@ -540,6 +544,8 @@ describe('VideoConferenceService', () => { const conferenceScope: VideoConferenceScope = VideoConferenceScope.COURSE; const scopeId = new ObjectId().toHexString(); + configService.get.mockReturnValue('https://api.example.com'); + return { userId, conferenceScope, @@ -605,6 +611,8 @@ describe('VideoConferenceService', () => { .withRoleAndUserId(roleFactory.build({ name: RoleName.EXPERT }), new ObjectId().toHexString()) .build(); + configService.get.mockReturnValue('https://api.example.com'); + return { user, userId, diff --git a/apps/server/src/modules/video-conference/service/video-conference.service.ts b/apps/server/src/modules/video-conference/service/video-conference.service.ts index 13604e32b12..f910e9e215b 100644 --- a/apps/server/src/modules/video-conference/service/video-conference.service.ts +++ b/apps/server/src/modules/video-conference/service/video-conference.service.ts @@ -3,7 +3,8 @@ import { AuthorizationContextBuilder, AuthorizationService } from '@modules/auth import { CourseService } from '@modules/learnroom'; import { LegacySchoolService } from '@modules/legacy-school'; import { UserService } from '@modules/user'; -import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common'; +import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { RoleReference, UserDO, VideoConferenceDO, VideoConferenceOptionsDO } from '@shared/domain/domainobject'; import { Course, TeamEntity, TeamUserEntity, User } from '@shared/domain/entity'; import { Permission, RoleName, VideoConferenceScope } from '@shared/domain/interface'; @@ -11,13 +12,14 @@ import { EntityId, SchoolFeature } from '@shared/domain/types'; import { TeamsRepo, VideoConferenceRepo } from '@shared/repo'; import { BBBRole } from '../bbb'; import { ErrorStatus } from '../error'; -import { IVideoConferenceSettings, VideoConferenceOptions, VideoConferenceSettings } from '../interface'; +import { VideoConferenceOptions } from '../interface'; import { ScopeInfo, VideoConferenceState } from '../uc/dto'; +import { VideoConferenceConfig } from '../video-conference-config'; @Injectable() export class VideoConferenceService { constructor( - @Inject(VideoConferenceSettings) private readonly vcSettings: IVideoConferenceSettings, + private readonly configService: ConfigService, private readonly courseService: CourseService, private readonly calendarService: CalendarService, private readonly authorizationService: AuthorizationService, @@ -28,21 +30,25 @@ export class VideoConferenceService { ) {} get hostUrl(): string { - return this.vcSettings.hostUrl; + return this.configService.get('HOST'); } get isVideoConferenceFeatureEnabled(): boolean { - return this.vcSettings.enabled; + return this.configService.get('FEATURE_VIDEOCONFERENCE_ENABLED'); } - canGuestJoin(isGuest: boolean, state: VideoConferenceState, waitingRoomEnabled: boolean): boolean { + public canGuestJoin(isGuest: boolean, state: VideoConferenceState, waitingRoomEnabled: boolean): boolean { if ((isGuest && state === VideoConferenceState.NOT_STARTED) || (isGuest && !waitingRoomEnabled)) { return false; } return true; } - async hasExpertRole(userId: EntityId, conferenceScope: VideoConferenceScope, scopeId: string): Promise { + public async hasExpertRole( + userId: EntityId, + conferenceScope: VideoConferenceScope, + scopeId: string + ): Promise { let isExpert = false; switch (conferenceScope) { case VideoConferenceScope.COURSE: { @@ -136,7 +142,7 @@ export class VideoConferenceService { throw new ForbiddenException(ErrorStatus.INSUFFICIENT_PERMISSION); } - async throwOnFeaturesDisabled(schoolId: EntityId): Promise { + public async throwOnFeaturesDisabled(schoolId: EntityId): Promise { if (!this.isVideoConferenceFeatureEnabled) { throw new ForbiddenException( ErrorStatus.SCHOOL_FEATURE_DISABLED, @@ -150,11 +156,11 @@ export class VideoConferenceService { } } - sanitizeString(text: string) { + public sanitizeString(text: string) { return text.replace(/[^\dA-Za-zÀ-ÖØ-öø-ÿ.\-=_`´ ]/g, ''); } - async getScopeInfo(userId: EntityId, scopeId: string, scope: VideoConferenceScope): Promise { + public async getScopeInfo(userId: EntityId, scopeId: string, scope: VideoConferenceScope): Promise { switch (scope) { case VideoConferenceScope.COURSE: { const course: Course = await this.courseService.findById(scopeId); @@ -181,7 +187,7 @@ export class VideoConferenceService { } } - async getUserRoleAndGuestStatusByUserIdForBbb( + public async getUserRoleAndGuestStatusByUserIdForBbb( userId: string, scopeId: EntityId, scope: VideoConferenceScope @@ -195,7 +201,7 @@ export class VideoConferenceService { return { role, isGuest: isBbbGuest }; } - async findVideoConferenceByScopeIdAndScope( + public async findVideoConferenceByScopeIdAndScope( scopeId: EntityId, scope: VideoConferenceScope ): Promise { @@ -204,7 +210,7 @@ export class VideoConferenceService { return videoConference; } - async createOrUpdateVideoConferenceForScopeWithOptions( + public async createOrUpdateVideoConferenceForScopeWithOptions( scopeId: EntityId, scope: VideoConferenceScope, options: VideoConferenceOptions diff --git a/apps/server/src/modules/video-conference/video-conference-config.ts b/apps/server/src/modules/video-conference/video-conference-config.ts index 7e65d0e7155..0753cd811eb 100644 --- a/apps/server/src/modules/video-conference/video-conference-config.ts +++ b/apps/server/src/modules/video-conference/video-conference-config.ts @@ -1,17 +1,4 @@ -import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { IBbbSettings } from './bbb'; -import { IVideoConferenceSettings } from './interface'; - -export default class VideoConferenceConfiguration { - static bbb: IBbbSettings = { - host: Configuration.get('VIDEOCONFERENCE_HOST') as string, - salt: Configuration.get('VIDEOCONFERENCE_SALT') as string, - presentationUrl: Configuration.get('VIDEOCONFERENCE_DEFAULT_PRESENTATION') as string, - }; - - static videoConference: IVideoConferenceSettings = { - enabled: Configuration.get('FEATURE_VIDEOCONFERENCE_ENABLED') as boolean, - hostUrl: Configuration.get('HOST') as string, - bbb: VideoConferenceConfiguration.bbb, - }; +export interface VideoConferenceConfig { + HOST: string; + FEATURE_VIDEOCONFERENCE_ENABLED: boolean; } diff --git a/apps/server/src/modules/video-conference/video-conference.module.ts b/apps/server/src/modules/video-conference/video-conference.module.ts index db0a539e5fc..0b7bf02841a 100644 --- a/apps/server/src/modules/video-conference/video-conference.module.ts +++ b/apps/server/src/modules/video-conference/video-conference.module.ts @@ -10,12 +10,10 @@ import { TeamsRepo } from '@shared/repo'; import { VideoConferenceRepo } from '@shared/repo/videoconference/video-conference.repo'; import { LoggerModule } from '@src/core/logger'; import { LearnroomModule } from '../learnroom'; -import { BBBService, BbbSettings } from './bbb'; +import { BBBService } from './bbb'; import { VideoConferenceDeprecatedController } from './controller'; -import { VideoConferenceSettings } from './interface'; import { VideoConferenceService } from './service'; import { VideoConferenceDeprecatedUc } from './uc'; -import VideoConferenceConfiguration from './video-conference-config'; @Module({ imports: [ @@ -30,14 +28,6 @@ import VideoConferenceConfiguration from './video-conference-config'; UserModule, ], providers: [ - { - provide: VideoConferenceSettings, - useValue: VideoConferenceConfiguration.videoConference, - }, - { - provide: BbbSettings, - useValue: VideoConferenceConfiguration.bbb, - }, BBBService, VideoConferenceRepo, // TODO: N21-1010 clean up video conferences - remove repos From 324b9e65b850368d97b0a35d5b95cb70b0cd1742 Mon Sep 17 00:00:00 2001 From: hoeppner-dataport <106819770+hoeppner-dataport@users.noreply.github.com> Date: Mon, 15 Jul 2024 16:21:02 +0200 Subject: [PATCH 2/2] BC-6854 - basic load tests (#5099) Implementation of basic load test capabilities for websocket functionality on the board. * improve performance of findCards by adding new function getBoardAuthorizables * track number of action calls in separate metric * implementation of a load testing shell script * added option to pass target and scenario as parameters to the shell script * readme.md for board related load testing --------- Co-authored-by: Thomas Feldtkeller --- .gitignore | 2 + .../gateway/board-collaboration.gateway.ts | 14 ++ .../src/modules/board/loadtest/readme.md | 76 ++++++++++ .../src/modules/board/loadtest/runScenario.sh | 139 ++++++++++++++++++ .../loadtest/scenarios/30users_5minutes.yml | 75 ++++++++++ .../board/loadtest/scenarios/3users.yml | 57 +++++++ .../scenarios/6createCard_6UpdateTitle.yml | 70 +++++++++ .../modules/board/metrics/metrics.service.ts | 52 ++++++- .../board-node-authorizable.service.ts | 49 +++++- .../board/service/board-node.service.ts | 6 + .../src/modules/board/uc/card.uc.spec.ts | 23 ++- apps/server/src/modules/board/uc/card.uc.ts | 17 +-- package-lock.json | 18 ++- package.json | 2 +- 14 files changed, 574 insertions(+), 26 deletions(-) create mode 100644 apps/server/src/modules/board/loadtest/readme.md create mode 100644 apps/server/src/modules/board/loadtest/runScenario.sh create mode 100644 apps/server/src/modules/board/loadtest/scenarios/30users_5minutes.yml create mode 100644 apps/server/src/modules/board/loadtest/scenarios/3users.yml create mode 100644 apps/server/src/modules/board/loadtest/scenarios/6createCard_6UpdateTitle.yml diff --git a/.gitignore b/.gitignore index 130655cf16d..a045507c02b 100644 --- a/.gitignore +++ b/.gitignore @@ -85,3 +85,5 @@ build /coverage /.nyc_output /.idea/ +/apps/server/src/modules/board/loadtest/**/*.html +/apps/server/src/modules/board/loadtest/artilleryreport.json diff --git a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts index a04045d0b53..c9a6cc7163b 100644 --- a/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts +++ b/apps/server/src/modules/board/gateway/board-collaboration.gateway.ts @@ -61,6 +61,10 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { trackExecutionTime(methodName: string, executionTimeMs: number) { if (this.metricsService) { this.metricsService.setExecutionTime(methodName, executionTimeMs); + this.metricsService.incrementActionCount(methodName); + this.metricsService.incrementActionGauge(methodName); + this.metricsService.incrementActionCount('all'); + this.metricsService.incrementActionGauge('all'); } } @@ -128,6 +132,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('update-card-height-request') + @TrackExecutionTime() @UseRequestContext() async updateCardHeight(socket: Socket, data: UpdateCardHeightMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'update-card-height' }); @@ -142,6 +147,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('delete-card-request') + @TrackExecutionTime() @UseRequestContext() async deleteCard(socket: Socket, data: DeleteCardMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'delete-card' }); @@ -178,6 +184,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('create-column-request') + @TrackExecutionTime() @UseRequestContext() async createColumn(socket: Socket, data: CreateColumnMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'create-column' }); @@ -219,6 +226,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('move-card-request') + @TrackExecutionTime() @UseRequestContext() async moveCard(socket: Socket, data: MoveCardMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'move-card' }); @@ -233,6 +241,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('move-column-request') + @TrackExecutionTime() @UseRequestContext() async moveColumn(socket: Socket, data: MoveColumnMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'move-column' }); @@ -267,6 +276,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('update-board-visibility-request') + @TrackExecutionTime() @UseRequestContext() async updateBoardVisibility(socket: Socket, data: UpdateBoardVisibilityMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'update-board-visibility' }); @@ -281,6 +291,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('delete-column-request') + @TrackExecutionTime() @UseRequestContext() async deleteColumn(socket: Socket, data: DeleteColumnMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'delete-column' }); @@ -312,6 +323,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('create-element-request') + @TrackExecutionTime() @UseRequestContext() async createElement(socket: Socket, data: CreateContentElementMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'create-element' }); @@ -346,6 +358,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('delete-element-request') + @TrackExecutionTime() @UseRequestContext() async deleteElement(socket: Socket, data: DeleteContentElementMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'delete-element' }); @@ -361,6 +374,7 @@ export class BoardCollaborationGateway implements OnGatewayDisconnect { } @SubscribeMessage('move-element-request') + @TrackExecutionTime() @UseRequestContext() async moveElement(socket: Socket, data: MoveContentElementMessageParams) { const emitter = this.buildBoardSocketEmitter({ socket, action: 'move-element' }); diff --git a/apps/server/src/modules/board/loadtest/readme.md b/apps/server/src/modules/board/loadtest/readme.md new file mode 100644 index 00000000000..24cbd18605c --- /dev/null +++ b/apps/server/src/modules/board/loadtest/readme.md @@ -0,0 +1,76 @@ +# Loadtesting the boards + +The socket.io documentation suggests to use the tool artillery in order to load test a socket-io tool like our board-collaboration service. + +For defining scenarios you need to use/create Yaml-files that define which operations with which parameters need to be executed in which order. + +Some sceneraios were already prepared and are stored in the subfolder scenarios. + +## install artillery + +To run artillery from your local environment you need to install it first including an adapter that supports socketio-v3-websocket communication: + +```sh +npm install -g artillery artillery-engine-socketio-v3 +``` + +## manual execution + +To execute a scenario you can run artillery from the shell / commandline...: + +Using the `--variables` parameter it is possible to define several variables and there values that can be used in the scenerio-yaml-file: + +- **target**: defines the base url for all requests (REST and WebSocket) + e.g. `https://main.dbc.dbildungscloud.dev` +- **token**: a valid JWT for the targeted system +- **board_id**: id of an existing board the tests should be executed on + +```bash +npx artillery run --variables "{'target': 'https://main.dbc.dbildungscloud.dev', 'token': 'eJ....', 'board_id': '668d0e03bf3689d12e1e86fb' }" './scenarios/3users.yml' --output artilleryreport.json +``` + +On Windows Powershell, the variables value needs to be wrapped in singlequotes, and inside the json you need to use backslash-escaped doublequotes: + +```powershell +npx artillery run --variables '{\"target\": \"https://main.dbc.dbildungscloud.dev\", \"token\": \"eJ....\", \"board_id\": \"668d0e03bf3689d12e1e86fb\" }' './scenarios/3users.yml' --output artilleryreport.json +``` + +## visualizing the recorded results + +It is possible to generate a HTML-report based on the recorded data. + +```powershell +npx artillery report --output=$board_title.html artilleryreport.json +``` + +## automatic execution + +You can run one of the existing scenarios by executing: + +```bash +bash runScenario.sh +``` + +This will: + +1. let you choose from scenario-files +2. create a fresh JWT-webtoken +3. create a fresh board (in one of the courses) the user has access to +4. name the board by a combination of datetime and the scenario name. +5. output a link to the generated board (in order open and see the test live) +6. start the execution of the scenario against this newly created board +7. generate a html report in the end + +You can also provide the target as the first and the name of the scenario as the second parameter - to avoid the need to select those. Here is an example: + +```bash +bash runScenario.sh https://bc-6854-basic-load-tests.nbc.dbildungscloud.dev 3users +``` + +## password + +By typeing `export CARL_CORD_PASSWORD=realpassword` the script will not ask you anymore for the password to create a token. + +## Todos + +- [ ] enable optional parameter course_id diff --git a/apps/server/src/modules/board/loadtest/runScenario.sh b/apps/server/src/modules/board/loadtest/runScenario.sh new file mode 100644 index 00000000000..0187f1c579f --- /dev/null +++ b/apps/server/src/modules/board/loadtest/runScenario.sh @@ -0,0 +1,139 @@ +#!/bin/bash + +function select_target() { + declare -a targets=("https://main.nbc.dbildungscloud.dev" "https://bc-6854-basic-load-tests.nbc.dbildungscloud.dev") + echo "Please select the target for the test:" >&2 + select target in "${targets[@]}"; do + if [[ -n $target ]]; then + break + else + echo "Invalid selection. Please try again." >&2 + fi + done +} + +function select_scenario() { + # list files in the scenarios directory + scenarios_dir="./scenarios" + declare -a scenario_files=($(ls $scenarios_dir)) + + echo "Please select a scenario file for the test:" >&2 + select scenario_file in "${scenario_files[@]}"; do + if [[ -n $scenario_file ]]; then + echo "You have selected: $scenario_file" >&2 + break + else + echo "Invalid selection. Please try again." >&2 + fi + done + + scenario_name="${scenario_file%.*}" +} + +function get_credentials() { + if [ -z "$CARL_CORD_PASSWORD" ]; then + echo "Password for Carl Cord is unknown. Provide it as an enviroment variable (CARL_CORD_PASSWORD) or enter it:" + read CARL_CORD_PASSWORD + export CARL_CORD_PASSWORD + fi +} + +function get_token() { + response=$(curl -s -f -X 'POST' \ + "$target/api/v3/authentication/local" \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -d "{ + \"username\": \"lehrer@schul-cloud.org\", + \"password\": \"$CARL_CORD_PASSWORD\" + }") + + if [ $? -ne 0 ]; then + echo "ERROR: Failed to get token. Please check your credentials and target URL." >&2 + exit 1 + fi + + token=$(echo $response | sed -n 's/.*"accessToken":"\([^"]*\)".*/\1/p') +} + +function get_course_id() { + response=$(curl -s -f -X 'GET' \ + "$target/api/v3/courses" \ + -H "Accept: application/json" \ + -H "Authorization: Bearer $token") + + if [ $? -ne 0 ]; then + echo "ERROR: Failed to get course list. Please check your credentials and target URL." >&2 + exit 1 + fi + + course_id=$(echo $response | sed -n 's/.*"id":"\([^"]*\)".*/\1/p') +} + +function create_board_title() { + current_date=$(date +%Y-%m-%d_%H:%M) + board_title="${current_date}_$1" +} + +function create_board() { + response=$(curl -s -f -X 'POST' \ + "$target/api/v3/boards" \ + -H 'accept: application/json' \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer $token" \ + -d "{ + \"title\": \"$board_title\", + \"parentId\": \"$course_id\", + \"parentType\": \"course\", + \"layout\": \"columns\" + }") + + if [ $? -ne 0 ]; then + echo "ERROR: Failed to create a board." >&2 + exit 1 + fi + + board_id=$(echo $response | sed -n 's/.*"id":"\([^"]*\)".*/\1/p' ) +} + +if [ -z "$1" ]; then + select_target +else + target=$1 +fi +echo " " +echo "target: $target" + + +if [ -z "$2" ]; then + select_scenario + echo "scenario_name: $scenario_name" +else + scenario_name="$2" + scenario_name=${scenario_name//.yml/} +fi +echo "scenario_name: $scenario_name" + +get_credentials + +get_token +echo "token: ${token:0:50}..." +echo " " + +get_course_id +echo "course_id: $course_id" +echo " " + +create_board_title $scenario_name +echo "board_title: $board_title" + +create_board +echo "board_id $board_id" + +echo "board: $target/rooms/$board_id/board" +echo " " +echo "Running artillery test..." + +npx artillery run --variables "{\"target\": \"$target\", \"token\": \"$token\", \"board_id\": \"$board_id\" }" "./scenarios/$scenario_name.yml" --output artilleryreport.json + +npx artillery report --output=$board_title.html artilleryreport.json diff --git a/apps/server/src/modules/board/loadtest/scenarios/30users_5minutes.yml b/apps/server/src/modules/board/loadtest/scenarios/30users_5minutes.yml new file mode 100644 index 00000000000..567cbaf703a --- /dev/null +++ b/apps/server/src/modules/board/loadtest/scenarios/30users_5minutes.yml @@ -0,0 +1,75 @@ +config: + target: '{{ target }}' + engines: + socketio: + transport: ['websocket', 'polling'] + path: '/board-collaboration' + socketio-v3: + path: '/board-collaboration' + timeout: 1000000 + extraHeaders: + Cookie: 'jwt={{ token }}' + + phases: + - duration: 300 + arrivalRate: 10 + maxVusers: 30 + +scenarios: + - name: create card + engine: socketio-v3 + socketio-v3: + extraHeaders: + Cookie: 'jwt={{ token }}' + flow: + - think: 1 + + - emit: + channel: 'fetch-board-request' + data: + boardId: '{{ board_id }}' + + - think: 1 + + - emit: + channel: 'create-column-request' + data: + boardId: '{{ board_id }}' + response: + on: 'create-column-success' + capture: + - json: $.newColumn.id + as: columnId + + - think: 1 + + - loop: + - emit: + channel: 'create-card-request' + data: + columnId: '{{ columnId}}' + response: + on: 'create-card-success' + capture: + - json: $.newCard.id + as: cardId + + - think: 1 + + - emit: + channel: 'fetch-card-request' + data: + cardIds: + - '{{ cardId }}' + + - think: 2 + + - emit: + channel: 'update-card-title-request' + data: + cardId: '{{ cardId }}' + newTitle: 'Card {{ cardId}}' + + - think: 1 + + count: 20 diff --git a/apps/server/src/modules/board/loadtest/scenarios/3users.yml b/apps/server/src/modules/board/loadtest/scenarios/3users.yml new file mode 100644 index 00000000000..4fbeef037c8 --- /dev/null +++ b/apps/server/src/modules/board/loadtest/scenarios/3users.yml @@ -0,0 +1,57 @@ +config: + target: '{{ target }}' + engines: + socketio: + transport: ['websocket', 'polling'] + path: '/board-collaboration' + socketio-v3: + path: '/board-collaboration' + timeout: 1000000 + extraHeaders: + Cookie: 'jwt={{ token }}' + + phases: + - duration: 1 + arrivalRate: 3 + +scenarios: + - name: create card + engine: socketio-v3 + socketio-v3: + extraHeaders: + Cookie: 'jwt={{ token }}' + flow: + - log: '{{ target }}' + - think: 1 + + - emit: + channel: 'create-column-request' + data: + boardId: '{{ board_id }}' + response: + on: 'create-column-success' + capture: + - json: $.newColumn.id + as: columnId + + - think: 1 + + - emit: + channel: 'create-card-request' + data: + columnId: '{{ columnId}}' + response: + on: 'create-card-success' + capture: + - json: $.newCard.id + as: cardId + + - think: 1 + + - emit: + channel: 'update-card-title-request' + data: + cardId: '{{ cardId }}' + newTitle: 'One {{ cardId}}' + + - think: 2 diff --git a/apps/server/src/modules/board/loadtest/scenarios/6createCard_6UpdateTitle.yml b/apps/server/src/modules/board/loadtest/scenarios/6createCard_6UpdateTitle.yml new file mode 100644 index 00000000000..ad7e993f829 --- /dev/null +++ b/apps/server/src/modules/board/loadtest/scenarios/6createCard_6UpdateTitle.yml @@ -0,0 +1,70 @@ +config: + target: '{{ target }}' + engines: + socketio: + transport: ['websocket', 'polling'] + path: '/board-collaboration' + socketio-v3: + path: '/board-collaboration' + timeout: 1000000 + extraHeaders: + Cookie: 'jwt={{ token }}' + + phases: + - duration: 2 + arrivalRate: 50 + +scenarios: + - name: create card + engine: socketio-v3 + socketio: + extraHeaders: + Cookie: 'jwt={{ token }}' + socketio-v3: + extraHeaders: + Cookie: 'jwt={{ token }}' + flow: + - think: 1 + + - emit: + channel: 'create-column-request' + data: + boardId: '{{ board_id }}' + response: + on: 'create-column-success' + capture: + - json: $.newColumn.id + as: columnId + + - think: 2 + + - loop: + - emit: + channel: 'create-card-request' + data: + columnId: '{{ columnId}}' + response: + on: 'create-card-success' + capture: + - json: $.newCard.id + as: cardId + + - think: 1 + + - emit: + channel: 'fetch-card-request' + data: + cardIds: + - '{{ cardId }}' + + - think: 2 + + - emit: + channel: 'update-card-title-request' + data: + cardId: '{{ cardId }}' + newTitle: 'Card {{ cardId}}' + + - think: 1 + + count: 6 diff --git a/apps/server/src/modules/board/metrics/metrics.service.ts b/apps/server/src/modules/board/metrics/metrics.service.ts index 8019220b40e..b2a54d5ce75 100644 --- a/apps/server/src/modules/board/metrics/metrics.service.ts +++ b/apps/server/src/modules/board/metrics/metrics.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { UserDO } from '@shared/domain/domainobject'; import { RoleName } from '@shared/domain/interface'; import { UserService } from '@src/modules/user'; -import { Gauge, Summary, register } from 'prom-client'; +import { Gauge, Summary, register, Counter } from 'prom-client'; type ClientId = string; type Role = 'owner' | 'editor' | 'viewer'; @@ -19,6 +19,10 @@ export class MetricsService { private executionTimesSummary: Map> = new Map(); + private actionCounters: Map> = new Map(); + + private actionGauges: Map> = new Map(); + constructor(private readonly userService: UserService) { this.numberOfBoardroomsOnServerCounter = new Gauge({ name: 'sc_boards_rooms', @@ -88,13 +92,55 @@ export class MetricsService { summary = new Summary({ name: `sc_boards_execution_time_${actionName}`, help: 'Average execution time of a specific action in milliseconds', - maxAgeSeconds: 60, + maxAgeSeconds: 600, ageBuckets: 5, - percentiles: [0.01, 0.1, 0.9, 0.99], + percentiles: [0.01, 0.1, 0.5, 0.9, 0.99], + pruneAgedBuckets: true, }); this.executionTimesSummary.set(actionName, summary); register.registerMetric(summary); } + console.log(actionName, `executionTime: ${value.toFixed(3)} ms`); summary.observe(value); } + + public incrementActionCount(actionName: string): void { + let counter = this.actionCounters.get(actionName); + + if (!counter) { + counter = new Counter({ + name: `sc_boards_count_${actionName}`, + help: 'Number of calls for a specific action per minute', + // async collect() { + // // Invoked when the registry collects its metrics' values. + // const currentValue = await somethingAsync(); + // this.set(currentValue); + // }, + }); + this.actionCounters.set(actionName, counter); + register.registerMetric(counter); + } + counter.inc(); + console.log(actionName, `count increased`); + } + + public incrementActionGauge(actionName: string): void { + let counter = this.actionGauges.get(actionName); + + if (!counter) { + counter = new Gauge({ + name: `sc_boards_count2_${actionName}`, + help: 'Number of calls for a specific action per minute', + // async collect() { + // // Invoked when the registry collects its metrics' values. + // const currentValue = await somethingAsync(); + // this.set(currentValue); + // }, + }); + this.actionGauges.set(actionName, counter); + register.registerMetric(counter); + } + counter.inc(); + console.log(actionName, `count increased`); + } } diff --git a/apps/server/src/modules/board/service/board-node-authorizable.service.ts b/apps/server/src/modules/board/service/board-node-authorizable.service.ts index bc9a846311f..b813761163b 100644 --- a/apps/server/src/modules/board/service/board-node-authorizable.service.ts +++ b/apps/server/src/modules/board/service/board-node-authorizable.service.ts @@ -1,7 +1,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { type EntityId } from '@shared/domain/types'; import { type AuthorizationLoaderService } from '@modules/authorization'; -import { AnyBoardNode, BoardNodeAuthorizable } from '../domain'; +import { AnyBoardNode, BoardNodeAuthorizable, UserWithBoardRoles } from '../domain'; import { BoardNodeRepo } from '../repo'; import { BoardContextService } from './internal/board-context.service'; import { BoardNodeService } from './board-node.service'; @@ -40,4 +40,51 @@ export class BoardNodeAuthorizableService implements AuthorizationLoaderService return boardNodeAuthorizable; } + + async getBoardAuthorizables(boardNodes: AnyBoardNode[]): Promise { + const rootIds = boardNodes.map((node) => node.rootId); + const parentIds = boardNodes.map((node) => node.parentId).filter((defined) => defined) as EntityId[]; + const boardNodeMap = await this.getBoardNodeMap([...rootIds, ...parentIds]); + const promises = boardNodes.map((boardNode) => { + const rootNode = boardNodeMap[boardNode.rootId]; + return this.boardContextService.getUsersWithBoardRoles(rootNode).then((users) => { + return { id: boardNode.id, users }; + }); + }); + + const results = await Promise.all(promises); + const usersMap = results.reduce((acc, { id, users }) => { + acc[id] = users; + return acc; + }, {} as Record); + + const boardNodeAuthorizables = boardNodes.map((boardNode) => { + const rootNode = boardNodeMap[boardNode.rootId]; + const parentNode = boardNode.parentId ? boardNodeMap[boardNode.parentId] : undefined; + const users = usersMap[boardNode.id]; + const boardNodeAuthorizable = new BoardNodeAuthorizable({ + users, + id: boardNode.id, + boardNode, + rootNode, + parentNode, + }); + return boardNodeAuthorizable; + }); + + return boardNodeAuthorizables; + } + + private async getBoardNodeMap(ids: EntityId[]): Promise> { + const idsUnique = Array.from(new Set(ids)); + const boardNodes = await this.boardNodeService.findByIds(idsUnique, 1); + const nodesMap: Record = boardNodes.reduce( + (map: Record, boardNode) => { + map[boardNode.id] = boardNode; + return map; + }, + {} as Record + ); + return nodesMap; + } } diff --git a/apps/server/src/modules/board/service/board-node.service.ts b/apps/server/src/modules/board/service/board-node.service.ts index bd8092a2e78..0ac96b627b1 100644 --- a/apps/server/src/modules/board/service/board-node.service.ts +++ b/apps/server/src/modules/board/service/board-node.service.ts @@ -76,6 +76,12 @@ export class BoardNodeService { return boardNode; } + async findByIds(ids: EntityId[], depth?: number): Promise { + const boardNode = this.boardNodeRepo.findByIds(ids, depth); + + return boardNode; + } + async findByClassAndId( Constructor: { new (props: S): T }, id: EntityId, diff --git a/apps/server/src/modules/board/uc/card.uc.spec.ts b/apps/server/src/modules/board/uc/card.uc.spec.ts index 19a37861ea4..be5bba4afa8 100644 --- a/apps/server/src/modules/board/uc/card.uc.spec.ts +++ b/apps/server/src/modules/board/uc/card.uc.spec.ts @@ -1,5 +1,4 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ObjectId } from '@mikro-orm/mongodb'; import { Action, AuthorizationService } from '@modules/authorization'; import { Test, TestingModule } from '@nestjs/testing'; import { setupEntities, userFactory } from '@shared/testing'; @@ -73,14 +72,26 @@ describe(CardUc.name, () => { const cards = cardFactory.buildList(3); const cardIds = cards.map((c) => c.id); - boardNodeAuthorizableService.getBoardAuthorizable.mockResolvedValue( + boardNodeAuthorizableService.getBoardAuthorizables.mockResolvedValue([ new BoardNodeAuthorizable({ users: [], - id: new ObjectId().toHexString(), + id: cards[0].id, boardNode: cards[0], rootNode: columnBoardFactory.build(), - }) - ); + }), + new BoardNodeAuthorizable({ + users: [], + id: cards[1].id, + boardNode: cards[1], + rootNode: columnBoardFactory.build(), + }), + new BoardNodeAuthorizable({ + users: [], + id: cards[2].id, + boardNode: cards[2], + rootNode: columnBoardFactory.build(), + }), + ]); authorizationService.hasPermission.mockReturnValue(true); return { user, cards, cardIds }; @@ -109,7 +120,7 @@ describe(CardUc.name, () => { await uc.findCards(user.id, cardIds); - expect(boardNodeAuthorizableService.getBoardAuthorizable).toHaveBeenCalledTimes(3); + expect(boardNodeAuthorizableService.getBoardAuthorizables).toHaveBeenCalledTimes(1); }); it('should call the service to check the user permission', async () => { diff --git a/apps/server/src/modules/board/uc/card.uc.ts b/apps/server/src/modules/board/uc/card.uc.ts index 9c627d0aa99..6ab2f392766 100644 --- a/apps/server/src/modules/board/uc/card.uc.ts +++ b/apps/server/src/modules/board/uc/card.uc.ts @@ -3,7 +3,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; import { LegacyLogger } from '@src/core/logger'; -import { AnyContentElement, BoardNodeFactory, Card, ContentElementType } from '../domain'; +import { AnyBoardNode, AnyContentElement, BoardNodeFactory, Card, ContentElementType } from '../domain'; import { BoardNodeAuthorizableService, BoardNodePermissionService, BoardNodeService } from '../service'; @Injectable() @@ -29,19 +29,14 @@ export class CardUc { const user = await this.authorizationService.getUserWithPermissions(userId); const context: AuthorizationContext = { action: Action.read, requiredPermissions: [] }; - const promises = cards.map((card) => - this.boardNodeAuthorizableService.getBoardAuthorizable(card).then((boardNodeAuthorizable) => { - return { boardNodeAuthorizable, boardNode: card }; - }) - ); - const result = await Promise.all(promises); - - const allowedCards = result.reduce((allowedNodes: Card[], { boardNodeAuthorizable, boardNode }) => { + const boardAuthorizables = await this.boardNodeAuthorizableService.getBoardAuthorizables(cards); + + const allowedCards = boardAuthorizables.reduce((allowedNodes: AnyBoardNode[], boardNodeAuthorizable) => { if (this.authorizationService.hasPermission(user, boardNodeAuthorizable, context)) { - allowedNodes.push(boardNode); + allowedNodes.push(boardNodeAuthorizable.boardNode); } return allowedNodes; - }, []); + }, []) as Card[]; return allowedCards; } diff --git a/package-lock.json b/package-lock.json index 52c01244aa7..d2cbd72d353 100644 --- a/package-lock.json +++ b/package-lock.json @@ -118,7 +118,7 @@ "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "pdfmake": "^0.2.9", - "prom-client": "^13.1.0", + "prom-client": "^15.1.3", "qs": "^6.9.7", "read-chunk": "^3.0.0", "reflect-metadata": "^0.1.13", @@ -5407,6 +5407,14 @@ "node": ">=8" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@panva/asn1.js": { "version": "1.0.0", "license": "MIT", @@ -19281,13 +19289,15 @@ } }, "node_modules/prom-client": { - "version": "13.2.0", - "license": "Apache-2.0", + "version": "15.1.3", + "resolved": "https://registry.npmjs.org/prom-client/-/prom-client-15.1.3.tgz", + "integrity": "sha512-6ZiOBfCywsD4k1BN9IX0uZhF+tJkV8q8llP64G5Hajs4JOeVLPCwpPVcpXy3BwYiUGgyJzsJJQeOIv7+hDSq8g==", "dependencies": { + "@opentelemetry/api": "^1.4.0", "tdigest": "^0.1.1" }, "engines": { - "node": ">=10" + "node": "^16 || ^18 || >=20" } }, "node_modules/promise-breaker": { diff --git a/package.json b/package.json index c064843247d..2cad05a7150 100644 --- a/package.json +++ b/package.json @@ -234,7 +234,7 @@ "passport-jwt": "^4.0.1", "passport-local": "^1.0.0", "pdfmake": "^0.2.9", - "prom-client": "^13.1.0", + "prom-client": "^15.1.3", "qs": "^6.9.7", "read-chunk": "^3.0.0", "reflect-metadata": "^0.1.13",