From 580daf4761b2ee8a2b2a7ba51fe10f243dfb703c Mon Sep 17 00:00:00 2001 From: mrikallab <93978883+mrikallab@users.noreply.github.com> Date: Thu, 30 May 2024 17:06:10 +0200 Subject: [PATCH] N21-1971 cancel migration wizard (#5026) --- .../src/modules/server/server.config.ts | 5 + .../api-test/import-user.api.spec.ts | 70 +++++++- .../controller/import-user.controller.ts | 17 +- .../src/modules/user-import/loggable/index.ts | 2 + .../user-already-migrated.loggable.spec.ts | 26 +++ .../user-already-migrated.loggable.ts | 14 ++ .../user-migration-canceled.loggable.spec.ts | 30 ++++ .../user-migration-canceled.loggable.ts | 16 ++ .../user-import/uc/user-import.uc.spec.ts | 156 +++++++++++++++++- .../modules/user-import/uc/user-import.uc.ts | 35 +++- .../modules/user-import/user-import-config.ts | 3 + config/default.schema.json | 5 + 12 files changed, 369 insertions(+), 10 deletions(-) create mode 100644 apps/server/src/modules/user-import/loggable/user-already-migrated.loggable.spec.ts create mode 100644 apps/server/src/modules/user-import/loggable/user-already-migrated.loggable.ts create mode 100644 apps/server/src/modules/user-import/loggable/user-migration-canceled.loggable.spec.ts create mode 100644 apps/server/src/modules/user-import/loggable/user-migration-canceled.loggable.ts create mode 100644 apps/server/src/modules/user-import/user-import-config.ts diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index 632b1712861..dd0659ffa56 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -25,6 +25,7 @@ import { LanguageType } from '@shared/domain/interface'; import { SchulcloudTheme } from '@shared/domain/types'; import type { CoreModuleConfig } from '@src/core'; import type { MailConfig } from '@src/infra/mail/interfaces/mail-config'; +import { UserImportConfig } from '../user-import/user-import-config'; import { Timezone } from './types/timezone.enum'; export enum NodeEnvType { @@ -61,6 +62,7 @@ export interface ServerConfig DeletionConfig, CollaborativeTextEditorConfig, ProvisioningConfig, + UserImportConfig, AlertConfig { NODE_ENV: NodeEnvType; SC_DOMAIN: string; @@ -232,6 +234,9 @@ const config: ServerConfig = { I18N__DEFAULT_LANGUAGE: Configuration.get('I18N__DEFAULT_LANGUAGE') as unknown as LanguageType, I18N__FALLBACK_LANGUAGE: Configuration.get('I18N__FALLBACK_LANGUAGE') as unknown as LanguageType, I18N__DEFAULT_TIMEZONE: Configuration.get('I18N__DEFAULT_TIMEZONE') as Timezone, + IMPORTUSER_SAVE_ALL_MATCHES_REQUEST_TIMEOUT_MS: Configuration.get( + 'IMPORTUSER_SAVE_ALL_MATCHES_REQUEST_TIMEOUT_MS' + ) as number, SCHULCONNEX_CLIENT__API_URL: Configuration.has('SCHULCONNEX_CLIENT__API_URL') ? (Configuration.get('SCHULCONNEX_CLIENT__API_URL') as string) : undefined, diff --git a/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts b/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts index 67a21383f8c..4727f5a1dbe 100644 --- a/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts +++ b/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts @@ -23,13 +23,13 @@ import { ImportUser, MatchCreator, SchoolEntity, SystemEntity, User } from '@sha import { Permission, RoleName, SortOrder } from '@shared/domain/interface'; import { SchoolFeature } from '@shared/domain/types'; import { - TestApiClient, - UserAndAccountTestFactory, cleanupCollections, importUserFactory, roleFactory, schoolEntityFactory, systemEntityFactory, + TestApiClient, + UserAndAccountTestFactory, userFactory, } from '@shared/testing'; import { AccountEntity } from '@src/modules/account/domain/entity/account.entity'; @@ -46,11 +46,12 @@ describe('ImportUser Controller (API)', () => { const authenticatedUser = async ( permissions: Permission[] = [], features: SchoolFeature[] = [], - schoolHasExternalId = true + schoolHasExternalId = true, + officialSchoolNumber = 'foo' ) => { const system = systemEntityFactory.buildWithId(); const school = schoolEntityFactory.build({ - officialSchoolNumber: 'foo', + officialSchoolNumber, features, systems: [system], externalId: schoolHasExternalId ? system.id : undefined, @@ -1118,4 +1119,65 @@ describe('ImportUser Controller (API)', () => { }); }); }); + + describe('[POST] /user/import/cancel', () => { + describe('when user is unauthorized', () => { + const setup = () => { + return { + notLoggedInClient: new TestApiClient(app, 'user/import'), + }; + }; + + it('should return unauthorized', async () => { + const { notLoggedInClient } = setup(); + + await notLoggedInClient.post('cancel').expect(HttpStatus.UNAUTHORIZED); + }); + }); + + describe('when user has no permission', () => { + const setup = async () => { + const { account, system } = await authenticatedUser([]); + setConfig(system._id.toString()); + const loggedInClient = await testApiClient.login(account); + + return { + loggedInClient, + }; + }; + + it('should return unauthorized', async () => { + const { loggedInClient } = await setup(); + + await loggedInClient.post('cancel').expect(HttpStatus.UNAUTHORIZED); + }); + }); + + describe('when import was successfully canceled', () => { + const setup = async () => { + const { school, system, account } = await authenticatedUser( + [Permission.SCHOOL_IMPORT_USERS_MIGRATE], + [], + true, + '00100' + ); + setConfig(system._id.toString()); + + const importusers = importUserFactory.buildList(10, { school }); + await em.persistAndFlush(importusers); + + const loggedInClient = await testApiClient.login(account); + + return { + loggedInClient, + }; + }; + + it('should return no content', async () => { + const { loggedInClient } = await setup(); + + await loggedInClient.post('cancel').expect(HttpStatus.NO_CONTENT); + }); + }); + }); }); diff --git a/apps/server/src/modules/user-import/controller/import-user.controller.ts b/apps/server/src/modules/user-import/controller/import-user.controller.ts index 4ba271e0e9e..8f82b461c50 100644 --- a/apps/server/src/modules/user-import/controller/import-user.controller.ts +++ b/apps/server/src/modules/user-import/controller/import-user.controller.ts @@ -1,9 +1,10 @@ import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; -import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; +import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Patch, Post, Query } from '@nestjs/common'; import { ApiBadRequestResponse, ApiCreatedResponse, ApiForbiddenResponse, + ApiNoContentResponse, ApiOperation, ApiServiceUnavailableResponse, ApiTags, @@ -103,6 +104,7 @@ export class ImportUserController { return response as unknown as UserMatchListResponse; } + @RequestTimeout('IMPORTUSER_SAVE_ALL_MATCHES_REQUEST_TIMEOUT_MS') @Post('migrate') async saveAllUsersMatches(@CurrentUser() currentUser: ICurrentUser): Promise { await this.userImportUc.saveAllUsersMatches(currentUser.userId); @@ -135,4 +137,17 @@ export class ImportUserController { async populateImportUsers(@CurrentUser() currentUser: ICurrentUser): Promise { await this.userImportFetchUc.populateImportUsers(currentUser.userId); } + + @Post('cancel') + @ApiOperation({ + summary: 'Cancel migration wizard', + description: 'Cancel current migration process', + }) + @ApiNoContentResponse() + @ApiUnauthorizedResponse() + @ApiForbiddenResponse() + @HttpCode(HttpStatus.NO_CONTENT) + async cancelMigration(@CurrentUser() currentUser: ICurrentUser): Promise { + await this.userImportUc.cancelMigration(currentUser.userId); + } } diff --git a/apps/server/src/modules/user-import/loggable/index.ts b/apps/server/src/modules/user-import/loggable/index.ts index 7daf1e12b1d..6b4929ce0b9 100644 --- a/apps/server/src/modules/user-import/loggable/index.ts +++ b/apps/server/src/modules/user-import/loggable/index.ts @@ -9,3 +9,5 @@ export { UserImportSchoolExternalIdMissingLoggableException } from './user-impor export { SchoolNotMigratedLoggableException } from './school-not-migrated.loggable-exception'; export { UserMigrationIsNotEnabled } from './user-migration-not-enable.loggable'; export { UserMigrationIsNotEnabledLoggableException } from './user-migration-not-enable-loggable-exception'; +export { UserMigrationCanceledLoggable } from './user-migration-canceled.loggable'; +export { UserAlreadyMigratedLoggable } from './user-already-migrated.loggable'; diff --git a/apps/server/src/modules/user-import/loggable/user-already-migrated.loggable.spec.ts b/apps/server/src/modules/user-import/loggable/user-already-migrated.loggable.spec.ts new file mode 100644 index 00000000000..105e9e32ffa --- /dev/null +++ b/apps/server/src/modules/user-import/loggable/user-already-migrated.loggable.spec.ts @@ -0,0 +1,26 @@ +import { UserAlreadyMigratedLoggable } from './user-already-migrated.loggable'; + +describe('UserAlreadyMigratedLoggable', () => { + describe('getLogMessage', () => { + const setup = () => { + const userId = 'userId'; + const loggable = new UserAlreadyMigratedLoggable(userId); + + return { + loggable, + userId, + }; + }; + + it('should return the correct log message', () => { + const { loggable, userId } = setup(); + + expect(loggable.getLogMessage()).toEqual({ + message: 'The user has migrated already and will be skipped during migration process.', + data: { + userId, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-import/loggable/user-already-migrated.loggable.ts b/apps/server/src/modules/user-import/loggable/user-already-migrated.loggable.ts new file mode 100644 index 00000000000..c4de7ad7acc --- /dev/null +++ b/apps/server/src/modules/user-import/loggable/user-already-migrated.loggable.ts @@ -0,0 +1,14 @@ +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; + +export class UserAlreadyMigratedLoggable implements Loggable { + constructor(private readonly userId: string) {} + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + message: 'The user has migrated already and will be skipped during migration process.', + data: { + userId: this.userId, + }, + }; + } +} diff --git a/apps/server/src/modules/user-import/loggable/user-migration-canceled.loggable.spec.ts b/apps/server/src/modules/user-import/loggable/user-migration-canceled.loggable.spec.ts new file mode 100644 index 00000000000..7a53884cd95 --- /dev/null +++ b/apps/server/src/modules/user-import/loggable/user-migration-canceled.loggable.spec.ts @@ -0,0 +1,30 @@ +import { legacySchoolDoFactory } from '@shared/testing'; +import { UserMigrationCanceledLoggable } from './user-migration-canceled.loggable'; + +describe('UserMigrationCanceledLoggable', () => { + describe('getLogMessage', () => { + const setup = () => { + const school = legacySchoolDoFactory.buildWithId({ name: 'Schoolname' }); + const loggable = new UserMigrationCanceledLoggable(school); + + return { + loggable, + school, + }; + }; + + it('should return the correct log message', () => { + const { loggable, school } = setup(); + + const message = loggable.getLogMessage(); + + expect(message).toEqual({ + message: 'The user migration was canceled.', + data: { + schoolName: school.name, + schoolId: school.id, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-import/loggable/user-migration-canceled.loggable.ts b/apps/server/src/modules/user-import/loggable/user-migration-canceled.loggable.ts new file mode 100644 index 00000000000..925f6498cbc --- /dev/null +++ b/apps/server/src/modules/user-import/loggable/user-migration-canceled.loggable.ts @@ -0,0 +1,16 @@ +import { LegacySchoolDo } from '@shared/domain/domainobject'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; + +export class UserMigrationCanceledLoggable implements Loggable { + constructor(private readonly school: LegacySchoolDo) {} + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + message: 'The user migration was canceled.', + data: { + schoolName: this.school.name, + schoolId: this.school.id, + }, + }; + } +} diff --git a/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts b/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts index a9abc508185..7f96ff0eff8 100644 --- a/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts +++ b/apps/server/src/modules/user-import/uc/user-import.uc.spec.ts @@ -20,13 +20,20 @@ import { legacySchoolDoFactory, schoolEntityFactory, setupEntities, + userDoFactory, userFactory, userLoginMigrationDOFactory, } from '@shared/testing'; import { systemEntityFactory } from '@shared/testing/factory/systemEntityFactory'; import { Logger } from '@src/core/logger'; +import { UserService } from '@modules/user'; import { IUserImportFeatures, UserImportFeatures } from '../config'; -import { SchoolNotMigratedLoggableException } from '../loggable'; +import { + SchoolNotMigratedLoggableException, + UserAlreadyMigratedLoggable, + UserMigrationCanceledLoggable, +} from '../loggable'; + import { UserImportService } from '../service'; import { LdapAlreadyPersistedException, @@ -44,10 +51,12 @@ describe('[ImportUserModule]', () => { let schoolService: DeepMocked; let systemRepo: DeepMocked; let userRepo: DeepMocked; + let userService: DeepMocked; let authorizationService: DeepMocked; let userImportService: DeepMocked; let userLoginMigrationService: DeepMocked; let userMigrationService: DeepMocked; + let logger: DeepMocked; let userImportFeatures: IUserImportFeatures; @@ -77,6 +86,10 @@ describe('[ImportUserModule]', () => { provide: UserRepo, useValue: createMock(), }, + { + provide: UserService, + useValue: createMock(), + }, { provide: AuthorizationService, useValue: createMock(), @@ -110,11 +123,13 @@ describe('[ImportUserModule]', () => { schoolService = module.get(LegacySchoolService); systemRepo = module.get(LegacySystemRepo); userRepo = module.get(UserRepo); + userService = module.get(UserService); authorizationService = module.get(AuthorizationService); userImportService = module.get(UserImportService); userImportFeatures = module.get(UserImportFeatures); userLoginMigrationService = module.get(UserLoginMigrationService); userMigrationService = module.get(UserMigrationService); + logger = module.get(Logger); }); beforeEach(() => { @@ -129,6 +144,10 @@ describe('[ImportUserModule]', () => { await module.close(); }); + afterEach(() => { + jest.clearAllMocks(); + }); + const createMockSchoolDo = (school?: SchoolEntity): LegacySchoolDo => { const name = school ? school.name : 'testSchool'; const id = school ? school.id : 'someId'; @@ -630,6 +649,7 @@ describe('[ImportUserModule]', () => { }), matchedBy: MatchCreator.AUTO, system, + externalId: 'externalId', }); const importUserWithoutUser = importUserFactory.buildWithId({ school: schoolEntity, @@ -637,6 +657,7 @@ describe('[ImportUserModule]', () => { }); userRepo.findById.mockResolvedValueOnce(user); + userService.findByExternalId.mockResolvedValueOnce(null); schoolService.getSchoolById.mockResolvedValueOnce(school); importUserRepo.findImportUsers.mockResolvedValueOnce([[importUser, importUserWithoutUser], 2]); userImportFeatures.useWithUserLoginMigration = true; @@ -672,6 +693,69 @@ describe('[ImportUserModule]', () => { ); }); }); + describe('when user is already migrated', () => { + const setup = () => { + const system = systemEntityFactory.buildWithId(); + const schoolEntity = schoolEntityFactory.buildWithId(); + const user = userFactory.buildWithId({ + school: schoolEntity, + externalId: 'externalId', + }); + const school = legacySchoolDoFactory.build({ + id: schoolEntity.id, + externalId: 'externalId', + officialSchoolNumber: 'officialSchoolNumber', + inUserMigration: true, + inMaintenanceSince: new Date(), + systems: [system.id], + }); + const importUser = importUserFactory.buildWithId({ + school: schoolEntity, + user: userFactory.buildWithId({ + school: schoolEntity, + }), + matchedBy: MatchCreator.AUTO, + system, + }); + const importUserWithoutUser = importUserFactory.buildWithId({ + school: schoolEntity, + system, + }); + + userRepo.findById.mockResolvedValueOnce(user); + schoolService.getSchoolById.mockResolvedValueOnce(school); + userService.findByExternalId.mockResolvedValueOnce( + userDoFactory.buildWithId({ id: user.id, externalId: user.externalId }) + ); + importUserRepo.findImportUsers.mockResolvedValueOnce([[importUser, importUserWithoutUser], 2]); + userImportFeatures.useWithUserLoginMigration = true; + + return { + user, + importUser, + importUserWithoutUser, + }; + }; + + it('should skip import users with externalId', async () => { + const { user, importUser } = setup(); + + await uc.saveAllUsersMatches(user.id); + + expect(userMigrationService.migrateUser).not.toHaveBeenCalledWith( + importUser.user?.id, + importUser.user?.externalId, + importUser.system.id + ); + }); + it('should log information for skipped user ', async () => { + const { user, importUser } = setup(); + + await uc.saveAllUsersMatches(user.id); + + expect(logger.notice).toHaveBeenCalledWith(new UserAlreadyMigratedLoggable(importUser.user!.id)); + }); + }); }); describe('when the user does not have an account', () => { @@ -1075,5 +1159,75 @@ describe('[ImportUserModule]', () => { }); }); }); + + describe('cancelMigration', () => { + describe('when user is given', () => { + const setup = () => { + const school = legacySchoolDoFactory.buildWithId({ + inMaintenanceSince: new Date(2024, 1, 1), + inUserMigration: true, + }); + const user = userFactory.buildWithId(); + + userRepo.findById.mockResolvedValueOnce(user); + schoolService.getSchoolById.mockResolvedValueOnce(school); + userImportFeatures.useWithUserLoginMigration = true; + + return { + user, + school, + }; + }; + + it('should check users permissions', async () => { + const { user } = setup(); + + await uc.cancelMigration(user.id); + + expect(authorizationService.checkAllPermissions).toHaveBeenCalledWith(user, [ + Permission.SCHOOL_IMPORT_USERS_MIGRATE, + ]); + }); + + it('should check if feature is enabled', async () => { + setup(); + + await uc.cancelMigration(new ObjectId().toHexString()); + + expect(userImportService.checkFeatureEnabled).toHaveBeenCalled(); + }); + + it('should delete import users for school', async () => { + const { user } = setup(); + + await uc.cancelMigration(user.id); + + expect(importUserRepo.deleteImportUsersBySchool).toHaveBeenCalledWith(user.school); + }); + + it('should save school with reset migration flags', async () => { + const { user, school } = setup(); + + await uc.cancelMigration(user.id); + + expect(schoolService.save).toHaveBeenCalledWith( + { + ...school, + inUserMigration: undefined, + inMaintenanceSince: undefined, + }, + true + ); + }); + + it('should log canceled migration', async () => { + const { user } = setup(); + + await uc.cancelMigration(user.id); + + expect(logger.notice).toHaveBeenCalledWith(new UserMigrationCanceledLoggable(expect.any(LegacySchoolDo))); + }); + }); + }); }); }); diff --git a/apps/server/src/modules/user-import/uc/user-import.uc.ts b/apps/server/src/modules/user-import/uc/user-import.uc.ts index bf7ef08ba9a..b7c60c0ab77 100644 --- a/apps/server/src/modules/user-import/uc/user-import.uc.ts +++ b/apps/server/src/modules/user-import/uc/user-import.uc.ts @@ -6,12 +6,13 @@ import { UserLoginMigrationService, UserMigrationService } from '@modules/user-l import { BadRequestException, ForbiddenException, Inject, Injectable } from '@nestjs/common'; import { UserAlreadyAssignedToImportUserError } from '@shared/common'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; -import { LegacySchoolDo, UserLoginMigrationDO } from '@shared/domain/domainobject'; +import { LegacySchoolDo, UserDO, UserLoginMigrationDO } from '@shared/domain/domainobject'; import { ImportUser, MatchCreator, SystemEntity, User } from '@shared/domain/entity'; import { IFindOptions, Permission } from '@shared/domain/interface'; import { Counted, EntityId, IImportUserScope, MatchCreatorScope, NameMatch } from '@shared/domain/types'; import { ImportUserRepo, LegacySystemRepo, UserRepo } from '@shared/repo'; import { Logger } from '@src/core/logger'; +import { UserService } from '@modules/user'; import { IUserImportFeatures, UserImportFeatures } from '../config'; import { MigrationMayBeCompleted, @@ -20,7 +21,10 @@ import { SchoolInUserMigrationEndLoggable, SchoolInUserMigrationStartLoggable, SchoolNotMigratedLoggableException, + UserAlreadyMigratedLoggable, + UserMigrationCanceledLoggable, } from '../loggable'; + import { UserImportService } from '../service'; import { LdapAlreadyPersistedException, @@ -42,6 +46,7 @@ export class UserImportUc { private readonly schoolService: LegacySchoolService, private readonly systemRepo: LegacySystemRepo, private readonly userRepo: UserRepo, + private readonly userService: UserService, private readonly logger: Logger, private readonly userImportService: UserImportService, @Inject(UserImportFeatures) private readonly userImportFeatures: IUserImportFeatures, @@ -232,7 +237,7 @@ export class UserImportUc { await this.schoolService.save(school); } - async startSchoolInUserMigration(currentUserId: EntityId, useCentralLdap = true): Promise { + public async startSchoolInUserMigration(currentUserId: EntityId, useCentralLdap = true): Promise { const { useWithUserLoginMigration } = this.userImportFeatures; if (useWithUserLoginMigration) { @@ -298,7 +303,7 @@ export class UserImportUc { } } - async endSchoolInMaintenance(currentUserId: EntityId): Promise { + public async endSchoolInMaintenance(currentUserId: EntityId): Promise { const currentUser = await this.getCurrentUser(currentUserId, Permission.SCHOOL_IMPORT_USERS_MIGRATE); const school: LegacySchoolDo = await this.schoolService.getSchoolById(currentUser.school.id); @@ -321,6 +326,22 @@ export class UserImportUc { this.logger.notice(new SchoolInUserMigrationEndLoggable(school.name)); } + public async cancelMigration(currentUserId: EntityId): Promise { + const currentUser = await this.getCurrentUser(currentUserId, Permission.SCHOOL_IMPORT_USERS_MIGRATE); + const school: LegacySchoolDo = await this.schoolService.getSchoolById(currentUser.school.id); + + this.userImportService.checkFeatureEnabled(school); + + await this.importUserRepo.deleteImportUsersBySchool(currentUser.school); + + school.inUserMigration = undefined; + school.inMaintenanceSince = undefined; + + await this.schoolService.save(school, true); + + this.logger.notice(new UserMigrationCanceledLoggable(school)); + } + private async getCurrentUser(currentUserId: EntityId, permission: UserImportPermissions): Promise { const currentUser = await this.userRepo.findById(currentUserId, true); this.authorizationService.checkAllPermissions(currentUser, [permission]); @@ -363,7 +384,13 @@ export class UserImportUc { return; } - await this.userMigrationService.migrateUser(importUser.user.id, importUser.externalId, importUser.system.id); + const user: UserDO | null = await this.userService.findByExternalId(importUser.externalId, importUser.system.id); + + if (!user) { + await this.userMigrationService.migrateUser(importUser.user.id, importUser.externalId, importUser.system.id); + } else { + this.logger.notice(new UserAlreadyMigratedLoggable(importUser.user.id)); + } } private async getAccount(user: User): Promise { diff --git a/apps/server/src/modules/user-import/user-import-config.ts b/apps/server/src/modules/user-import/user-import-config.ts new file mode 100644 index 00000000000..a7145a56d1b --- /dev/null +++ b/apps/server/src/modules/user-import/user-import-config.ts @@ -0,0 +1,3 @@ +export interface UserImportConfig { + IMPORTUSER_SAVE_ALL_MATCHES_REQUEST_TIMEOUT_MS: number; +} diff --git a/config/default.schema.json b/config/default.schema.json index c1f4938f68e..1ce96620db7 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1193,6 +1193,11 @@ "default": "", "description": "SystemId for migration of users from system. To use this please set FEATURE_USER_MIGRATION_CENTRAL_LDAP in the client" }, + "IMPORTUSER_SAVE_ALL_MATCHES_REQUEST_TIMEOUT_MS": { + "type": "integer", + "description": "Timeout in milliseconds for saving all import user matches", + "default": 60000 + }, "FEATURE_COPY_SERVICE_ENABLED": { "type": "boolean", "default": false,