diff --git a/apps/server/src/infra/schulconnex-client/index.ts b/apps/server/src/infra/schulconnex-client/index.ts index bf68886a92a..e31b5de998f 100644 --- a/apps/server/src/infra/schulconnex-client/index.ts +++ b/apps/server/src/infra/schulconnex-client/index.ts @@ -2,5 +2,5 @@ export { SchulconnexRestClientOptions } from './schulconnex-rest-client-options' export { SchulconnexClientModule } from './schulconnex-client.module'; export { SchulconnexRestClient } from './schulconnex-rest-client'; export * from './response'; -export { schulconnexResponseFactory, schulconnexLizenzInfoResponseFactory } from './testing'; +export { schulconnexResponseFactory, schulconnexPoliciesInfoResponseFactory } from './testing'; export { SchulconnexClientConfig } from './schulconnex-client-config'; diff --git a/apps/server/src/infra/schulconnex-client/response/index.ts b/apps/server/src/infra/schulconnex-client/response/index.ts index d453e030115..91618ef7b0a 100644 --- a/apps/server/src/infra/schulconnex-client/response/index.ts +++ b/apps/server/src/infra/schulconnex-client/response/index.ts @@ -15,4 +15,4 @@ export { SchulconnexResponseValidationGroups } from './schulconnex-response-vali export { SchulconnexErreichbarkeitenResponse } from './schulconnex-erreichbarkeiten-response'; export { SchulconnexCommunicationType } from './schulconnex-communication-type'; export { SchulconnexLaufzeitResponse, lernperiodeFormat } from './schulconnex-laufzeit-response'; -export * from './lizenz-info'; +export * from './policies-info'; diff --git a/apps/server/src/infra/schulconnex-client/response/lizenz-info/index.ts b/apps/server/src/infra/schulconnex-client/response/lizenz-info/index.ts deleted file mode 100644 index 1d594c71882..00000000000 --- a/apps/server/src/infra/schulconnex-client/response/lizenz-info/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { SchulconnexLizenzInfoTargetResponse } from './schulconnex-lizenz-info-target-response'; -export { SchulconnexLizenzInfoResponse } from './schulconnex-lizenz-info-response'; -export { SchulconnexLizenzInfoActionType } from './schulconnex-lizenz-info-action-type'; -export { SchulconnexLizenzInfoPermissionResponse } from './schulconnex-lizenz-info-permission-response'; diff --git a/apps/server/src/infra/schulconnex-client/response/lizenz-info/schulconnex-lizenz-info-action-type.ts b/apps/server/src/infra/schulconnex-client/response/lizenz-info/schulconnex-lizenz-info-action-type.ts deleted file mode 100644 index 18ea70e9bb8..00000000000 --- a/apps/server/src/infra/schulconnex-client/response/lizenz-info/schulconnex-lizenz-info-action-type.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum SchulconnexLizenzInfoActionType { - EXECUTE = 'execute', -} diff --git a/apps/server/src/infra/schulconnex-client/response/lizenz-info/schulconnex-lizenz-info-permission-response.ts b/apps/server/src/infra/schulconnex-client/response/lizenz-info/schulconnex-lizenz-info-permission-response.ts deleted file mode 100644 index 3f5d3ea12fd..00000000000 --- a/apps/server/src/infra/schulconnex-client/response/lizenz-info/schulconnex-lizenz-info-permission-response.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { IsArray } from 'class-validator'; -import { SchulconnexLizenzInfoActionType } from './schulconnex-lizenz-info-action-type'; - -export class SchulconnexLizenzInfoPermissionResponse { - @IsArray() - action!: SchulconnexLizenzInfoActionType[]; -} diff --git a/apps/server/src/infra/schulconnex-client/response/lizenz-info/schulconnex-lizenz-info-response.ts b/apps/server/src/infra/schulconnex-client/response/lizenz-info/schulconnex-lizenz-info-response.ts deleted file mode 100644 index bab0a913871..00000000000 --- a/apps/server/src/infra/schulconnex-client/response/lizenz-info/schulconnex-lizenz-info-response.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Type } from 'class-transformer'; -import { IsArray, IsObject, ValidateNested } from 'class-validator'; -import { SchulconnexLizenzInfoPermissionResponse } from './schulconnex-lizenz-info-permission-response'; -import { SchulconnexLizenzInfoTargetResponse } from './schulconnex-lizenz-info-target-response'; - -export class SchulconnexLizenzInfoResponse { - @IsObject() - @ValidateNested() - @Type(() => SchulconnexLizenzInfoTargetResponse) - target!: SchulconnexLizenzInfoTargetResponse; - - @IsArray() - @ValidateNested() - @Type(() => SchulconnexLizenzInfoPermissionResponse) - permission!: SchulconnexLizenzInfoPermissionResponse[]; -} diff --git a/apps/server/src/infra/schulconnex-client/response/policies-info/index.ts b/apps/server/src/infra/schulconnex-client/response/policies-info/index.ts new file mode 100644 index 00000000000..d2d85b0601f --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/response/policies-info/index.ts @@ -0,0 +1,4 @@ +export { SchulconnexPoliciesInfoTargetResponse } from './schulconnex-policies-info-target-response'; +export { SchulconnexPoliciesInfoResponse } from './schulconnex-policies-info-response'; +export { SchulconnexPoliciesInfoActionType } from './schulconnex-policies-info-action-type'; +export { SchulconnexPoliciesInfoPermissionResponse } from './schulconnex-policies-info-permission-response'; diff --git a/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-action-type.ts b/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-action-type.ts new file mode 100644 index 00000000000..ef9a5a2713c --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-action-type.ts @@ -0,0 +1,3 @@ +export enum SchulconnexPoliciesInfoActionType { + EXECUTE = 'execute', +} diff --git a/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-permission-response.ts b/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-permission-response.ts new file mode 100644 index 00000000000..7e929d22957 --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-permission-response.ts @@ -0,0 +1,7 @@ +import { IsArray } from 'class-validator'; +import { SchulconnexPoliciesInfoActionType } from './schulconnex-policies-info-action-type'; + +export class SchulconnexPoliciesInfoPermissionResponse { + @IsArray() + action!: SchulconnexPoliciesInfoActionType[]; +} diff --git a/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-response.ts b/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-response.ts new file mode 100644 index 00000000000..304d444efd5 --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-response.ts @@ -0,0 +1,16 @@ +import { Type } from 'class-transformer'; +import { IsArray, IsObject, ValidateNested } from 'class-validator'; +import { SchulconnexPoliciesInfoPermissionResponse } from './schulconnex-policies-info-permission-response'; +import { SchulconnexPoliciesInfoTargetResponse } from './schulconnex-policies-info-target-response'; + +export class SchulconnexPoliciesInfoResponse { + @IsObject() + @ValidateNested() + @Type(() => SchulconnexPoliciesInfoTargetResponse) + target!: SchulconnexPoliciesInfoTargetResponse; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => SchulconnexPoliciesInfoPermissionResponse) + permission!: SchulconnexPoliciesInfoPermissionResponse[]; +} diff --git a/apps/server/src/infra/schulconnex-client/response/lizenz-info/schulconnex-lizenz-info-target-response.ts b/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-target-response.ts similarity index 71% rename from apps/server/src/infra/schulconnex-client/response/lizenz-info/schulconnex-lizenz-info-target-response.ts rename to apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-target-response.ts index e832cb7bb59..a7a7f0aa3da 100644 --- a/apps/server/src/infra/schulconnex-client/response/lizenz-info/schulconnex-lizenz-info-target-response.ts +++ b/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-target-response.ts @@ -1,6 +1,6 @@ import { IsOptional, IsString } from 'class-validator'; -export class SchulconnexLizenzInfoTargetResponse { +export class SchulconnexPoliciesInfoTargetResponse { @IsString() uid!: string; diff --git a/apps/server/src/infra/schulconnex-client/schulconnex-api.interface.ts b/apps/server/src/infra/schulconnex-client/schulconnex-api.interface.ts index dbbdbf8a62e..45d90ff3a17 100644 --- a/apps/server/src/infra/schulconnex-client/schulconnex-api.interface.ts +++ b/apps/server/src/infra/schulconnex-client/schulconnex-api.interface.ts @@ -1,10 +1,10 @@ import { SchulconnexPersonenInfoParams } from './request'; -import { SchulconnexLizenzInfoResponse, SchulconnexResponse } from './response'; +import { SchulconnexPoliciesInfoResponse, SchulconnexResponse } from './response'; export interface SchulconnexApiInterface { getPersonInfo(accessToken: string, options?: { overrideUrl: string }): Promise; getPersonenInfo(params: SchulconnexPersonenInfoParams): Promise; - getLizenzInfo(accessToken: string, options?: { overrideUrl: string }): Promise; + getPoliciesInfo(accessToken: string, options?: { overrideUrl: string }): Promise; } diff --git a/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.spec.ts b/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.spec.ts index 4c4884a21cd..89d5b2f6711 100644 --- a/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.spec.ts +++ b/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.spec.ts @@ -5,11 +5,10 @@ import { axiosResponseFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; import { of } from 'rxjs'; import { SchulconnexConfigurationMissingLoggable } from './loggable'; -import { SchulconnexLizenzInfoResponse, SchulconnexResponse } from './response'; +import { SchulconnexPoliciesInfoResponse, SchulconnexResponse } from './response'; import { SchulconnexRestClient } from './schulconnex-rest-client'; import { SchulconnexRestClientOptions } from './schulconnex-rest-client-options'; -import { schulconnexResponseFactory } from './testing'; -import { schulconnexLizenzInfoResponseFactory } from './testing/schulconnex-lizenz-info-response-factory'; +import { schulconnexPoliciesInfoResponseFactory, schulconnexResponseFactory } from './testing'; describe(SchulconnexRestClient.name, () => { let client: SchulconnexRestClient; @@ -196,11 +195,11 @@ describe(SchulconnexRestClient.name, () => { }); }); - describe('getLizenzInfo', () => { - describe('when requesting lizenz-info', () => { + describe('getPoliciesInfo', () => { + describe('when requesting policies-info', () => { const setup = () => { const accessToken = 'accessToken'; - const response: SchulconnexLizenzInfoResponse[] = schulconnexLizenzInfoResponseFactory.buildList(1); + const response: SchulconnexPoliciesInfoResponse[] = schulconnexPoliciesInfoResponseFactory.buildList(1); httpService.get.mockReturnValueOnce(of(axiosResponseFactory.build({ data: response }))); @@ -212,9 +211,9 @@ describe(SchulconnexRestClient.name, () => { it('should make a request to a SchulConneX-API', async () => { const { accessToken } = setup(); - await client.getLizenzInfo(accessToken); + await client.getPoliciesInfo(accessToken); - expect(httpService.get).toHaveBeenCalledWith(`${options.apiUrl ?? ''}/lizenz-info`, { + expect(httpService.get).toHaveBeenCalledWith(`${options.apiUrl ?? ''}/policies-info`, { headers: { Authorization: `Bearer ${accessToken}`, 'Accept-Encoding': 'gzip', @@ -225,7 +224,7 @@ describe(SchulconnexRestClient.name, () => { it('should return the response', async () => { const { accessToken } = setup(); - const result: SchulconnexLizenzInfoResponse[] = await client.getLizenzInfo(accessToken); + const result: SchulconnexPoliciesInfoResponse[] = await client.getPoliciesInfo(accessToken); expect(result).toBeDefined(); }); @@ -234,8 +233,8 @@ describe(SchulconnexRestClient.name, () => { describe('when overriding the url', () => { const setup = () => { const accessToken = 'accessToken'; - const customUrl = 'https://override.url/lizenz-info'; - const response: SchulconnexLizenzInfoResponse[] = schulconnexLizenzInfoResponseFactory.buildList(1); + const customUrl = 'https://override.url/policies-info'; + const response: SchulconnexPoliciesInfoResponse[] = schulconnexPoliciesInfoResponseFactory.buildList(1); httpService.get.mockReturnValueOnce(of(axiosResponseFactory.build({ data: response }))); @@ -248,7 +247,7 @@ describe(SchulconnexRestClient.name, () => { it('should make a request to a SchulConneX-API', async () => { const { accessToken, customUrl } = setup(); - await client.getLizenzInfo(accessToken, { overrideUrl: customUrl }); + await client.getPoliciesInfo(accessToken, { overrideUrl: customUrl }); expect(httpService.get).toHaveBeenCalledWith(customUrl, expect.anything()); }); diff --git a/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.ts b/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.ts index 8fe17ecde1b..5bc86efd21e 100644 --- a/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.ts +++ b/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.ts @@ -8,7 +8,7 @@ import QueryString from 'qs'; import { lastValueFrom, Observable } from 'rxjs'; import { SchulconnexConfigurationMissingLoggable } from './loggable'; import { SchulconnexPersonenInfoParams } from './request'; -import { SchulconnexLizenzInfoResponse, SchulconnexResponse } from './response'; +import { SchulconnexPoliciesInfoResponse, SchulconnexResponse } from './response'; import { SchulconnexApiInterface } from './schulconnex-api.interface'; import { SchulconnexRestClientOptions } from './schulconnex-rest-client-options'; @@ -48,13 +48,13 @@ export class SchulconnexRestClient implements SchulconnexApiInterface { return response; } - public async getLizenzInfo( + public async getPoliciesInfo( accessToken: string, options?: { overrideUrl: string } - ): Promise { - const url: URL = new URL(options?.overrideUrl ?? `${this.SCHULCONNEX_API_BASE_URL}/lizenz-info`); + ): Promise { + const url: URL = new URL(options?.overrideUrl ?? `${this.SCHULCONNEX_API_BASE_URL}/policies-info`); - const response: Promise = this.getRequest( + const response: Promise = this.getRequest( url, accessToken ); diff --git a/apps/server/src/infra/schulconnex-client/testing/index.ts b/apps/server/src/infra/schulconnex-client/testing/index.ts index 23b700123f1..7ab3afe2ff1 100644 --- a/apps/server/src/infra/schulconnex-client/testing/index.ts +++ b/apps/server/src/infra/schulconnex-client/testing/index.ts @@ -1,2 +1,2 @@ export { schulconnexResponseFactory } from './schulconnex-response-factory'; -export { schulconnexLizenzInfoResponseFactory } from './schulconnex-lizenz-info-response-factory'; +export { schulconnexPoliciesInfoResponseFactory } from './schulconnex-policies-info-response-factory'; diff --git a/apps/server/src/infra/schulconnex-client/testing/schulconnex-lizenz-info-response-factory.ts b/apps/server/src/infra/schulconnex-client/testing/schulconnex-lizenz-info-response-factory.ts deleted file mode 100644 index c89989a180e..00000000000 --- a/apps/server/src/infra/schulconnex-client/testing/schulconnex-lizenz-info-response-factory.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Factory } from 'fishery'; -import { SchulconnexLizenzInfoActionType, SchulconnexLizenzInfoResponse } from '../response'; - -export const schulconnexLizenzInfoResponseFactory = Factory.define(() => { - return { - target: { - uid: 'bildungscloud', - partOf: '', - }, - permission: [ - { - action: [SchulconnexLizenzInfoActionType.EXECUTE], - }, - ], - }; -}); diff --git a/apps/server/src/infra/schulconnex-client/testing/schulconnex-policies-info-response-factory.ts b/apps/server/src/infra/schulconnex-client/testing/schulconnex-policies-info-response-factory.ts new file mode 100644 index 00000000000..38541e9e4cf --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/testing/schulconnex-policies-info-response-factory.ts @@ -0,0 +1,16 @@ +import { Factory } from 'fishery'; +import { SchulconnexPoliciesInfoActionType, SchulconnexPoliciesInfoResponse } from '../response'; + +export const schulconnexPoliciesInfoResponseFactory = Factory.define(() => { + return { + target: { + uid: 'bildungscloud', + partOf: '', + }, + permission: [ + { + action: [SchulconnexPoliciesInfoActionType.EXECUTE], + }, + ], + }; +}); diff --git a/apps/server/src/migrations/mikro-orm/Migration20240724090901.ts b/apps/server/src/migrations/mikro-orm/Migration20240724090901.ts new file mode 100644 index 00000000000..1fea5a9cb07 --- /dev/null +++ b/apps/server/src/migrations/mikro-orm/Migration20240724090901.ts @@ -0,0 +1,37 @@ +import { Migration } from '@mikro-orm/migrations-mongodb'; + +export class Migration20240724090901 extends Migration { + async up(): Promise { + const superheroRoleUpdate = await this.getCollection('roles').updateOne( + { name: 'superhero' }, + { + $addToSet: { + permissions: { + $each: ['USER_LOGIN_MIGRATION_FORCE'], + }, + }, + } + ); + + if (superheroRoleUpdate.modifiedCount > 0) { + console.info('Permission USER_LOGIN_MIGRATION_FORCE was added to role superhero.'); + } + } + + async down(): Promise { + const superheroRoleUpdate = await this.getCollection('roles').updateOne( + { name: 'superhero' }, + { + $pull: { + permissions: { + $in: ['USER_LOGIN_MIGRATION_FORCE'], + }, + }, + } + ); + + if (superheroRoleUpdate.modifiedCount > 0) { + console.info('Rollback: Removed permission USER_LOGIN_MIGRATION_FORCE from role superhero.'); + } + } +} diff --git a/apps/server/src/modules/board/service/column-board.service.ts b/apps/server/src/modules/board/service/column-board.service.ts index 7230ff3cb8c..5d50ba32c9f 100644 --- a/apps/server/src/modules/board/service/column-board.service.ts +++ b/apps/server/src/modules/board/service/column-board.service.ts @@ -1,6 +1,6 @@ +import { CopyStatus } from '@modules/copy-helper'; import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; -import { CopyStatus } from '@modules/copy-helper'; import { BoardExternalReference, BoardExternalReferenceType, ColumnBoard, isColumnBoard } from '../domain'; import { BoardNodeRepo } from '../repo'; import { BoardNodeService } from './board-node.service'; @@ -27,7 +27,7 @@ export class ColumnBoardService { const boards = boardNodes.filter((bn) => isColumnBoard(bn)); - return boards as ColumnBoard[]; + return boards; } async updateVisibility(columbBoard: ColumnBoard, visibility: boolean): Promise { diff --git a/apps/server/src/modules/board/service/internal/column-board-reference.service.ts b/apps/server/src/modules/board/service/internal/column-board-reference.service.ts index 8d0424938d8..ccc6026c7a4 100644 --- a/apps/server/src/modules/board/service/internal/column-board-reference.service.ts +++ b/apps/server/src/modules/board/service/internal/column-board-reference.service.ts @@ -11,6 +11,6 @@ export class ColumnBoardReferenceService { const boards = boardNodes.filter((bn) => isColumnBoard(bn)); - return boards as ColumnBoard[]; + return boards; } } diff --git a/apps/server/src/modules/copy-helper/service/copy-helper.service.ts b/apps/server/src/modules/copy-helper/service/copy-helper.service.ts index 3a578c2618b..d56608596f2 100644 --- a/apps/server/src/modules/copy-helper/service/copy-helper.service.ts +++ b/apps/server/src/modules/copy-helper/service/copy-helper.service.ts @@ -36,10 +36,10 @@ export class CopyHelperService { return name; } let num = 1; - const matches = name.match(/^(?.*) \((?\d+)\)$/); - if (matches && matches.groups) { - ({ name } = matches.groups); - num = Number(matches.groups.number) + 1; + const matches = name.match(/^(.*) \((\d+)\)$/); + if (matches) { + name = matches[1]; + num = Number(matches[2]) + 1; } const composedName = `${name} (${num})`; if (existingNames.includes(composedName)) { diff --git a/apps/server/src/modules/learnroom/service/common-cartridge-export.service.ts b/apps/server/src/modules/learnroom/service/common-cartridge-export.service.ts index 3300d60af1c..a02b0c27eed 100644 --- a/apps/server/src/modules/learnroom/service/common-cartridge-export.service.ts +++ b/apps/server/src/modules/learnroom/service/common-cartridge-export.service.ts @@ -138,7 +138,7 @@ export class CommonCartridgeExportService { column.children .filter((child) => isCard(child)) - .forEach((card) => this.addCardToOrganization(card as Card, columnOrganization)); + .forEach((card) => this.addCardToOrganization(card, columnOrganization)); } private addCardToOrganization(card: Card, columnOrganization: CommonCartridgeOrganizationNode): void { diff --git a/apps/server/src/modules/provisioning/loggable/fetching-policies-info-failed.loggable.spec.ts b/apps/server/src/modules/provisioning/loggable/fetching-policies-info-failed.loggable.spec.ts new file mode 100644 index 00000000000..60818f61582 --- /dev/null +++ b/apps/server/src/modules/provisioning/loggable/fetching-policies-info-failed.loggable.spec.ts @@ -0,0 +1,31 @@ +import { ExternalUserDto } from '../dto'; +import { FetchingPoliciesInfoFailedLoggable } from './fetching-policies-info-failed.loggable'; + +describe(FetchingPoliciesInfoFailedLoggable.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const externalUserDto: ExternalUserDto = { + externalId: 'someId', + }; + const policiesInfoEndpoint = 'someEndpoint'; + + const loggable = new FetchingPoliciesInfoFailedLoggable(externalUserDto, policiesInfoEndpoint); + + return { loggable, externalUserDto, policiesInfoEndpoint }; + }; + + it('should return a loggable message', () => { + const { loggable, externalUserDto, policiesInfoEndpoint } = setup(); + + const message = loggable.getLogMessage(); + + expect(message).toEqual({ + message: 'Could not fetch policies info for user. The provisioning of licenses will be skipped.', + data: { + externalUserId: externalUserDto.externalId, + policiesInfoEndpoint, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/provisioning/loggable/fetching-policies-info-failed.loggable.ts b/apps/server/src/modules/provisioning/loggable/fetching-policies-info-failed.loggable.ts new file mode 100644 index 00000000000..fd4df0e9b5e --- /dev/null +++ b/apps/server/src/modules/provisioning/loggable/fetching-policies-info-failed.loggable.ts @@ -0,0 +1,16 @@ +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; +import { ExternalUserDto } from '../dto'; + +export class FetchingPoliciesInfoFailedLoggable implements Loggable { + constructor(private readonly user: ExternalUserDto, private readonly policiesInfoEndpoint: string) {} + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + message: 'Could not fetch policies info for user. The provisioning of licenses will be skipped.', + data: { + externalUserId: this.user.externalId, + policiesInfoEndpoint: this.policiesInfoEndpoint, + }, + }; + } +} diff --git a/apps/server/src/modules/provisioning/loggable/index.ts b/apps/server/src/modules/provisioning/loggable/index.ts index 89b4baf9b7a..ee2c20e74f0 100644 --- a/apps/server/src/modules/provisioning/loggable/index.ts +++ b/apps/server/src/modules/provisioning/loggable/index.ts @@ -2,3 +2,4 @@ export * from './user-for-group-not-found.loggable'; export * from './school-for-group-not-found.loggable'; export * from './group-role-unknown.loggable'; export { SchoolExternalToolCreatedLoggable } from './school-external-tool-created.loggable'; +export { FetchingPoliciesInfoFailedLoggable } from './fetching-policies-info-failed.loggable'; diff --git a/apps/server/src/modules/provisioning/provisioning.config.ts b/apps/server/src/modules/provisioning/provisioning.config.ts index 4d7906e59c6..0314bf8b277 100644 --- a/apps/server/src/modules/provisioning/provisioning.config.ts +++ b/apps/server/src/modules/provisioning/provisioning.config.ts @@ -1,7 +1,7 @@ export interface ProvisioningConfig { FEATURE_SCHULCONNEX_COURSE_SYNC_ENABLED: boolean; FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED: boolean; - PROVISIONING_SCHULCONNEX_LIZENZ_INFO_URL: string; + PROVISIONING_SCHULCONNEX_POLICIES_INFO_URL: string; FEATURE_SANIS_GROUP_PROVISIONING_ENABLED: boolean; FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED: boolean; } 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 58d54a44495..377080049bf 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 @@ -6,8 +6,8 @@ import { SchulconnexResponseValidationGroups, SchulconnexRestClient, } from '@infra/schulconnex-client'; -import { SchulconnexLizenzInfoResponse } from '@infra/schulconnex-client/response'; -import { schulconnexLizenzInfoResponseFactory } from '@infra/schulconnex-client/testing/schulconnex-lizenz-info-response-factory'; +import { SchulconnexPoliciesInfoResponse } from '@infra/schulconnex-client/response'; +import { schulconnexPoliciesInfoResponseFactory } from '@infra/schulconnex-client/testing/schulconnex-policies-info-response-factory'; import { GroupService } from '@modules/group'; import { GroupTypes } from '@modules/group/domain'; import { InternalServerErrorException } from '@nestjs/common'; @@ -16,6 +16,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ValidationErrorLoggableException } from '@shared/common/loggable-exception'; import { RoleName } from '@shared/domain/interface'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; +import { Logger } from '@src/core/logger'; import * as classValidator from 'class-validator'; import { ExternalGroupDto, @@ -45,6 +46,7 @@ describe(SanisProvisioningStrategy.name, () => { let strategy: SanisProvisioningStrategy; let mapper: DeepMocked; + let logger: DeepMocked; let validationFunction: SpyInstance< ReturnType, @@ -100,12 +102,17 @@ describe(SanisProvisioningStrategy.name, () => { provide: SchulconnexRestClient, useValue: createMock(), }, + { + provide: Logger, + useValue: createMock(), + }, ], }).compile(); strategy = module.get(SanisProvisioningStrategy); mapper = module.get(SchulconnexResponseMapper); schulconnexRestClient = module.get(SchulconnexRestClient); + logger = module.get(Logger); validationFunction = jest.spyOn(classValidator, 'validate'); }); @@ -163,8 +170,8 @@ describe(SanisProvisioningStrategy.name, () => { }, }), ]; - const schulconnexLizenzInfoResponses: SchulconnexLizenzInfoResponse[] = - schulconnexLizenzInfoResponseFactory.buildList(1); + const schulconnexLizenzInfoResponses: SchulconnexPoliciesInfoResponse[] = + schulconnexPoliciesInfoResponseFactory.buildList(1); const schulconnexLizenzInfoResponse = schulconnexLizenzInfoResponses[0]; const licenses: ExternalLicenseDto[] = SchulconnexResponseMapper.mapToExternalLicenses([ schulconnexLizenzInfoResponse, @@ -178,7 +185,7 @@ describe(SanisProvisioningStrategy.name, () => { mapper.mapToExternalGroupDtos.mockReturnValue(groups); validationFunction.mockResolvedValueOnce([]); validationFunction.mockResolvedValueOnce([]); - schulconnexRestClient.getLizenzInfo.mockResolvedValueOnce(schulconnexLizenzInfoResponses); + schulconnexRestClient.getPoliciesInfo.mockResolvedValueOnce(schulconnexLizenzInfoResponses); validationFunction.mockResolvedValueOnce([]); return { @@ -307,6 +314,57 @@ describe(SanisProvisioningStrategy.name, () => { }); }); + describe('when fetching policies info from schulconnex fails', () => { + const setup = () => { + const provisioningUrl = 'sanisProvisioningUrl'; + const input: OauthDataStrategyInputDto = new OauthDataStrategyInputDto({ + system: new ProvisioningSystemDto({ + systemId: 'systemId', + provisioningStrategy: SystemProvisioningStrategy.SANIS, + provisioningUrl, + }), + idToken: 'sanisIdToken', + accessToken: 'sanisAccessToken', + }); + const schulconnexResponse: SchulconnexResponse = setupSchulconnexResponse(); + const user: ExternalUserDto = new ExternalUserDto({ + externalId: 'externalUserId', + }); + const school: ExternalSchoolDto = new ExternalSchoolDto({ + externalId: 'externalSchoolId', + name: 'schoolName', + }); + + schulconnexRestClient.getPersonInfo.mockResolvedValueOnce(schulconnexResponse); + mapper.mapToExternalUserDto.mockReturnValue(user); + mapper.mapToExternalSchoolDto.mockReturnValue(school); + validationFunction.mockResolvedValueOnce([]); + schulconnexRestClient.getPoliciesInfo.mockRejectedValueOnce(new Error()); + + config.FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED = true; + + return { + input, + }; + }; + + it('should log a warning', async () => { + const { input } = setup(); + + await strategy.getData(input); + + expect(logger.warning).toHaveBeenCalled(); + }); + + it('should return undefined external licenses ', async () => { + const { input } = setup(); + + const oauthDataDto: OauthDataDto = await strategy.getData(input); + + expect(oauthDataDto.externalLicenses).toBeUndefined(); + }); + }); + describe('when FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED is false', () => { const setup = () => { const provisioningUrl = 'sanisProvisioningUrl'; @@ -347,7 +405,7 @@ describe(SanisProvisioningStrategy.name, () => { await strategy.getData(input); - expect(schulconnexRestClient.getLizenzInfo).not.toHaveBeenCalled(); + expect(schulconnexRestClient.getPoliciesInfo).not.toHaveBeenCalled(); }); }); 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 e55305ade14..6c118efcd1c 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts @@ -1,5 +1,5 @@ import { - SchulconnexLizenzInfoResponse, + SchulconnexPoliciesInfoResponse, SchulconnexResponse, SchulconnexResponseValidationGroups, } from '@infra/schulconnex-client/response'; @@ -10,6 +10,7 @@ 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 { Logger } from '@src/core/logger'; import { plainToClass } from 'class-transformer'; import { validate, ValidationError } from 'class-validator'; import { @@ -20,6 +21,7 @@ import { OauthDataDto, OauthDataStrategyInputDto, } from '../../dto'; +import { FetchingPoliciesInfoFailedLoggable } from '../../loggable'; import { ProvisioningConfig } from '../../provisioning.config'; import { SchulconnexProvisioningStrategy } from '../oidc'; import { @@ -44,7 +46,8 @@ export class SanisProvisioningStrategy extends SchulconnexProvisioningStrategy { protected readonly schulconnexToolProvisioningService: SchulconnexToolProvisioningService, protected readonly configService: ConfigService, private readonly responseMapper: SchulconnexResponseMapper, - private readonly schulconnexRestClient: SchulconnexRestClient + private readonly schulconnexRestClient: SchulconnexRestClient, + private readonly logger: Logger ) { super( schulconnexSchoolProvisioningService, @@ -97,18 +100,24 @@ export class SanisProvisioningStrategy extends SchulconnexProvisioningStrategy { let externalLicenses: ExternalLicenseDto[] | undefined; if (this.configService.get('FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED')) { - const schulconnexLizenzInfoAxiosResponse: SchulconnexLizenzInfoResponse[] = - await this.schulconnexRestClient.getLizenzInfo(input.accessToken, { - overrideUrl: this.configService.get('PROVISIONING_SCHULCONNEX_LIZENZ_INFO_URL'), - }); - - const schulconnexLizenzInfoResponses: SchulconnexLizenzInfoResponse[] = plainToClass( - SchulconnexLizenzInfoResponse, - schulconnexLizenzInfoAxiosResponse - ); - await this.checkResponseValidation(schulconnexLizenzInfoResponses); - - externalLicenses = SchulconnexResponseMapper.mapToExternalLicenses(schulconnexLizenzInfoResponses); + const policiesInfoUrl = this.configService.get('PROVISIONING_SCHULCONNEX_POLICIES_INFO_URL'); + try { + const schulconnexPoliciesInfoAxiosResponse = await this.schulconnexRestClient.getPoliciesInfo( + input.accessToken, + { + overrideUrl: policiesInfoUrl, + } + ); + + const schulconnexLizenzInfoResponses = plainToClass( + SchulconnexPoliciesInfoResponse, + schulconnexPoliciesInfoAxiosResponse + ); + await this.checkResponseValidation(schulconnexLizenzInfoResponses); + externalLicenses = SchulconnexResponseMapper.mapToExternalLicenses(schulconnexLizenzInfoResponses); + } catch (error) { + this.logger.warning(new FetchingPoliciesInfoFailedLoggable(externalUser, policiesInfoUrl)); + } } const oauthData: OauthDataDto = new OauthDataDto({ @@ -123,7 +132,7 @@ export class SanisProvisioningStrategy extends SchulconnexProvisioningStrategy { } private async checkResponseValidation( - response: SchulconnexResponse | SchulconnexLizenzInfoResponse | SchulconnexLizenzInfoResponse[], + response: SchulconnexResponse | SchulconnexPoliciesInfoResponse | SchulconnexPoliciesInfoResponse[], groups?: SchulconnexResponseValidationGroups[] ): Promise { const responsesArray = Array.isArray(response) ? response : [response]; 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 4173c8d2ed0..663d53fe2da 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 @@ -3,9 +3,9 @@ import { SchulconnexGroupRole, SchulconnexGroupType, SchulconnexGruppenResponse, - SchulconnexLizenzInfoResponse, - schulconnexLizenzInfoResponseFactory, SchulconnexPersonenkontextResponse, + SchulconnexPoliciesInfoResponse, + schulconnexPoliciesInfoResponseFactory, SchulconnexResponse, schulconnexResponseFactory, SchulconnexSonstigeGruppenzugehoerigeResponse, @@ -561,7 +561,7 @@ describe(SchulconnexResponseMapper.name, () => { describe('mapToExternalLicenses', () => { describe('when a license response has a medium id and no media source', () => { const setup = () => { - const licenseResponse: SchulconnexLizenzInfoResponse[] = schulconnexLizenzInfoResponseFactory.buildList(1, { + const licenseResponse: SchulconnexPoliciesInfoResponse[] = schulconnexPoliciesInfoResponseFactory.buildList(1, { target: { uid: 'bildungscloud', partOf: '' }, }); @@ -573,7 +573,8 @@ describe(SchulconnexResponseMapper.name, () => { it('should map the response to an ExternalLicenseDto', () => { const { licenseResponse } = setup(); - const result: ExternalLicenseDto[] = SchulconnexResponseMapper.mapToExternalLicenses(licenseResponse); + const result: ExternalLicenseDto[] | undefined = + SchulconnexResponseMapper.mapToExternalLicenses(licenseResponse); expect(result).toEqual([ { @@ -586,7 +587,7 @@ describe(SchulconnexResponseMapper.name, () => { describe('when a license response has a medium id and a media source', () => { const setup = () => { - const licenseResponse: SchulconnexLizenzInfoResponse[] = schulconnexLizenzInfoResponseFactory.buildList(1, { + const licenseResponse: SchulconnexPoliciesInfoResponse[] = schulconnexPoliciesInfoResponseFactory.buildList(1, { target: { uid: 'bildungscloud', partOf: 'bildungscloud-source' }, }); @@ -598,7 +599,8 @@ describe(SchulconnexResponseMapper.name, () => { it('should map the response to an ExternalLicenseDto', () => { const { licenseResponse } = setup(); - const result: ExternalLicenseDto[] = SchulconnexResponseMapper.mapToExternalLicenses(licenseResponse); + const result: ExternalLicenseDto[] | undefined = + SchulconnexResponseMapper.mapToExternalLicenses(licenseResponse); expect(result).toEqual([ { @@ -611,7 +613,7 @@ describe(SchulconnexResponseMapper.name, () => { describe('when a license response has no medium id', () => { const setup = () => { - const licenseResponse: SchulconnexLizenzInfoResponse[] = schulconnexLizenzInfoResponseFactory.buildList(1, { + const licenseResponse: SchulconnexPoliciesInfoResponse[] = schulconnexPoliciesInfoResponseFactory.buildList(1, { target: { uid: '', partOf: 'bildungscloud-source' }, }); @@ -623,7 +625,8 @@ describe(SchulconnexResponseMapper.name, () => { it('should should be filtered out', () => { const { licenseResponse } = setup(); - const result: ExternalLicenseDto[] = SchulconnexResponseMapper.mapToExternalLicenses(licenseResponse); + const result: ExternalLicenseDto[] | undefined = + SchulconnexResponseMapper.mapToExternalLicenses(licenseResponse); expect(result).toEqual([]); }); 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 34a31f9a55e..b76db0ea58e 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 @@ -6,7 +6,7 @@ import { SchulconnexGroupType, SchulconnexGruppenResponse, SchulconnexLaufzeitResponse, - SchulconnexLizenzInfoResponse, + SchulconnexPoliciesInfoResponse, SchulconnexResponse, SchulconnexRole, SchulconnexSonstigeGruppenzugehoerigeResponse, @@ -144,9 +144,9 @@ export class SchulconnexResponseMapper { let otherUsers: ExternalGroupUserDto[] | undefined; if (this.configService.get('FEATURE_OTHER_GROUPUSERS_PROVISIONING_ENABLED')) { otherUsers = group.sonstige_gruppenzugehoerige - ? (group.sonstige_gruppenzugehoerige + ? group.sonstige_gruppenzugehoerige .map((relation): ExternalGroupUserDto | null => this.mapToExternalGroupUser(relation)) - .filter((otherUser: ExternalGroupUserDto | null) => otherUser !== null) as ExternalGroupUserDto[]) + .filter((otherUser: ExternalGroupUserDto | null) => otherUser !== null) : []; } @@ -240,9 +240,9 @@ export class SchulconnexResponseMapper { }; } - public static mapToExternalLicenses(licenseInfos: SchulconnexLizenzInfoResponse[]): ExternalLicenseDto[] { + public static mapToExternalLicenses(licenseInfos: SchulconnexPoliciesInfoResponse[]): ExternalLicenseDto[] { const externalLicenseDtos: ExternalLicenseDto[] = licenseInfos - .map((license: SchulconnexLizenzInfoResponse) => { + .map((license: SchulconnexPoliciesInfoResponse) => { if (license.target.partOf === '') { license.target.partOf = undefined; } diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index 98703dd62db..aa6b186fdf7 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -261,7 +261,7 @@ const config: ServerConfig = { ) as boolean, ALERT_CACHE_INTERVAL_MIN: Configuration.get('ALERT_CACHE_INTERVAL_MIN') as number, FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED: Configuration.get('FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED') as boolean, - PROVISIONING_SCHULCONNEX_LIZENZ_INFO_URL: Configuration.get('PROVISIONING_SCHULCONNEX_LIZENZ_INFO_URL') as string, + PROVISIONING_SCHULCONNEX_POLICIES_INFO_URL: Configuration.get('PROVISIONING_SCHULCONNEX_POLICIES_INFO_URL') as string, BOARD_COLLABORATION_URI: Configuration.get('BOARD_COLLABORATION_URI') as string, 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, diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts index 15701e10cfe..71eac9eed71 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts @@ -71,7 +71,7 @@ export class ExternalToolService { }) ); - tools.data = resolvedTools.filter((tool) => tool !== undefined) as ExternalTool[]; + tools.data = resolvedTools.filter((tool) => tool !== undefined); return tools; } diff --git a/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts b/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts index cb5be880941..b2373b55a4c 100644 --- a/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts +++ b/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts @@ -1,6 +1,6 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { SchulconnexResponse, SchulconnexRole } from '@infra/schulconnex-client'; -import { SchulconnexLizenzInfoResponse } from '@infra/schulconnex-client/response'; +import { SchulconnexPoliciesInfoResponse } from '@infra/schulconnex-client/response'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { OauthTokenResponse } from '@modules/oauth/service/dto'; import { ServerTestModule } from '@modules/server'; @@ -27,8 +27,7 @@ import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { UUID } from 'bson'; import { Response } from 'supertest'; -import { UserLoginMigrationResponse } from '../dto'; -import { Oauth2MigrationParams } from '../dto/oauth2-migration.params'; +import { ForceMigrationParams, Oauth2MigrationParams, UserLoginMigrationResponse } from '../dto'; jest.mock('jwks-rsa', () => () => { return { @@ -464,8 +463,8 @@ describe('UserLoginMigrationController (API)', () => { }, ], }) - .onGet(configService.get('PROVISIONING_SCHULCONNEX_LIZENZ_INFO_URL')) - .replyOnce(200, []); + .onGet(configService.get('PROVISIONING_SCHULCONNEX_POLICIES_INFO_URL')) + .replyOnce(200, []); }; describe('when providing a code and being eligible to migrate', () => { @@ -1405,4 +1404,99 @@ describe('UserLoginMigrationController (API)', () => { }); }); }); + + describe('[GET] /user-login-migrations/force-migration', () => { + describe('when forcing a school to migrate', () => { + const setup = async () => { + const targetSystem: SystemEntity = systemEntityFactory + .withOauthConfig() + .buildWithId({ alias: 'SANIS', provisioningStrategy: SystemProvisioningStrategy.SANIS }); + + const sourceSystem: SystemEntity = systemEntityFactory.buildWithId(); + + const school: SchoolEntity = schoolEntityFactory.buildWithId({ + systems: [sourceSystem], + }); + + const email = 'admin@test.com'; + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({ + email, + school, + }); + const { superheroAccount, superheroUser } = UserAndAccountTestFactory.buildSuperhero(); + + await em.persistAndFlush([ + sourceSystem, + targetSystem, + school, + superheroAccount, + superheroUser, + adminAccount, + adminUser, + ]); + em.clear(); + + const loggedInClient = await testApiClient.login(superheroAccount); + + const requestBody: ForceMigrationParams = new ForceMigrationParams(); + requestBody.email = email; + requestBody.externalUserId = 'externalUserId'; + requestBody.externalSchoolId = 'externalSchoolId'; + + return { + requestBody, + loggedInClient, + sourceSystem, + targetSystem, + school, + adminUser, + }; + }; + + it('should start the migration for the school and migrate the user and school', async () => { + const { requestBody, loggedInClient, school, sourceSystem, targetSystem, adminUser } = await setup(); + + const response: Response = await loggedInClient.post(`/force-migration`, requestBody); + + expect(response.status).toEqual(HttpStatus.CREATED); + + const userLoginMigration = await em.findOneOrFail(UserLoginMigrationEntity, { school: school.id }); + expect(userLoginMigration.sourceSystem?.id).toEqual(sourceSystem.id); + expect(userLoginMigration.targetSystem.id).toEqual(targetSystem.id); + + expect(await em.findOne(User, adminUser.id)).toEqual( + expect.objectContaining({ + externalId: requestBody.externalUserId, + }) + ); + + expect(await em.findOne(SchoolEntity, school.id)).toEqual( + expect.objectContaining({ + externalId: requestBody.externalSchoolId, + }) + ); + }); + }); + + describe('when authentication of user failed', () => { + const setup = () => { + const requestBody: ForceMigrationParams = new ForceMigrationParams(); + requestBody.email = 'fail@test.com'; + requestBody.externalUserId = 'externalUserId'; + requestBody.externalSchoolId = 'externalSchoolId'; + + return { + requestBody, + }; + }; + + it('should throw an UnauthorizedException', async () => { + const { requestBody } = setup(); + + const response: Response = await testApiClient.post(`/force-migration`, requestBody); + + expect(response.status).toEqual(HttpStatus.UNAUTHORIZED); + }); + }); + }); }); diff --git a/apps/server/src/modules/user-login-migration/controller/dto/request/force-migration.params.ts b/apps/server/src/modules/user-login-migration/controller/dto/request/force-migration.params.ts new file mode 100644 index 00000000000..416bb27ee47 --- /dev/null +++ b/apps/server/src/modules/user-login-migration/controller/dto/request/force-migration.params.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsEmail, IsNotEmpty, IsString } from 'class-validator'; + +export class ForceMigrationParams { + @IsEmail() + @ApiProperty({ description: 'Email of the administrator' }) + email!: string; + + @IsString() + @IsNotEmpty() + @ApiProperty({ description: 'Target externalId to link it with an external account' }) + externalUserId!: string; + + @IsString() + @IsNotEmpty() + @ApiProperty({ description: 'Target externalId to link it with an external school' }) + externalSchoolId!: string; +} diff --git a/apps/server/src/modules/user-login-migration/controller/dto/request/index.ts b/apps/server/src/modules/user-login-migration/controller/dto/request/index.ts index f6f234cd7e4..a571ed5137e 100644 --- a/apps/server/src/modules/user-login-migration/controller/dto/request/index.ts +++ b/apps/server/src/modules/user-login-migration/controller/dto/request/index.ts @@ -2,3 +2,5 @@ export { UserIdParams } from './user-id.params'; export { SchoolIdParams } from './school-id.params'; export { UserLoginMigrationSearchParams } from './user-login-migration-search.params'; export { UserLoginMigrationMandatoryParams } from './user-login-migration-mandatory.params'; +export { Oauth2MigrationParams } from './oauth2-migration.params'; +export { ForceMigrationParams } from './force-migration.params'; diff --git a/apps/server/src/modules/user-login-migration/controller/dto/oauth2-migration.params.ts b/apps/server/src/modules/user-login-migration/controller/dto/request/oauth2-migration.params.ts similarity index 100% rename from apps/server/src/modules/user-login-migration/controller/dto/oauth2-migration.params.ts rename to apps/server/src/modules/user-login-migration/controller/dto/request/oauth2-migration.params.ts diff --git a/apps/server/src/modules/user-login-migration/controller/user-login-migration.controller.ts b/apps/server/src/modules/user-login-migration/controller/user-login-migration.controller.ts index 81019ee19a2..e5b48fdc05e 100644 --- a/apps/server/src/modules/user-login-migration/controller/user-login-migration.controller.ts +++ b/apps/server/src/modules/user-login-migration/controller/user-login-migration.controller.ts @@ -1,6 +1,7 @@ import { Authenticate, CurrentUser, ICurrentUser, JWT } from '@modules/authentication'; import { Body, Controller, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common'; import { + ApiCreatedResponse, ApiForbiddenResponse, ApiInternalServerErrorResponse, ApiNoContentResponse, @@ -29,13 +30,14 @@ import { UserLoginMigrationUc, } from '../uc'; import { + ForceMigrationParams, + Oauth2MigrationParams, SchoolIdParams, UserLoginMigrationMandatoryParams, UserLoginMigrationResponse, UserLoginMigrationSearchListResponse, UserLoginMigrationSearchParams, } from './dto'; -import { Oauth2MigrationParams } from './dto/oauth2-migration.params'; @ApiTags('UserLoginMigration') @Controller('user-login-migrations') @@ -219,7 +221,7 @@ export class UserLoginMigrationController { @Post('migrate-to-oauth2') @ApiOkResponse({ description: 'The User has been successfully migrated.', status: 200 }) - @ApiInternalServerErrorResponse({ description: 'The migration of the User was not possible.' }) + @ApiUnprocessableEntityResponse({ description: 'The migration of the User was not possible.' }) async migrateUserLogin( @JWT() jwt: string, @CurrentUser() currentUser: ICurrentUser, @@ -227,4 +229,27 @@ export class UserLoginMigrationController { ): Promise { await this.userLoginMigrationUc.migrate(jwt, currentUser.userId, body.systemId, body.code, body.redirectUri); } + + @Post('force-migration') + @ApiOperation({ summary: 'Force migrate an administrator account and its school' }) + @ApiCreatedResponse({ description: 'The user and their school were successfully migrated' }) + @ApiUnprocessableEntityResponse({ + description: + 'There are multiple users with the email,' + + 'or the user is not an administrator,' + + 'or the school is already migrated,' + + 'or the external user id is already assigned', + }) + @ApiNotFoundResponse({ description: 'There is no user with the email' }) + public async forceMigration( + @CurrentUser() currentUser: ICurrentUser, + @Body() forceMigrationParams: ForceMigrationParams + ): Promise { + await this.userLoginMigrationUc.forceMigration( + currentUser.userId, + forceMigrationParams.email, + forceMigrationParams.externalUserId, + forceMigrationParams.externalSchoolId + ); + } } diff --git a/apps/server/src/modules/user-login-migration/loggable/index.ts b/apps/server/src/modules/user-login-migration/loggable/index.ts index 0a8aad87844..d67203098a0 100644 --- a/apps/server/src/modules/user-login-migration/loggable/index.ts +++ b/apps/server/src/modules/user-login-migration/loggable/index.ts @@ -14,4 +14,7 @@ export * from './identical-user-login-migration-system.loggable-exception'; export * from './moin-schule-system-not-found.loggable-exception'; export { UserNotMigratedLoggableException } from './user-not-migrated.loggable-exception'; export { UserMigrationRollbackSuccessfulLoggable } from './user-migration-rollback-successful.loggable'; +export { UserLoginMigrationSchoolAlreadyMigratedLoggableException } from './user-login-migration-school-already-migrated.loggable-exception'; +export { UserLoginMigrationInvalidAdminLoggableException } from './user-login-migration-invalid-admin.loggable-exception'; +export { UserLoginMigrationMultipleEmailUsersLoggableException } from './user-login-migration-multiple-email-users.loggable-exception'; export * from './debug'; diff --git a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-invalid-admin.loggable-exception.spec.ts b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-invalid-admin.loggable-exception.spec.ts new file mode 100644 index 00000000000..0caa1817a40 --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-invalid-admin.loggable-exception.spec.ts @@ -0,0 +1,31 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { UserLoginMigrationInvalidAdminLoggableException } from './user-login-migration-invalid-admin.loggable-exception'; + +describe(UserLoginMigrationInvalidAdminLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const userId = new ObjectId().toHexString(); + const exception = new UserLoginMigrationInvalidAdminLoggableException(userId); + + return { + exception, + userId, + }; + }; + + it('should return the correct log message', () => { + const { exception, userId } = setup(); + + const message = exception.getLogMessage(); + + expect(message).toEqual({ + type: 'USER_LOGIN_MIGRATION_INVALID_ADMIN', + message: 'The user is not an administrator', + stack: exception.stack, + data: { + userId, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-invalid-admin.loggable-exception.ts b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-invalid-admin.loggable-exception.ts new file mode 100644 index 00000000000..606d6fbba96 --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-invalid-admin.loggable-exception.ts @@ -0,0 +1,19 @@ +import { UnprocessableEntityException } from '@nestjs/common'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; + +export class UserLoginMigrationInvalidAdminLoggableException extends UnprocessableEntityException implements Loggable { + constructor(private readonly userId: string) { + super(); + } + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + type: 'USER_LOGIN_MIGRATION_INVALID_ADMIN', + message: 'The user is not an administrator', + stack: this.stack, + data: { + userId: this.userId, + }, + }; + } +} diff --git a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-multiple-email-users.loggable-exception.spec.ts b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-multiple-email-users.loggable-exception.spec.ts new file mode 100644 index 00000000000..bd6390b6d07 --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-multiple-email-users.loggable-exception.spec.ts @@ -0,0 +1,30 @@ +import { UserLoginMigrationMultipleEmailUsersLoggableException } from './user-login-migration-multiple-email-users.loggable-exception'; + +describe(UserLoginMigrationMultipleEmailUsersLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const email = 'test@test.de'; + const exception = new UserLoginMigrationMultipleEmailUsersLoggableException(email); + + return { + exception, + email, + }; + }; + + it('should return the correct log message', () => { + const { exception, email } = setup(); + + const message = exception.getLogMessage(); + + expect(message).toEqual({ + type: 'USER_LOGIN_MIGRATION_MULTIPLE_EMAIL_USERS', + message: 'There is multiple users with this email', + stack: exception.stack, + data: { + email, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-multiple-email-users.loggable-exception.ts b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-multiple-email-users.loggable-exception.ts new file mode 100644 index 00000000000..fe48277e13f --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-multiple-email-users.loggable-exception.ts @@ -0,0 +1,22 @@ +import { UnprocessableEntityException } from '@nestjs/common'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; + +export class UserLoginMigrationMultipleEmailUsersLoggableException + extends UnprocessableEntityException + implements Loggable +{ + constructor(private readonly email: string) { + super(); + } + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + type: 'USER_LOGIN_MIGRATION_MULTIPLE_EMAIL_USERS', + message: 'There is multiple users with this email', + stack: this.stack, + data: { + email: this.email, + }, + }; + } +} diff --git a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-school-already-migrated.loggable-exception.spec.ts b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-school-already-migrated.loggable-exception.spec.ts new file mode 100644 index 00000000000..43e695e9bdb --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-school-already-migrated.loggable-exception.spec.ts @@ -0,0 +1,31 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { UserLoginMigrationSchoolAlreadyMigratedLoggableException } from './user-login-migration-school-already-migrated.loggable-exception'; + +describe(UserLoginMigrationSchoolAlreadyMigratedLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const schoolId = new ObjectId().toHexString(); + const exception = new UserLoginMigrationSchoolAlreadyMigratedLoggableException(schoolId); + + return { + exception, + schoolId, + }; + }; + + it('should return the correct log message', () => { + const { exception, schoolId } = setup(); + + const message = exception.getLogMessage(); + + expect(message).toEqual({ + type: 'USER_LOGIN_MIGRATION_SCHOOL_HAS_ALREADY_MIGRATED', + message: 'School has already migrated', + stack: exception.stack, + data: { + schoolId, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-school-already-migrated.loggable-exception.ts b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-school-already-migrated.loggable-exception.ts new file mode 100644 index 00000000000..874ff87d8fd --- /dev/null +++ b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-school-already-migrated.loggable-exception.ts @@ -0,0 +1,22 @@ +import { UnprocessableEntityException } from '@nestjs/common'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; + +export class UserLoginMigrationSchoolAlreadyMigratedLoggableException + extends UnprocessableEntityException + implements Loggable +{ + constructor(private readonly schoolId: string) { + super(); + } + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + type: 'USER_LOGIN_MIGRATION_SCHOOL_HAS_ALREADY_MIGRATED', + message: 'School has already migrated', + stack: this.stack, + data: { + schoolId: this.schoolId, + }, + }; + } +} diff --git a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-user-already-migrated.loggable-exception.ts b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-user-already-migrated.loggable-exception.ts index 03dec25939a..a6e3310b7b8 100644 --- a/apps/server/src/modules/user-login-migration/loggable/user-login-migration-user-already-migrated.loggable-exception.ts +++ b/apps/server/src/modules/user-login-migration/loggable/user-login-migration-user-already-migrated.loggable-exception.ts @@ -10,7 +10,7 @@ export class UserLoginMigrationUserAlreadyMigratedLoggableException extends Busi title: 'User has already migrated', defaultMessage: 'User with externalId has already migrated', }, - HttpStatus.INTERNAL_SERVER_ERROR, + HttpStatus.UNPROCESSABLE_ENTITY, { multipleUsersFound: true, } diff --git a/apps/server/src/modules/user-login-migration/service/school-migration.service.ts b/apps/server/src/modules/user-login-migration/service/school-migration.service.ts index 27289296348..8c15c4313aa 100644 --- a/apps/server/src/modules/user-login-migration/service/school-migration.service.ts +++ b/apps/server/src/modules/user-login-migration/service/school-migration.service.ts @@ -87,7 +87,7 @@ export class SchoolMigrationService { } } - private hasSchoolMigrated(sourceExternalId: string | undefined, targetExternalId: string): boolean { + public hasSchoolMigrated(sourceExternalId: string | undefined, targetExternalId: string): boolean { const isExternalIdEquivalent: boolean = sourceExternalId === targetExternalId; return isExternalIdEquivalent; diff --git a/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.spec.ts b/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.spec.ts index 0da0459e6a0..1903d351b87 100644 --- a/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.spec.ts +++ b/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.spec.ts @@ -12,22 +12,30 @@ import { ProvisioningSystemDto, } from '@modules/provisioning'; import { SystemEntity } from '@modules/system/entity'; +import { UserService } from '@modules/user'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; -import { LegacySchoolDo, Page, UserLoginMigrationDO } from '@shared/domain/domainobject'; +import { LegacySchoolDo, Page, RoleReference, UserLoginMigrationDO } from '@shared/domain/domainobject'; import { User } from '@shared/domain/entity'; -import { Permission } from '@shared/domain/interface'; +import { Permission, RoleName } from '@shared/domain/interface'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { legacySchoolDoFactory, setupEntities, systemEntityFactory, + userDoFactory, userFactory, userLoginMigrationDOFactory, } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { ExternalSchoolNumberMissingLoggableException, InvalidUserLoginMigrationLoggableException } from '../loggable'; +import { + ExternalSchoolNumberMissingLoggableException, + InvalidUserLoginMigrationLoggableException, + UserLoginMigrationInvalidAdminLoggableException, + UserLoginMigrationMultipleEmailUsersLoggableException, + UserLoginMigrationSchoolAlreadyMigratedLoggableException, +} from '../loggable'; import { SchoolMigrationService, UserLoginMigrationService, UserMigrationService } from '../service'; import { UserLoginMigrationUc } from './user-login-migration.uc'; @@ -42,6 +50,8 @@ describe(UserLoginMigrationUc.name, () => { let userMigrationService: DeepMocked; let authenticationService: DeepMocked; let authorizationService: DeepMocked; + let schoolService: DeepMocked; + let userService: DeepMocked; beforeAll(async () => { await setupEntities(); @@ -81,6 +91,10 @@ describe(UserLoginMigrationUc.name, () => { provide: LegacySchoolService, useValue: createMock(), }, + { + provide: UserService, + useValue: createMock(), + }, { provide: Logger, useValue: createMock(), @@ -97,6 +111,8 @@ describe(UserLoginMigrationUc.name, () => { userMigrationService = module.get(UserMigrationService); authenticationService = module.get(AuthenticationService); authorizationService = module.get(AuthorizationService); + schoolService = module.get(LegacySchoolService); + userService = module.get(UserService); }); afterAll(async () => { @@ -104,7 +120,7 @@ describe(UserLoginMigrationUc.name, () => { }); afterEach(() => { - jest.clearAllMocks(); + jest.resetAllMocks(); }); describe('getMigrations', () => { @@ -608,4 +624,288 @@ describe(UserLoginMigrationUc.name, () => { }); }); }); + + describe('forceMigration', () => { + describe('when the user and their school can successfully migrate', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const userDo = userDoFactory.build({ + id: user.id, + roles: [ + new RoleReference({ + id: new ObjectId().toHexString(), + name: RoleName.ADMINISTRATOR, + }), + ], + }); + const externalUserId = 'externalUserId'; + const externalSchoolId = 'externalSchoolId'; + const school = legacySchoolDoFactory.build({ + id: user.school.id, + }); + const userLoginMigration = userLoginMigrationDOFactory.buildWithId({ + schoolId: user.school.id, + }); + + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + userService.findByEmail.mockResolvedValueOnce([userDo]); + userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(null); + userLoginMigrationService.startMigration.mockResolvedValueOnce(userLoginMigration); + schoolService.getSchoolById.mockResolvedValueOnce(school); + schoolMigrationService.hasSchoolMigrated.mockReturnValueOnce(false); + + return { + user, + externalUserId, + externalSchoolId, + userLoginMigration, + school, + }; + }; + + it('should check permission', async () => { + const { user, externalUserId, externalSchoolId } = setup(); + + await uc.forceMigration(user.id, user.email, externalUserId, externalSchoolId); + + expect(authorizationService.checkAllPermissions).toHaveBeenCalledWith(user, [ + Permission.USER_LOGIN_MIGRATION_FORCE, + ]); + }); + + it('should migrate the school', async () => { + const { user, externalUserId, externalSchoolId, userLoginMigration, school } = setup(); + + await uc.forceMigration(user.id, user.email, externalUserId, externalSchoolId); + + expect(schoolMigrationService.migrateSchool).toHaveBeenCalledWith( + school, + externalSchoolId, + userLoginMigration.targetSystemId + ); + }); + + it('should migrate the user', async () => { + const { user, externalUserId, externalSchoolId, userLoginMigration } = setup(); + + await uc.forceMigration(user.id, user.email, externalUserId, externalSchoolId); + + expect(userMigrationService.migrateUser).toHaveBeenCalledWith( + user.id, + externalUserId, + userLoginMigration.targetSystemId + ); + }); + }); + + describe('when there is no user with the email', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const externalUserId = 'externalUserId'; + const externalSchoolId = 'externalSchoolId'; + + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + userService.findByEmail.mockResolvedValueOnce([]); + + return { + user, + externalUserId, + externalSchoolId, + }; + }; + + it('should throw an error', async () => { + const { user, externalUserId, externalSchoolId } = setup(); + + await expect(uc.forceMigration(user.id, user.email, externalUserId, externalSchoolId)).rejects.toThrow( + NotFoundLoggableException + ); + }); + }); + + describe('when there are multiple users with the email', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const userDo = userDoFactory.build({ + id: user.id, + roles: [ + new RoleReference({ + id: new ObjectId().toHexString(), + name: RoleName.ADMINISTRATOR, + }), + ], + }); + const externalUserId = 'externalUserId'; + const externalSchoolId = 'externalSchoolId'; + + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + userService.findByEmail.mockResolvedValueOnce([userDo, userDo]); + + return { + user, + externalUserId, + externalSchoolId, + }; + }; + + it('should throw an error', async () => { + const { user, externalUserId, externalSchoolId } = setup(); + + await expect(uc.forceMigration(user.id, user.email, externalUserId, externalSchoolId)).rejects.toThrow( + UserLoginMigrationMultipleEmailUsersLoggableException + ); + }); + }); + + describe('when there is no user id', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const userDo = userDoFactory.build({ + id: undefined, + roles: [ + new RoleReference({ + id: new ObjectId().toHexString(), + name: RoleName.ADMINISTRATOR, + }), + ], + }); + const externalUserId = 'externalUserId'; + const externalSchoolId = 'externalSchoolId'; + + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + userService.findByEmail.mockResolvedValueOnce([userDo]); + + return { + user, + externalUserId, + externalSchoolId, + }; + }; + + it('should throw an error', async () => { + const { user, externalUserId, externalSchoolId } = setup(); + + await expect(uc.forceMigration(user.id, user.email, externalUserId, externalSchoolId)).rejects.toThrow( + NotFoundLoggableException + ); + }); + }); + + describe('when the user is not an administrator', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const userDo = userDoFactory.build({ + id: user.id, + roles: [ + new RoleReference({ + id: new ObjectId().toHexString(), + name: RoleName.TEACHER, + }), + ], + }); + const externalUserId = 'externalUserId'; + const externalSchoolId = 'externalSchoolId'; + + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + userService.findByEmail.mockResolvedValueOnce([userDo]); + + return { + user, + externalUserId, + externalSchoolId, + }; + }; + + it('should throw an error', async () => { + const { user, externalUserId, externalSchoolId } = setup(); + + await expect(uc.forceMigration(user.id, user.email, externalUserId, externalSchoolId)).rejects.toThrow( + UserLoginMigrationInvalidAdminLoggableException + ); + }); + }); + + describe('when there is already a user login migration active', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const userDo = userDoFactory.build({ + id: user.id, + roles: [ + new RoleReference({ + id: new ObjectId().toHexString(), + name: RoleName.ADMINISTRATOR, + }), + ], + }); + const externalUserId = 'externalUserId'; + const externalSchoolId = 'externalSchoolId'; + const userLoginMigration = userLoginMigrationDOFactory.buildWithId({ + schoolId: user.school.id, + }); + + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + userService.findByEmail.mockResolvedValueOnce([userDo]); + userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(userLoginMigration); + + return { + user, + externalUserId, + externalSchoolId, + }; + }; + + it('should throw an error', async () => { + const { user, externalUserId, externalSchoolId } = setup(); + + await expect(uc.forceMigration(user.id, user.email, externalUserId, externalSchoolId)).rejects.toThrow( + UserLoginMigrationSchoolAlreadyMigratedLoggableException + ); + }); + }); + + describe('when the school is already migrated', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const userDo = userDoFactory.build({ + id: user.id, + roles: [ + new RoleReference({ + id: new ObjectId().toHexString(), + name: RoleName.ADMINISTRATOR, + }), + ], + }); + const externalUserId = 'externalUserId'; + const externalSchoolId = 'externalSchoolId'; + const school = legacySchoolDoFactory.build({ + id: user.school.id, + externalId: externalSchoolId, + }); + const userLoginMigration = userLoginMigrationDOFactory.buildWithId({ + schoolId: user.school.id, + }); + + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + userService.findByEmail.mockResolvedValueOnce([userDo]); + userLoginMigrationService.findMigrationBySchool.mockResolvedValueOnce(null); + userLoginMigrationService.startMigration.mockResolvedValueOnce(userLoginMigration); + schoolService.getSchoolById.mockResolvedValueOnce(school); + schoolMigrationService.hasSchoolMigrated.mockReturnValueOnce(true); + + return { + user, + externalUserId, + externalSchoolId, + }; + }; + + it('should throw an error', async () => { + const { user, externalUserId, externalSchoolId } = setup(); + + await expect(uc.forceMigration(user.id, user.email, externalUserId, externalSchoolId)).rejects.toThrow( + UserLoginMigrationSchoolAlreadyMigratedLoggableException + ); + }); + }); + }); }); diff --git a/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.ts b/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.ts index 7eec1701d4c..817c729a74d 100644 --- a/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.ts +++ b/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.ts @@ -1,18 +1,23 @@ import { AuthenticationService } from '@modules/authentication'; import { Action, AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school'; import { OAuthService, OAuthTokenDto } from '@modules/oauth'; import { OauthDataDto, ProvisioningService } from '@modules/provisioning'; +import { UserService } from '@modules/user'; import { ForbiddenException, Injectable } from '@nestjs/common'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; -import { LegacySchoolDo, Page, UserLoginMigrationDO } from '@shared/domain/domainobject'; +import { LegacySchoolDo, Page, RoleReference, UserDO, UserLoginMigrationDO } from '@shared/domain/domainobject'; import { User } from '@shared/domain/entity'; -import { Permission } from '@shared/domain/interface'; +import { Permission, RoleName } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { Logger } from '@src/core/logger'; import { ExternalSchoolNumberMissingLoggableException, InvalidUserLoginMigrationLoggableException, SchoolMigrationSuccessfulLoggable, + UserLoginMigrationInvalidAdminLoggableException, + UserLoginMigrationMultipleEmailUsersLoggableException, + UserLoginMigrationSchoolAlreadyMigratedLoggableException, UserMigrationStartedLoggable, UserMigrationSuccessfulLoggable, } from '../loggable'; @@ -29,6 +34,8 @@ export class UserLoginMigrationUc { private readonly schoolMigrationService: SchoolMigrationService, private readonly authenticationService: AuthenticationService, private readonly authorizationService: AuthorizationService, + private readonly userService: UserService, + private readonly schoolService: LegacySchoolService, private readonly logger: Logger ) {} @@ -87,7 +94,7 @@ export class UserLoginMigrationUc { const tokenDto: OAuthTokenDto = await this.oauthService.authenticateUser(targetSystemId, redirectUri, code); - this.logger.debug(new UserMigrationStartedLoggable(currentUserId, userLoginMigration)); + this.logger.info(new UserMigrationStartedLoggable(currentUserId, userLoginMigration)); const data: OauthDataDto = await this.provisioningService.getData( targetSystemId, @@ -113,14 +120,72 @@ export class UserLoginMigrationUc { targetSystemId ); - this.logger.debug(new SchoolMigrationSuccessfulLoggable(schoolToMigrate, userLoginMigration)); + this.logger.info(new SchoolMigrationSuccessfulLoggable(schoolToMigrate, userLoginMigration)); } } await this.userMigrationService.migrateUser(currentUserId, data.externalUser.externalId, targetSystemId); - this.logger.debug(new UserMigrationSuccessfulLoggable(currentUserId, userLoginMigration)); + this.logger.info(new UserMigrationSuccessfulLoggable(currentUserId, userLoginMigration)); await this.authenticationService.removeJwtFromWhitelist(userJwt); } + + async forceMigration( + userId: EntityId, + email: string, + externalUserId: string, + externalSchoolId: string + ): Promise { + const user: User = await this.authorizationService.getUserWithPermissions(userId); + this.authorizationService.checkAllPermissions(user, [Permission.USER_LOGIN_MIGRATION_FORCE]); + + const schoolAdminUsers: UserDO[] = await this.userService.findByEmail(email); + + if (schoolAdminUsers.length === 0) { + throw new NotFoundLoggableException('User', { email }); + } + if (schoolAdminUsers.length > 1) { + throw new UserLoginMigrationMultipleEmailUsersLoggableException(email); + } + + const schoolAdminUser: UserDO = schoolAdminUsers[0]; + // TODO Use new domain object to always have an id + if (!schoolAdminUser.id) { + throw new NotFoundLoggableException('User', { email }); + } + + const isAdmin = !!schoolAdminUser.roles.find((value: RoleReference) => value.name === RoleName.ADMINISTRATOR); + if (!isAdmin) { + throw new UserLoginMigrationInvalidAdminLoggableException(schoolAdminUser.id); + } + + const activeUserLoginMigration: UserLoginMigrationDO | null = + await this.userLoginMigrationService.findMigrationBySchool(schoolAdminUser.schoolId); + if (activeUserLoginMigration) { + throw new UserLoginMigrationSchoolAlreadyMigratedLoggableException(activeUserLoginMigration.schoolId); + } + + const userLoginMigration: UserLoginMigrationDO = await this.userLoginMigrationService.startMigration( + schoolAdminUser.schoolId + ); + + const school: LegacySchoolDo = await this.schoolService.getSchoolById(schoolAdminUser.schoolId); + + const hasSchoolMigrated: boolean = this.schoolMigrationService.hasSchoolMigrated( + school.externalId, + externalSchoolId + ); + if (hasSchoolMigrated) { + throw new UserLoginMigrationSchoolAlreadyMigratedLoggableException(schoolAdminUser.schoolId); + } + + await this.schoolMigrationService.migrateSchool(school, externalSchoolId, userLoginMigration.targetSystemId); + + this.logger.info(new SchoolMigrationSuccessfulLoggable(school, userLoginMigration)); + + await this.userMigrationService.migrateUser(schoolAdminUser.id, externalUserId, userLoginMigration.targetSystemId); + + this.logger.info(new UserMigrationSuccessfulLoggable(schoolAdminUser.id, userLoginMigration)); + } } diff --git a/apps/server/src/modules/user-login-migration/user-login-migration-api.module.ts b/apps/server/src/modules/user-login-migration/user-login-migration-api.module.ts index cf2151e31eb..b2e1478e37f 100644 --- a/apps/server/src/modules/user-login-migration/user-login-migration-api.module.ts +++ b/apps/server/src/modules/user-login-migration/user-login-migration-api.module.ts @@ -3,6 +3,7 @@ import { AuthorizationModule } from '@modules/authorization'; import { LegacySchoolModule } from '@modules/legacy-school'; import { OauthModule } from '@modules/oauth'; import { ProvisioningModule } from '@modules/provisioning'; +import { UserModule } from '@modules/user'; import { ImportUserModule } from '@modules/user-import'; import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; @@ -29,6 +30,7 @@ import { UserLoginMigrationModule } from './user-login-migration.module'; LoggerModule, LegacySchoolModule, ImportUserModule, + UserModule, ], providers: [ UserLoginMigrationUc, diff --git a/apps/server/src/shared/domain/interface/permission.enum.ts b/apps/server/src/shared/domain/interface/permission.enum.ts index 2159ec1b820..d85ed328e55 100644 --- a/apps/server/src/shared/domain/interface/permission.enum.ts +++ b/apps/server/src/shared/domain/interface/permission.enum.ts @@ -155,6 +155,7 @@ export enum Permission { USER_CREATE = 'USER_CREATE', USER_LOGIN_MIGRATION_ADMIN = 'USER_LOGIN_MIGRATION_ADMIN', USER_LOGIN_MIGRATION_ROLLBACK = 'USER_LOGIN_MIGRATION_ROLLBACK', + USER_LOGIN_MIGRATION_FORCE = 'USER_LOGIN_MIGRATION_FORCE', USER_MIGRATE = 'USER_MIGRATE', USER_UPDATE = 'USER_UPDATE', YEARS_EDIT = 'YEARS_EDIT', diff --git a/apps/server/src/shared/repo/importuser/importuser.repo.ts b/apps/server/src/shared/repo/importuser/importuser.repo.ts index 24609727546..e1447cb8790 100644 --- a/apps/server/src/shared/repo/importuser/importuser.repo.ts +++ b/apps/server/src/shared/repo/importuser/importuser.repo.ts @@ -64,7 +64,7 @@ export class ImportUserRepo extends BaseRepo { const [importUserEntities, count] = await this._em.findAndCount(ImportUser, query, queryOptions); const userMatches = importUserEntities.map((importUser) => importUser.user).filter((user) => user != null); // load role names of referenced users - await this._em.populate(userMatches as User[], ['roles']); + await this._em.populate(userMatches, ['roles']); return [importUserEntities, count]; } diff --git a/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts b/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts index 46add47b8e5..20f1ecba378 100644 --- a/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts @@ -7,6 +7,7 @@ import { SystemEntity } from '@modules/system/entity'; import { UserQuery } from '@modules/user/service/user-query.type'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityNotFoundError } from '@shared/common'; +import { RoleReference } from '@shared/domain/domainobject'; import { Page } from '@shared/domain/domainobject/page'; import { UserDO } from '@shared/domain/domainobject/user.do'; import { Role, SchoolEntity, User } from '@shared/domain/entity'; @@ -250,6 +251,29 @@ describe('UserRepo', () => { result = await repo.findByEmail('.*'); expect(result).toHaveLength(0); }); + + it('should populate the roles', async () => { + const email = 'USER@EXAMPLE.COM'; + const role = roleFactory.buildWithId(); + const user = userFactory.build({ email, roles: [role] }); + await em.persistAndFlush([user]); + em.clear(); + + const result = await repo.findByEmail(email); + + expect(result).toHaveLength(1); + expect(result[0]).toEqual( + expect.objectContaining({ + email, + roles: [ + new RoleReference({ + id: role.id, + name: role.name, + }), + ], + }) + ); + }); }); describe('mapEntityToDO', () => { diff --git a/apps/server/src/shared/repo/user/user-do.repo.ts b/apps/server/src/shared/repo/user/user-do.repo.ts index 4dba7fb1dd7..2f894280d0a 100644 --- a/apps/server/src/shared/repo/user/user-do.repo.ts +++ b/apps/server/src/shared/repo/user/user-do.repo.ts @@ -101,6 +101,8 @@ export class UserDORepo extends BaseDORepo { email: new RegExp(`^${email.replace(/\W/g, '\\$&')}$`, 'i'), }); + await this._em.populate(userEntitys, ['roles']); + const userDos: UserDO[] = userEntitys.map((userEntity: User): UserDO => this.mapEntityToDO(userEntity)); return userDos; diff --git a/apps/server/src/shared/testing/user-role-permissions.ts b/apps/server/src/shared/testing/user-role-permissions.ts index ef368925d93..6a3008efaf4 100644 --- a/apps/server/src/shared/testing/user-role-permissions.ts +++ b/apps/server/src/shared/testing/user-role-permissions.ts @@ -145,4 +145,8 @@ export const adminPermissions = [ Permission.USER_LOGIN_MIGRATION_ADMIN, ]; -export const superheroPermissions = [Permission.USER_LOGIN_MIGRATION_ROLLBACK, Permission.INSTANCE_VIEW]; +export const superheroPermissions = [ + Permission.USER_LOGIN_MIGRATION_FORCE, + Permission.USER_LOGIN_MIGRATION_ROLLBACK, + Permission.INSTANCE_VIEW, +]; diff --git a/backup/setup/migrations.json b/backup/setup/migrations.json index dbba0355eee..7a6b74f4f1b 100644 --- a/backup/setup/migrations.json +++ b/backup/setup/migrations.json @@ -178,5 +178,14 @@ "created_at": { "$date": "2024-06-28T07:07:10.278Z" } + }, + { + "_id": { + "$oid": "66a0c52f1935f91a45b9c261" + }, + "name": "Migration20240724090901", + "created_at": { + "$date": "2024-07-24T09:11:11.359Z" + } } ] diff --git a/backup/setup/roles.json b/backup/setup/roles.json index 0dc34c41f22..03922f4ff3b 100644 --- a/backup/setup/roles.json +++ b/backup/setup/roles.json @@ -200,6 +200,7 @@ "USER_CHANGE_OWN_NAME", "ACCOUNT_VIEW", "ACCOUNT_DELETE", + "USER_LOGIN_MIGRATION_FORCE", "USER_LOGIN_MIGRATION_ROLLBACK", "INSTANCE_VIEW" ], diff --git a/config/default.schema.json b/config/default.schema.json index 49275e94272..5ea033edcb0 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1635,11 +1635,11 @@ "default": false, "description": "Enables the storing and checking media license of users" }, - "PROVISIONING_SCHULCONNEX_LIZENZ_INFO_URL": { + "PROVISIONING_SCHULCONNEX_POLICIES_INFO_URL": { "type": "string", "default": "", - "description": "URL for fetching lizenz info from moin.schule schulconnex", - "examples": ["https://api-dienste.stage.niedersachsen-login.schule/v1/lizenz-info"] + "description": "URL for fetching policies info from moin.schule schulconnex", + "examples": ["https://api-dienste.stage.niedersachsen-login.schule/v1/policies-info"] }, "BOARD_COLLABORATION_URI": { "type": "string", diff --git a/config/development.json b/config/development.json index f0bd91d62d2..5cf48962fa7 100644 --- a/config/development.json +++ b/config/development.json @@ -85,7 +85,7 @@ "PAD_URI": "http://localhost:9001/p", "URI": "http://localhost:9001/api/1.2.14" }, - "PROVISIONING_SCHULCONNEX_LIZENZ_INFO_URL": "http://localhost:8888/v1/lizenzinfo", + "PROVISIONING_SCHULCONNEX_POLICIES_INFO_URL": "http://localhost:8888/v1/policies-info", "BOARD_COLLABORATION_URI": "ws://localhost:4450", "ADMIN_API": { "ALLOWED_API_KEYS": "thisisasupersecureapikeythatisabsolutelysave" diff --git a/package-lock.json b/package-lock.json index 53c6d70a6ed..f8d95a5ea3d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -167,7 +167,7 @@ "@types/express-session": "^1.17.5", "@types/jest": "^29.2.1", "@types/lodash": "^4.14.196", - "@types/node": "^16.18.11", + "@types/node": "^18.19.41", "@types/passport-jwt": "^3.0.5", "@types/passport-local": "^1.0.33", "@types/response-time": "^2.3.5", @@ -186,7 +186,7 @@ "chai-http": "^4.2.0", "copyfiles": "^2.4.0", "esbuild": "^0.17.10", - "esbuild-plugin-d.ts": "^1.1.0", + "esbuild-plugin-d.ts": "^1.3.0", "eslint": "^8.30.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^17.0.0", @@ -220,7 +220,7 @@ "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "tsconfig-paths": "^4.1.1", - "typescript": "^4.9.4" + "typescript": "^5.5.3" }, "engines": { "node": "18", @@ -2367,6 +2367,346 @@ "kuler": "^2.0.0" } }, + "node_modules/@esbuild/android-arm": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.17.10.tgz", + "integrity": "sha512-7YEBfZ5lSem9Tqpsz+tjbdsEshlO9j/REJrfv4DXgKTt1+/MHqGwbtlyxQuaSlMeUZLxUKBaX8wdzlTfHkmnLw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.17.10.tgz", + "integrity": "sha512-ht1P9CmvrPF5yKDtyC+z43RczVs4rrHpRqrmIuoSvSdn44Fs1n6DGlpZKdK6rM83pFLbVaSUwle8IN+TPmkv7g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.17.10.tgz", + "integrity": "sha512-CYzrm+hTiY5QICji64aJ/xKdN70IK8XZ6iiyq0tZkd3tfnwwSWTYH1t3m6zyaaBxkuj40kxgMyj1km/NqdjQZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.17.10.tgz", + "integrity": "sha512-3HaGIowI+nMZlopqyW6+jxYr01KvNaLB5znXfbyyjuo4lE0VZfvFGcguIJapQeQMS4cX/NEispwOekJt3gr5Dg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.17.10.tgz", + "integrity": "sha512-J4MJzGchuCRG5n+B4EHpAMoJmBeAE1L3wGYDIN5oWNqX0tEr7VKOzw0ymSwpoeSpdCa030lagGUfnfhS7OvzrQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.10.tgz", + "integrity": "sha512-ZkX40Z7qCbugeK4U5/gbzna/UQkM9d9LNV+Fro8r7HA7sRof5Rwxc46SsqeMvB5ZaR0b1/ITQ/8Y1NmV2F0fXQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.17.10.tgz", + "integrity": "sha512-0m0YX1IWSLG9hWh7tZa3kdAugFbZFFx9XrvfpaCMMvrswSTvUZypp0NFKriUurHpBA3xsHVE9Qb/0u2Bbi/otg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.17.10.tgz", + "integrity": "sha512-whRdrrl0X+9D6o5f0sTZtDM9s86Xt4wk1bf7ltx6iQqrIIOH+sre1yjpcCdrVXntQPCNw/G+XqsD4HuxeS+2QA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.17.10.tgz", + "integrity": "sha512-g1EZJR1/c+MmCgVwpdZdKi4QAJ8DCLP5uTgLWSAVd9wlqk9GMscaNMEViG3aE1wS+cNMzXXgdWiW/VX4J+5nTA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.17.10.tgz", + "integrity": "sha512-1vKYCjfv/bEwxngHERp7huYfJ4jJzldfxyfaF7hc3216xiDA62xbXJfRlradiMhGZbdNLj2WA1YwYFzs9IWNPw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.17.10.tgz", + "integrity": "sha512-XilKPgM2u1zR1YuvCsFQWl9Fc35BqSqktooumOY2zj7CSn5czJn279j9TE1JEqSqz88izJo7yE4x3LSf7oxHzg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.17.10.tgz", + "integrity": "sha512-kM4Rmh9l670SwjlGkIe7pYWezk8uxKHX4Lnn5jBZYBNlWpKMBCVfpAgAJqp5doLobhzF3l64VZVrmGeZ8+uKmQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.17.10.tgz", + "integrity": "sha512-r1m9ZMNJBtOvYYGQVXKy+WvWd0BPvSxMsVq8Hp4GzdMBQvfZRvRr5TtX/1RdN6Va8JMVQGpxqde3O+e8+khNJQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.17.10.tgz", + "integrity": "sha512-LsY7QvOLPw9WRJ+fU5pNB3qrSfA00u32ND5JVDrn/xG5hIQo3kvTxSlWFRP0NJ0+n6HmhPGG0Q4jtQsb6PFoyg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.17.10.tgz", + "integrity": "sha512-zJUfJLebCYzBdIz/Z9vqwFjIA7iSlLCFvVi7glMgnu2MK7XYigwsonXshy9wP9S7szF+nmwrelNaP3WGanstEg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.17.10.tgz", + "integrity": "sha512-lOMkailn4Ok9Vbp/q7uJfgicpDTbZFlXlnKT2DqC8uBijmm5oGtXAJy2ZZVo5hX7IOVXikV9LpCMj2U8cTguWA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.17.10.tgz", + "integrity": "sha512-/VE0Kx6y7eekqZ+ZLU4AjMlB80ov9tEz4H067Y0STwnGOYL8CsNg4J+cCmBznk1tMpxMoUOf0AbWlb1d2Pkbig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.17.10.tgz", + "integrity": "sha512-ERNO0838OUm8HfUjjsEs71cLjLMu/xt6bhOlxcJ0/1MG3hNqCmbWaS+w/8nFLa0DDjbwZQuGKVtCUJliLmbVgg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.17.10.tgz", + "integrity": "sha512-fXv+L+Bw2AeK+XJHwDAQ9m3NRlNemG6Z6ijLwJAAVdu4cyoFbBWbEtyZzDeL+rpG2lWI51cXeMt70HA8g2MqIg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.17.10.tgz", + "integrity": "sha512-3s+HADrOdCdGOi5lnh5DMQEzgbsFsd4w57L/eLKKjMnN0CN4AIEP0DCP3F3N14xnxh3ruNc32A0Na9zYe1Z/AQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, "node_modules/@esbuild/win32-x64": { "version": "0.17.10", "cpu": [ @@ -2591,18 +2931,6 @@ "@types/node": "*" } }, - "node_modules/@feathersjs/authentication/node_modules/typescript": { - "version": "5.3.2", - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/@feathersjs/authentication/node_modules/uuid": { "version": "9.0.1", "funding": [ @@ -2668,18 +2996,6 @@ "typescript": ">=5.3" } }, - "node_modules/@feathersjs/configuration/node_modules/typescript": { - "version": "5.3.2", - "license": "Apache-2.0", - "peer": true, - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, "node_modules/@feathersjs/errors": { "version": "5.0.12", "license": "MIT", @@ -6075,8 +6391,13 @@ } }, "node_modules/@types/node": { - "version": "16.18.11", - "license": "MIT" + "version": "18.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.41.tgz", + "integrity": "sha512-LX84pRJ+evD2e2nrgYCHObGWkiQJ1mL+meAgbvnwk/US6vmMY7S2ygBTGV2Jw91s9vUsLSXeDEkUHZIJGLrhsg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } }, "node_modules/@types/parse-json": { "version": "4.0.0", @@ -7004,11 +7325,6 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "node_modules/any-promise": { - "version": "1.3.0", - "dev": true, - "license": "MIT" - }, "node_modules/anymatch": { "version": "3.1.2", "dev": true, @@ -7962,20 +8278,6 @@ "version": "1.3.3", "license": "MIT" }, - "node_modules/bundle-require": { - "version": "3.1.2", - "dev": true, - "license": "MIT", - "dependencies": { - "load-tsconfig": "^0.2.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "peerDependencies": { - "esbuild": ">=0.13" - } - }, "node_modules/bunyan": { "version": "1.8.15", "engines": [ @@ -8012,14 +8314,6 @@ "node": ">= 0.8" } }, - "node_modules/cac": { - "version": "6.7.14", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, "node_modules/cache-manager": { "version": "5.4.0", "license": "MIT", @@ -8377,6 +8671,21 @@ "node": ">=4.2.0" } }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/clone": { "version": "1.0.4", "license": "MIT", @@ -8694,13 +9003,6 @@ "version": "1.14.1", "license": "0BSD" }, - "node_modules/concurrently/node_modules/y18n": { - "version": "5.0.8", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/concurrently/node_modules/yargs": { "version": "16.2.0", "license": "MIT", @@ -8821,14 +9123,6 @@ "wrap-ansi": "^7.0.0" } }, - "node_modules/copyfiles/node_modules/y18n": { - "version": "5.0.8", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/copyfiles/node_modules/yargs": { "version": "16.2.0", "dev": true, @@ -9384,6 +9678,23 @@ "node": ">=0.10" } }, + "node_modules/dts-bundle-generator": { + "version": "9.5.1", + "resolved": "https://registry.npmjs.org/dts-bundle-generator/-/dts-bundle-generator-9.5.1.tgz", + "integrity": "sha512-DxpJOb2FNnEyOzMkG11sxO2dmxPjthoVWxfKqWYJ/bI/rT1rvTMktF5EKjAYrRZu6Z6t3NhOUZ0sZ5ZXevOfbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "typescript": ">=5.0.2", + "yargs": "^17.6.0" + }, + "bin": { + "dts-bundle-generator": "dist/bin/dts-bundle-generator.js" + }, + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/duplexer2": { "version": "0.1.4", "license": "BSD-3-Clause", @@ -9838,14 +10149,15 @@ } }, "node_modules/esbuild-plugin-d.ts": { - "version": "1.1.0", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/esbuild-plugin-d.ts/-/esbuild-plugin-d.ts-1.3.0.tgz", + "integrity": "sha512-vd7g7dSP2fK4s/OI7PsnnRRBrui9TqQmTZa7nqhokziX52tNWBcXeO3fr59jecIwm/I8AXh/FRFb2N5BL3XppQ==", "dev": true, "license": "MIT", "dependencies": { - "chalk": "4.x", - "jju": "^1.4.0", - "tmp": "^0.2.1", - "tsup": "^5.11.1" + "chalk": "4.1.2", + "dts-bundle-generator": "^9.5.1", + "lodash.merge": "^4.6.2" }, "engines": { "node": ">=12.0.0" @@ -9910,27 +10222,18 @@ "node": ">=8" } }, - "node_modules/esbuild-plugin-d.ts/node_modules/tmp": { - "version": "0.2.1", - "dev": true, - "license": "MIT", - "dependencies": { - "rimraf": "^3.0.0" - }, - "engines": { - "node": ">=8.17.0" - } - }, - "node_modules/esbuild-windows-64": { - "version": "0.14.54", + "node_modules/esbuild/node_modules/@esbuild/linux-loong64": { + "version": "0.17.10", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.17.10.tgz", + "integrity": "sha512-mvwAr75q3Fgc/qz3K6sya3gBmJIYZCgcJ0s7XshpoqIAIBszzfXsqhpRrRdVFAyV1G9VUjj7VopL2HnAS8aHFA==", "cpu": [ - "x64" + "loong64" ], "dev": true, "license": "MIT", "optional": true, "os": [ - "win32" + "linux" ], "engines": { "node": ">=12" @@ -13356,19 +13659,6 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "node_modules/jest-cli/node_modules/cliui": { - "version": "8.0.1", - "dev": true, - "license": "ISC", - "dependencies": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.1", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/jest-cli/node_modules/color-convert": { "version": "2.0.1", "dev": true, @@ -13380,53 +13670,20 @@ "node": ">=7.0.0" } }, - "node_modules/jest-cli/node_modules/color-name": { - "version": "1.1.4", - "dev": true, - "license": "MIT" - }, - "node_modules/jest-cli/node_modules/supports-color": { - "version": "7.2.0", - "dev": true, - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/jest-cli/node_modules/y18n": { - "version": "5.0.8", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/jest-cli/node_modules/yargs": { - "version": "17.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "cliui": "^8.0.1", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.3", - "y18n": "^5.0.5", - "yargs-parser": "^21.0.0" - }, - "engines": { - "node": ">=12" - } + "node_modules/jest-cli/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "license": "MIT" }, - "node_modules/jest-cli/node_modules/yargs-parser": { - "version": "21.1.1", + "node_modules/jest-cli/node_modules/supports-color": { + "version": "7.2.0", "dev": true, - "license": "ISC", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, "engines": { - "node": ">=12" + "node": ">=8" } }, "node_modules/jest-config": { @@ -15038,14 +15295,6 @@ "url": "https://github.com/sponsors/panva" } }, - "node_modules/joycon": { - "version": "3.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/js-sdsl": { "version": "2.1.4", "license": "MIT", @@ -15604,14 +15853,6 @@ "version": "1.10.24", "license": "MIT" }, - "node_modules/lilconfig": { - "version": "2.1.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=10" - } - }, "node_modules/limiter": { "version": "1.1.5" }, @@ -15620,14 +15861,6 @@ "dev": true, "license": "MIT" }, - "node_modules/load-tsconfig": { - "version": "0.2.3", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, "node_modules/loader-runner": { "version": "4.2.0", "dev": true, @@ -15714,11 +15947,6 @@ "version": "4.1.1", "license": "MIT" }, - "node_modules/lodash.sortby": { - "version": "4.7.0", - "dev": true, - "license": "MIT" - }, "node_modules/lodash.truncate": { "version": "4.4.2", "dev": true, @@ -16137,6 +16365,16 @@ "version": "1.2.6", "license": "MIT" }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, "node_modules/mixwith": { "version": "0.1.1", "license": "Apache-2.0" @@ -16304,14 +16542,6 @@ "node": ">= 8" } }, - "node_modules/mocha/node_modules/y18n": { - "version": "5.0.8", - "dev": true, - "license": "ISC", - "engines": { - "node": ">=10" - } - }, "node_modules/mocha/node_modules/yargs": { "version": "16.2.0", "dev": true, @@ -16913,16 +17143,6 @@ "rimraf": "bin.js" } }, - "node_modules/mz": { - "version": "2.7.0", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0", - "object-assign": "^4.0.1", - "thenify-all": "^1.0.0" - } - }, "node_modules/nan": { "version": "2.15.0", "license": "MIT", @@ -18049,35 +18269,28 @@ "license": "MIT" }, "node_modules/path-scurry": { - "version": "1.10.1", + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", "dev": true, "license": "BlueOak-1.0.0", "dependencies": { - "lru-cache": "^9.1.1 || ^10.0.0", + "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=16 || 14 >=14.18" }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "10.0.1", - "dev": true, - "license": "ISC", - "engines": { - "node": "14 || >=16.14" - } - }, - "node_modules/path-scurry/node_modules/minipass": { - "version": "7.0.3", + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "license": "ISC", - "engines": { - "node": ">=16 || 14 >=14.17" - } + "license": "ISC" }, "node_modules/path-to-regexp": { "version": "3.2.0", @@ -18301,34 +18514,6 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/postcss-load-config": { - "version": "3.1.4", - "dev": true, - "license": "MIT", - "dependencies": { - "lilconfig": "^2.0.5", - "yaml": "^1.10.2" - }, - "engines": { - "node": ">= 10" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - "peerDependencies": { - "postcss": ">=8.0.9", - "ts-node": ">=9.0.0" - }, - "peerDependenciesMeta": { - "postcss": { - "optional": true - }, - "ts-node": { - "optional": true - } - } - }, "node_modules/precond": { "version": "0.2.3", "engines": { @@ -19865,20 +20050,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/rollup": { - "version": "2.79.1", - "dev": true, - "license": "MIT", - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=10.0.0" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, "node_modules/rrweb-cssom": { "version": "0.6.0", "license": "MIT" @@ -21053,53 +21224,6 @@ "url": "https://github.com/sponsors/Borewit" } }, - "node_modules/sucrase": { - "version": "3.29.0", - "dev": true, - "license": "MIT", - "dependencies": { - "commander": "^4.0.0", - "glob": "7.1.6", - "lines-and-columns": "^1.1.6", - "mz": "^2.7.0", - "pirates": "^4.0.1", - "ts-interface-checker": "^0.1.9" - }, - "bin": { - "sucrase": "bin/sucrase", - "sucrase-node": "bin/sucrase-node" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/sucrase/node_modules/commander": { - "version": "4.1.1", - "dev": true, - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/sucrase/node_modules/glob": { - "version": "7.1.6", - "dev": true, - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "node_modules/superagent": { "version": "3.8.3", "dev": true, @@ -21450,25 +21574,6 @@ "dev": true, "license": "MIT" }, - "node_modules/thenify": { - "version": "3.3.1", - "dev": true, - "license": "MIT", - "dependencies": { - "any-promise": "^1.0.0" - } - }, - "node_modules/thenify-all": { - "version": "1.6.0", - "dev": true, - "license": "MIT", - "dependencies": { - "thenify": ">= 3.1.0 < 4" - }, - "engines": { - "node": ">=0.8" - } - }, "node_modules/through": { "version": "2.3.8", "license": "MIT" @@ -21704,11 +21809,6 @@ "version": "1.3.0", "license": "MIT" }, - "node_modules/ts-interface-checker": { - "version": "0.1.13", - "dev": true, - "license": "Apache-2.0" - }, "node_modules/ts-jest": { "version": "29.0.5", "dev": true, @@ -21999,124 +22099,6 @@ "version": "2.6.2", "license": "0BSD" }, - "node_modules/tsup": { - "version": "5.12.9", - "dev": true, - "license": "MIT", - "dependencies": { - "bundle-require": "^3.0.2", - "cac": "^6.7.12", - "chokidar": "^3.5.1", - "debug": "^4.3.1", - "esbuild": "^0.14.25", - "execa": "^5.0.0", - "globby": "^11.0.3", - "joycon": "^3.0.1", - "postcss-load-config": "^3.0.1", - "resolve-from": "^5.0.0", - "rollup": "^2.74.1", - "source-map": "0.8.0-beta.0", - "sucrase": "^3.20.3", - "tree-kill": "^1.2.2" - }, - "bin": { - "tsup": "dist/cli-default.js", - "tsup-node": "dist/cli-node.js" - }, - "peerDependencies": { - "@swc/core": "^1", - "postcss": "^8.4.12", - "typescript": "^4.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "postcss": { - "optional": true - }, - "typescript": { - "optional": true - } - } - }, - "node_modules/tsup/node_modules/esbuild": { - "version": "0.14.54", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/linux-loong64": "0.14.54", - "esbuild-android-64": "0.14.54", - "esbuild-android-arm64": "0.14.54", - "esbuild-darwin-64": "0.14.54", - "esbuild-darwin-arm64": "0.14.54", - "esbuild-freebsd-64": "0.14.54", - "esbuild-freebsd-arm64": "0.14.54", - "esbuild-linux-32": "0.14.54", - "esbuild-linux-64": "0.14.54", - "esbuild-linux-arm": "0.14.54", - "esbuild-linux-arm64": "0.14.54", - "esbuild-linux-mips64le": "0.14.54", - "esbuild-linux-ppc64le": "0.14.54", - "esbuild-linux-riscv64": "0.14.54", - "esbuild-linux-s390x": "0.14.54", - "esbuild-netbsd-64": "0.14.54", - "esbuild-openbsd-64": "0.14.54", - "esbuild-sunos-64": "0.14.54", - "esbuild-windows-32": "0.14.54", - "esbuild-windows-64": "0.14.54", - "esbuild-windows-arm64": "0.14.54" - } - }, - "node_modules/tsup/node_modules/resolve-from": { - "version": "5.0.0", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/tsup/node_modules/source-map": { - "version": "0.8.0-beta.0", - "dev": true, - "license": "BSD-3-Clause", - "dependencies": { - "whatwg-url": "^7.0.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/tsup/node_modules/tr46": { - "version": "1.0.1", - "dev": true, - "license": "MIT", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/tsup/node_modules/webidl-conversions": { - "version": "4.0.2", - "dev": true, - "license": "BSD-2-Clause" - }, - "node_modules/tsup/node_modules/whatwg-url": { - "version": "7.1.0", - "dev": true, - "license": "MIT", - "dependencies": { - "lodash.sortby": "^4.7.0", - "tr46": "^1.0.1", - "webidl-conversions": "^4.0.2" - } - }, "node_modules/tsutils": { "version": "3.21.0", "dev": true, @@ -22272,15 +22254,16 @@ } }, "node_modules/typescript": { - "version": "4.9.5", - "dev": true, + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" }, "engines": { - "node": ">=4.2.0" + "node": ">=14.17" } }, "node_modules/uid": { @@ -22412,6 +22395,12 @@ "node": ">=14.0" } }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, "node_modules/unicode-properties": { "version": "1.4.1", "license": "MIT", @@ -23201,6 +23190,15 @@ "yjs": "^13.0.0" } }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "license": "ISC", + "engines": { + "node": ">=10" + } + }, "node_modules/yaassertion": { "version": "1.0.2", "license": "MIT" @@ -23217,6 +23215,25 @@ "node": ">= 6" } }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/yargs-parser": { "version": "20.2.4", "license": "ISC", @@ -23260,6 +23277,16 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yargs/node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, "node_modules/yauzl": { "version": "2.10.0", "license": "MIT", diff --git a/package.json b/package.json index 80726ac5b97..37e2770a7b4 100644 --- a/package.json +++ b/package.json @@ -283,7 +283,7 @@ "@types/express-session": "^1.17.5", "@types/jest": "^29.2.1", "@types/lodash": "^4.14.196", - "@types/node": "^16.18.11", + "@types/node": "^18.19.41", "@types/passport-jwt": "^3.0.5", "@types/passport-local": "^1.0.33", "@types/response-time": "^2.3.5", @@ -302,7 +302,7 @@ "chai-http": "^4.2.0", "copyfiles": "^2.4.0", "esbuild": "^0.17.10", - "esbuild-plugin-d.ts": "^1.1.0", + "esbuild-plugin-d.ts": "^1.3.0", "eslint": "^8.30.0", "eslint-config-airbnb-base": "^15.0.0", "eslint-config-airbnb-typescript": "^17.0.0", @@ -336,6 +336,6 @@ "ts-loader": "^9.4.2", "ts-node": "^10.9.1", "tsconfig-paths": "^4.1.1", - "typescript": "^4.9.4" + "typescript": "^5.5.3" } } diff --git a/src/utils/rabbitmq.js b/src/utils/rabbitmq.js index 350241251cf..e7c12d8019a 100644 --- a/src/utils/rabbitmq.js +++ b/src/utils/rabbitmq.js @@ -43,7 +43,7 @@ class Channel { async connect() { try { const conn = await getConnection(); - this.channel = await conn.createChannel(); + this.channel = await conn.createConfirmChannel(); // the default is 0, 0 means get absolutely everything, internet claims this is limited by rabbitmq by 2000, which basically defeats the purpose of using separate processes await this.channel.prefetch(Configuration.get('LEGACY_RABBITMQ_GLOBAL_PREFETCH_COUNT'), true); await this.channel.assertQueue(this.queue, this.queueOptions);