diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index d82458e0673..94b3514822f 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -51,8 +51,6 @@ jobs: - name: test image exists run: | - mkdir -p ~/.docker - echo '{"experimental": "enabled"}' >> ~/.docker/config.json echo "IMAGE_EXISTS=$(docker manifest inspect ghcr.io/${{ github.repository }}:${{ needs.branch_meta.outputs.sha }} > /dev/null && echo 1 || echo 0)" >> $GITHUB_ENV - name: Set up Docker Buildx @@ -83,8 +81,6 @@ jobs: - name: test image exists (file storage) run: | - mkdir -p ~/.docker - echo '{"experimental": "enabled"}' >> ~/.docker/config.json echo "IMAGE_EXISTS=$(docker manifest inspect ghcr.io/${{ github.repository }}:file-storage-${{ needs.branch_meta.outputs.sha }} > /dev/null && echo 1 || echo 0)" >> $GITHUB_ENV - name: Set up Docker Buildx (file storage) diff --git a/ansible/roles/schulcloud-server-init/templates/configmap_file_init.yml.j2 b/ansible/roles/schulcloud-server-init/templates/configmap_file_init.yml.j2 index 8066854ad63..6b17e522753 100644 --- a/ansible/roles/schulcloud-server-init/templates/configmap_file_init.yml.j2 +++ b/ansible/roles/schulcloud-server-init/templates/configmap_file_init.yml.j2 @@ -253,6 +253,49 @@ data: # ========== End of the Dev IServ configuration section. + # ========== Start of the Test BRB Univention LDAP system (also used on the REF BRB) configuration section. + + # This is currently performed for any 'brb-*' namespace ('brb-main' for example). + + if [[ "$NS" =~ ^brb-[^\s]+$ ]]; then + UNIVENTION_LDAP_SYSTEM_ID=621beef78ec63ea12a3adae6 + UNIVENTION_LDAP_FEDERAL_STATE_ID=0000b186816abba584714c53 + + # Encrypt LDAP server's search user password. + UNIVENTION_LDAP_SEARCH_USER_PASSWORD=$(node scripts/secret.js -s $AES_KEY -e $UNIVENTION_LDAP_SEARCH_USER_PASSWORD) + + # Add (or replace) document with the test BRB Univention LDAP system configuration. + mongosh $DATABASE__URL --eval 'db.systems.replaceOne( + { + "_id": ObjectId("'$UNIVENTION_LDAP_SYSTEM_ID'"), + }, + { + "_id": ObjectId("'$UNIVENTION_LDAP_SYSTEM_ID'"), + "alias": "TEST BRB UNIVENTION LDAP", + "ldapConfig": { + "active": true, + "federalState": ObjectId("'$UNIVENTION_LDAP_FEDERAL_STATE_ID'"), + "url": "'$UNIVENTION_LDAP_URL'", + "rootPath": "'$UNIVENTION_LDAP_ROOT_PATH'", + "searchUser": "'$UNIVENTION_LDAP_SEARCH_USER'", + "searchUserPassword": "'$UNIVENTION_LDAP_SEARCH_USER_PASSWORD'", + "provider": "univention", + "providerOptions": { + "userAttributeNameMapping": {}, + "roleAttributeNameMapping": {}, + "classAttributeNameMapping": {} + } + }, + "type": "ldap" + }, + { + "upsert": true + } + );' + fi + + # ========== End of the Test BRB Univention LDAP system (also used on the REF BRB) configuration section. + # ========== Start of the Bettermarks tool configuration section. # This is currently performed only for the following 4 namespaces: diff --git a/apps/server/doc/summary.json b/apps/server/doc/summary.json index 3ef4d9cf96f..834b92cb5f4 100644 --- a/apps/server/doc/summary.json +++ b/apps/server/doc/summary.json @@ -58,6 +58,10 @@ { "title": "Code Style", "file": "code-style.md" + }, + { + "title": "S3ClientModule", + "file": "../src/shared/infra/s3-client/README.md" } ] } diff --git a/apps/server/src/apps/server.app.ts b/apps/server/src/apps/server.app.ts index c3cbeb8a921..81a35f6bfa6 100644 --- a/apps/server/src/apps/server.app.ts +++ b/apps/server/src/apps/server.app.ts @@ -8,6 +8,7 @@ import { enableOpenApiDocs } from '@shared/controller/swagger'; import { Mail, MailService } from '@shared/infra/mail'; import { LegacyLogger, Logger } from '@src/core/logger'; import { AccountService } from '@src/modules/account/services/account.service'; +import { TeamService } from '@src/modules/teams/service/team.service'; import { AccountValidationService } from '@src/modules/account/services/account.validation.service'; import { AccountUc } from '@src/modules/account/uc/account.uc'; import { CollaborativeStorageUc } from '@src/modules/collaborative-storage/uc/collaborative-storage.uc'; @@ -76,6 +77,8 @@ async function bootstrap() { feathersExpress.services['nest-account-uc'] = nestApp.get(AccountUc); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment feathersExpress.services['nest-collaborative-storage-uc'] = nestApp.get(CollaborativeStorageUc); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access + feathersExpress.services['nest-team-service'] = nestApp.get(TeamService); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-assignment feathersExpress.services['nest-orm'] = orm; diff --git a/apps/server/src/console/api-test/database-management.console.api.spec.ts b/apps/server/src/console/api-test/database-management.console.api.spec.ts index 985cd9d518a..6a2a8de7e7f 100644 --- a/apps/server/src/console/api-test/database-management.console.api.spec.ts +++ b/apps/server/src/console/api-test/database-management.console.api.spec.ts @@ -1,29 +1,34 @@ import { INestApplicationContext } from '@nestjs/common'; -import { BootstrapConsole, ConsoleService } from 'nestjs-console'; +import { ConsoleWriterService } from '@shared/infra/console'; import { ServerConsoleModule } from '@src/console/console.module'; import { CommanderError } from 'commander'; -import { execute, TestBootstrapConsole } from './test-bootstrap.console'; +import { BootstrapConsole, ConsoleService } from 'nestjs-console'; +import { TestBootstrapConsole, execute } from './test-bootstrap.console'; describe('DatabaseManagementConsole (API)', () => { let app: INestApplicationContext; - let console: BootstrapConsole; + let bootstrap: BootstrapConsole; let consoleService: ConsoleService; - beforeAll(async () => { - console = new TestBootstrapConsole({ + let consoleWriter: ConsoleWriterService; + + beforeEach(async () => { + bootstrap = new TestBootstrapConsole({ module: ServerConsoleModule, useDecorators: true, }); - app = await console.init(); + app = await bootstrap.init(); await app.init(); consoleService = app.get(ConsoleService); + consoleWriter = app.get(ConsoleWriterService); }); - afterAll(async () => { + afterEach(async () => { + consoleService.resetCli(); await app.close(); }); describe('Command "database"', () => { - beforeEach(() => { + const setup = () => { const cli = consoleService.getCli('database'); const exitFn = (err: CommanderError) => { if (err.exitCode !== 0) throw err; @@ -31,22 +36,44 @@ describe('DatabaseManagementConsole (API)', () => { cli?.exitOverride(exitFn); const rootCli = consoleService.getRootCli(); rootCli.exitOverride(exitFn); - }); + const spyConsoleWriterInfo = jest.spyOn(consoleWriter, 'info'); + return { spyConsoleWriterInfo }; + }; + describe('when command not exists', () => { + it('should fail for unknown command', async () => { + setup(); + await expect(execute(bootstrap, ['database', 'not_existing_command'])).rejects.toThrow( + `error: unknown command 'not_existing_command'` + ); - afterEach(() => { - consoleService.resetCli(); + consoleService.resetCli(); + }); }); - it('should fail for unknown command', async () => { - await expect(execute(console, ['database', 'not_existing_command'])).rejects.toThrow( - `error: unknown command 'not_existing_command'` - ); - }); - it('should provide command "seed"', async () => { - await execute(console, ['database', 'seed']); - }); - it('should provide command "export"', async () => { - await execute(console, ['database', 'export']); + describe('when command exists', () => { + it('should provide command "seed"', async () => { + const { spyConsoleWriterInfo } = setup(); + + await execute(bootstrap, ['database', 'seed']); + + expect(spyConsoleWriterInfo).toBeCalled(); + }); + + it('should provide command "export"', async () => { + const { spyConsoleWriterInfo } = setup(); + + await execute(bootstrap, ['database', 'export']); + + expect(spyConsoleWriterInfo).toBeCalled(); + }); + + it('should provide command "sync-indexes"', async () => { + const { spyConsoleWriterInfo } = setup(); + + await execute(bootstrap, ['database', 'sync-indexes']); + + expect(spyConsoleWriterInfo).toBeCalled(); + }); }); }); }); diff --git a/apps/server/src/console/api-test/test-bootstrap.console.ts b/apps/server/src/console/api-test/test-bootstrap.console.ts index 02511d7abf4..282d448b05d 100644 --- a/apps/server/src/console/api-test/test-bootstrap.console.ts +++ b/apps/server/src/console/api-test/test-bootstrap.console.ts @@ -1,11 +1,19 @@ +import { createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; +import { ConsoleWriterService } from '@shared/infra/console'; +import { DatabaseManagementUc } from '@src/modules/management/uc/database-management.uc'; import { AbstractBootstrapConsole, BootstrapConsole } from 'nestjs-console'; export class TestBootstrapConsole extends AbstractBootstrapConsole { create(): Promise { return Test.createTestingModule({ imports: [this.options.module], - }).compile(); + }) + .overrideProvider(DatabaseManagementUc) + .useValue(createMock()) + .overrideProvider(ConsoleWriterService) + .useValue(createMock()) + .compile(); } } diff --git a/apps/server/src/modules/authentication/authentication.module.ts b/apps/server/src/modules/authentication/authentication.module.ts index 1cfdca45fde..e0fb4570dcd 100644 --- a/apps/server/src/modules/authentication/authentication.module.ts +++ b/apps/server/src/modules/authentication/authentication.module.ts @@ -3,7 +3,7 @@ import { JwtModule, JwtModuleOptions } from '@nestjs/jwt'; import { PassportModule } from '@nestjs/passport'; import { CacheWrapperModule } from '@shared/infra/cache'; import { IdentityManagementModule } from '@shared/infra/identity-management'; -import { LegacySchoolRepo, SystemRepo, UserRepo } from '@shared/repo'; +import { SchoolRepo, SystemRepo, UserRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; import { AccountModule } from '@src/modules/account'; import { OauthModule } from '@src/modules/oauth/oauth.module'; @@ -70,7 +70,7 @@ const jwtModuleOptions: JwtModuleOptions = { JwtValidationAdapter, UserRepo, SystemRepo, - LegacySchoolRepo, + SchoolRepo, LocalStrategy, AuthenticationService, LdapService, diff --git a/apps/server/src/modules/authentication/strategy/jwt-validation.adapter.spec.ts b/apps/server/src/modules/authentication/strategy/jwt-validation.adapter.spec.ts index 1ed62962333..936deb866e4 100644 --- a/apps/server/src/modules/authentication/strategy/jwt-validation.adapter.spec.ts +++ b/apps/server/src/modules/authentication/strategy/jwt-validation.adapter.spec.ts @@ -1,6 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; -import { CACHE_MANAGER } from '@nestjs/common'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Test, TestingModule } from '@nestjs/testing'; import { CacheService } from '@shared/infra/cache'; import { CacheStoreType } from '@shared/infra/cache/interface/cache-store-type.enum'; diff --git a/apps/server/src/modules/authentication/strategy/jwt-validation.adapter.ts b/apps/server/src/modules/authentication/strategy/jwt-validation.adapter.ts index 2177d631abc..3af5db2061b 100644 --- a/apps/server/src/modules/authentication/strategy/jwt-validation.adapter.ts +++ b/apps/server/src/modules/authentication/strategy/jwt-validation.adapter.ts @@ -1,4 +1,5 @@ -import { CACHE_MANAGER, Inject, Injectable } from '@nestjs/common'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { Inject, Injectable } from '@nestjs/common'; import { CacheService } from '@shared/infra/cache'; import { CacheStoreType } from '@shared/infra/cache/interface/cache-store-type.enum'; import { diff --git a/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts b/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts index 4815921ca1b..44403b0adaf 100644 --- a/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts +++ b/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts @@ -2,13 +2,14 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { UnauthorizedException } from '@nestjs/common'; import { PassportModule } from '@nestjs/passport'; import { Test, TestingModule } from '@nestjs/testing'; -import { LegacySchoolDo, RoleName, System, User } from '@shared/domain'; -import { LegacySchoolRepo, SystemRepo, UserRepo } from '@shared/repo'; +import { RoleName, System, User } from '@shared/domain'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; +import { SchoolRepo, SystemRepo, UserRepo } from '@shared/repo'; import { accountDtoFactory, defaultTestPassword, defaultTestPasswordHash, - legacySchoolDoFactory, + schoolDOFactory, schoolFactory, setupEntities, systemFactory, @@ -27,7 +28,7 @@ describe('LdapStrategy', () => { let strategy: LdapStrategy; let userRepoMock: DeepMocked; - let schoolRepoMock: DeepMocked; + let schoolRepoMock: DeepMocked; let authenticationServiceMock: DeepMocked; let ldapServiceMock: DeepMocked; let systemRepo: DeepMocked; @@ -52,8 +53,8 @@ describe('LdapStrategy', () => { useValue: createMock(), }, { - provide: LegacySchoolRepo, - useValue: createMock(), + provide: SchoolRepo, + useValue: createMock(), }, { provide: SystemRepo, @@ -68,7 +69,7 @@ describe('LdapStrategy', () => { strategy = module.get(LdapStrategy); authenticationServiceMock = module.get(AuthenticationService); - schoolRepoMock = module.get(LegacySchoolRepo); + schoolRepoMock = module.get(SchoolRepo); userRepoMock = module.get(UserRepo); ldapServiceMock = module.get(LdapService); systemRepo = module.get(SystemRepo); @@ -91,7 +92,7 @@ describe('LdapStrategy', () => { const user: User = userFactory.withRoleByName(RoleName.STUDENT).buildWithId({ ldapDn: undefined }); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ systems: [system.id] }, user.school.id); + const school: SchoolDO = schoolDOFactory.buildWithId({ systems: [system.id] }, user.school.id); const account: AccountDto = accountDtoFactory.build({ systemId: system.id, @@ -138,7 +139,7 @@ describe('LdapStrategy', () => { const user: User = userFactory.withRoleByName(RoleName.STUDENT).buildWithId({ ldapDn: 'mockLdapDn' }); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ systems: [] }, user.school.id); + const school: SchoolDO = schoolDOFactory.buildWithId({ systems: [] }, user.school.id); const account: AccountDto = accountDtoFactory.build({ systemId: system.id, @@ -185,7 +186,7 @@ describe('LdapStrategy', () => { const user: User = userFactory.withRoleByName(RoleName.STUDENT).buildWithId({ ldapDn: 'mockLdapDn' }); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ systems: undefined }, user.school.id); + const school: SchoolDO = schoolDOFactory.buildWithId({ systems: undefined }, user.school.id); const account: AccountDto = accountDtoFactory.build({ systemId: system.id, @@ -232,7 +233,7 @@ describe('LdapStrategy', () => { const user: User = userFactory.withRoleByName(RoleName.STUDENT).buildWithId({ ldapDn: 'mockLdapDn' }); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ systems: [system.id] }, user.school.id); + const school: SchoolDO = schoolDOFactory.buildWithId({ systems: [system.id] }, user.school.id); const account: AccountDto = accountDtoFactory.build({ systemId: system.id, @@ -279,7 +280,7 @@ describe('LdapStrategy', () => { const user: User = userFactory.withRoleByName(RoleName.STUDENT).buildWithId({ ldapDn: 'mockLdapDn' }); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ systems: [system.id] }, user.school.id); + const school: SchoolDO = schoolDOFactory.buildWithId({ systems: [system.id] }, user.school.id); const account: AccountDto = accountDtoFactory.build({ systemId: system.id, @@ -331,7 +332,7 @@ describe('LdapStrategy', () => { const user: User = userFactory.withRoleByName(RoleName.STUDENT).buildWithId({ ldapDn: 'mockLdapDn' }); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ systems: [system.id] }, user.school.id); + const school: SchoolDO = schoolDOFactory.buildWithId({ systems: [system.id] }, user.school.id); const account: AccountDto = accountDtoFactory.build({ systemId: system.id, @@ -388,7 +389,7 @@ describe('LdapStrategy', () => { .withRoleByName(RoleName.STUDENT) .buildWithId({ ldapDn: 'mockLdapDn', school: schoolFactory.buildWithId() }); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId( + const school: SchoolDO = schoolDOFactory.buildWithId( { systems: [system.id], previousExternalId: undefined }, user.school.id ); @@ -450,7 +451,7 @@ describe('LdapStrategy', () => { .withRoleByName(RoleName.STUDENT) .buildWithId({ ldapDn: 'mockLdapDn', school: schoolFactory.buildWithId() }); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId( + const school: SchoolDO = schoolDOFactory.buildWithId( { systems: [system.id], previousExternalId: 'previousExternalId' }, user.school.id ); diff --git a/apps/server/src/modules/authentication/strategy/ldap.strategy.ts b/apps/server/src/modules/authentication/strategy/ldap.strategy.ts index df823592378..3e5327c62aa 100644 --- a/apps/server/src/modules/authentication/strategy/ldap.strategy.ts +++ b/apps/server/src/modules/authentication/strategy/ldap.strategy.ts @@ -1,7 +1,8 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; -import { LegacySchoolDo, System, User } from '@shared/domain'; -import { LegacySchoolRepo, SystemRepo, UserRepo } from '@shared/repo'; +import { System, User } from '@shared/domain'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; +import { SchoolRepo, SystemRepo, UserRepo } from '@shared/repo'; import { ErrorLoggable } from '@src/core/error/loggable/error.loggable'; import { Logger } from '@src/core/logger'; import { AccountDto } from '@src/modules/account/services/dto'; @@ -16,7 +17,7 @@ import { LdapService } from '../services/ldap.service'; export class LdapStrategy extends PassportStrategy(Strategy, 'ldap') { constructor( private readonly systemRepo: SystemRepo, - private readonly schoolRepo: LegacySchoolRepo, + private readonly schoolRepo: SchoolRepo, private readonly ldapService: LdapService, private readonly authenticationService: AuthenticationService, private readonly userRepo: UserRepo, @@ -30,7 +31,7 @@ export class LdapStrategy extends PassportStrategy(Strategy, 'ldap') { const system: System = await this.systemRepo.findById(systemId); - const school: LegacySchoolDo = await this.schoolRepo.findById(schoolId); + const school: SchoolDO = await this.schoolRepo.findById(schoolId); if (!school.systems || !school.systems.includes(systemId)) { throw new UnauthorizedException(`School ${schoolId} does not have the selected system ${systemId}`); @@ -83,7 +84,7 @@ export class LdapStrategy extends PassportStrategy(Strategy, 'ldap') { } } - private async loadAccount(username: string, systemId: string, school: LegacySchoolDo): Promise { + private async loadAccount(username: string, systemId: string, school: SchoolDO): Promise { const externalSchoolId = this.checkValue(school.externalId); let account: AccountDto; diff --git a/apps/server/src/modules/authorization/authorization.module.ts b/apps/server/src/modules/authorization/authorization.module.ts index a77bb82444c..fbb33c0c4ea 100644 --- a/apps/server/src/modules/authorization/authorization.module.ts +++ b/apps/server/src/modules/authorization/authorization.module.ts @@ -6,7 +6,7 @@ import { CourseRepo, LessonRepo, SchoolExternalToolRepo, - LegacySchoolRepo, + SchoolRepo, SubmissionRepo, TaskRepo, TeamsRepo, @@ -35,7 +35,7 @@ import { RuleManager } from './rule-manager'; CourseRepo, CourseGroupRepo, TaskRepo, - LegacySchoolRepo, + SchoolRepo, LessonRepo, TeamsRepo, SubmissionRepo, diff --git a/apps/server/src/modules/authorization/reference.loader.spec.ts b/apps/server/src/modules/authorization/reference.loader.spec.ts index 9bba78b1880..76501434a4e 100644 --- a/apps/server/src/modules/authorization/reference.loader.spec.ts +++ b/apps/server/src/modules/authorization/reference.loader.spec.ts @@ -8,7 +8,7 @@ import { CourseRepo, LessonRepo, SchoolExternalToolRepo, - LegacySchoolRepo, + SchoolRepo, SubmissionRepo, TaskRepo, TeamsRepo, @@ -26,7 +26,7 @@ describe('reference.loader', () => { let courseRepo: DeepMocked; let courseGroupRepo: DeepMocked; let taskRepo: DeepMocked; - let schoolRepo: DeepMocked; + let schoolRepo: DeepMocked; let lessonRepo: DeepMocked; let teamsRepo: DeepMocked; let submissionRepo: DeepMocked; @@ -58,8 +58,8 @@ describe('reference.loader', () => { useValue: createMock(), }, { - provide: LegacySchoolRepo, - useValue: createMock(), + provide: SchoolRepo, + useValue: createMock(), }, { provide: LessonRepo, @@ -93,7 +93,7 @@ describe('reference.loader', () => { courseRepo = await module.get(CourseRepo); courseGroupRepo = await module.get(CourseGroupRepo); taskRepo = await module.get(TaskRepo); - schoolRepo = await module.get(LegacySchoolRepo); + schoolRepo = await module.get(SchoolRepo); lessonRepo = await module.get(LessonRepo); teamsRepo = await module.get(TeamsRepo); submissionRepo = await module.get(SubmissionRepo); diff --git a/apps/server/src/modules/authorization/reference.loader.ts b/apps/server/src/modules/authorization/reference.loader.ts index 9afe013fd24..550f1f02762 100644 --- a/apps/server/src/modules/authorization/reference.loader.ts +++ b/apps/server/src/modules/authorization/reference.loader.ts @@ -6,7 +6,7 @@ import { CourseRepo, LessonRepo, SchoolExternalToolRepo, - LegacySchoolRepo, + SchoolRepo, SubmissionRepo, TaskRepo, TeamsRepo, @@ -21,7 +21,7 @@ type RepoType = | TaskRepo | CourseRepo | UserRepo - | LegacySchoolRepo + | SchoolRepo | LessonRepo | TeamsRepo | CourseGroupRepo @@ -44,7 +44,7 @@ export class ReferenceLoader { private readonly courseRepo: CourseRepo, private readonly courseGroupRepo: CourseGroupRepo, private readonly taskRepo: TaskRepo, - private readonly schoolRepo: LegacySchoolRepo, + private readonly schoolRepo: SchoolRepo, private readonly lessonRepo: LessonRepo, private readonly teamsRepo: TeamsRepo, private readonly submissionRepo: SubmissionRepo, diff --git a/apps/server/src/modules/authorization/rule-manager.spec.ts b/apps/server/src/modules/authorization/rule-manager.spec.ts index 0a2b90c7639..f7bd1edde5c 100644 --- a/apps/server/src/modules/authorization/rule-manager.spec.ts +++ b/apps/server/src/modules/authorization/rule-manager.spec.ts @@ -8,7 +8,7 @@ import { CourseRule, LessonRule, SchoolExternalToolRule, - LegacySchoolRule, + SchoolRule, SubmissionRule, TaskRule, TeamRule, @@ -24,7 +24,7 @@ describe('RuleManager', () => { let courseRule: DeepMocked; let courseGroupRule: DeepMocked; let lessonRule: DeepMocked; - let legacySchoolRule: DeepMocked; + let schoolRule: DeepMocked; let userRule: DeepMocked; let taskRule: DeepMocked; let teamRule: DeepMocked; @@ -43,7 +43,7 @@ describe('RuleManager', () => { { provide: CourseRule, useValue: createMock() }, { provide: CourseGroupRule, useValue: createMock() }, { provide: LessonRule, useValue: createMock() }, - { provide: LegacySchoolRule, useValue: createMock() }, + { provide: SchoolRule, useValue: createMock() }, { provide: UserRule, useValue: createMock() }, { provide: TaskRule, useValue: createMock() }, { provide: TeamRule, useValue: createMock() }, @@ -59,7 +59,7 @@ describe('RuleManager', () => { courseRule = await module.get(CourseRule); courseGroupRule = await module.get(CourseGroupRule); lessonRule = await module.get(LessonRule); - legacySchoolRule = await module.get(LegacySchoolRule); + schoolRule = await module.get(SchoolRule); userRule = await module.get(UserRule); taskRule = await module.get(TaskRule); teamRule = await module.get(TeamRule); @@ -89,7 +89,7 @@ describe('RuleManager', () => { courseRule.isApplicable.mockReturnValueOnce(true); courseGroupRule.isApplicable.mockReturnValueOnce(false); lessonRule.isApplicable.mockReturnValueOnce(false); - legacySchoolRule.isApplicable.mockReturnValueOnce(false); + schoolRule.isApplicable.mockReturnValueOnce(false); userRule.isApplicable.mockReturnValueOnce(false); taskRule.isApplicable.mockReturnValueOnce(false); teamRule.isApplicable.mockReturnValueOnce(false); @@ -110,7 +110,7 @@ describe('RuleManager', () => { expect(courseRule.isApplicable).toBeCalled(); expect(courseGroupRule.isApplicable).toBeCalled(); expect(lessonRule.isApplicable).toBeCalled(); - expect(legacySchoolRule.isApplicable).toBeCalled(); + expect(schoolRule.isApplicable).toBeCalled(); expect(userRule.isApplicable).toBeCalled(); expect(taskRule.isApplicable).toBeCalled(); expect(teamRule.isApplicable).toBeCalled(); @@ -139,7 +139,7 @@ describe('RuleManager', () => { courseRule.isApplicable.mockReturnValueOnce(false); courseGroupRule.isApplicable.mockReturnValueOnce(false); lessonRule.isApplicable.mockReturnValueOnce(false); - legacySchoolRule.isApplicable.mockReturnValueOnce(false); + schoolRule.isApplicable.mockReturnValueOnce(false); userRule.isApplicable.mockReturnValueOnce(false); taskRule.isApplicable.mockReturnValueOnce(false); teamRule.isApplicable.mockReturnValueOnce(false); @@ -168,7 +168,7 @@ describe('RuleManager', () => { courseRule.isApplicable.mockReturnValueOnce(true); courseGroupRule.isApplicable.mockReturnValueOnce(true); lessonRule.isApplicable.mockReturnValueOnce(false); - legacySchoolRule.isApplicable.mockReturnValueOnce(false); + schoolRule.isApplicable.mockReturnValueOnce(false); userRule.isApplicable.mockReturnValueOnce(false); taskRule.isApplicable.mockReturnValueOnce(false); teamRule.isApplicable.mockReturnValueOnce(false); diff --git a/apps/server/src/modules/authorization/rule-manager.ts b/apps/server/src/modules/authorization/rule-manager.ts index 3aece68402a..8d0fa8a629d 100644 --- a/apps/server/src/modules/authorization/rule-manager.ts +++ b/apps/server/src/modules/authorization/rule-manager.ts @@ -7,7 +7,7 @@ import { CourseRule, LessonRule, SchoolExternalToolRule, - LegacySchoolRule, + SchoolRule, SubmissionRule, TaskRule, TeamRule, @@ -25,7 +25,7 @@ export class RuleManager { private readonly courseRule: CourseRule, private readonly courseGroupRule: CourseGroupRule, private readonly lessonRule: LessonRule, - private readonly legaySchoolRule: LegacySchoolRule, + private readonly schoolRule: SchoolRule, private readonly taskRule: TaskRule, private readonly userRule: UserRule, private readonly teamRule: TeamRule, @@ -42,7 +42,7 @@ export class RuleManager { this.taskRule, this.teamRule, this.userRule, - this.legaySchoolRule, + this.schoolRule, this.submissionRule, this.schoolExternalToolRule, this.boardDoRule, diff --git a/apps/server/src/modules/authorization/types/authorization-loader-service.ts b/apps/server/src/modules/authorization/types/authorization-loader-service.ts index 7a6616591b7..b15f0059cac 100644 --- a/apps/server/src/modules/authorization/types/authorization-loader-service.ts +++ b/apps/server/src/modules/authorization/types/authorization-loader-service.ts @@ -4,3 +4,8 @@ import { AuthorizableObject } from '@shared/domain/domain-object'; // fix import export interface AuthorizationLoaderService { findById(id: EntityId): Promise; } + +export interface AuthorizationLoaderServiceGeneric + extends AuthorizationLoaderService { + findById(id: EntityId): Promise; +} diff --git a/apps/server/src/modules/board/controller/api-test/submission-item-create.api.spec.ts b/apps/server/src/modules/board/controller/api-test/submission-item-create.api.spec.ts index 49cd3af657c..3e41ee48be0 100644 --- a/apps/server/src/modules/board/controller/api-test/submission-item-create.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/submission-item-create.api.spec.ts @@ -114,7 +114,9 @@ describe('submission create (api)', () => { expect(result.id).toBeDefined(); expect(result.timestamps.createdAt).toBeDefined(); expect(result.timestamps.lastUpdatedAt).toBeDefined(); - expect(result.userId).toBe(studentUser.id); + expect(result.userData.userId).toBe(studentUser.id); + expect(result.userData.firstName).toBe('John'); + expect(result.userData.lastName).toBe('Mr Doe'); }); it('should actually create the submission item', async () => { diff --git a/apps/server/src/modules/board/controller/dto/index.ts b/apps/server/src/modules/board/controller/dto/index.ts index fe7b031a7c4..0fcd862a516 100644 --- a/apps/server/src/modules/board/controller/dto/index.ts +++ b/apps/server/src/modules/board/controller/dto/index.ts @@ -4,3 +4,4 @@ export * from './card.url.params'; export * from './element'; export * from './submission-item'; export * from './timestamps.response'; +export * from './user-data.response'; diff --git a/apps/server/src/modules/board/controller/dto/submission-item/submission-item.response.ts b/apps/server/src/modules/board/controller/dto/submission-item/submission-item.response.ts index c375184e327..02c3936d843 100644 --- a/apps/server/src/modules/board/controller/dto/submission-item/submission-item.response.ts +++ b/apps/server/src/modules/board/controller/dto/submission-item/submission-item.response.ts @@ -1,12 +1,13 @@ import { ApiProperty } from '@nestjs/swagger'; import { TimestampsResponse } from '../timestamps.response'; +import { UserDataResponse } from '../user-data.response'; export class SubmissionItemResponse { - constructor({ id, timestamps, completed, userId }: SubmissionItemResponse) { + constructor({ id, timestamps, completed, userData }: SubmissionItemResponse) { this.id = id; this.timestamps = timestamps; this.completed = completed; - this.userId = userId; + this.userData = userData; } @ApiProperty({ pattern: '[a-f0-9]{24}' }) @@ -19,5 +20,5 @@ export class SubmissionItemResponse { completed: boolean; @ApiProperty() - userId: string; + userData: UserDataResponse; } diff --git a/apps/server/src/modules/board/controller/dto/user-data.response.ts b/apps/server/src/modules/board/controller/dto/user-data.response.ts new file mode 100644 index 00000000000..78b71d0de8a --- /dev/null +++ b/apps/server/src/modules/board/controller/dto/user-data.response.ts @@ -0,0 +1,18 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class UserDataResponse { + constructor({ userId, firstName, lastName }: UserDataResponse) { + this.userId = userId; + this.firstName = firstName; + this.lastName = lastName; + } + + @ApiProperty() + firstName!: string; + + @ApiProperty() + lastName!: string; + + @ApiProperty() + userId!: string; +} diff --git a/apps/server/src/modules/board/controller/mapper/submission-item-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/submission-item-response.mapper.ts index 6364ed1a347..c2c613da8c6 100644 --- a/apps/server/src/modules/board/controller/mapper/submission-item-response.mapper.ts +++ b/apps/server/src/modules/board/controller/mapper/submission-item-response.mapper.ts @@ -1,5 +1,5 @@ import { SubmissionItem } from '@shared/domain'; -import { SubmissionItemResponse, TimestampsResponse } from '../dto'; +import { SubmissionItemResponse, TimestampsResponse, UserDataResponse } from '../dto'; export class SubmissionItemResponseMapper { private static instance: SubmissionItemResponseMapper; @@ -14,13 +14,18 @@ export class SubmissionItemResponseMapper { public mapToResponse(submissionItem: SubmissionItem): SubmissionItemResponse { const result = new SubmissionItemResponse({ + completed: submissionItem.completed, id: submissionItem.id, timestamps: new TimestampsResponse({ lastUpdatedAt: submissionItem.updatedAt, createdAt: submissionItem.createdAt, }), - completed: submissionItem.completed, - userId: submissionItem.userId, + userData: new UserDataResponse({ + // TODO: put valid user info here which comes from the submission owner + firstName: 'John', + lastName: 'Mr Doe', + userId: submissionItem.userId, + }), }); return result; diff --git a/apps/server/src/modules/class/class.module.ts b/apps/server/src/modules/class/class.module.ts new file mode 100644 index 00000000000..550b50bd454 --- /dev/null +++ b/apps/server/src/modules/class/class.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { ClassService } from './service'; +import { ClassesRepo } from './repo'; + +@Module({ + providers: [ClassService, ClassesRepo], + exports: [ClassService], +}) +export class ClassModule {} diff --git a/apps/server/src/modules/class/domain/class-source-options.do.ts b/apps/server/src/modules/class/domain/class-source-options.do.ts new file mode 100644 index 00000000000..8340c92b338 --- /dev/null +++ b/apps/server/src/modules/class/domain/class-source-options.do.ts @@ -0,0 +1,15 @@ +export interface ClassSourceOptionsProps { + tspUid?: string; +} + +export class ClassSourceOptions { + protected props: ClassSourceOptionsProps; + + constructor(props: ClassSourceOptionsProps) { + this.props = props; + } + + get tspUid(): string | undefined { + return this.props.tspUid; + } +} diff --git a/apps/server/src/modules/class/domain/class.do.ts b/apps/server/src/modules/class/domain/class.do.ts new file mode 100644 index 00000000000..fd6449a9d46 --- /dev/null +++ b/apps/server/src/modules/class/domain/class.do.ts @@ -0,0 +1,77 @@ +import { EntityId } from '@shared/domain/types'; +import { AuthorizableObject, DomainObject } from '../../../shared/domain/domain-object'; +import { ClassSourceOptions } from './class-source-options.do'; + +export interface ClassProps extends AuthorizableObject { + name: string; + schoolId: EntityId; + userIds?: EntityId[]; + teacherIds: EntityId[]; + invitationLink?: string; + year?: EntityId; + gradeLevel?: number; + ldapDN?: string; + successor?: EntityId; + source?: string; + sourceOptions?: ClassSourceOptions; + createdAt: Date; + updatedAt: Date; +} + +export class Class extends DomainObject { + get name(): string { + return this.props.name; + } + + get schoolId(): EntityId { + return this.props.schoolId; + } + + get userIds(): EntityId[] | undefined { + return this.props.userIds; + } + + get teacherIds(): EntityId[] { + return this.props.teacherIds; + } + + get invitationLink(): string | undefined { + return this.props.invitationLink; + } + + get year(): EntityId | undefined { + return this.props.year; + } + + get gradeLevel(): number | undefined { + return this.props.gradeLevel; + } + + get ldapDN(): string | undefined { + return this.props.ldapDN; + } + + get successor(): EntityId | undefined { + return this.props.successor; + } + + get source(): string | undefined { + return this.props.source; + } + + get sourceOptions(): ClassSourceOptions | undefined { + return this.props.sourceOptions; + } + + get createdAt(): Date { + return this.props.createdAt; + } + + get updatedAt(): Date { + return this.props.updatedAt; + } + + public removeUser(userId: string) { + this.props.userIds = this.props.userIds?.filter((userId1) => userId1 !== userId); + } +} diff --git a/apps/server/src/modules/class/domain/index.ts b/apps/server/src/modules/class/domain/index.ts new file mode 100644 index 00000000000..06c22071d2c --- /dev/null +++ b/apps/server/src/modules/class/domain/index.ts @@ -0,0 +1 @@ +export * from './class.do'; diff --git a/apps/server/src/modules/class/domain/testing/class-source-options.do.spec.ts b/apps/server/src/modules/class/domain/testing/class-source-options.do.spec.ts new file mode 100644 index 00000000000..6ca0b4a11aa --- /dev/null +++ b/apps/server/src/modules/class/domain/testing/class-source-options.do.spec.ts @@ -0,0 +1,43 @@ +import { ClassSourceOptions } from '../class-source-options.do'; + +describe(ClassSourceOptions.name, () => { + describe('constructor', () => { + describe('When a contructor is called', () => { + const setup = () => { + const domainObject = new ClassSourceOptions({ tspUid: '12345' }); + + return { domainObject }; + }; + + it('should create empty object', () => { + const domainObject = new ClassSourceOptions({}); + + expect(domainObject).toEqual(expect.objectContaining({})); + }); + + it('should contain valid tspUid ', () => { + const { domainObject } = setup(); + + const classSourceOptionsDo: ClassSourceOptions = new ClassSourceOptions(domainObject); + + expect(classSourceOptionsDo.tspUid).toEqual(domainObject.tspUid); + }); + }); + }); + describe('getters', () => { + describe('When getters are used', () => { + it('getters should return proper value', () => { + const props = { + tspUid: '12345', + }; + + const classSourceOptionsDo = new ClassSourceOptions(props); + const gettersValues = { + tspUid: classSourceOptionsDo.tspUid, + }; + + expect(gettersValues).toEqual(props); + }); + }); + }); +}); diff --git a/apps/server/src/modules/class/domain/testing/class.do.spec.ts b/apps/server/src/modules/class/domain/testing/class.do.spec.ts new file mode 100644 index 00000000000..510af786665 --- /dev/null +++ b/apps/server/src/modules/class/domain/testing/class.do.spec.ts @@ -0,0 +1,89 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { Class } from '../class.do'; +import { classFactory } from './factory/class.factory'; +import { ClassSourceOptions } from '../class-source-options.do'; + +describe(Class.name, () => { + describe('constructor', () => { + describe('When constructor is called', () => { + it('should create a class by passing required properties', () => { + const domainObject: Class = classFactory.build(); + + expect(domainObject instanceof Class).toEqual(true); + }); + }); + + describe('when passed a valid id', () => { + const setup = () => { + const domainObject: Class = classFactory.build({ id: new ObjectId().toHexString() }); + + return { domainObject }; + }; + + it('should set the id', () => { + const { domainObject } = setup(); + + const classDomainObject: Class = new Class(domainObject); + + expect(classDomainObject.id).toEqual(domainObject.id); + }); + }); + }); + describe('getters', () => { + describe('When getters are used', () => { + it('getters should return proper values', () => { + const props = { + id: new ObjectId().toHexString(), + name: `name1`, + schoolId: new ObjectId().toHexString(), + userIds: [new ObjectId().toHexString(), new ObjectId().toHexString()], + teacherIds: [new ObjectId().toHexString(), new ObjectId().toHexString()], + invitationLink: `link-1`, + year: new ObjectId().toHexString(), + gradeLevel: 1, + ldapDN: `dn-$1`, + successor: new ObjectId().toHexString(), + source: `source-1`, + sourceOptions: new ClassSourceOptions({ tspUid: `id-1` }), + createdAt: new Date(), + updatedAt: new Date(), + }; + + const classDo = new Class(props); + const gettersValues = { + id: classDo.id, + name: classDo.name, + schoolId: classDo.schoolId, + userIds: classDo.userIds, + teacherIds: classDo.teacherIds, + invitationLink: classDo.invitationLink, + year: classDo.year, + gradeLevel: classDo.gradeLevel, + ldapDN: classDo.ldapDN, + successor: classDo.successor, + source: classDo.source, + sourceOptions: classDo.sourceOptions, + createdAt: classDo.createdAt, + updatedAt: classDo.updatedAt, + }; + + expect(gettersValues).toEqual(props); + }); + }); + }); + describe('removeUsers', () => { + describe('When function is called', () => { + it('domainObject userIds table should be updated and userId should be removed', () => { + const userToDeleteId = new ObjectId().toHexString(); + const user2 = new ObjectId().toHexString(); + const user3 = new ObjectId().toHexString(); + + const domainObject = classFactory.withUserIds([userToDeleteId, user2, user3]).build(); + + domainObject.removeUser(userToDeleteId); + + expect(domainObject.userIds).toEqual([user2, user3]); + }); + }); + }); +}); diff --git a/apps/server/src/modules/class/domain/testing/factory/class.factory.ts b/apps/server/src/modules/class/domain/testing/factory/class.factory.ts new file mode 100644 index 00000000000..63ae07d6809 --- /dev/null +++ b/apps/server/src/modules/class/domain/testing/factory/class.factory.ts @@ -0,0 +1,34 @@ +import { DoBaseFactory } from '@shared/testing'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { DeepPartial } from 'fishery'; +import { Class, ClassProps } from '../../class.do'; +import { ClassSourceOptions } from '../../class-source-options.do'; + +class ClassFactory extends DoBaseFactory { + withUserIds(userIds: string[]): this { + const params: DeepPartial = { + userIds, + }; + + return this.params(params); + } +} + +export const classFactory = ClassFactory.define(Class, ({ sequence }) => { + return { + id: new ObjectId().toHexString(), + name: `name-${sequence}`, + schoolId: new ObjectId().toHexString(), + userIds: [new ObjectId().toHexString(), new ObjectId().toHexString()], + teacherIds: [new ObjectId().toHexString(), new ObjectId().toHexString()], + invitationLink: `link-${sequence}`, + year: new ObjectId().toHexString(), + gradeLevel: sequence, + ldapDN: `dn-${sequence}`, + successor: new ObjectId().toHexString(), + source: `source-${sequence}`, + sourceOptions: new ClassSourceOptions({ tspUid: `id-${sequence}` }), + createdAt: new Date(), + updatedAt: new Date(), + }; +}); diff --git a/apps/server/src/modules/class/entity/class-source-options.entity.ts b/apps/server/src/modules/class/entity/class-source-options.entity.ts new file mode 100644 index 00000000000..4d85ade8b7b --- /dev/null +++ b/apps/server/src/modules/class/entity/class-source-options.entity.ts @@ -0,0 +1,17 @@ +import { Embeddable, Property } from '@mikro-orm/core'; + +export interface ClassSourceOptionsEntityProps { + tspUid?: string; +} + +@Embeddable() +export class ClassSourceOptionsEntity { + @Property({ nullable: true }) + tspUid?: string; + + constructor(props: ClassSourceOptionsEntityProps) { + if (props.tspUid !== undefined) { + this.tspUid = props.tspUid; + } + } +} diff --git a/apps/server/src/modules/class/entity/class.entity.ts b/apps/server/src/modules/class/entity/class.entity.ts new file mode 100644 index 00000000000..3f08ba49582 --- /dev/null +++ b/apps/server/src/modules/class/entity/class.entity.ts @@ -0,0 +1,111 @@ +import { Embedded, Entity, Index, Property } from '@mikro-orm/core'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; +import { EntityId } from '@shared/domain'; +import { ClassSourceOptionsEntity } from './class-source-options.entity'; + +export interface IClassEntityProps { + id?: EntityId; + name: string; + schoolId: ObjectId; + userIds?: ObjectId[]; + teacherIds: ObjectId[]; + invitationLink?: string; + year?: ObjectId; + gradeLevel?: number; + ldapDN?: string; + successor?: ObjectId; + source?: string; + sourceOptions?: ClassSourceOptionsEntity; +} + +@Entity({ tableName: 'classes' }) +@Index({ properties: ['year', 'ldapDN'] }) +export class ClassEntity extends BaseEntityWithTimestamps { + @Property() + name: string; + + @Property() + @Index() + schoolId: ObjectId; + + @Property({ nullable: true }) + @Index() + userIds?: ObjectId[]; + + @Property() + @Index() + teacherIds: ObjectId[]; + + @Property({ nullable: true }) + invitationLink?: string; + + @Property({ nullable: true }) + year?: ObjectId; + + @Property({ nullable: true }) + gradeLevel?: number; + + @Property({ nullable: true }) + ldapDN?: string; + + @Property({ nullable: true }) + successor?: ObjectId; + + @Property({ nullable: true }) + @Index() + source?: string; + + @Embedded(() => ClassSourceOptionsEntity, { object: true, nullable: true }) + sourceOptions?: ClassSourceOptionsEntity; + + private validate(props: IClassEntityProps) { + if (props.gradeLevel !== undefined && (props.gradeLevel < 1 || props.gradeLevel > 13)) { + throw new Error('gradeLevel must be value beetween 1 and 13'); + } + } + + constructor(props: IClassEntityProps) { + super(); + this.validate(props); + + if (props.id !== undefined) { + this.id = props.id; + } + + this.name = props.name; + this.schoolId = props.schoolId; + + if (props.userIds !== undefined) { + this.userIds = props.userIds; + } + + this.teacherIds = props.teacherIds; + + if (props.invitationLink !== undefined) { + this.invitationLink = props.invitationLink; + } + + if (props.year !== undefined) { + this.year = props.year; + } + if (props.gradeLevel !== undefined) { + this.gradeLevel = props.gradeLevel; + } + if (props.ldapDN !== undefined) { + this.ldapDN = props.ldapDN; + } + + if (props.successor !== undefined) { + this.successor = props.successor; + } + + if (props.source !== undefined) { + this.source = props.source; + } + + if (props.sourceOptions !== undefined) { + this.sourceOptions = props.sourceOptions; + } + } +} diff --git a/apps/server/src/modules/class/entity/index.ts b/apps/server/src/modules/class/entity/index.ts new file mode 100644 index 00000000000..faf5ab7557b --- /dev/null +++ b/apps/server/src/modules/class/entity/index.ts @@ -0,0 +1,2 @@ +export * from './class.entity'; +export * from './class-source-options.entity'; diff --git a/apps/server/src/modules/class/entity/testing/class-source-options.entity.spec.ts b/apps/server/src/modules/class/entity/testing/class-source-options.entity.spec.ts new file mode 100644 index 00000000000..13cc083b162 --- /dev/null +++ b/apps/server/src/modules/class/entity/testing/class-source-options.entity.spec.ts @@ -0,0 +1,27 @@ +import { ClassSourceOptionsEntity } from '../class-source-options.entity'; + +describe(ClassSourceOptionsEntity.name, () => { + describe('constructor', () => { + describe('When a contructor is called', () => { + const setup = () => { + const entity = new ClassSourceOptionsEntity({ tspUid: '12345' }); + + return { entity }; + }; + + it('should create empty object', () => { + const entity = new ClassSourceOptionsEntity({}); + + expect(entity).toEqual(expect.objectContaining({})); + }); + + it('should contain valid tspUid ', () => { + const { entity } = setup(); + + const classSourceOptionsEntity: ClassSourceOptionsEntity = new ClassSourceOptionsEntity(entity); + + expect(classSourceOptionsEntity.tspUid).toEqual(entity.tspUid); + }); + }); + }); +}); diff --git a/apps/server/src/modules/class/entity/testing/class.entity.spec.ts b/apps/server/src/modules/class/entity/testing/class.entity.spec.ts new file mode 100644 index 00000000000..eb16f608238 --- /dev/null +++ b/apps/server/src/modules/class/entity/testing/class.entity.spec.ts @@ -0,0 +1,75 @@ +/* eslint-disable no-new */ +import { setupEntities } from '@shared/testing'; +import { classEntityFactory } from '@src/modules/class/entity/testing/factory/class.entity.factory'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { ClassEntity } from '../class.entity'; + +describe(ClassEntity.name, () => { + beforeAll(async () => { + await setupEntities(); + }); + + describe('constructor', () => { + describe('When wrong gradeLevel value is passed', () => { + it('should throw an error by wrong gradeLevel value', () => { + expect(() => { + new ClassEntity({ + name: 'classTest', + schoolId: new ObjectId(), + teacherIds: [new ObjectId()], + gradeLevel: 0, + }); + }).toThrow(); + + expect(() => { + new ClassEntity({ + name: 'classTest', + schoolId: new ObjectId(), + teacherIds: [new ObjectId()], + gradeLevel: 14, + }); + }).toThrow(); + }); + }); + + describe('When constructor is called', () => { + it('should create a class by passing required properties', () => { + const entity: ClassEntity = classEntityFactory.build(); + + expect(entity instanceof ClassEntity).toEqual(true); + }); + }); + + describe('when passed undefined id', () => { + const setup = () => { + const entity: ClassEntity = classEntityFactory.build(); + + return { entity }; + }; + + it('should not set the id', () => { + const { entity } = setup(); + + const classEntity = new ClassEntity(entity); + + expect(classEntity.id).toBeNull(); + }); + }); + + describe('when passed a valid id', () => { + const setup = () => { + const entity: ClassEntity = classEntityFactory.build({ id: new ObjectId().toHexString() }); + + return { entity }; + }; + + it('should set the id', () => { + const { entity } = setup(); + + const classEntity: ClassEntity = new ClassEntity(entity); + + expect(classEntity.id).toEqual(entity.id); + }); + }); + }); +}); diff --git a/apps/server/src/modules/class/entity/testing/factory/class.entity.factory.ts b/apps/server/src/modules/class/entity/testing/factory/class.entity.factory.ts new file mode 100644 index 00000000000..68e7514d3bc --- /dev/null +++ b/apps/server/src/modules/class/entity/testing/factory/class.entity.factory.ts @@ -0,0 +1,30 @@ +import { DeepPartial } from 'fishery'; +import { BaseFactory } from '@shared/testing/factory/base.factory'; +import { ClassEntity, ClassSourceOptionsEntity, IClassEntityProps } from '@src/modules/class/entity'; +import { ObjectId } from 'bson'; + +class ClassEntityFactory extends BaseFactory { + withUserIds(userIds: ObjectId[]): this { + const params: DeepPartial = { + userIds, + }; + + return this.params(params); + } +} + +export const classEntityFactory = ClassEntityFactory.define(ClassEntity, ({ sequence }) => { + return { + name: `name-${sequence}`, + schoolId: new ObjectId(), + userIds: new Array(), + teacherIds: [new ObjectId(), new ObjectId()], + invitationLink: `link-${sequence}`, + year: new ObjectId(), + gradeLevel: sequence, + ldapDN: `dn-${sequence}`, + successor: new ObjectId(), + source: `source-${sequence}`, + sourceOptions: new ClassSourceOptionsEntity({ tspUid: `id-${sequence}` }), + }; +}); diff --git a/apps/server/src/modules/class/index.ts b/apps/server/src/modules/class/index.ts new file mode 100644 index 00000000000..cf226f1b02e --- /dev/null +++ b/apps/server/src/modules/class/index.ts @@ -0,0 +1,2 @@ +export * from './class.module'; +export * from './service'; diff --git a/apps/server/src/modules/class/repo/classes.repo.spec.ts b/apps/server/src/modules/class/repo/classes.repo.spec.ts new file mode 100644 index 00000000000..c7c519c9435 --- /dev/null +++ b/apps/server/src/modules/class/repo/classes.repo.spec.ts @@ -0,0 +1,114 @@ +import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { TestingModule } from '@nestjs/testing/testing-module'; +import { Test } from '@nestjs/testing'; +import { cleanupCollections } from '@shared/testing'; +import { classEntityFactory } from '@src/modules/class/entity/testing/factory/class.entity.factory'; +import { ClassesRepo } from './classes.repo'; +import { ClassEntity } from '../entity'; +import { ClassMapper } from './mapper'; +import { Class } from '../domain'; + +describe(ClassesRepo.name, () => { + let module: TestingModule; + let repo: ClassesRepo; + let em: EntityManager; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [MongoMemoryDatabaseModule.forRoot()], + providers: [ClassesRepo, ClassMapper], + }).compile(); + + repo = module.get(ClassesRepo); + em = module.get(EntityManager); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(async () => { + await cleanupCollections(em); + }); + + describe('findAllByUserId', () => { + describe('when user is not found in classes', () => { + it('should return empty array', async () => { + const result = await repo.findAllByUserId(new ObjectId().toHexString()); + + expect(result).toEqual([]); + }); + }); + describe('when user is in classes', () => { + const setup = async () => { + const testUser = new ObjectId(); + const class1: ClassEntity = classEntityFactory.withUserIds([testUser, new ObjectId()]).buildWithId(); + const class2: ClassEntity = classEntityFactory.withUserIds([testUser, new ObjectId()]).buildWithId(); + const class3: ClassEntity = classEntityFactory.withUserIds([new ObjectId(), new ObjectId()]).buildWithId(); + await em.persistAndFlush([class1, class2, class3]); + + return { + class1, + class2, + class3, + }; + }; + + it('should find classes with particular userId', async () => { + const { class1, class2 } = await setup(); + + const a = class1.userIds?.at(0) as ObjectId; + const result = await repo.findAllByUserId(a.toHexString()); + + expect(result.length).toEqual(2); + + expect(result[0].id).toEqual(class1.id); + expect(result[1].id).toEqual(class2.id); + }); + }); + }); + + describe('updateMany', () => { + describe('When deleting user data from classes', () => { + const setup = async () => { + const testUser1 = new ObjectId(); + const testUser2 = new ObjectId(); + const testUser3 = new ObjectId(); + const class1: ClassEntity = classEntityFactory.withUserIds([testUser1, testUser2]).buildWithId(); + const class2: ClassEntity = classEntityFactory.withUserIds([testUser1, testUser3]).buildWithId(); + const class3: ClassEntity = classEntityFactory.withUserIds([testUser2, testUser3]).buildWithId(); + await em.persistAndFlush([class1, class2, class3]); + + return { + class1, + class2, + testUser1, + testUser2, + testUser3, + }; + }; + + it('should update classes without deleted user', async () => { + const { class1, class2, testUser1, testUser2, testUser3 } = await setup(); + + class1.userIds = [testUser2]; + class2.userIds = [testUser3]; + + const updatedArray: ClassEntity[] = [class1, class2]; + const domainObjectsArray: Class[] = ClassMapper.mapToDOs(updatedArray); + + await repo.updateMany(domainObjectsArray); + + const result1 = await repo.findAllByUserId(testUser1.toHexString()); + expect(result1).toHaveLength(0); + + const result2 = await repo.findAllByUserId(testUser2.toHexString()); + expect(result2).toHaveLength(2); + + const result3 = await repo.findAllByUserId(testUser3.toHexString()); + expect(result3).toHaveLength(2); + }); + }); + }); +}); diff --git a/apps/server/src/modules/class/repo/classes.repo.ts b/apps/server/src/modules/class/repo/classes.repo.ts new file mode 100644 index 00000000000..1e4b3ba750e --- /dev/null +++ b/apps/server/src/modules/class/repo/classes.repo.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@nestjs/common'; + +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { EntityId } from '@shared/domain'; +import { ClassEntity } from '../entity'; +import { Class } from '../domain'; +import { ClassMapper } from './mapper'; + +@Injectable() +export class ClassesRepo { + constructor(private readonly em: EntityManager, private readonly mapper: ClassMapper) {} + + async findAllByUserId(userId: EntityId): Promise { + const classes: ClassEntity[] = await this.em.find(ClassEntity, { userIds: new ObjectId(userId) }); + return ClassMapper.mapToDOs(classes); + } + + async updateMany(classes: Class[]): Promise { + const classesEntities = ClassMapper.mapToEntities(classes); + const referencedEntities = classesEntities.map((classEntity) => this.em.getReference(ClassEntity, classEntity.id)); + + await this.em.persistAndFlush(referencedEntities); + } +} diff --git a/apps/server/src/modules/class/repo/index.ts b/apps/server/src/modules/class/repo/index.ts new file mode 100644 index 00000000000..d93472feca7 --- /dev/null +++ b/apps/server/src/modules/class/repo/index.ts @@ -0,0 +1 @@ +export * from './classes.repo'; diff --git a/apps/server/src/modules/class/repo/mapper/class.mapper.spec.ts b/apps/server/src/modules/class/repo/mapper/class.mapper.spec.ts new file mode 100644 index 00000000000..ce7a357e38e --- /dev/null +++ b/apps/server/src/modules/class/repo/mapper/class.mapper.spec.ts @@ -0,0 +1,87 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { ClassEntity } from '../../entity'; +import { ClassMapper } from './class.mapper'; +import { Class } from '../../domain'; +import { ClassSourceOptions } from '../../domain/class-source-options.do'; +import { classFactory } from '../../domain/testing/factory/class.factory'; +import { classEntityFactory } from '../../entity/testing/factory/class.entity.factory'; + +describe(ClassMapper.name, () => { + describe('mapToDOs', () => { + describe('When empty entities array is mapped for an empty domainObjects array', () => { + it('should return empty domain objects array for an empty entities array', () => { + const domainObjects = ClassMapper.mapToDOs([]); + + expect(domainObjects).toEqual([]); + }); + }); + + describe('When entities array is mapped for domainObjects array', () => { + it('should properly map the entities to the domain objects', () => { + const entities = [classEntityFactory.build()]; + + const domainObjects = ClassMapper.mapToDOs(entities); + + const expectedDomainObjects = entities.map( + (entity) => + new Class({ + id: entity.id, + name: entity.name, + schoolId: entity.schoolId.toHexString(), + invitationLink: entity.invitationLink, + ldapDN: entity.ldapDN, + source: entity.source, + sourceOptions: new ClassSourceOptions({ + tspUid: entity.sourceOptions?.tspUid, + }), + userIds: entity.userIds?.map((userId) => userId.toHexString()), + successor: entity.successor?.toHexString(), + teacherIds: entity.teacherIds.map((teacherId) => teacherId.toHexString()), + gradeLevel: entity.gradeLevel, + year: entity.year?.toHexString(), + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + }) + ); + + expect(domainObjects).toEqual(expectedDomainObjects); + }); + }); + }); + + describe('mapToEntities', () => { + describe('When empty domainObjects array is mapped for an entities array', () => { + it('should return empty entities array for an empty domain objects array', () => { + const entities = ClassMapper.mapToEntities([]); + + expect(entities).toEqual([]); + }); + }); + describe('When domainObjects array is mapped for entities array', () => { + it('should properly map the domainObjects to the entities', () => { + const domainObjects = [classFactory.build()]; + + const entities = ClassMapper.mapToEntities(domainObjects); + + const expectedEntities = domainObjects.map( + (domainObject) => + new ClassEntity({ + id: domainObject.id, + name: domainObject.name, + schoolId: new ObjectId(domainObject.schoolId), + teacherIds: domainObject.teacherIds.map((teacherId) => new ObjectId(teacherId)), + invitationLink: domainObject.invitationLink, + ldapDN: domainObject.ldapDN, + source: domainObject.source, + gradeLevel: domainObject.gradeLevel, + sourceOptions: domainObject.sourceOptions, + successor: new ObjectId(domainObject.successor), + userIds: domainObject.userIds?.map((userId) => new ObjectId(userId)), + year: new ObjectId(domainObject.year), + }) + ); + expect(entities).toEqual(expectedEntities); + }); + }); + }); +}); diff --git a/apps/server/src/modules/class/repo/mapper/class.mapper.ts b/apps/server/src/modules/class/repo/mapper/class.mapper.ts new file mode 100644 index 00000000000..6340ffce7b0 --- /dev/null +++ b/apps/server/src/modules/class/repo/mapper/class.mapper.ts @@ -0,0 +1,50 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { Class } from '../../domain'; +import { ClassSourceOptions } from '../../domain/class-source-options.do'; +import { ClassEntity } from '../../entity'; + +export class ClassMapper { + private static mapToDO(entity: ClassEntity): Class { + return new Class({ + id: entity.id, + name: entity.name, + schoolId: entity.schoolId.toHexString(), + userIds: entity.userIds?.map((userId) => userId.toHexString()), + teacherIds: entity.teacherIds.map((teacherId) => teacherId.toHexString()), + invitationLink: entity.invitationLink, + year: entity.year?.toHexString(), + gradeLevel: entity.gradeLevel, + ldapDN: entity.ldapDN, + successor: entity.successor?.toHexString(), + source: entity.source, + sourceOptions: new ClassSourceOptions({ tspUid: entity.sourceOptions?.tspUid }), + createdAt: entity.createdAt, + updatedAt: entity.updatedAt, + }); + } + + private static mapToEntity(domainObject: Class): ClassEntity { + return new ClassEntity({ + id: domainObject.id, + name: domainObject.name, + schoolId: new ObjectId(domainObject.schoolId), + teacherIds: domainObject.teacherIds.map((teacherId) => new ObjectId(teacherId)), + userIds: domainObject.userIds?.map((userId) => new ObjectId(userId)), + invitationLink: domainObject.invitationLink, + year: domainObject.year !== undefined ? new ObjectId(domainObject.year) : undefined, + gradeLevel: domainObject.gradeLevel, + ldapDN: domainObject.ldapDN, + successor: domainObject.successor !== undefined ? new ObjectId(domainObject.successor) : undefined, + source: domainObject.source, + sourceOptions: domainObject.sourceOptions, + }); + } + + static mapToDOs(entities: ClassEntity[]): Class[] { + return entities.map((entity) => this.mapToDO(entity)); + } + + static mapToEntities(domainObjects: Class[]): ClassEntity[] { + return domainObjects.map((domainObject) => this.mapToEntity(domainObject)); + } +} diff --git a/apps/server/src/modules/class/repo/mapper/index.ts b/apps/server/src/modules/class/repo/mapper/index.ts new file mode 100644 index 00000000000..5b0298fae64 --- /dev/null +++ b/apps/server/src/modules/class/repo/mapper/index.ts @@ -0,0 +1 @@ +export * from './class.mapper'; diff --git a/apps/server/src/modules/class/service/class.service.spec.ts b/apps/server/src/modules/class/service/class.service.spec.ts new file mode 100644 index 00000000000..d851a38f62e --- /dev/null +++ b/apps/server/src/modules/class/service/class.service.spec.ts @@ -0,0 +1,129 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { EntityId } from '@shared/domain'; +import { InternalServerErrorException } from '@nestjs/common'; +import { classEntityFactory } from '@src/modules/class/entity/testing/factory/class.entity.factory'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { setupEntities } from '@shared/testing'; +import { ClassService } from './class.service'; +import { ClassesRepo } from '../repo'; +import { ClassMapper } from '../repo/mapper'; + +describe(ClassService.name, () => { + let module: TestingModule; + let service: ClassService; + let classesRepo: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + ClassService, + { + provide: ClassesRepo, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(ClassService); + classesRepo = module.get(ClassesRepo); + + await setupEntities(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('findUserDataFromClasses', () => { + describe('when finding by userId', () => { + const setup = () => { + const userId1 = new ObjectId(); + const userId2 = new ObjectId(); + const userId3 = new ObjectId(); + const class1 = classEntityFactory.withUserIds([userId1, userId2]).build(); + const class2 = classEntityFactory.withUserIds([userId1, userId3]).build(); + classEntityFactory.withUserIds([userId2, userId3]).build(); + + const mappedClasses = ClassMapper.mapToDOs([class1, class2]); + + classesRepo.findAllByUserId.mockResolvedValue(mappedClasses); + + return { + userId1, + }; + }; + + it('should call classesRepo.findAllByUserId', async () => { + const { userId1 } = setup(); + await service.deleteUserDataFromClasses(userId1.toHexString()); + + expect(classesRepo.findAllByUserId).toBeCalledWith(userId1.toHexString()); + }); + + it('should return array of two teams with user', async () => { + const { userId1 } = setup(); + + const result = await service.findUserDataFromClasses(userId1.toHexString()); + + expect(result.length).toEqual(2); + }); + }); + }); + + describe('deleteUserDataFromClasses', () => { + describe('when user is missing', () => { + const setup = () => { + const userId = undefined as unknown as EntityId; + + return { + userId, + }; + }; + + it('should throw and error', async () => { + const { userId } = setup(); + + await expect(service.deleteUserDataFromClasses(userId)).rejects.toThrowError(InternalServerErrorException); + }); + }); + + describe('when deleting by userId', () => { + const setup = () => { + const userId1 = new ObjectId(); + const userId2 = new ObjectId(); + const userId3 = new ObjectId(); + const class1 = classEntityFactory.withUserIds([userId1, userId2]).build(); + const class2 = classEntityFactory.withUserIds([userId1, userId3]).build(); + classEntityFactory.withUserIds([userId2, userId3]).build(); + + const mappedClasses = ClassMapper.mapToDOs([class1, class2]); + + classesRepo.findAllByUserId.mockResolvedValue(mappedClasses); + + return { + userId1, + }; + }; + + it('should call classesRepo.findAllByUserId', async () => { + const { userId1 } = setup(); + await service.deleteUserDataFromClasses(userId1.toHexString()); + + expect(classesRepo.findAllByUserId).toBeCalledWith(userId1.toHexString()); + }); + + it('should update classes without updated user', async () => { + const { userId1 } = setup(); + + const result = await service.deleteUserDataFromClasses(userId1.toHexString()); + + expect(result).toEqual(2); + }); + }); + }); +}); diff --git a/apps/server/src/modules/class/service/class.service.ts b/apps/server/src/modules/class/service/class.service.ts new file mode 100644 index 00000000000..7a42606769a --- /dev/null +++ b/apps/server/src/modules/class/service/class.service.ts @@ -0,0 +1,34 @@ +import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { ClassesRepo } from '../repo'; +import { Class } from '../domain'; + +@Injectable() +export class ClassService { + constructor(private readonly classesRepo: ClassesRepo) {} + + public async findUserDataFromClasses(userId: EntityId): Promise { + const classes = await this.classesRepo.findAllByUserId(userId); + + return classes; + } + + public async deleteUserDataFromClasses(userId: EntityId): Promise { + if (!userId) { + throw new InternalServerErrorException('User id is missing'); + } + + const domainObjects = await this.classesRepo.findAllByUserId(userId); + + const updatedClasses: Class[] = domainObjects.map((domainObject) => { + if (domainObject.userIds !== undefined) { + domainObject.removeUser(userId); + } + return domainObject; + }); + + await this.classesRepo.updateMany(updatedClasses); + + return updatedClasses.length; + } +} diff --git a/apps/server/src/modules/class/service/index.ts b/apps/server/src/modules/class/service/index.ts new file mode 100644 index 00000000000..dc17837c2c9 --- /dev/null +++ b/apps/server/src/modules/class/service/index.ts @@ -0,0 +1 @@ +export * from './class.service'; diff --git a/apps/server/src/modules/files-storage-client/mapper/error.mapper.spec.ts b/apps/server/src/modules/files-storage-client/mapper/error.mapper.spec.ts index 8f403890b0f..c3cf506e541 100644 --- a/apps/server/src/modules/files-storage-client/mapper/error.mapper.spec.ts +++ b/apps/server/src/modules/files-storage-client/mapper/error.mapper.spec.ts @@ -4,8 +4,8 @@ import { ForbiddenException, InternalServerErrorException, } from '@nestjs/common'; -import _ from 'lodash'; import { IError } from '@shared/infra/rabbitmq'; +import _ from 'lodash'; import { ErrorMapper } from './error.mapper'; describe('ErrorMapper', () => { @@ -45,6 +45,7 @@ describe('ErrorMapper', () => { const result = ErrorMapper.mapRpcErrorResponseToDomainError(json); expect(result).toStrictEqual(new InternalServerErrorException('Internal Server Error Exception')); + // @ts-expect-error cause is always unknown expect(result.cause?.message).toContain(errorText); }); }); diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-copy-files.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-copy-files.api.spec.ts index 0c52a119770..ff8f00e0a11 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-copy-files.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-copy-files.api.spec.ts @@ -17,7 +17,7 @@ import { } from '@shared/testing'; import { ICurrentUser } from '@src/modules/authentication'; import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { FilesStorageTestModule } from '@src/modules/files-storage'; +import { FILES_STORAGE_S3_CONNECTION, FilesStorageTestModule } from '@src/modules/files-storage'; import { CopyFileParams, CopyFilesOfParentParams, @@ -90,7 +90,7 @@ describe(`${baseRouteName} (api)`, () => { }) .overrideProvider(AntivirusService) .useValue(createMock()) - .overrideProvider(S3ClientAdapter) + .overrideProvider(FILES_STORAGE_S3_CONNECTION) .useValue(createMock()) .overrideGuard(JwtAuthGuard) .useValue({ diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-delete-files.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-delete-files.api.spec.ts index b90820bb19d..f210acb4e83 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-delete-files.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-delete-files.api.spec.ts @@ -16,7 +16,7 @@ import { } from '@shared/testing'; import { ICurrentUser } from '@src/modules/authentication'; import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { FilesStorageTestModule } from '@src/modules/files-storage'; +import { FILES_STORAGE_S3_CONNECTION, FilesStorageTestModule } from '@src/modules/files-storage'; import { FileRecordListResponse, FileRecordResponse } from '@src/modules/files-storage/controller/dto'; import { Request } from 'express'; import request from 'supertest'; @@ -84,7 +84,7 @@ describe(`${baseRouteName} (api)`, () => { }) .overrideProvider(AntivirusService) .useValue(createMock()) - .overrideProvider(S3ClientAdapter) + .overrideProvider(FILES_STORAGE_S3_CONNECTION) .useValue(createMock()) .overrideGuard(JwtAuthGuard) .useValue({ diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-download-upload.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-download-upload.api.spec.ts index ae5bbdc0cab..15edf45666a 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-download-upload.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-download-upload.api.spec.ts @@ -9,7 +9,7 @@ import { S3ClientAdapter } from '@shared/infra/s3-client'; import { cleanupCollections, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; import { ICurrentUser } from '@src/modules/authentication'; import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { FilesStorageTestModule } from '@src/modules/files-storage'; +import { FILES_STORAGE_S3_CONNECTION, FilesStorageTestModule } from '@src/modules/files-storage'; import { FileRecordResponse } from '@src/modules/files-storage/controller/dto'; import { Request } from 'express'; import request from 'supertest'; @@ -97,7 +97,7 @@ describe('files-storage controller (API)', () => { }) .overrideProvider(AntivirusService) .useValue(createMock()) - .overrideProvider(S3ClientAdapter) + .overrideProvider(FILES_STORAGE_S3_CONNECTION) .useValue(createMock()) .overrideGuard(JwtAuthGuard) .useValue({ @@ -114,7 +114,7 @@ describe('files-storage controller (API)', () => { await a.listen(appPort); em = module.get(EntityManager); - s3ClientAdapter = module.get(S3ClientAdapter); + s3ClientAdapter = module.get(FILES_STORAGE_S3_CONNECTION); api = new API(app); }); @@ -292,7 +292,7 @@ describe('files-storage controller (API)', () => { name: 'test (1).txt', parentId: validId, creatorId: currentUser.userId, - mimeType: 'image/webp', + mimeType: 'application/octet-stream', parentType: 'schools', securityCheckStatus: 'pending', }) diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-preview.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-preview.api.spec.ts index 70cdd1c6cba..65e0d8a58f5 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-preview.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-preview.api.spec.ts @@ -9,7 +9,7 @@ import { S3ClientAdapter } from '@shared/infra/s3-client'; import { cleanupCollections, mapUserToCurrentUser, roleFactory, schoolFactory, userFactory } from '@shared/testing'; import { ICurrentUser } from '@src/modules/authentication'; import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { FilesStorageTestModule } from '@src/modules/files-storage'; +import { FILES_STORAGE_S3_CONNECTION, FilesStorageTestModule } from '@src/modules/files-storage'; import { FileRecordResponse } from '@src/modules/files-storage/controller/dto'; import { Request } from 'express'; import request from 'supertest'; @@ -94,7 +94,7 @@ describe('File Controller (API) - preview', () => { }) .overrideProvider(AntivirusService) .useValue(createMock()) - .overrideProvider(S3ClientAdapter) + .overrideProvider(FILES_STORAGE_S3_CONNECTION) .useValue(createMock()) .overrideGuard(JwtAuthGuard) .useValue({ @@ -111,7 +111,7 @@ describe('File Controller (API) - preview', () => { await a.listen(appPort); em = module.get(EntityManager); - s3ClientAdapter = module.get(S3ClientAdapter); + s3ClientAdapter = module.get(FILES_STORAGE_S3_CONNECTION); api = new API(app); }); diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-restore-files.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-restore-files.api.spec.ts index 470c51e60c1..4997f647b80 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-restore-files.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-restore-files.api.spec.ts @@ -16,7 +16,7 @@ import { } from '@shared/testing'; import { ICurrentUser } from '@src/modules/authentication'; import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { FilesStorageTestModule } from '@src/modules/files-storage'; +import { FILES_STORAGE_S3_CONNECTION, FilesStorageTestModule } from '@src/modules/files-storage'; import { FileRecordListResponse, FileRecordResponse } from '@src/modules/files-storage/controller/dto'; import { Request } from 'express'; import request from 'supertest'; @@ -109,7 +109,7 @@ describe(`${baseRouteName} (api)`, () => { }) .overrideProvider(AntivirusService) .useValue(createMock()) - .overrideProvider(S3ClientAdapter) + .overrideProvider(FILES_STORAGE_S3_CONNECTION) .useValue(createMock()) .overrideGuard(JwtAuthGuard) .useValue({ diff --git a/apps/server/src/modules/files-storage/files-storage.config.ts b/apps/server/src/modules/files-storage/files-storage.config.ts index 0c9ab34265b..f3532b157e8 100644 --- a/apps/server/src/modules/files-storage/files-storage.config.ts +++ b/apps/server/src/modules/files-storage/files-storage.config.ts @@ -2,6 +2,7 @@ import { Configuration } from '@hpi-schul-cloud/commons'; import { S3Config } from '@shared/infra/s3-client'; import { ICoreModuleConfig } from '@src/core'; +export const FILES_STORAGE_S3_CONNECTION = 'FILES_STORAGE_S3_CONNECTION'; export interface IFileStorageConfig extends ICoreModuleConfig { MAX_FILE_SIZE: number; MAX_SECURITY_CHECK_FILE_SIZE: number; @@ -19,6 +20,7 @@ const fileStorageConfig: IFileStorageConfig = { // config/development.json for development // config/test.json for tests export const s3Config: S3Config = { + connectionName: FILES_STORAGE_S3_CONNECTION, endpoint: Configuration.get('FILES_STORAGE__S3_ENDPOINT') as string, region: Configuration.get('FILES_STORAGE__S3_REGION') as string, bucket: Configuration.get('FILES_STORAGE__S3_BUCKET') as string, diff --git a/apps/server/src/modules/files-storage/files-storage.module.ts b/apps/server/src/modules/files-storage/files-storage.module.ts index 3332c63d1fc..b33abd62b91 100644 --- a/apps/server/src/modules/files-storage/files-storage.module.ts +++ b/apps/server/src/modules/files-storage/files-storage.module.ts @@ -24,7 +24,7 @@ const imports = [ exchange: Configuration.get('ANTIVIRUS_EXCHANGE') as string, routingKey: Configuration.get('ANTIVIRUS_ROUTING_KEY') as string, }), - S3ClientModule.register(s3Config), + S3ClientModule.register([s3Config]), ]; const providers = [FilesStorageService, PreviewService, FileRecordRepo]; diff --git a/apps/server/src/modules/files-storage/mapper/file-dto.builder.ts b/apps/server/src/modules/files-storage/mapper/file-dto.builder.ts index fbd9efa5da9..1e402cce85c 100644 --- a/apps/server/src/modules/files-storage/mapper/file-dto.builder.ts +++ b/apps/server/src/modules/files-storage/mapper/file-dto.builder.ts @@ -17,7 +17,7 @@ export class FileDtoBuilder { } public static buildFromAxiosResponse(name: string, response: AxiosResponse): FileDto { - const mimeType = response.headers['content-type']; + const mimeType = response.headers['Content-Type']?.toString() || 'application/octet-stream'; const file = FileDtoBuilder.build(name, response.data, mimeType); return file; diff --git a/apps/server/src/modules/files-storage/service/files-storage-copy.service.spec.ts b/apps/server/src/modules/files-storage/service/files-storage-copy.service.spec.ts index 6f23b06a078..3605ae74080 100644 --- a/apps/server/src/modules/files-storage/service/files-storage-copy.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/files-storage-copy.service.spec.ts @@ -8,6 +8,7 @@ import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { FileRecordParams } from '../controller/dto'; import { FileRecord, FileRecordParentType, ScanStatus } from '../entity'; +import { FILES_STORAGE_S3_CONNECTION } from '../files-storage.config'; import { createCopyFiles } from '../helper'; import { CopyFileResponseBuilder } from '../mapper'; import { FileRecordRepo } from '../repo'; @@ -46,7 +47,7 @@ describe('FilesStorageService copy methods', () => { providers: [ FilesStorageService, { - provide: S3ClientAdapter, + provide: FILES_STORAGE_S3_CONNECTION, useValue: createMock(), }, { @@ -69,7 +70,7 @@ describe('FilesStorageService copy methods', () => { }).compile(); service = module.get(FilesStorageService); - storageClient = module.get(S3ClientAdapter); + storageClient = module.get(FILES_STORAGE_S3_CONNECTION); fileRecordRepo = module.get(FileRecordRepo); antivirusService = module.get(AntivirusService); }); diff --git a/apps/server/src/modules/files-storage/service/files-storage-delete.service.spec.ts b/apps/server/src/modules/files-storage/service/files-storage-delete.service.spec.ts index eda7609d7ed..627f35d91da 100644 --- a/apps/server/src/modules/files-storage/service/files-storage-delete.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/files-storage-delete.service.spec.ts @@ -9,6 +9,7 @@ import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { FileRecordParams } from '../controller/dto'; import { FileRecord, FileRecordParentType } from '../entity'; +import { FILES_STORAGE_S3_CONNECTION } from '../files-storage.config'; import { getPaths } from '../helper'; import { FileRecordRepo } from '../repo'; import { FilesStorageService } from './files-storage.service'; @@ -45,7 +46,7 @@ describe('FilesStorageService delete methods', () => { providers: [ FilesStorageService, { - provide: S3ClientAdapter, + provide: FILES_STORAGE_S3_CONNECTION, useValue: createMock(), }, { @@ -68,7 +69,7 @@ describe('FilesStorageService delete methods', () => { }).compile(); service = module.get(FilesStorageService); - storageClient = module.get(S3ClientAdapter); + storageClient = module.get(FILES_STORAGE_S3_CONNECTION); fileRecordRepo = module.get(FileRecordRepo); }); diff --git a/apps/server/src/modules/files-storage/service/files-storage-download.service.spec.ts b/apps/server/src/modules/files-storage/service/files-storage-download.service.spec.ts index f6b174c7bd3..a578bca51a1 100644 --- a/apps/server/src/modules/files-storage/service/files-storage-download.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/files-storage-download.service.spec.ts @@ -10,6 +10,7 @@ import { LegacyLogger } from '@src/core/logger'; import { FileRecordParams } from '../controller/dto'; import { FileRecord, FileRecordParentType, ScanStatus } from '../entity'; import { ErrorType } from '../error'; +import { FILES_STORAGE_S3_CONNECTION } from '../files-storage.config'; import { createPath } from '../helper'; import { FileResponseBuilder } from '../mapper'; import { FileRecordRepo } from '../repo'; @@ -46,7 +47,7 @@ describe('FilesStorageService download methods', () => { providers: [ FilesStorageService, { - provide: S3ClientAdapter, + provide: FILES_STORAGE_S3_CONNECTION, useValue: createMock(), }, { @@ -69,7 +70,7 @@ describe('FilesStorageService download methods', () => { }).compile(); service = module.get(FilesStorageService); - storageClient = module.get(S3ClientAdapter); + storageClient = module.get(FILES_STORAGE_S3_CONNECTION); }); beforeEach(() => { diff --git a/apps/server/src/modules/files-storage/service/files-storage-get.service.spec.ts b/apps/server/src/modules/files-storage/service/files-storage-get.service.spec.ts index bf187203bac..3565e5a328a 100644 --- a/apps/server/src/modules/files-storage/service/files-storage-get.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/files-storage-get.service.spec.ts @@ -8,6 +8,7 @@ import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { FileRecordParams, SingleFileParams } from '../controller/dto'; import { FileRecord, FileRecordParentType } from '../entity'; +import { FILES_STORAGE_S3_CONNECTION } from '../files-storage.config'; import { FileRecordRepo } from '../repo'; import { FilesStorageService } from './files-storage.service'; @@ -54,7 +55,7 @@ describe('FilesStorageService get methods', () => { providers: [ FilesStorageService, { - provide: S3ClientAdapter, + provide: FILES_STORAGE_S3_CONNECTION, useValue: createMock(), }, { diff --git a/apps/server/src/modules/files-storage/service/files-storage-restore.service.spec.ts b/apps/server/src/modules/files-storage/service/files-storage-restore.service.spec.ts index 883ea3c024a..1c3460c3926 100644 --- a/apps/server/src/modules/files-storage/service/files-storage-restore.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/files-storage-restore.service.spec.ts @@ -8,6 +8,7 @@ import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { FileRecordParams } from '../controller/dto'; import { FileRecord, FileRecordParentType } from '../entity'; +import { FILES_STORAGE_S3_CONNECTION } from '../files-storage.config'; import { getPaths, unmarkForDelete } from '../helper'; import { FileRecordRepo } from '../repo'; import { FilesStorageService } from './files-storage.service'; @@ -44,7 +45,7 @@ describe('FilesStorageService restore methods', () => { providers: [ FilesStorageService, { - provide: S3ClientAdapter, + provide: FILES_STORAGE_S3_CONNECTION, useValue: createMock(), }, { @@ -67,7 +68,7 @@ describe('FilesStorageService restore methods', () => { }).compile(); service = module.get(FilesStorageService); - storageClient = module.get(S3ClientAdapter); + storageClient = module.get(FILES_STORAGE_S3_CONNECTION); fileRecordRepo = module.get(FileRecordRepo); }); diff --git a/apps/server/src/modules/files-storage/service/files-storage-update.service.spec.ts b/apps/server/src/modules/files-storage/service/files-storage-update.service.spec.ts index 5bc4e3f11ae..9da800baf90 100644 --- a/apps/server/src/modules/files-storage/service/files-storage-update.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/files-storage-update.service.spec.ts @@ -11,6 +11,7 @@ import _ from 'lodash'; import { FileRecordParams, RenameFileParams, ScanResultParams, SingleFileParams } from '../controller/dto'; import { FileRecord, FileRecordParentType } from '../entity'; import { ErrorType } from '../error'; +import { FILES_STORAGE_S3_CONNECTION } from '../files-storage.config'; import { FileRecordMapper, FilesStorageMapper } from '../mapper'; import { FileRecordRepo } from '../repo'; import { FilesStorageService } from './files-storage.service'; @@ -58,7 +59,7 @@ describe('FilesStorageService update methods', () => { providers: [ FilesStorageService, { - provide: S3ClientAdapter, + provide: FILES_STORAGE_S3_CONNECTION, useValue: createMock(), }, { diff --git a/apps/server/src/modules/files-storage/service/files-storage-upload.service.spec.ts b/apps/server/src/modules/files-storage/service/files-storage-upload.service.spec.ts index 8f07780385f..dd3f2c60def 100644 --- a/apps/server/src/modules/files-storage/service/files-storage-upload.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/files-storage-upload.service.spec.ts @@ -13,6 +13,7 @@ import { FileRecordParams } from '../controller/dto'; import { FileDto } from '../dto'; import { FileRecord, FileRecordParentType } from '../entity'; import { ErrorType } from '../error'; +import { FILES_STORAGE_S3_CONNECTION } from '../files-storage.config'; import { createFileRecord, resolveFileNameDuplicates } from '../helper'; import { FileRecordRepo } from '../repo'; import { FilesStorageService } from './files-storage.service'; @@ -57,7 +58,7 @@ describe('FilesStorageService upload methods', () => { providers: [ FilesStorageService, { - provide: S3ClientAdapter, + provide: FILES_STORAGE_S3_CONNECTION, useValue: createMock(), }, { @@ -80,7 +81,7 @@ describe('FilesStorageService upload methods', () => { }).compile(); service = module.get(FilesStorageService); - storageClient = module.get(S3ClientAdapter); + storageClient = module.get(FILES_STORAGE_S3_CONNECTION); fileRecordRepo = module.get(FileRecordRepo); antivirusService = module.get(AntivirusService); configService = module.get(ConfigService); diff --git a/apps/server/src/modules/files-storage/service/files-storage.service.ts b/apps/server/src/modules/files-storage/service/files-storage.service.ts index 2de0d90c701..acdab712f78 100644 --- a/apps/server/src/modules/files-storage/service/files-storage.service.ts +++ b/apps/server/src/modules/files-storage/service/files-storage.service.ts @@ -1,6 +1,7 @@ import { BadRequestException, ConflictException, + Inject, Injectable, NotAcceptableException, NotFoundException, @@ -24,7 +25,7 @@ import { import { FileDto } from '../dto'; import { FileRecord, ScanStatus } from '../entity'; import { ErrorType } from '../error'; -import { IFileStorageConfig } from '../files-storage.config'; +import { FILES_STORAGE_S3_CONNECTION, IFileStorageConfig } from '../files-storage.config'; import { createCopyFiles, createFileRecord, @@ -42,7 +43,7 @@ import { FileRecordRepo } from '../repo'; export class FilesStorageService { constructor( private readonly fileRecordRepo: FileRecordRepo, - private readonly storageClient: S3ClientAdapter, + @Inject(FILES_STORAGE_S3_CONNECTION) private readonly storageClient: S3ClientAdapter, private readonly antivirusService: AntivirusService, private readonly configService: ConfigService, private logger: LegacyLogger diff --git a/apps/server/src/modules/files-storage/service/preview.service.spec.ts b/apps/server/src/modules/files-storage/service/preview.service.spec.ts index 7e18338a4e0..ed4592b97da 100644 --- a/apps/server/src/modules/files-storage/service/preview.service.spec.ts +++ b/apps/server/src/modules/files-storage/service/preview.service.spec.ts @@ -9,6 +9,7 @@ import { Readable } from 'stream'; import { FileRecordParams } from '../controller/dto'; import { FileRecord, FileRecordParentType, ScanStatus } from '../entity'; import { ErrorType } from '../error'; +import { FILES_STORAGE_S3_CONNECTION } from '../files-storage.config'; import { createPreviewDirectoryPath, createPreviewFilePath, createPreviewNameHash } from '../helper'; import { TestHelper } from '../helper/test-helper'; import { PreviewWidth } from '../interface'; @@ -75,7 +76,7 @@ describe('PreviewService', () => { useValue: createMock(), }, { - provide: S3ClientAdapter, + provide: FILES_STORAGE_S3_CONNECTION, useValue: createMock(), }, { @@ -87,7 +88,7 @@ describe('PreviewService', () => { previewService = module.get(PreviewService); fileStorageService = module.get(FilesStorageService); - s3ClientAdapter = module.get(S3ClientAdapter); + s3ClientAdapter = module.get(FILES_STORAGE_S3_CONNECTION); }); beforeEach(() => { @@ -328,7 +329,7 @@ describe('PreviewService', () => { await previewService.getPreview(fileRecord, downloadParams, previewParams); - expect(resizeMock).toHaveBeenCalledWith(previewParams.width); + expect(resizeMock).toHaveBeenCalledWith(previewParams.width, undefined, '>'); expect(resizeMock).toHaveBeenCalledTimes(1); }); @@ -520,7 +521,7 @@ describe('PreviewService', () => { await previewService.getPreview(fileRecord, downloadParams, previewParams); - expect(resizeMock).toHaveBeenCalledWith(previewParams.width); + expect(resizeMock).toHaveBeenCalledWith(previewParams.width, undefined, '>'); expect(resizeMock).toHaveBeenCalledTimes(1); }); diff --git a/apps/server/src/modules/files-storage/service/preview.service.ts b/apps/server/src/modules/files-storage/service/preview.service.ts index d05630eaa60..7cf33afe5fe 100644 --- a/apps/server/src/modules/files-storage/service/preview.service.ts +++ b/apps/server/src/modules/files-storage/service/preview.service.ts @@ -1,4 +1,4 @@ -import { Injectable, NotFoundException, UnprocessableEntityException } from '@nestjs/common'; +import { Inject, Injectable, NotFoundException, UnprocessableEntityException } from '@nestjs/common'; import { S3ClientAdapter } from '@shared/infra/s3-client'; import { LegacyLogger } from '@src/core/logger'; import { subClass } from 'gm'; @@ -6,6 +6,7 @@ import { PassThrough } from 'stream'; import { DownloadFileParams, PreviewParams } from '../controller/dto'; import { FileRecord, PreviewStatus } from '../entity'; import { ErrorType } from '../error'; +import { FILES_STORAGE_S3_CONNECTION } from '../files-storage.config'; import { createPreviewDirectoryPath, createPreviewFilePath, createPreviewNameHash } from '../helper'; import { GetFileResponse, PreviewFileParams } from '../interface'; import { PreviewOutputMimeTypes } from '../interface/preview-output-mime-types.enum'; @@ -15,7 +16,7 @@ import { FilesStorageService } from './files-storage.service'; @Injectable() export class PreviewService { constructor( - private readonly storageClient: S3ClientAdapter, + @Inject(FILES_STORAGE_S3_CONNECTION) private readonly storageClient: S3ClientAdapter, private readonly fileStorageService: FilesStorageService, private logger: LegacyLogger ) { @@ -121,7 +122,7 @@ export class PreviewService { const { width } = previewParams; if (width) { - preview.resize(width); + preview.resize(width, undefined, '>'); } const result = preview.stream(format); diff --git a/apps/server/src/modules/files-storage/uc/files-storage-upload.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-upload.uc.spec.ts index fdcf1362e2a..78348078565 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-upload.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-upload.uc.spec.ts @@ -6,10 +6,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain'; import { AntivirusService } from '@shared/infra/antivirus/antivirus.service'; import { S3ClientAdapter } from '@shared/infra/s3-client'; -import { fileRecordFactory, setupEntities } from '@shared/testing'; +import { AxiosHeadersKeyValue, axiosResponseFactory, fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { Action, AuthorizationService } from '@src/modules/authorization'; -import { AxiosRequestConfig, AxiosResponse, AxiosResponseHeaders } from 'axios'; +import { AxiosRequestConfig, AxiosResponse } from 'axios'; import { Request } from 'express'; import { of } from 'rxjs'; import { Readable } from 'stream'; @@ -22,29 +22,16 @@ import { FilesStorageService } from '../service/files-storage.service'; import { PreviewService } from '../service/preview.service'; import { FilesStorageUC } from './files-storage.uc'; -const createAxiosResponse = (data: Readable, headers: AxiosResponseHeaders = {}): AxiosResponse => { - const response: AxiosResponse = { +const createAxiosResponse = (data: T, headers?: AxiosHeadersKeyValue) => + axiosResponseFactory.build({ data, - status: 0, - statusText: '', headers, - config: {}, - }; - - return response; -}; + }); const createAxiosErrorResponse = (): AxiosResponse => { - const headers: AxiosResponseHeaders = {}; - const config: AxiosRequestConfig = {}; - const errorResponse: AxiosResponse = { - data: {}, + const errorResponse: AxiosResponse = axiosResponseFactory.build({ status: 404, - statusText: 'errorText', - headers, - config, - request: {}, - }; + }); return errorResponse; }; diff --git a/apps/server/src/modules/fwu-learning-contents/controller/api-test/fwu-learning-contents.api.spec.ts b/apps/server/src/modules/fwu-learning-contents/controller/api-test/fwu-learning-contents.api.spec.ts index e7e1d78fe54..9f64e07129a 100644 --- a/apps/server/src/modules/fwu-learning-contents/controller/api-test/fwu-learning-contents.api.spec.ts +++ b/apps/server/src/modules/fwu-learning-contents/controller/api-test/fwu-learning-contents.api.spec.ts @@ -7,6 +7,7 @@ import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; import { Readable } from 'stream'; import request from 'supertest'; import { FwuLearningContentsTestModule } from '../../fwu-learning-contents-test.module'; +import { FWU_CONTENT_S3_CONNECTION } from '../../fwu-learning-contents.config'; class API { constructor(private app: INestApplication) { @@ -37,13 +38,13 @@ describe('FwuLearningContents Controller (api)', () => { return true; }, }) - .overrideProvider(S3ClientAdapter) + .overrideProvider(FWU_CONTENT_S3_CONNECTION) .useValue(createMock()) .compile(); app = module.createNestApplication(); await app.init(); - s3ClientAdapter = module.get(S3ClientAdapter); + s3ClientAdapter = module.get(FWU_CONTENT_S3_CONNECTION); api = new API(app); }); diff --git a/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents-test.module.ts b/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents-test.module.ts index 8c314f695be..4e633ca4e1b 100644 --- a/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents-test.module.ts +++ b/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents-test.module.ts @@ -5,14 +5,14 @@ import { Account, Role, School, SchoolYear, System, User } from '@shared/domain' import { MongoMemoryDatabaseModule } from '@shared/infra/database'; import { MongoDatabaseModuleOptions } from '@shared/infra/database/mongo-memory-database/types'; import { RabbitMQWrapperTestModule } from '@shared/infra/rabbitmq'; -import { S3ClientAdapter } from '@shared/infra/s3-client'; +import { S3ClientModule } from '@shared/infra/s3-client'; import { createConfigModuleOptions } from '@src/config'; import { CoreModule } from '@src/core'; import { LoggerModule } from '@src/core/logger'; import { AuthenticationModule } from '@src/modules/authentication/authentication.module'; import { AuthorizationModule } from '@src/modules/authorization'; import { FwuLearningContentsController } from './controller/fwu-learning-contents.controller'; -import { config } from './fwu-learning-contents.config'; +import { config, s3Config } from './fwu-learning-contents.config'; import { FwuLearningContentsUc } from './uc/fwu-learning-contents.uc'; const imports = [ @@ -24,9 +24,10 @@ const imports = [ CoreModule, LoggerModule, RabbitMQWrapperTestModule, + S3ClientModule.register([s3Config]), ]; const controllers = [FwuLearningContentsController]; -const providers = [FwuLearningContentsUc, S3ClientAdapter]; +const providers = [FwuLearningContentsUc]; @Module({ imports, controllers, diff --git a/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents.config.ts b/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents.config.ts index a1bc2f2fe5e..56ae93e0205 100644 --- a/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents.config.ts +++ b/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents.config.ts @@ -1,7 +1,10 @@ import { Configuration } from '@hpi-schul-cloud/commons'; -import { S3Config } from './interface/config'; +import { S3Config } from '@shared/infra/s3-client'; + +export const FWU_CONTENT_S3_CONNECTION = 'FWU_CONTENT_S3_CONNECTION'; export const s3Config: S3Config = { + connectionName: FWU_CONTENT_S3_CONNECTION, endpoint: Configuration.get('FWU_CONTENT__S3_ENDPOINT') as string, region: Configuration.get('FWU_CONTENT__S3_REGION') as string, bucket: Configuration.get('FWU_CONTENT__S3_BUCKET') as string, diff --git a/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents.module.ts b/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents.module.ts index a6c7682498b..55c083b3061 100644 --- a/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents.module.ts +++ b/apps/server/src/modules/fwu-learning-contents/fwu-learning-contents.module.ts @@ -4,13 +4,13 @@ import { HttpModule } from '@nestjs/axios'; import { Module, NotFoundException } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { Account, Role, School, SchoolYear, System, User } from '@shared/domain'; +import { RabbitMQWrapperModule } from '@shared/infra/rabbitmq'; import { S3ClientModule } from '@shared/infra/s3-client'; import { DB_PASSWORD, DB_URL, DB_USERNAME, createConfigModuleOptions } from '@src/config'; import { CoreModule } from '@src/core'; import { LoggerModule } from '@src/core/logger'; -import { AuthenticationModule } from '@src/modules/authentication/authentication.module'; import { AuthorizationModule } from '@src/modules/authorization'; -import { FilesStorageAMQPModule } from '../files-storage/files-storage-amqp.module'; +import { AuthenticationModule } from '../authentication/authentication.module'; import { FwuLearningContentsController } from './controller/fwu-learning-contents.controller'; import { config, s3Config } from './fwu-learning-contents.config'; import { FwuLearningContentsUc } from './uc/fwu-learning-contents.uc'; @@ -28,7 +28,7 @@ const defaultMikroOrmOptions: MikroOrmModuleSyncOptions = { CoreModule, LoggerModule, HttpModule, - FilesStorageAMQPModule, + RabbitMQWrapperModule, MikroOrmModule.forRoot({ ...defaultMikroOrmOptions, type: 'mongo', @@ -41,7 +41,7 @@ const defaultMikroOrmOptions: MikroOrmModuleSyncOptions = { // debug: true, // use it for locally debugging of querys }), ConfigModule.forRoot(createConfigModuleOptions(config)), - S3ClientModule.register(s3Config), + S3ClientModule.register([s3Config]), ], controllers: [FwuLearningContentsController], providers: [FwuLearningContentsUc], diff --git a/apps/server/src/modules/fwu-learning-contents/uc/fwu-learning-contents.uc.spec.ts b/apps/server/src/modules/fwu-learning-contents/uc/fwu-learning-contents.uc.spec.ts index e2451d56317..80240e0ea8e 100644 --- a/apps/server/src/modules/fwu-learning-contents/uc/fwu-learning-contents.uc.spec.ts +++ b/apps/server/src/modules/fwu-learning-contents/uc/fwu-learning-contents.uc.spec.ts @@ -1,11 +1,9 @@ -import { S3Client } from '@aws-sdk/client-s3'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { S3ClientAdapter } from '@shared/infra/s3-client'; import { LegacyLogger } from '@src/core/logger'; import { Readable } from 'stream'; -import { S3Config } from '../interface/config'; +import { FWU_CONTENT_S3_CONNECTION } from '../fwu-learning-contents.config'; import { FwuLearningContentsUc } from './fwu-learning-contents.uc'; describe('FwuLearningContentsUC', () => { @@ -22,22 +20,15 @@ describe('FwuLearningContentsUC', () => { }; let module: TestingModule; let fwuLearningContentsUc: FwuLearningContentsUc; - let s3client: DeepMocked; + let s3client: DeepMocked; beforeAll(async () => { - const { config } = createParameter(); - module = await Test.createTestingModule({ providers: [ FwuLearningContentsUc, - S3ClientAdapter, - { - provide: 'S3_Client', - useValue: createMock(), - }, { - provide: 'S3_Config', - useValue: createMock(config), + provide: FWU_CONTENT_S3_CONNECTION, + useValue: createMock(), }, { provide: LegacyLogger, @@ -47,7 +38,7 @@ describe('FwuLearningContentsUC', () => { }).compile(); fwuLearningContentsUc = module.get(FwuLearningContentsUc); - s3client = module.get('S3_Client'); + s3client = module.get(FWU_CONTENT_S3_CONNECTION); }); beforeEach(() => { @@ -75,7 +66,7 @@ describe('FwuLearningContentsUC', () => { }; // @ts-expect-error Testcase - s3client.send.mockResolvedValueOnce(resultObj); + s3client.get.mockResolvedValueOnce(resultObj); return { pathToFile, config }; }; @@ -88,48 +79,12 @@ describe('FwuLearningContentsUC', () => { expect(result).toBeInstanceOf(Object); }); - it('should call send() of client', async () => { - const { pathToFile, config } = setup(); + it('should call get() of client', async () => { + const { pathToFile } = setup(); await fwuLearningContentsUc.get(pathToFile); - expect(s3client.send).toBeCalledWith( - expect.objectContaining({ - input: { - Bucket: config.bucket, - Key: pathToFile, - }, - }) - ); - }); - }); - - describe('when client throws error', () => { - const setup = (error: ErrorType) => { - const { pathToFile } = createParameter(); - - // @ts-expect-error Testcase - s3client.send.mockRejectedValueOnce(error); - - return { error, pathToFile }; - }; - - it('should throw NotFoundException', async () => { - const { pathToFile } = setup({ name: 'NoSuchKey', stack: 'NoSuchKey at ...' }); - - await expect(fwuLearningContentsUc.get(pathToFile)).rejects.toThrowError(InternalServerErrorException); - }); - - it('should throw error', async () => { - const { pathToFile } = setup({ name: 'UnknownError' }); - - await expect(fwuLearningContentsUc.get(pathToFile)).rejects.toThrowError(InternalServerErrorException); - }); - - it('should throw error', async () => { - const { pathToFile } = setup('Not an Error object'); - - await expect(fwuLearningContentsUc.get(pathToFile)).rejects.toThrowError(InternalServerErrorException); + expect(s3client.get).toBeCalledWith(pathToFile, undefined); }); }); }); diff --git a/apps/server/src/modules/fwu-learning-contents/uc/fwu-learning-contents.uc.ts b/apps/server/src/modules/fwu-learning-contents/uc/fwu-learning-contents.uc.ts index ac5164130e4..afab92d46a5 100644 --- a/apps/server/src/modules/fwu-learning-contents/uc/fwu-learning-contents.uc.ts +++ b/apps/server/src/modules/fwu-learning-contents/uc/fwu-learning-contents.uc.ts @@ -1,10 +1,14 @@ -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { S3ClientAdapter } from '@shared/infra/s3-client'; import { LegacyLogger } from '@src/core/logger'; +import { FWU_CONTENT_S3_CONNECTION } from '../fwu-learning-contents.config'; @Injectable() export class FwuLearningContentsUc { - constructor(private logger: LegacyLogger, private readonly storageClient: S3ClientAdapter) { + constructor( + private logger: LegacyLogger, + @Inject(FWU_CONTENT_S3_CONNECTION) private readonly storageClient: S3ClientAdapter + ) { this.logger.setContext(FwuLearningContentsUc.name); } diff --git a/apps/server/src/modules/group/domain/group-types.ts b/apps/server/src/modules/group/domain/group-types.ts new file mode 100644 index 00000000000..fa1311a0495 --- /dev/null +++ b/apps/server/src/modules/group/domain/group-types.ts @@ -0,0 +1,3 @@ +export enum GroupTypes { + CLASS = 'class', +} diff --git a/apps/server/src/modules/group/domain/group-user.ts b/apps/server/src/modules/group/domain/group-user.ts new file mode 100644 index 00000000000..8f098ebc660 --- /dev/null +++ b/apps/server/src/modules/group/domain/group-user.ts @@ -0,0 +1,12 @@ +import { EntityId } from '@shared/domain'; + +export class GroupUser { + userId: EntityId; + + roleId: EntityId; + + constructor(props: GroupUser) { + this.userId = props.userId; + this.roleId = props.roleId; + } +} diff --git a/apps/server/src/modules/group/domain/group.ts b/apps/server/src/modules/group/domain/group.ts new file mode 100644 index 00000000000..8ebd8b7ab04 --- /dev/null +++ b/apps/server/src/modules/group/domain/group.ts @@ -0,0 +1,24 @@ +import { EntityId, ExternalSource } from '@shared/domain'; +import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; +import { GroupTypes } from './group-types'; +import { GroupUser } from './group-user'; + +export interface GroupProps extends AuthorizableObject { + id: EntityId; + + name: string; + + type: GroupTypes; + + validFrom?: Date; + + validUntil?: Date; + + externalSource?: ExternalSource; + + users: GroupUser[]; + + organizationId?: string; +} + +export class Group extends DomainObject {} diff --git a/apps/server/src/modules/group/domain/index.ts b/apps/server/src/modules/group/domain/index.ts new file mode 100644 index 00000000000..f140dc330a6 --- /dev/null +++ b/apps/server/src/modules/group/domain/index.ts @@ -0,0 +1,3 @@ +export * from './group'; +export * from './group-user'; +export * from './group-types'; diff --git a/apps/server/src/modules/group/entity/group-user.entity.ts b/apps/server/src/modules/group/entity/group-user.entity.ts new file mode 100644 index 00000000000..e202de7a400 --- /dev/null +++ b/apps/server/src/modules/group/entity/group-user.entity.ts @@ -0,0 +1,22 @@ +import { Embeddable, ManyToOne } from '@mikro-orm/core'; +import { Role, User } from '@shared/domain/entity'; + +export interface GroupUserEntityProps { + user: User; + + role: Role; +} + +@Embeddable() +export class GroupUserEntity { + @ManyToOne(() => User) + user: User; + + @ManyToOne(() => Role) + role: Role; + + constructor(props: GroupUserEntityProps) { + this.user = props.user; + this.role = props.role; + } +} diff --git a/apps/server/src/modules/group/entity/group-valid-period.entity.ts b/apps/server/src/modules/group/entity/group-valid-period.entity.ts new file mode 100644 index 00000000000..f3f656241c6 --- /dev/null +++ b/apps/server/src/modules/group/entity/group-valid-period.entity.ts @@ -0,0 +1,21 @@ +import { Embeddable, Property } from '@mikro-orm/core'; + +export interface GroupValidPeriodEntityProps { + from: Date; + + until: Date; +} + +@Embeddable() +export class GroupValidPeriodEntity { + @Property() + from: Date; + + @Property() + until: Date; + + constructor(props: GroupValidPeriodEntityProps) { + this.from = props.from; + this.until = props.until; + } +} diff --git a/apps/server/src/modules/group/entity/group.entity.ts b/apps/server/src/modules/group/entity/group.entity.ts new file mode 100644 index 00000000000..0abd954c329 --- /dev/null +++ b/apps/server/src/modules/group/entity/group.entity.ts @@ -0,0 +1,61 @@ +import { Embedded, Entity, Enum, ManyToOne, Property } from '@mikro-orm/core'; +import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; +import { ExternalSourceEntity } from '@shared/domain/entity/external-source.entity'; +import { School } from '@shared/domain/entity/school.entity'; +import { EntityId } from '@shared/domain/types'; +import { GroupUserEntity } from './group-user.entity'; +import { GroupValidPeriodEntity } from './group-valid-period.entity'; + +export enum GroupEntityTypes { + CLASS = 'class', +} + +export interface GroupEntityProps { + id?: EntityId; + + name: string; + + type: GroupEntityTypes; + + externalSource?: ExternalSourceEntity; + + validPeriod?: GroupValidPeriodEntity; + + users: GroupUserEntity[]; + + organization?: School; +} + +@Entity({ tableName: 'groups' }) +export class GroupEntity extends BaseEntityWithTimestamps { + @Property() + name: string; + + @Enum() + type: GroupEntityTypes; + + @Embedded(() => ExternalSourceEntity, { nullable: true }) + externalSource?: ExternalSourceEntity; + + @Embedded(() => GroupValidPeriodEntity, { nullable: true }) + validPeriod?: GroupValidPeriodEntity; + + @Embedded(() => GroupUserEntity, { array: true }) + users: GroupUserEntity[]; + + @ManyToOne(() => School, { nullable: true }) + organization?: School; + + constructor(props: GroupEntityProps) { + super(); + if (props.id) { + this.id = props.id; + } + this.name = props.name; + this.type = props.type; + this.externalSource = props.externalSource; + this.validPeriod = props.validPeriod; + this.users = props.users; + this.organization = props.organization; + } +} diff --git a/apps/server/src/modules/group/entity/index.ts b/apps/server/src/modules/group/entity/index.ts new file mode 100644 index 00000000000..1aff7d6deeb --- /dev/null +++ b/apps/server/src/modules/group/entity/index.ts @@ -0,0 +1,3 @@ +export * from './group.entity'; +export * from './group-user.entity'; +export * from './group-valid-period.entity'; diff --git a/apps/server/src/modules/group/group-api.module.ts b/apps/server/src/modules/group/group-api.module.ts new file mode 100644 index 00000000000..1be422855a9 --- /dev/null +++ b/apps/server/src/modules/group/group-api.module.ts @@ -0,0 +1,7 @@ +import { Module } from '@nestjs/common'; +import { GroupModule } from './group.module'; + +@Module({ + imports: [GroupModule], +}) +export class GroupApiModule {} diff --git a/apps/server/src/modules/group/group.module.ts b/apps/server/src/modules/group/group.module.ts new file mode 100644 index 00000000000..58bce2070d0 --- /dev/null +++ b/apps/server/src/modules/group/group.module.ts @@ -0,0 +1,9 @@ +import { Module } from '@nestjs/common'; +import { GroupRepo } from './repo'; +import { GroupService } from './service'; + +@Module({ + providers: [GroupRepo, GroupService], + exports: [GroupService], +}) +export class GroupModule {} diff --git a/apps/server/src/modules/group/index.ts b/apps/server/src/modules/group/index.ts new file mode 100644 index 00000000000..01004d5e52e --- /dev/null +++ b/apps/server/src/modules/group/index.ts @@ -0,0 +1,3 @@ +export * from './group.module'; +export * from './domain'; +export { GroupService } from './service'; diff --git a/apps/server/src/modules/group/repo/group-domain.mapper.ts b/apps/server/src/modules/group/repo/group-domain.mapper.ts new file mode 100644 index 00000000000..7b9802e8d46 --- /dev/null +++ b/apps/server/src/modules/group/repo/group-domain.mapper.ts @@ -0,0 +1,98 @@ +import { EntityManager } from '@mikro-orm/mongodb'; +import { ExternalSource, ExternalSourceEntity, Role, School, System, User } from '@shared/domain'; +import { Group, GroupProps, GroupTypes, GroupUser } from '../domain'; +import { GroupEntity, GroupEntityProps, GroupEntityTypes, GroupUserEntity, GroupValidPeriodEntity } from '../entity'; + +const GroupEntityTypesToGroupTypesMapping: Record = { + [GroupEntityTypes.CLASS]: GroupTypes.CLASS, +}; + +const GroupTypesToGroupEntityTypesMapping: Record = { + [GroupTypes.CLASS]: GroupEntityTypes.CLASS, +}; + +export class GroupDomainMapper { + static mapDomainObjectToEntityProperties(group: Group, em: EntityManager): GroupEntityProps { + const props: GroupProps = group.getProps(); + + let validPeriod: GroupValidPeriodEntity | undefined; + if (props.validFrom && props.validUntil) { + validPeriod = new GroupValidPeriodEntity({ + from: props.validFrom, + until: props.validUntil, + }); + } + + const mapped: GroupEntityProps = { + id: props.id, + name: props.name, + type: GroupTypesToGroupEntityTypesMapping[props.type], + externalSource: props.externalSource + ? this.mapExternalSourceToExternalSourceEntity(props.externalSource, em) + : undefined, + users: props.users.map( + (groupUser): GroupUserEntity => GroupDomainMapper.mapGroupUserToGroupUserEntity(groupUser, em) + ), + validPeriod, + organization: props.organizationId ? em.getReference(School, props.organizationId) : undefined, + }; + + return mapped; + } + + static mapEntityToDomainObjectProperties(entity: GroupEntity): GroupProps { + const mapped: GroupProps = { + id: entity.id, + users: entity.users.map((groupUser): GroupUser => this.mapGroupUserEntityToGroupUser(groupUser)), + validFrom: entity.validPeriod ? entity.validPeriod.from : undefined, + validUntil: entity.validPeriod ? entity.validPeriod.until : undefined, + externalSource: entity.externalSource + ? this.mapExternalSourceEntityToExternalSource(entity.externalSource) + : undefined, + type: GroupEntityTypesToGroupTypesMapping[entity.type], + name: entity.name, + organizationId: entity.organization?.id, + }; + + return mapped; + } + + static mapExternalSourceToExternalSourceEntity( + externalSource: ExternalSource, + em: EntityManager + ): ExternalSourceEntity { + const mapped = new ExternalSourceEntity({ + externalId: externalSource.externalId, + system: em.getReference(System, externalSource.systemId), + }); + + return mapped; + } + + static mapExternalSourceEntityToExternalSource(entity: ExternalSourceEntity): ExternalSource { + const mapped = new ExternalSource({ + externalId: entity.externalId, + systemId: entity.system.id, + }); + + return mapped; + } + + static mapGroupUserToGroupUserEntity(groupUser: GroupUser, em: EntityManager): GroupUserEntity { + const mapped = new GroupUserEntity({ + user: em.getReference(User, groupUser.userId), + role: em.getReference(Role, groupUser.roleId), + }); + + return mapped; + } + + static mapGroupUserEntityToGroupUser(entity: GroupUserEntity): GroupUser { + const mapped = new GroupUser({ + userId: entity.user.id, + roleId: entity.role.id, + }); + + return mapped; + } +} diff --git a/apps/server/src/modules/group/repo/group.repo.spec.ts b/apps/server/src/modules/group/repo/group.repo.spec.ts new file mode 100644 index 00000000000..a7c7454dae4 --- /dev/null +++ b/apps/server/src/modules/group/repo/group.repo.spec.ts @@ -0,0 +1,264 @@ +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ExternalSource } from '@shared/domain'; +import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { cleanupCollections, groupEntityFactory, groupFactory } from '@shared/testing'; +import { Group, GroupProps, GroupTypes, GroupUser } from '../domain'; +import { GroupEntity } from '../entity'; +import { GroupRepo } from './group.repo'; + +describe('GroupRepo', () => { + let module: TestingModule; + let repo: GroupRepo; + let em: EntityManager; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [MongoMemoryDatabaseModule.forRoot()], + providers: [GroupRepo], + }).compile(); + + repo = module.get(GroupRepo); + em = module.get(EntityManager); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(async () => { + await cleanupCollections(em); + }); + + describe('findById', () => { + describe('when an entity with the id exists', () => { + const setup = async () => { + const group: GroupEntity = groupEntityFactory.buildWithId(); + + await em.persistAndFlush(group); + em.clear(); + + return { + group, + }; + }; + + it('should return the group', async () => { + const { group } = await setup(); + + const result: Group | null = await repo.findById(group.id); + + expect(result?.getProps()).toEqual({ + id: group.id, + name: group.name, + type: GroupTypes.CLASS, + externalSource: new ExternalSource({ + externalId: group.externalSource?.externalId ?? '', + systemId: group.externalSource?.system.id ?? '', + }), + users: [ + new GroupUser({ + userId: group.users[0].user.id, + roleId: group.users[0].role.id, + }), + new GroupUser({ + userId: group.users[1].user.id, + roleId: group.users[1].role.id, + }), + ], + organizationId: group.organization?.id, + validFrom: group.validPeriod?.from, + validUntil: group.validPeriod?.until, + }); + }); + }); + + describe('when no entity with the id exists', () => { + it('should return null', async () => { + const result: Group | null = await repo.findById(new ObjectId().toHexString()); + + expect(result).toBeNull(); + }); + }); + }); + + describe('save', () => { + describe('when a new object is provided', () => { + const setup = () => { + const groupId = new ObjectId().toHexString(); + + const group: Group = groupFactory.build({ id: groupId }); + + return { + group, + groupId, + }; + }; + + it('should create a new entity', async () => { + const { group, groupId } = setup(); + + await repo.save(group); + + await expect(em.findOneOrFail(GroupEntity, groupId)).resolves.toBeDefined(); + }); + + it('should return the object', async () => { + const { group } = setup(); + + const result: Group = await repo.save(group); + + expect(result).toEqual(group); + }); + }); + + describe('when an entity with the id exists', () => { + const setup = async () => { + const groupId = new ObjectId().toHexString(); + const groupEntity: GroupEntity = groupEntityFactory.buildWithId({ name: 'Initial Name' }, groupId); + + await em.persistAndFlush(groupEntity); + em.clear(); + + const newName = 'New Name'; + const group: Group = groupFactory.build({ id: groupId, name: newName }); + + return { + groupEntity, + group, + groupId, + newName, + }; + }; + + it('should update the entity', async () => { + const { group, groupId, newName } = await setup(); + + await repo.save(group); + + await expect(em.findOneOrFail(GroupEntity, groupId)).resolves.toEqual( + expect.objectContaining({ name: newName }) + ); + }); + + it('should return the object', async () => { + const { group } = await setup(); + + const result: Group = await repo.save(group); + + expect(result).toEqual(group); + }); + }); + }); + + describe('delete', () => { + describe('when an entity exists', () => { + const setup = async () => { + const groupId = new ObjectId().toHexString(); + const groupEntity: GroupEntity = groupEntityFactory.buildWithId(undefined, groupId); + + await em.persistAndFlush(groupEntity); + em.clear(); + + const group: Group = groupFactory.build({ id: groupId }); + + return { + group, + groupId, + }; + }; + + it('should delete the entity', async () => { + const { group, groupId } = await setup(); + + await repo.delete(group); + + expect(await em.findOne(GroupEntity, groupId)).toBeNull(); + }); + + it('should return true', async () => { + const { group } = await setup(); + + const result: boolean = await repo.delete(group); + + expect(result).toEqual(true); + }); + }); + + describe('when no entity exists', () => { + const setup = () => { + const group: Group = groupFactory.build(); + + return { + group, + }; + }; + + it('should return false', async () => { + const { group } = setup(); + + const result: boolean = await repo.delete(group); + + expect(result).toEqual(false); + }); + }); + }); + + describe('findByExternalSource', () => { + describe('when an entity with the external source exists', () => { + const setup = async () => { + const groupEntity: GroupEntity = groupEntityFactory.buildWithId(); + + await em.persistAndFlush(groupEntity); + em.clear(); + + return { + groupEntity, + }; + }; + + it('should return the group', async () => { + const { groupEntity } = await setup(); + + const result: Group | null = await repo.findByExternalSource( + groupEntity.externalSource?.externalId ?? '', + groupEntity.externalSource?.system.id ?? '' + ); + + expect(result?.getProps()).toEqual({ + id: groupEntity.id, + name: groupEntity.name, + type: GroupTypes.CLASS, + externalSource: new ExternalSource({ + externalId: groupEntity.externalSource?.externalId ?? '', + systemId: groupEntity.externalSource?.system.id ?? '', + }), + users: [ + new GroupUser({ + userId: groupEntity.users[0].user.id, + roleId: groupEntity.users[0].role.id, + }), + new GroupUser({ + userId: groupEntity.users[1].user.id, + roleId: groupEntity.users[1].role.id, + }), + ], + organizationId: groupEntity.organization?.id, + validFrom: groupEntity.validPeriod?.from, + validUntil: groupEntity.validPeriod?.until, + }); + }); + }); + + describe('when no entity with the external source exists', () => { + it('should return null', async () => { + const result: Group | null = await repo.findByExternalSource( + new ObjectId().toHexString(), + new ObjectId().toHexString() + ); + + expect(result).toBeNull(); + }); + }); + }); +}); diff --git a/apps/server/src/modules/group/repo/group.repo.ts b/apps/server/src/modules/group/repo/group.repo.ts new file mode 100644 index 00000000000..a5477908d6c --- /dev/null +++ b/apps/server/src/modules/group/repo/group.repo.ts @@ -0,0 +1,81 @@ +import { EntityManager } from '@mikro-orm/mongodb'; +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain/types'; +import { Group, GroupProps } from '../domain'; +import { GroupEntity, GroupEntityProps } from '../entity'; +import { GroupDomainMapper } from './group-domain.mapper'; + +@Injectable() +export class GroupRepo { + constructor(private readonly em: EntityManager) {} + + async findById(id: EntityId): Promise { + const entity: GroupEntity | null = await this.em.findOne(GroupEntity, { id }); + + if (!entity) { + return null; + } + + const props: GroupProps = GroupDomainMapper.mapEntityToDomainObjectProperties(entity); + + const domainObject: Group = new Group(props); + + return domainObject; + } + + async findByExternalSource(externalId: string, systemId: EntityId): Promise { + const entity: GroupEntity | null = await this.em.findOne(GroupEntity, { + externalSource: { + externalId, + system: systemId, + }, + }); + + if (!entity) { + return null; + } + + const props: GroupProps = GroupDomainMapper.mapEntityToDomainObjectProperties(entity); + + const domainObject: Group = new Group(props); + + return domainObject; + } + + async save(domainObject: Group): Promise { + const entityProps: GroupEntityProps = GroupDomainMapper.mapDomainObjectToEntityProperties(domainObject, this.em); + + const newEntity: GroupEntity = new GroupEntity(entityProps); + + const existingEntity: GroupEntity | null = await this.em.findOne(GroupEntity, { id: domainObject.id }); + + let savedEntity: GroupEntity; + if (existingEntity) { + savedEntity = this.em.assign(existingEntity, newEntity); + } else { + this.em.persist(newEntity); + + savedEntity = newEntity; + } + + await this.em.flush(); + + const savedProps: GroupProps = GroupDomainMapper.mapEntityToDomainObjectProperties(savedEntity); + + const savedDomainObject: Group = new Group(savedProps); + + return savedDomainObject; + } + + async delete(domainObject: Group): Promise { + const entity: GroupEntity | null = await this.em.findOne(GroupEntity, { id: domainObject.id }); + + if (!entity) { + return false; + } + + await this.em.removeAndFlush(entity); + + return true; + } +} diff --git a/apps/server/src/modules/group/repo/index.ts b/apps/server/src/modules/group/repo/index.ts new file mode 100644 index 00000000000..6933b9d64c5 --- /dev/null +++ b/apps/server/src/modules/group/repo/index.ts @@ -0,0 +1 @@ +export * from './group.repo'; diff --git a/apps/server/src/modules/group/service/group.service.spec.ts b/apps/server/src/modules/group/service/group.service.spec.ts new file mode 100644 index 00000000000..3bcc8fa287e --- /dev/null +++ b/apps/server/src/modules/group/service/group.service.spec.ts @@ -0,0 +1,207 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { NotFoundLoggableException } from '@shared/common/loggable-exception'; +import { groupFactory } from '@shared/testing'; +import { Group } from '../domain'; +import { GroupRepo } from '../repo'; +import { GroupService } from './group.service'; + +describe('GroupService', () => { + let module: TestingModule; + let service: GroupService; + + let groupRepo: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + GroupService, + { + provide: GroupRepo, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(GroupService); + groupRepo = module.get(GroupRepo); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('findById', () => { + describe('when a group with the id exists', () => { + const setup = () => { + const group: Group = groupFactory.build(); + + groupRepo.findById.mockResolvedValue(group); + + return { + group, + }; + }; + + it('should return the group', async () => { + const { group } = setup(); + + const result: Group = await service.findById(group.id); + + expect(result).toEqual(group); + }); + }); + + describe('when a group with the id does not exists', () => { + const setup = () => { + const group: Group = groupFactory.build(); + + groupRepo.findById.mockResolvedValue(null); + + return { + group, + }; + }; + + it('should throw NotFoundLoggableException', async () => { + const { group } = setup(); + + const func = () => service.findById(group.id); + + await expect(func).rejects.toThrow(NotFoundLoggableException); + }); + }); + }); + + describe('tryFindById', () => { + describe('when a group with the id exists', () => { + const setup = () => { + const group: Group = groupFactory.build(); + + groupRepo.findById.mockResolvedValue(group); + + return { + group, + }; + }; + + it('should return the group', async () => { + const { group } = setup(); + + const result: Group | null = await service.tryFindById(group.id); + + expect(result).toEqual(group); + }); + }); + + describe('when a group with the id does not exists', () => { + const setup = () => { + const group: Group = groupFactory.build(); + + groupRepo.findById.mockResolvedValue(null); + + return { + group, + }; + }; + + it('should return null', async () => { + const { group } = setup(); + + const result: Group | null = await service.tryFindById(group.id); + + expect(result).toBeNull(); + }); + }); + }); + + describe('save', () => { + describe('when saving a group', () => { + const setup = () => { + const group: Group = groupFactory.build(); + + groupRepo.save.mockResolvedValue(group); + + return { + group, + }; + }; + + it('should call repo.save', async () => { + const { group } = setup(); + + await service.save(group); + + expect(groupRepo.save).toHaveBeenCalledWith(group); + }); + + it('should return the group', async () => { + const { group } = setup(); + + const result: Group | null = await service.save(group); + + expect(result).toEqual(group); + }); + }); + }); + + describe('delete', () => { + describe('when saving a group', () => { + const setup = () => { + const group: Group = groupFactory.build(); + + return { + group, + }; + }; + + it('should call repo.delete', async () => { + const { group } = setup(); + + await service.delete(group); + + expect(groupRepo.delete).toHaveBeenCalledWith(group); + }); + }); + }); + + describe('findByExternalSource', () => { + describe('when a group with the externalId exists', () => { + const setup = () => { + const group: Group = groupFactory.build(); + + groupRepo.findByExternalSource.mockResolvedValue(group); + + return { + group, + }; + }; + + it('should return the group', async () => { + const { group } = setup(); + + const result: Group | null = await service.findByExternalSource('externalId', 'systemId'); + + expect(result).toEqual(group); + }); + }); + + describe('when a group with the externalId does not exists', () => { + const setup = () => { + groupRepo.findByExternalSource.mockResolvedValue(null); + }; + + it('should return null', async () => { + setup(); + + const result: Group | null = await service.findByExternalSource('externalId', 'systemId'); + + expect(result).toBeNull(); + }); + }); + }); +}); diff --git a/apps/server/src/modules/group/service/group.service.ts b/apps/server/src/modules/group/service/group.service.ts new file mode 100644 index 00000000000..030f3eb6685 --- /dev/null +++ b/apps/server/src/modules/group/service/group.service.ts @@ -0,0 +1,43 @@ +import { Injectable } from '@nestjs/common'; +import { NotFoundLoggableException } from '@shared/common/loggable-exception'; +import { EntityId } from '@shared/domain'; +import { AuthorizationLoaderServiceGeneric } from '@src/modules/authorization'; +import { Group } from '../domain'; +import { GroupRepo } from '../repo'; + +@Injectable() +export class GroupService implements AuthorizationLoaderServiceGeneric { + constructor(private readonly groupRepo: GroupRepo) {} + + async findById(id: EntityId): Promise { + const group: Group | null = await this.groupRepo.findById(id); + + if (!group) { + throw new NotFoundLoggableException(Group.name, 'id', id); + } + + return group; + } + + async findByExternalSource(externalId: string, systemId: EntityId): Promise { + const group: Group | null = await this.groupRepo.findByExternalSource(externalId, systemId); + + return group; + } + + async tryFindById(id: EntityId): Promise { + const group: Group | null = await this.groupRepo.findById(id); + + return group; + } + + async save(group: Group): Promise { + const savedGroup: Group = await this.groupRepo.save(group); + + return savedGroup; + } + + async delete(group: Group): Promise { + await this.groupRepo.delete(group); + } +} diff --git a/apps/server/src/modules/group/service/index.ts b/apps/server/src/modules/group/service/index.ts new file mode 100644 index 00000000000..ee9dd3ff064 --- /dev/null +++ b/apps/server/src/modules/group/service/index.ts @@ -0,0 +1 @@ +export * from './group.service'; diff --git a/apps/server/src/modules/oauth-provider/service/id-token.service.spec.ts b/apps/server/src/modules/oauth-provider/service/id-token.service.spec.ts index 07b9d21f8d1..454874571f9 100644 --- a/apps/server/src/modules/oauth-provider/service/id-token.service.spec.ts +++ b/apps/server/src/modules/oauth-provider/service/id-token.service.spec.ts @@ -3,16 +3,16 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { Test, TestingModule } from '@nestjs/testing'; import { Pseudonym, TeamEntity, UserDO } from '@shared/domain'; import { TeamsRepo } from '@shared/repo'; -import { externalToolFactory, setupEntities, userDoFactory, pseudonymFactory } from '@shared/testing'; +import { externalToolFactory, pseudonymFactory, setupEntities, userDoFactory } from '@shared/testing'; import { teamFactory } from '@shared/testing/factory/team.factory'; import { IdToken } from '@src/modules/oauth-provider/interface/id-token'; import { OauthScope } from '@src/modules/oauth-provider/interface/oauth-scope.enum'; import { IdTokenService } from '@src/modules/oauth-provider/service/id-token.service'; import { PseudonymService } from '@src/modules/pseudonym/service'; -import { UserService } from '@src/modules/user/service/user.service'; import { ExternalTool } from '@src/modules/tool/external-tool/domain'; -import { OauthProviderLoginFlowService } from './oauth-provider.login-flow.service'; +import { UserService } from '@src/modules/user/service/user.service'; import { IdTokenCreationLoggableException } from '../error/id-token-creation-exception.loggable'; +import { OauthProviderLoginFlowService } from './oauth-provider.login-flow.service'; import resetAllMocks = jest.resetAllMocks; describe('IdTokenService', () => { @@ -85,7 +85,7 @@ describe('IdTokenService', () => { const tool: ExternalTool = externalToolFactory.withOauth2Config().buildWithId(); - const pseudonym: Pseudonym = pseudonymFactory.buildWithId({ pseudonym: 'pseudonym' }); + const pseudonym: Pseudonym = pseudonymFactory.build({ pseudonym: 'pseudonym' }); userService.findById.mockResolvedValue(user); userService.getDisplayName.mockResolvedValue(displayName); @@ -123,7 +123,7 @@ describe('IdTokenService', () => { const tool: ExternalTool = externalToolFactory.withOauth2Config().buildWithId(); - const pseudonym: Pseudonym = pseudonymFactory.buildWithId({ pseudonym: 'pseudonym' }); + const pseudonym: Pseudonym = pseudonymFactory.build({ pseudonym: 'pseudonym' }); teamsRepo.findByUserId.mockResolvedValue([team]); userService.findById.mockResolvedValue(user); @@ -167,7 +167,7 @@ describe('IdTokenService', () => { const tool: ExternalTool = externalToolFactory.withOauth2Config().buildWithId(); - const pseudonym: Pseudonym = pseudonymFactory.buildWithId({ pseudonym: 'pseudonym' }); + const pseudonym: Pseudonym = pseudonymFactory.build({ pseudonym: 'pseudonym' }); userService.findById.mockResolvedValue(user); userService.getDisplayName.mockResolvedValue(displayName); @@ -204,7 +204,7 @@ describe('IdTokenService', () => { const tool: ExternalTool = externalToolFactory.withOauth2Config().buildWithId(); - const pseudonym: Pseudonym = pseudonymFactory.buildWithId({ pseudonym: 'pseudonym' }); + const pseudonym: Pseudonym = pseudonymFactory.build({ pseudonym: 'pseudonym' }); userService.findById.mockResolvedValue(user); userService.getDisplayName.mockResolvedValue(displayName); @@ -242,7 +242,7 @@ describe('IdTokenService', () => { const tool: ExternalTool = externalToolFactory.withOauth2Config().build({ id: undefined }); - const pseudonym: Pseudonym = pseudonymFactory.buildWithId({ pseudonym: 'pseudonym' }); + const pseudonym: Pseudonym = pseudonymFactory.build({ pseudonym: 'pseudonym' }); userService.findById.mockResolvedValue(user); userService.getDisplayName.mockResolvedValue(displayName); diff --git a/apps/server/src/modules/oauth/controller/api-test/oauth-sso.api.spec.ts b/apps/server/src/modules/oauth/controller/api-test/oauth-sso.api.spec.ts index f5a2756d149..af29ff68b08 100644 --- a/apps/server/src/modules/oauth/controller/api-test/oauth-sso.api.spec.ts +++ b/apps/server/src/modules/oauth/controller/api-test/oauth-sso.api.spec.ts @@ -18,7 +18,7 @@ import { JwtTestFactory } from '@shared/testing/factory/jwt.test.factory'; import { userLoginMigrationFactory } from '@shared/testing/factory/user-login-migration.factory'; import { ICurrentUser } from '@src/modules/authentication'; import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { SanisResponse, SanisRole } from '@src/modules/provisioning'; +import { SanisResponse, SanisRole } from '@src/modules/provisioning/strategy/sanis/response'; import { ServerTestModule } from '@src/modules/server'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; @@ -292,16 +292,15 @@ describe('OAuth SSO Controller (API)', () => { }, personenkontexte: [ { - id: new UUID('aef1f4fd-c323-466e-962b-a84354c0e713'), + id: new UUID('aef1f4fd-c323-466e-962b-a84354c0e713').toString(), rolle: SanisRole.LEHR, organisation: { - id: new UUID('aef1f4fd-c323-466e-962b-a84354c0e713'), + id: new UUID('aef1f4fd-c323-466e-962b-a84354c0e713').toString(), kennung: officialSchoolNumber, name: 'schulName', typ: 'not necessary', }, personenstatus: 'not necessary', - email: 'email', }, ], }); diff --git a/apps/server/src/modules/oauth/service/hydra.service.spec.ts b/apps/server/src/modules/oauth/service/hydra.service.spec.ts index 7f17d1e72c9..4912bc039ca 100644 --- a/apps/server/src/modules/oauth/service/hydra.service.spec.ts +++ b/apps/server/src/modules/oauth/service/hydra.service.spec.ts @@ -8,11 +8,12 @@ import { LtiPrivacyPermission, LtiRoleType, OauthConfig } from '@shared/domain'; import { LtiToolDO } from '@shared/domain/domainobject/ltitool.do'; import { DefaultEncryptionService, SymetricKeyEncryptionService } from '@shared/infra/encryption'; import { LtiToolRepo } from '@shared/repo'; +import { axiosResponseFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { CookiesDto } from '@src/modules/oauth/service/dto/cookies.dto'; import { HydraRedirectDto } from '@src/modules/oauth/service/dto/hydra.redirect.dto'; import { HydraSsoService } from '@src/modules/oauth/service/hydra.service'; -import { AxiosRequestConfig, AxiosResponse } from 'axios'; +import { AxiosResponse } from 'axios'; import { of } from 'rxjs'; import { StatelessAuthorizationParams } from '../controller/dto/stateless-authorization.params'; @@ -28,15 +29,10 @@ jest.mock('nanoid', () => { }; }); -const createAxiosResponse = (data: T): AxiosResponse => { - return { +const createAxiosResponse = (data: T) => + axiosResponseFactory.build({ data, - status: 0, - statusText: '', - headers: {}, - config: {}, - }; -}; + }); describe('HydraService', () => { let module: TestingModule; @@ -133,8 +129,7 @@ describe('HydraService', () => { const expectedAuthParams: StatelessAuthorizationParams = { code: 'defaultAuthCode', }; - const axiosConfig: AxiosRequestConfig = { - headers: {}, + const axiosConfig = { withCredentials: true, maxRedirects: 0, validateStatus: jest.fn().mockImplementationOnce(() => true), @@ -145,7 +140,7 @@ describe('HydraService', () => { let responseDto2: HydraRedirectDto; it('should process a local request', async () => { - axiosResponse1 = { + axiosResponse1 = axiosResponseFactory.build({ data: expectedAuthParams, status: 302, statusText: '', @@ -154,7 +149,7 @@ describe('HydraService', () => { Referer: 'hydra', }, config: axiosConfig, - }; + }); responseDto1 = { axiosConfig, cookies: { localCookies: [], hydraCookies: [] }, @@ -169,11 +164,14 @@ describe('HydraService', () => { const resDto: HydraRedirectDto = await service.processRedirect(responseDto1); // Assert - expect(httpService.get).toHaveBeenCalledWith(`${apiHost}${axiosResponse1.headers.location}`, axiosConfig); + expect(httpService.get).toHaveBeenCalledWith( + `${apiHost}${axiosResponse1.headers.location as string}`, + axiosConfig + ); expect(resDto.response.data).toEqual(expectedAuthParams); }); it('should process a hydra request', async () => { - axiosResponse2 = { + axiosResponse2 = axiosResponseFactory.build({ data: expectedAuthParams, status: 200, statusText: '', @@ -182,7 +180,7 @@ describe('HydraService', () => { Referer: 'hydra', }, config: axiosConfig, - }; + }); responseDto2 = { axiosConfig, cookies: { localCookies: [], hydraCookies: [] }, @@ -196,7 +194,7 @@ describe('HydraService', () => { const resDto: HydraRedirectDto = await service.processRedirect(responseDto2); // Assert - expect(httpService.get).toHaveBeenCalledWith(`${axiosResponse2.headers.location}`, axiosConfig); + expect(httpService.get).toHaveBeenCalledWith(`${axiosResponse2.headers.location as string}`, axiosConfig); expect(resDto.response.data).toEqual(expectedAuthParams); }); }); diff --git a/apps/server/src/modules/oauth/service/hydra.service.ts b/apps/server/src/modules/oauth/service/hydra.service.ts index 477b87f4fd4..94ab8c66ff8 100644 --- a/apps/server/src/modules/oauth/service/hydra.service.ts +++ b/apps/server/src/modules/oauth/service/hydra.service.ts @@ -1,19 +1,19 @@ +import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { HttpService } from '@nestjs/axios'; +import { Inject, InternalServerErrorException } from '@nestjs/common'; import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; import { OauthConfig } from '@shared/domain'; -import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { LtiToolRepo } from '@shared/repo'; import { LtiToolDO } from '@shared/domain/domainobject/ltitool.do'; -import { Inject, InternalServerErrorException } from '@nestjs/common'; +import { DefaultEncryptionService, IEncryptionService } from '@shared/infra/encryption'; +import { LtiToolRepo } from '@shared/repo'; +import { LegacyLogger } from '@src/core/logger'; import { AuthorizationParams } from '@src/modules/oauth/controller/dto/authorization.params'; +import { CookiesDto } from '@src/modules/oauth/service/dto/cookies.dto'; +import { HydraRedirectDto } from '@src/modules/oauth/service/dto/hydra.redirect.dto'; import { AxiosRequestConfig, AxiosResponse } from 'axios'; -import QueryString from 'qs'; -import { HttpService } from '@nestjs/axios'; import { nanoid } from 'nanoid'; -import { firstValueFrom, Observable } from 'rxjs'; -import { HydraRedirectDto } from '@src/modules/oauth/service/dto/hydra.redirect.dto'; -import { CookiesDto } from '@src/modules/oauth/service/dto/cookies.dto'; -import { DefaultEncryptionService, IEncryptionService } from '@shared/infra/encryption'; -import { LegacyLogger } from '@src/core/logger'; +import QueryString from 'qs'; +import { Observable, firstValueFrom } from 'rxjs'; @Injectable() export class HydraSsoService { @@ -42,7 +42,12 @@ export class HydraSsoService { async processRedirect(dto: HydraRedirectDto): Promise { const localDto: HydraRedirectDto = new HydraRedirectDto(dto); - let { location } = localDto.response.headers; + let location = ''; + + if (typeof localDto.response.headers.location === 'string') { + ({ location } = localDto.response.headers); + } + const isLocal = !location.startsWith('http'); const isHydra = location.startsWith(Configuration.get('HYDRA_PUBLIC_URI') as string); diff --git a/apps/server/src/modules/oauth/service/oauth-adapter.service.spec.ts b/apps/server/src/modules/oauth/service/oauth-adapter.service.spec.ts index e0518af7987..897ca989c04 100644 --- a/apps/server/src/modules/oauth/service/oauth-adapter.service.spec.ts +++ b/apps/server/src/modules/oauth/service/oauth-adapter.service.spec.ts @@ -1,8 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { HttpService } from '@nestjs/axios'; import { Test, TestingModule } from '@nestjs/testing'; +import { axiosResponseFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { AxiosResponse } from 'axios'; import { of, throwError } from 'rxjs'; import { OAuthSSOError } from '../error/oauth-sso.error'; import { OAuthGrantType } from '../interface/oauth-grant-type.enum'; @@ -11,15 +11,10 @@ import { OauthAdapterService } from './oauth-adapter.service'; const publicKey = 'publicKey'; -const createAxiosResponse = (data: T): AxiosResponse => { - return { +const createAxiosResponse = (data: T) => + axiosResponseFactory.build({ data, - status: 0, - statusText: '', - headers: {}, - config: {}, - }; -}; + }); jest.mock('jwks-rsa', () => () => { return { diff --git a/apps/server/src/modules/oauth/service/oauth.service.spec.ts b/apps/server/src/modules/oauth/service/oauth.service.spec.ts index 0f3e16313f0..c092987b153 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.spec.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.spec.ts @@ -1,15 +1,18 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons'; import { Test, TestingModule } from '@nestjs/testing'; -import { LegacySchoolDo, OauthConfig, SchoolFeatures, System } from '@shared/domain'; +import { OauthConfig, SchoolFeatures, System } from '@shared/domain'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; import { UserDO } from '@shared/domain/domainobject/user.do'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { DefaultEncryptionService, IEncryptionService, SymetricKeyEncryptionService } from '@shared/infra/encryption'; -import { legacySchoolDoFactory, setupEntities, systemFactory, userDoFactory } from '@shared/testing'; +import { setupEntities, userDoFactory } from '@shared/testing'; +import { schoolDOFactory } from '@shared/testing/factory/domainobject/school.factory'; +import { systemFactory } from '@shared/testing/factory/system.factory'; import { LegacyLogger } from '@src/core/logger'; import { ProvisioningDto, ProvisioningService } from '@src/modules/provisioning'; import { ExternalSchoolDto, ExternalUserDto, OauthDataDto, ProvisioningSystemDto } from '@src/modules/provisioning/dto'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { OauthConfigDto } from '@src/modules/system/service'; import { SystemDto } from '@src/modules/system/service/dto/system.dto'; import { SystemService } from '@src/modules/system/service/system.service'; @@ -48,7 +51,7 @@ describe('OAuthService', () => { let userMigrationService: DeepMocked; let oauthAdapterService: DeepMocked; let migrationCheckService: DeepMocked; - let schoolService: DeepMocked; + let schoolService: DeepMocked; let testSystem: System; let testOauthConfig: OauthConfig; @@ -66,8 +69,8 @@ describe('OAuthService', () => { useValue: createMock(), }, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolService, + useValue: createMock(), }, { provide: DefaultEncryptionService, @@ -108,7 +111,7 @@ describe('OAuthService', () => { userMigrationService = module.get(UserMigrationService); oauthAdapterService = module.get(OauthAdapterService); migrationCheckService = module.get(MigrationCheckService); - schoolService = module.get(LegacySchoolService); + schoolService = module.get(SchoolService); }); afterAll(async () => { @@ -424,9 +427,7 @@ describe('OAuthService', () => { officialSchoolNumber: 'officialSchoolNumber', }), }); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ - features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], - }); + const school: SchoolDO = schoolDOFactory.buildWithId({ features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED] }); provisioningService.getData.mockResolvedValue(oauthData); schoolService.getSchoolBySchoolNumber.mockResolvedValue(school); @@ -466,7 +467,7 @@ describe('OAuthService', () => { officialSchoolNumber: 'officialSchoolNumber', }), }); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ features: [] }); + const school: SchoolDO = schoolDOFactory.buildWithId({ features: [] }); provisioningService.getData.mockResolvedValue(oauthData); schoolService.getSchoolBySchoolNumber.mockResolvedValue(school); @@ -512,7 +513,7 @@ describe('OAuthService', () => { officialSchoolNumber: 'officialSchoolNumber', }), }); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ features: [] }); + const school: SchoolDO = schoolDOFactory.buildWithId({ features: [] }); provisioningService.getData.mockResolvedValue(oauthData); schoolService.getSchoolBySchoolNumber.mockResolvedValue(school); diff --git a/apps/server/src/modules/oauth/service/oauth.service.ts b/apps/server/src/modules/oauth/service/oauth.service.ts index d17dca6c734..bb989486986 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.ts @@ -1,12 +1,14 @@ import { Configuration } from '@hpi-schul-cloud/commons'; import { Inject } from '@nestjs/common'; import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; -import { EntityId, LegacySchoolDo, OauthConfig, SchoolFeatures, UserDO } from '@shared/domain'; +import { EntityId, OauthConfig, SchoolFeatures } from '@shared/domain'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; +import { UserDO } from '@shared/domain/domainobject/user.do'; import { DefaultEncryptionService, IEncryptionService } from '@shared/infra/encryption'; import { LegacyLogger } from '@src/core/logger'; import { ProvisioningService } from '@src/modules/provisioning'; import { OauthDataDto } from '@src/modules/provisioning/dto'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { SystemService } from '@src/modules/system'; import { SystemDto } from '@src/modules/system/service'; import { UserService } from '@src/modules/user'; @@ -29,7 +31,7 @@ export class OAuthService { private readonly systemService: SystemService, private readonly userMigrationService: UserMigrationService, private readonly migrationCheckService: MigrationCheckService, - private readonly schoolService: LegacySchoolService + private readonly schoolService: SchoolService ) { this.logger.setContext(OAuthService.name); } @@ -130,7 +132,7 @@ export class OAuthService { } async isOauthProvisioningEnabledForSchool(officialSchoolNumber: string): Promise { - const school: LegacySchoolDo | null = await this.schoolService.getSchoolBySchoolNumber(officialSchoolNumber); + const school: SchoolDO | null = await this.schoolService.getSchoolBySchoolNumber(officialSchoolNumber); if (!school) { return true; diff --git a/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts b/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts index 8e4df083634..ffcc9abc393 100644 --- a/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts +++ b/apps/server/src/modules/oauth/uc/hydra-oauth.uc.spec.ts @@ -4,11 +4,12 @@ import { HttpModule } from '@nestjs/axios'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { OauthConfig } from '@shared/domain'; +import { axiosResponseFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { HydraRedirectDto } from '@src/modules/oauth/service/dto/hydra.redirect.dto'; import { HydraSsoService } from '@src/modules/oauth/service/hydra.service'; import { OAuthService } from '@src/modules/oauth/service/oauth.service'; -import { AxiosRequestConfig, AxiosResponse } from 'axios'; +import { AxiosResponse } from 'axios'; import { HydraOauthUc } from '.'; import { AuthorizationParams } from '../controller/dto'; import { StatelessAuthorizationParams } from '../controller/dto/stateless-authorization.params'; @@ -142,7 +143,6 @@ describe('HydraOauthUc', () => { describe('requestAuthCode', () => { let expectedAuthParams: StatelessAuthorizationParams; - let axiosConfig: AxiosRequestConfig; let axiosResponse1: AxiosResponse; let axiosResponse2: AxiosResponse; let responseDto1: HydraRedirectDto; @@ -152,13 +152,12 @@ describe('HydraOauthUc', () => { expectedAuthParams = { code: 'defaultAuthCode', }; - axiosConfig = { - headers: {}, + const axiosConfig = { withCredentials: true, maxRedirects: 0, validateStatus: jest.fn().mockImplementationOnce(() => true), }; - axiosResponse1 = { + axiosResponse1 = axiosResponseFactory.build({ data: expectedAuthParams, status: 302, statusText: '', @@ -167,8 +166,8 @@ describe('HydraOauthUc', () => { Referer: 'hydra', }, config: axiosConfig, - }; - axiosResponse2 = { + }); + axiosResponse2 = axiosResponseFactory.build({ data: expectedAuthParams, status: 200, statusText: '', @@ -177,7 +176,7 @@ describe('HydraOauthUc', () => { Referer: 'hydra', }, config: axiosConfig, - }; + }); responseDto1 = { axiosConfig, cookies: { localCookies: [], hydraCookies: [] }, diff --git a/apps/server/src/modules/oauth/uc/oauth.uc.spec.ts b/apps/server/src/modules/oauth/uc/oauth.uc.spec.ts index bad68fa190f..ca5fa3a5e1e 100644 --- a/apps/server/src/modules/oauth/uc/oauth.uc.spec.ts +++ b/apps/server/src/modules/oauth/uc/oauth.uc.spec.ts @@ -1,10 +1,11 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { UnauthorizedException, UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { LegacySchoolDo, UserDO } from '@shared/domain'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; +import { UserDO } from '@shared/domain/domainobject/user.do'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { ISession } from '@shared/domain/types/session'; -import { legacySchoolDoFactory, setupEntities } from '@shared/testing'; +import { schoolDOFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { ICurrentUser } from '@src/modules/authentication'; import { AuthenticationService } from '@src/modules/authentication/services/authentication.service'; @@ -12,7 +13,7 @@ import { OAuthSSOError } from '@src/modules/oauth/error/oauth-sso.error'; import { OauthUc } from '@src/modules/oauth/uc/oauth.uc'; import { ProvisioningService } from '@src/modules/provisioning'; import { ExternalUserDto, OauthDataDto, ProvisioningSystemDto } from '@src/modules/provisioning/dto'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { SystemService } from '@src/modules/system'; import { OauthConfigDto, SystemDto } from '@src/modules/system/service'; import { UserService } from '@src/modules/user'; @@ -76,8 +77,8 @@ describe('OAuthUc', () => { useValue: createMock(), }, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolService, + useValue: createMock(), }, { provide: UserMigrationService, @@ -658,7 +659,7 @@ describe('OAuthUc', () => { oauthService.requestToken.mockResolvedValue(tokenDto); provisioningService.getData.mockResolvedValue(oauthData); - const schoolToMigrate: LegacySchoolDo | void = legacySchoolDoFactory.build({ name: 'mockName' }); + const schoolToMigrate: SchoolDO | void = schoolDOFactory.build({ name: 'mockName' }); oauthService.authenticateUser.mockResolvedValue(tokenDto); schoolMigrationService.schoolToMigrate.mockResolvedValue(schoolToMigrate); userMigrationService.migrateUser.mockResolvedValue(userMigrationDto); diff --git a/apps/server/src/modules/oauth/uc/oauth.uc.ts b/apps/server/src/modules/oauth/uc/oauth.uc.ts index e4dd7e68264..c153501e338 100644 --- a/apps/server/src/modules/oauth/uc/oauth.uc.ts +++ b/apps/server/src/modules/oauth/uc/oauth.uc.ts @@ -1,5 +1,7 @@ import { Injectable, UnauthorizedException, UnprocessableEntityException } from '@nestjs/common'; -import { EntityId, LegacySchoolDo, UserDO } from '@shared/domain'; +import { EntityId } from '@shared/domain'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; +import { UserDO } from '@shared/domain/domainobject/user.do'; import { ISession } from '@shared/domain/types/session'; import { LegacyLogger } from '@src/core/logger'; import { ICurrentUser } from '@src/modules/authentication'; @@ -118,7 +120,7 @@ export class OauthUc { const data: OauthDataDto = await this.provisioningService.getData(systemId, tokenDto.idToken, tokenDto.accessToken); if (data.externalSchool) { - const schoolToMigrate: LegacySchoolDo | null = await this.schoolMigrationService.schoolToMigrate( + const schoolToMigrate: SchoolDO | null = await this.schoolMigrationService.schoolToMigrate( currentUserId, data.externalSchool.externalId, data.externalSchool.officialSchoolNumber diff --git a/apps/server/src/modules/provisioning/dto/external-group-user.dto.ts b/apps/server/src/modules/provisioning/dto/external-group-user.dto.ts new file mode 100644 index 00000000000..16ed7440319 --- /dev/null +++ b/apps/server/src/modules/provisioning/dto/external-group-user.dto.ts @@ -0,0 +1,12 @@ +import { RoleName } from '@shared/domain'; + +export class ExternalGroupUserDto { + externalUserId: string; + + roleName: RoleName; + + constructor(props: ExternalGroupUserDto) { + this.externalUserId = props.externalUserId; + this.roleName = props.roleName; + } +} diff --git a/apps/server/src/modules/provisioning/dto/external-group.dto.ts b/apps/server/src/modules/provisioning/dto/external-group.dto.ts new file mode 100644 index 00000000000..57cdc78e44c --- /dev/null +++ b/apps/server/src/modules/provisioning/dto/external-group.dto.ts @@ -0,0 +1,28 @@ +import { GroupTypes } from '@src/modules/group'; +import { ExternalGroupUserDto } from './external-group-user.dto'; + +export class ExternalGroupDto { + externalId: string; + + name: string; + + users: ExternalGroupUserDto[]; + + from: Date; + + until: Date; + + type: GroupTypes; + + externalOrganizationId?: string; + + constructor(props: ExternalGroupDto) { + this.externalId = props.externalId; + this.name = props.name; + this.users = props.users; + this.from = props.from; + this.until = props.until; + this.type = props.type; + this.externalOrganizationId = props.externalOrganizationId; + } +} diff --git a/apps/server/src/modules/provisioning/dto/index.ts b/apps/server/src/modules/provisioning/dto/index.ts index 11244218dfc..f88d98aa90f 100644 --- a/apps/server/src/modules/provisioning/dto/index.ts +++ b/apps/server/src/modules/provisioning/dto/index.ts @@ -4,3 +4,5 @@ export * from './provisioning-system.dto'; export * from './external-school.dto'; export * from './external-user.dto'; export * from './oauth-data.dto'; +export * from './external-group.dto'; +export * from './external-group-user.dto'; diff --git a/apps/server/src/modules/provisioning/dto/oauth-data.dto.ts b/apps/server/src/modules/provisioning/dto/oauth-data.dto.ts index e418233ea67..16e09659d46 100644 --- a/apps/server/src/modules/provisioning/dto/oauth-data.dto.ts +++ b/apps/server/src/modules/provisioning/dto/oauth-data.dto.ts @@ -1,6 +1,7 @@ import { ExternalUserDto } from './external-user.dto'; import { ExternalSchoolDto } from './external-school.dto'; import { ProvisioningSystemDto } from './provisioning-system.dto'; +import { ExternalGroupDto } from './external-group.dto'; export class OauthDataDto { system: ProvisioningSystemDto; @@ -9,9 +10,12 @@ export class OauthDataDto { externalSchool?: ExternalSchoolDto; + externalGroups?: ExternalGroupDto[]; + constructor(props: OauthDataDto) { this.system = props.system; this.externalUser = props.externalUser; this.externalSchool = props.externalSchool; + this.externalGroups = props.externalGroups; } } diff --git a/apps/server/src/modules/provisioning/loggable/group-role-unknown.loggable.spec.ts b/apps/server/src/modules/provisioning/loggable/group-role-unknown.loggable.spec.ts new file mode 100644 index 00000000000..04b2ad3cba5 --- /dev/null +++ b/apps/server/src/modules/provisioning/loggable/group-role-unknown.loggable.spec.ts @@ -0,0 +1,50 @@ +import { SanisGroupRole, SanisSonstigeGruppenzugehoerigeResponse } from '../strategy/sanis/response'; +import { GroupRoleUnknownLoggable } from './group-role-unknown.loggable'; + +describe('GroupRoleUnknownLoggable', () => { + describe('constructor', () => { + const setup = () => { + const sanisSonstigeGruppenzugehoerigeResponse: SanisSonstigeGruppenzugehoerigeResponse = { + ktid: 'ktid', + rollen: [SanisGroupRole.TEACHER], + }; + + return { sanisSonstigeGruppenzugehoerigeResponse }; + }; + + it('should create an instance of UserForGroupNotFoundLoggable', () => { + const { sanisSonstigeGruppenzugehoerigeResponse } = setup(); + + const loggable = new GroupRoleUnknownLoggable(sanisSonstigeGruppenzugehoerigeResponse); + + expect(loggable).toBeInstanceOf(GroupRoleUnknownLoggable); + }); + }); + + describe('getLogMessage', () => { + const setup = () => { + const sanisSonstigeGruppenzugehoerigeResponse: SanisSonstigeGruppenzugehoerigeResponse = { + ktid: 'ktid', + rollen: [SanisGroupRole.TEACHER], + }; + + const loggable = new GroupRoleUnknownLoggable(sanisSonstigeGruppenzugehoerigeResponse); + + return { loggable, sanisSonstigeGruppenzugehoerigeResponse }; + }; + + it('should return a loggable message', () => { + const { loggable, sanisSonstigeGruppenzugehoerigeResponse } = setup(); + + const message = loggable.getLogMessage(); + + expect(message).toEqual({ + message: 'Unable to add unknown user to group during provisioning.', + data: { + externalUserId: sanisSonstigeGruppenzugehoerigeResponse.ktid, + externalRoleName: sanisSonstigeGruppenzugehoerigeResponse.rollen[0], + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/provisioning/loggable/group-role-unknown.loggable.ts b/apps/server/src/modules/provisioning/loggable/group-role-unknown.loggable.ts new file mode 100644 index 00000000000..a146a7011b3 --- /dev/null +++ b/apps/server/src/modules/provisioning/loggable/group-role-unknown.loggable.ts @@ -0,0 +1,16 @@ +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; +import { SanisSonstigeGruppenzugehoerigeResponse } from '../strategy/sanis/response'; + +export class GroupRoleUnknownLoggable implements Loggable { + constructor(private readonly relation: SanisSonstigeGruppenzugehoerigeResponse) {} + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + message: 'Unable to add unknown user to group during provisioning.', + data: { + externalUserId: this.relation.ktid, + externalRoleName: this.relation.rollen[0], + }, + }; + } +} diff --git a/apps/server/src/modules/provisioning/loggable/index.ts b/apps/server/src/modules/provisioning/loggable/index.ts new file mode 100644 index 00000000000..a790e846f2d --- /dev/null +++ b/apps/server/src/modules/provisioning/loggable/index.ts @@ -0,0 +1,3 @@ +export * from './user-for-group-not-found.loggable'; +export * from './school-for-group-not-found.loggable'; +export * from './group-role-unknown.loggable'; diff --git a/apps/server/src/modules/provisioning/loggable/school-for-group-not-found.loggable.spec.ts b/apps/server/src/modules/provisioning/loggable/school-for-group-not-found.loggable.spec.ts new file mode 100644 index 00000000000..205c6481529 --- /dev/null +++ b/apps/server/src/modules/provisioning/loggable/school-for-group-not-found.loggable.spec.ts @@ -0,0 +1,45 @@ +import { externalGroupDtoFactory } from '@shared/testing/factory/external-group-dto.factory'; +import { ExternalGroupDto } from '../dto'; +import { SchoolForGroupNotFoundLoggable } from './school-for-group-not-found.loggable'; + +describe('SchoolForGroupNotFoundLoggable', () => { + describe('constructor', () => { + const setup = () => { + const externalGroupDto: ExternalGroupDto = externalGroupDtoFactory.build(); + + return { externalGroupDto }; + }; + + it('should create an instance of UserForGroupNotFoundLoggable', () => { + const { externalGroupDto } = setup(); + + const loggable = new SchoolForGroupNotFoundLoggable(externalGroupDto); + + expect(loggable).toBeInstanceOf(SchoolForGroupNotFoundLoggable); + }); + }); + + describe('getLogMessage', () => { + const setup = () => { + const externalGroupDto: ExternalGroupDto = externalGroupDtoFactory.build(); + + const loggable = new SchoolForGroupNotFoundLoggable(externalGroupDto); + + return { loggable, externalGroupDto }; + }; + + it('should return a loggable message', () => { + const { loggable, externalGroupDto } = setup(); + + const message = loggable.getLogMessage(); + + expect(message).toEqual({ + message: 'Unable to provision group, since the connected school cannot be found.', + data: { + externalGroupId: externalGroupDto.externalId, + externalOrganizationId: externalGroupDto.externalOrganizationId, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/provisioning/loggable/school-for-group-not-found.loggable.ts b/apps/server/src/modules/provisioning/loggable/school-for-group-not-found.loggable.ts new file mode 100644 index 00000000000..5fd8dd1f59e --- /dev/null +++ b/apps/server/src/modules/provisioning/loggable/school-for-group-not-found.loggable.ts @@ -0,0 +1,16 @@ +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; +import { ExternalGroupDto } from '../dto'; + +export class SchoolForGroupNotFoundLoggable implements Loggable { + constructor(private readonly group: ExternalGroupDto) {} + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + message: 'Unable to provision group, since the connected school cannot be found.', + data: { + externalGroupId: this.group.externalId, + externalOrganizationId: this.group.externalOrganizationId, + }, + }; + } +} diff --git a/apps/server/src/modules/provisioning/loggable/user-for-group-not-found.loggable.spec.ts b/apps/server/src/modules/provisioning/loggable/user-for-group-not-found.loggable.spec.ts new file mode 100644 index 00000000000..48728a17081 --- /dev/null +++ b/apps/server/src/modules/provisioning/loggable/user-for-group-not-found.loggable.spec.ts @@ -0,0 +1,51 @@ +import { RoleName } from '@shared/domain'; +import { UserForGroupNotFoundLoggable } from './user-for-group-not-found.loggable'; +import { ExternalGroupUserDto } from '../dto'; + +describe('UserForGroupNotFoundLoggable', () => { + describe('constructor', () => { + const setup = () => { + const externalGroupUserDto: ExternalGroupUserDto = new ExternalGroupUserDto({ + externalUserId: 'externalUserId', + roleName: RoleName.TEACHER, + }); + + return { externalGroupUserDto }; + }; + + it('should create an instance of UserForGroupNotFoundLoggable', () => { + const { externalGroupUserDto } = setup(); + + const loggable = new UserForGroupNotFoundLoggable(externalGroupUserDto); + + expect(loggable).toBeInstanceOf(UserForGroupNotFoundLoggable); + }); + }); + + describe('getLogMessage', () => { + const setup = () => { + const externalGroupUserDto: ExternalGroupUserDto = new ExternalGroupUserDto({ + externalUserId: 'externalUserId', + roleName: RoleName.TEACHER, + }); + + const loggable = new UserForGroupNotFoundLoggable(externalGroupUserDto); + + return { loggable, externalGroupUserDto }; + }; + + it('should return a loggable message', () => { + const { loggable, externalGroupUserDto } = setup(); + + const message = loggable.getLogMessage(); + + expect(message).toEqual({ + message: 'Unable to add unknown user to group during provisioning.', + data: { + externalUserId: externalGroupUserDto.externalUserId, + roleName: externalGroupUserDto.roleName, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/provisioning/loggable/user-for-group-not-found.loggable.ts b/apps/server/src/modules/provisioning/loggable/user-for-group-not-found.loggable.ts new file mode 100644 index 00000000000..1e8b6792969 --- /dev/null +++ b/apps/server/src/modules/provisioning/loggable/user-for-group-not-found.loggable.ts @@ -0,0 +1,16 @@ +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; +import { ExternalGroupUserDto } from '../dto'; + +export class UserForGroupNotFoundLoggable implements Loggable { + constructor(private readonly groupUser: ExternalGroupUserDto) {} + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + message: 'Unable to add unknown user to group during provisioning.', + data: { + externalUserId: this.groupUser.externalUserId, + roleName: this.groupUser.roleName, + }, + }; + } +} diff --git a/apps/server/src/modules/provisioning/provisioning.module.ts b/apps/server/src/modules/provisioning/provisioning.module.ts index fc0499c8d46..cc38e3dacf9 100644 --- a/apps/server/src/modules/provisioning/provisioning.module.ts +++ b/apps/server/src/modules/provisioning/provisioning.module.ts @@ -6,13 +6,14 @@ import { RoleModule } from '@src/modules/role'; import { SchoolModule } from '@src/modules/school/school.module'; import { SystemModule } from '@src/modules/system/system.module'; import { UserModule } from '@src/modules/user'; +import { GroupModule } from '@src/modules/group'; import { ProvisioningService } from './service/provisioning.service'; import { IservProvisioningStrategy, OidcMockProvisioningStrategy, SanisProvisioningStrategy } from './strategy'; import { OidcProvisioningService } from './strategy/oidc/service/oidc-provisioning.service'; import { SanisResponseMapper } from './strategy/sanis/sanis-response.mapper'; @Module({ - imports: [AccountModule, SchoolModule, UserModule, RoleModule, SystemModule, HttpModule, LoggerModule], + imports: [AccountModule, SchoolModule, UserModule, RoleModule, SystemModule, HttpModule, LoggerModule, GroupModule], providers: [ ProvisioningService, SanisResponseMapper, diff --git a/apps/server/src/modules/provisioning/strategy/index.ts b/apps/server/src/modules/provisioning/strategy/index.ts index 6f15e6540ab..369357cc351 100644 --- a/apps/server/src/modules/provisioning/strategy/index.ts +++ b/apps/server/src/modules/provisioning/strategy/index.ts @@ -3,4 +3,3 @@ export * from './iserv/iserv.strategy'; export * from './oidc/oidc.strategy'; export * from './oidc-mock/oidc-mock.strategy'; export * from './sanis/sanis.strategy'; -export * from './sanis/sanis.response'; diff --git a/apps/server/src/modules/provisioning/strategy/iserv/iserv-do.mapper.ts b/apps/server/src/modules/provisioning/strategy/iserv/iserv-do.mapper.ts index ff2373e3765..63803ac1577 100644 --- a/apps/server/src/modules/provisioning/strategy/iserv/iserv-do.mapper.ts +++ b/apps/server/src/modules/provisioning/strategy/iserv/iserv-do.mapper.ts @@ -1,8 +1,10 @@ -import { LegacySchoolDo, RoleName, UserDO } from '@shared/domain'; +import { RoleName } from '@shared/domain'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; +import { UserDO } from '@shared/domain/domainobject/user.do'; import { ExternalSchoolDto, ExternalUserDto } from '../../dto'; export class IservMapper { - static mapToExternalSchoolDto(schoolDO: LegacySchoolDo): ExternalSchoolDto { + static mapToExternalSchoolDto(schoolDO: SchoolDO): ExternalSchoolDto { return new ExternalSchoolDto({ name: schoolDO.name, externalId: schoolDO.externalId || '', diff --git a/apps/server/src/modules/provisioning/strategy/iserv/iserv.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/iserv/iserv.strategy.spec.ts index 8461f058b43..e39e489dd56 100644 --- a/apps/server/src/modules/provisioning/strategy/iserv/iserv.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/iserv/iserv.strategy.spec.ts @@ -1,11 +1,14 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { LegacySchoolDo, RoleName, User, UserDO } from '@shared/domain'; +import { RoleName, User } from '@shared/domain'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; +import { UserDO } from '@shared/domain/domainobject/user.do'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { legacySchoolDoFactory, schoolFactory, setupEntities, userDoFactory, userFactory } from '@shared/testing'; +import { schoolFactory, setupEntities, userDoFactory, userFactory } from '@shared/testing'; +import { schoolDOFactory } from '@shared/testing/factory/domainobject/school.factory'; import { OAuthSSOError } from '@src/modules/oauth/error/oauth-sso.error'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { UserService } from '@src/modules/user'; import jwt from 'jsonwebtoken'; import { RoleDto } from '../../../role/service/dto/role.dto'; @@ -25,7 +28,7 @@ describe('IservProvisioningStrategy', () => { let module: TestingModule; let strategy: IservProvisioningStrategy; - let schoolService: DeepMocked; + let schoolService: DeepMocked; let userService: DeepMocked; beforeAll(async () => { @@ -38,14 +41,14 @@ describe('IservProvisioningStrategy', () => { useValue: createMock(), }, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolService, + useValue: createMock(), }, ], }).compile(); strategy = module.get(IservProvisioningStrategy); - schoolService = module.get(LegacySchoolService); + schoolService = module.get(SchoolService); userService = module.get(UserService); }); @@ -89,7 +92,7 @@ describe('IservProvisioningStrategy', () => { const user: UserDO = userDoFactory.withRoles([{ id: 'roleId', name: RoleName.STUDENT }]).buildWithId({ externalId: userUUID, }); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ externalId: 'schoolExternalId' }); + const school: SchoolDO = schoolDOFactory.buildWithId({ externalId: 'schoolExternalId' }); const roleDto: RoleDto = new RoleDto({ name: RoleName.STUDENT, }); diff --git a/apps/server/src/modules/provisioning/strategy/iserv/iserv.strategy.ts b/apps/server/src/modules/provisioning/strategy/iserv/iserv.strategy.ts index d2072ec1836..647d03fb92a 100644 --- a/apps/server/src/modules/provisioning/strategy/iserv/iserv.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/iserv/iserv.strategy.ts @@ -1,8 +1,11 @@ import { Injectable } from '@nestjs/common'; -import { LegacySchoolDo, RoleName, RoleReference, User, UserDO } from '@shared/domain'; +import { RoleName, User } from '@shared/domain'; +import { RoleReference } from '@shared/domain/domainobject'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; +import { UserDO } from '@shared/domain/domainobject/user.do'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { OAuthSSOError } from '@src/modules/oauth/error/oauth-sso.error'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { UserService } from '@src/modules/user'; import jwt, { JwtPayload } from 'jsonwebtoken'; import { @@ -17,7 +20,7 @@ import { IservMapper } from './iserv-do.mapper'; @Injectable() export class IservProvisioningStrategy extends ProvisioningStrategy { - constructor(private readonly schoolService: LegacySchoolService, private readonly userService: UserService) { + constructor(private readonly schoolService: SchoolService, private readonly userService: UserService) { super(); } @@ -44,7 +47,7 @@ export class IservProvisioningStrategy extends ProvisioningStrategy { ); } - const ldapSchool: LegacySchoolDo = await this.schoolService.getSchoolById(ldapUser.schoolId); + const ldapSchool: SchoolDO = await this.schoolService.getSchoolById(ldapUser.schoolId); const roleNames: RoleName[] = ldapUser.roles.map((roleRef: RoleReference): RoleName => roleRef.name); const externalUser: ExternalUserDto = IservMapper.mapToExternalUserDto(ldapUser, roleNames); diff --git a/apps/server/src/modules/provisioning/strategy/oidc/oidc.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/oidc/oidc.strategy.spec.ts index c1442d37b65..3c1231c3e72 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/oidc.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/oidc.strategy.spec.ts @@ -1,9 +1,13 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { NotImplementedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { LegacySchoolDo, RoleName, UserDO } from '@shared/domain'; +import { RoleName } from '@shared/domain'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; +import { UserDO } from '@shared/domain/domainobject/user.do'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { legacySchoolDoFactory, userDoFactory } from '@shared/testing'; +import { schoolDOFactory, userDoFactory } from '@shared/testing'; +import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { externalGroupDtoFactory } from '@shared/testing/factory/external-group-dto.factory'; import { ExternalSchoolDto, ExternalUserDto, @@ -51,47 +55,51 @@ describe('OidcStrategy', () => { await module.close(); }); + afterEach(() => { + jest.resetAllMocks(); + }); + describe('apply is called', () => { - const setup = () => { - const externalUserId = 'externalUserId'; - const externalSchoolId = 'externalSchoolId'; - const schoolId = 'schoolId'; - const oauthData: OauthDataDto = new OauthDataDto({ - system: new ProvisioningSystemDto({ - systemId: 'systemId', - provisioningStrategy: SystemProvisioningStrategy.OIDC, - }), - externalSchool: new ExternalSchoolDto({ - externalId: externalSchoolId, - name: 'schoolName', - }), - externalUser: new ExternalUserDto({ + describe('when school data is provided', () => { + const setup = () => { + const externalUserId = 'externalUserId'; + const externalSchoolId = 'externalSchoolId'; + const schoolId = 'schoolId'; + const oauthData: OauthDataDto = new OauthDataDto({ + system: new ProvisioningSystemDto({ + systemId: 'systemId', + provisioningStrategy: SystemProvisioningStrategy.OIDC, + }), + externalSchool: new ExternalSchoolDto({ + externalId: externalSchoolId, + name: 'schoolName', + }), + externalUser: new ExternalUserDto({ + externalId: externalUserId, + }), + }); + const user: UserDO = userDoFactory.withRoles([{ id: 'roleId', name: RoleName.USER }]).build({ + firstName: 'firstName', + lastName: 'lastName', + email: 'email', + schoolId: 'schoolId', externalId: externalUserId, - }), - }); - const user: UserDO = userDoFactory.withRoles([{ id: 'roleId', name: RoleName.USER }]).build({ - firstName: 'firstName', - lastName: 'lastName', - email: 'email', - schoolId: 'schoolId', - externalId: externalUserId, - }); - const school: LegacySchoolDo = legacySchoolDoFactory.build({ - id: schoolId, - name: 'schoolName', - externalId: externalSchoolId, - }); + }); + const school: SchoolDO = schoolDOFactory.build({ + id: schoolId, + name: 'schoolName', + externalId: externalSchoolId, + }); - oidcProvisioningService.provisionExternalSchool.mockResolvedValue(school); - oidcProvisioningService.provisionExternalUser.mockResolvedValue(user); + oidcProvisioningService.provisionExternalSchool.mockResolvedValue(school); + oidcProvisioningService.provisionExternalUser.mockResolvedValue(user); - return { - oauthData, - schoolId, + return { + oauthData, + schoolId, + }; }; - }; - describe('when school data is provided', () => { it('should call the OidcProvisioningService.provisionExternalSchool', async () => { const { oauthData } = setup(); @@ -105,6 +113,45 @@ describe('OidcStrategy', () => { }); describe('when user data is provided', () => { + const setup = () => { + const externalUserId = 'externalUserId'; + const externalSchoolId = 'externalSchoolId'; + const schoolId = 'schoolId'; + const oauthData: OauthDataDto = new OauthDataDto({ + system: new ProvisioningSystemDto({ + systemId: 'systemId', + provisioningStrategy: SystemProvisioningStrategy.OIDC, + }), + externalSchool: new ExternalSchoolDto({ + externalId: externalSchoolId, + name: 'schoolName', + }), + externalUser: new ExternalUserDto({ + externalId: externalUserId, + }), + }); + const user: UserDO = userDoFactory.withRoles([{ id: 'roleId', name: RoleName.USER }]).build({ + firstName: 'firstName', + lastName: 'lastName', + email: 'email', + schoolId: 'schoolId', + externalId: externalUserId, + }); + const school: SchoolDO = schoolDOFactory.build({ + id: schoolId, + name: 'schoolName', + externalId: externalSchoolId, + }); + + oidcProvisioningService.provisionExternalSchool.mockResolvedValue(school); + oidcProvisioningService.provisionExternalUser.mockResolvedValue(user); + + return { + oauthData, + schoolId, + }; + }; + it('should call the OidcProvisioningService.provisionExternalUser', async () => { const { oauthData, schoolId } = setup(); @@ -125,5 +172,84 @@ describe('OidcStrategy', () => { expect(result).toEqual(new ProvisioningDto({ externalUserId: oauthData.externalUser.externalId })); }); }); + + describe('when group data is provided and the feature is enabled', () => { + const setup = () => { + Configuration.set('FEATURE_SANIS_GROUP_PROVISIONING_ENABLED', true); + + const externalUserId = 'externalUserId'; + const oauthData: OauthDataDto = new OauthDataDto({ + system: new ProvisioningSystemDto({ + systemId: 'systemId', + provisioningStrategy: SystemProvisioningStrategy.OIDC, + }), + externalUser: new ExternalUserDto({ + externalId: externalUserId, + }), + externalGroups: externalGroupDtoFactory.buildList(2), + }); + + const user: UserDO = userDoFactory.withRoles([{ id: 'roleId', name: RoleName.USER }]).build({ + externalId: externalUserId, + }); + + oidcProvisioningService.provisionExternalUser.mockResolvedValue(user); + + return { + oauthData, + }; + }; + + it('should call the OidcProvisioningService.provisionExternalGroup for each group', async () => { + const { oauthData } = setup(); + + await strategy.apply(oauthData); + + expect(oidcProvisioningService.provisionExternalGroup).toHaveBeenCalledWith( + oauthData.externalGroups?.[0], + oauthData.system.systemId + ); + expect(oidcProvisioningService.provisionExternalGroup).toHaveBeenCalledWith( + oauthData.externalGroups?.[1], + oauthData.system.systemId + ); + }); + }); + + describe('when group data is provided, but the feature is disabled', () => { + const setup = () => { + Configuration.set('FEATURE_SANIS_GROUP_PROVISIONING_ENABLED', false); + + const externalUserId = 'externalUserId'; + const oauthData: OauthDataDto = new OauthDataDto({ + system: new ProvisioningSystemDto({ + systemId: 'systemId', + provisioningStrategy: SystemProvisioningStrategy.OIDC, + }), + externalUser: new ExternalUserDto({ + externalId: externalUserId, + }), + externalGroups: externalGroupDtoFactory.buildList(2), + }); + + const user: UserDO = userDoFactory.withRoles([{ id: 'roleId', name: RoleName.USER }]).build({ + externalId: externalUserId, + }); + + oidcProvisioningService.provisionExternalUser.mockResolvedValue(user); + + return { + oauthData, + }; + }; + + it('should not call the OidcProvisioningService.provisionExternalGroup', async () => { + const { oauthData } = setup(); + + await strategy.apply(oauthData); + + expect(oidcProvisioningService.provisionExternalGroup).not.toHaveBeenCalled(); + }); + }); }); }); diff --git a/apps/server/src/modules/provisioning/strategy/oidc/oidc.strategy.ts b/apps/server/src/modules/provisioning/strategy/oidc/oidc.strategy.ts index a67d4233260..e52bdb497ee 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/oidc.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/oidc.strategy.ts @@ -1,5 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { LegacySchoolDo, UserDO } from '@shared/domain'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; +import { UserDO } from '@shared/domain/domainobject/user.do'; +import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { OauthDataDto, ProvisioningDto } from '../../dto'; import { ProvisioningStrategy } from '../base.strategy'; import { OidcProvisioningService } from './service/oidc-provisioning.service'; @@ -11,7 +13,7 @@ export abstract class OidcProvisioningStrategy extends ProvisioningStrategy { } override async apply(data: OauthDataDto): Promise { - let school: LegacySchoolDo | undefined; + let school: SchoolDO | undefined; if (data.externalSchool) { school = await this.oidcProvisioningService.provisionExternalSchool(data.externalSchool, data.system.systemId); } @@ -21,6 +23,17 @@ export abstract class OidcProvisioningStrategy extends ProvisioningStrategy { data.system.systemId, school?.id ); + + if (Configuration.get('FEATURE_SANIS_GROUP_PROVISIONING_ENABLED') && data.externalGroups) { + // TODO: N21-1212 remove user from groups + + await Promise.all( + data.externalGroups.map((externalGroup) => + this.oidcProvisioningService.provisionExternalGroup(externalGroup, data.system.systemId) + ) + ); + } + return new ProvisioningDto({ externalUserId: user.externalId || data.externalUser.externalId }); } } diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.spec.ts index a33735b2084..83a7baff0b8 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.spec.ts @@ -1,18 +1,29 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { LegacySchoolDo, RoleName, SchoolFeatures } from '@shared/domain'; +import { RoleName, SchoolFeatures } from '@shared/domain'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; import { UserDO } from '@shared/domain/domainobject/user.do'; -import { federalStateFactory, legacySchoolDoFactory, userDoFactory } from '@shared/testing'; -import { schoolYearFactory } from '@shared/testing/factory/schoolyear.factory'; +import { + externalGroupDtoFactory, + federalStateFactory, + groupFactory, + roleDtoFactory, + schoolDOFactory, + schoolYearFactory, + userDoFactory, +} from '@shared/testing'; +import { Logger } from '@src/core/logger'; import { AccountService } from '@src/modules/account/services/account.service'; import { AccountSaveDto } from '@src/modules/account/services/dto'; +import { Group, GroupService } from '@src/modules/group'; import { RoleService } from '@src/modules/role'; import { RoleDto } from '@src/modules/role/service/dto/role.dto'; -import { FederalStateService, LegacySchoolService, SchoolYearService } from '@src/modules/school'; +import { FederalStateService, SchoolService, SchoolYearService } from '@src/modules/school'; import { UserService } from '@src/modules/user'; import CryptoJS from 'crypto-js'; -import { ExternalSchoolDto, ExternalUserDto } from '../../../dto'; +import { ExternalGroupDto, ExternalSchoolDto, ExternalUserDto } from '../../../dto'; +import { SchoolForGroupNotFoundLoggable, UserForGroupNotFoundLoggable } from '../../../loggable'; import { OidcProvisioningService } from './oidc-provisioning.service'; jest.mock('crypto-js'); @@ -22,11 +33,13 @@ describe('OidcProvisioningService', () => { let service: OidcProvisioningService; let userService: DeepMocked; - let schoolService: DeepMocked; + let schoolService: DeepMocked; let roleService: DeepMocked; let accountService: DeepMocked; let schoolYearService: DeepMocked; let federalStateService: DeepMocked; + let groupService: DeepMocked; + let logger: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -37,8 +50,8 @@ describe('OidcProvisioningService', () => { useValue: createMock(), }, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolService, + useValue: createMock(), }, { provide: RoleService, @@ -56,16 +69,26 @@ describe('OidcProvisioningService', () => { provide: FederalStateService, useValue: createMock(), }, + { + provide: GroupService, + useValue: createMock(), + }, + { + provide: Logger, + useValue: createMock(), + }, ], }).compile(); service = module.get(OidcProvisioningService); userService = module.get(UserService); - schoolService = module.get(LegacySchoolService); + schoolService = module.get(SchoolService); roleService = module.get(RoleService); accountService = module.get(AccountService); schoolYearService = module.get(SchoolYearService); federalStateService = module.get(FederalStateService); + groupService = module.get(GroupService); + logger = module.get(Logger); }); afterAll(async () => { @@ -76,7 +99,7 @@ describe('OidcProvisioningService', () => { jest.resetAllMocks(); }); - describe('provisionExternalSchool is called', () => { + describe('provisionExternalSchool', () => { const setup = () => { const systemId = 'systemId'; const externalSchoolDto: ExternalSchoolDto = new ExternalSchoolDto({ @@ -84,7 +107,7 @@ describe('OidcProvisioningService', () => { name: 'name', officialSchoolNumber: 'officialSchoolNumber', }); - const savedSchoolDO = legacySchoolDoFactory.build({ + const savedSchoolDO = schoolDOFactory.build({ id: 'schoolId', externalId: 'externalId', name: 'name', @@ -92,7 +115,7 @@ describe('OidcProvisioningService', () => { systems: [systemId], features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], }); - const existingSchoolDO = legacySchoolDoFactory.build({ + const existingSchoolDO = schoolDOFactory.build({ id: 'schoolId', externalId: 'externalId', name: 'existingName', @@ -118,7 +141,7 @@ describe('OidcProvisioningService', () => { it('should save the new school', async () => { const { systemId, externalSchoolDto, savedSchoolDO } = setup(); - const result: LegacySchoolDo = await service.provisionExternalSchool(externalSchoolDto, systemId); + const result: SchoolDO = await service.provisionExternalSchool(externalSchoolDto, systemId); expect(result).toEqual(savedSchoolDO); }); @@ -130,7 +153,7 @@ describe('OidcProvisioningService', () => { schoolService.getSchoolByExternalId.mockResolvedValue(existingSchoolDO); - const result: LegacySchoolDo = await service.provisionExternalSchool(externalSchoolDto, systemId); + const result: SchoolDO = await service.provisionExternalSchool(externalSchoolDto, systemId); expect(result).toEqual(savedSchoolDO); }); @@ -177,7 +200,7 @@ describe('OidcProvisioningService', () => { }); }); - describe('provisionExternalUser is called', () => { + describe('provisionExternalUser', () => { const setupUser = () => { const systemId = 'systemId'; const schoolId = 'schoolId'; @@ -318,4 +341,200 @@ describe('OidcProvisioningService', () => { }); }); }); + + describe('provisionExternalGroup', () => { + describe('when the group has no users', () => { + const setup = () => { + const externalGroupDto: ExternalGroupDto = externalGroupDtoFactory.build({ users: [] }); + + return { + externalGroupDto, + }; + }; + + it('should not create a group', async () => { + const { externalGroupDto } = setup(); + + await service.provisionExternalGroup(externalGroupDto, 'systemId'); + + expect(groupService.save).not.toHaveBeenCalled(); + }); + }); + + describe('when group does not have an externalOrganizationId', () => { + const setup = () => { + const externalGroupDto: ExternalGroupDto = externalGroupDtoFactory.build({ externalOrganizationId: undefined }); + + return { + externalGroupDto, + }; + }; + + it('should not call schoolService.getSchoolByExternalId', async () => { + const { externalGroupDto } = setup(); + + await service.provisionExternalGroup(externalGroupDto, 'systemId'); + + expect(schoolService.getSchoolByExternalId).not.toHaveBeenCalled(); + }); + }); + + describe('when school for group could not be found', () => { + const setup = () => { + const externalGroupDto: ExternalGroupDto = externalGroupDtoFactory.build({ externalOrganizationId: 'orgaId' }); + const systemId = 'systemId'; + schoolService.getSchoolByExternalId.mockResolvedValueOnce(null); + + return { + externalGroupDto, + systemId, + }; + }; + + it('should log a SchoolForGroupNotFoundLoggable', async () => { + const { externalGroupDto, systemId } = setup(); + + await service.provisionExternalGroup(externalGroupDto, systemId); + + expect(logger.info).toHaveBeenCalledWith(new SchoolForGroupNotFoundLoggable(externalGroupDto)); + }); + + it('should not call groupService.save', async () => { + const { externalGroupDto, systemId } = setup(); + + await service.provisionExternalGroup(externalGroupDto, systemId); + + expect(groupService.save).not.toHaveBeenCalled(); + }); + }); + + describe('when externalGroup has no users', () => { + const setup = () => { + const externalGroupDto: ExternalGroupDto = externalGroupDtoFactory.build({ + users: [], + }); + + return { + externalGroupDto, + }; + }; + + it('should not call userService.findByExternalId', async () => { + const { externalGroupDto } = setup(); + + await service.provisionExternalGroup(externalGroupDto, 'systemId'); + + expect(userService.findByExternalId).not.toHaveBeenCalled(); + }); + + it('should not call roleService.findByNames', async () => { + const { externalGroupDto } = setup(); + + await service.provisionExternalGroup(externalGroupDto, 'systemId'); + + expect(roleService.findByNames).not.toHaveBeenCalled(); + }); + }); + + describe('when externalGroupUser could not been found', () => { + const setup = () => { + const externalGroupDto: ExternalGroupDto = externalGroupDtoFactory.build(); + const systemId = 'systemId'; + const school: SchoolDO = schoolDOFactory.buildWithId(); + + userService.findByExternalId.mockResolvedValue(null); + schoolService.getSchoolByExternalId.mockResolvedValue(school); + + return { + externalGroupDto, + systemId, + }; + }; + + it('should log a UserForGroupNotFoundLoggable', async () => { + const { externalGroupDto, systemId } = setup(); + + await service.provisionExternalGroup(externalGroupDto, systemId); + + expect(logger.info).toHaveBeenCalledWith(new UserForGroupNotFoundLoggable(externalGroupDto.users[0])); + }); + }); + + describe('when provision group', () => { + const setup = () => { + const group: Group = groupFactory.build(); + groupService.findByExternalSource.mockResolvedValue(group); + + const school: SchoolDO = schoolDOFactory.build({ id: 'schoolId' }); + schoolService.getSchoolByExternalId.mockResolvedValue(school); + + const student: UserDO = userDoFactory + .withRoles([{ id: 'studentRoleId', name: RoleName.STUDENT }]) + .build({ id: 'studentId', externalId: 'studentExternalId' }); + const teacher: UserDO = userDoFactory + .withRoles([{ id: 'teacherRoleId', name: RoleName.TEACHER }]) + .build({ id: 'teacherId', externalId: 'teacherExternalId' }); + userService.findByExternalId.mockResolvedValueOnce(student); + userService.findByExternalId.mockResolvedValueOnce(teacher); + const studentRole: RoleDto = roleDtoFactory.build({ name: RoleName.STUDENT }); + const teacherRole: RoleDto = roleDtoFactory.build({ name: RoleName.TEACHER }); + roleService.findByNames.mockResolvedValueOnce([studentRole]); + roleService.findByNames.mockResolvedValueOnce([teacherRole]); + const externalGroupDto: ExternalGroupDto = externalGroupDtoFactory.build({ + users: [ + { + externalUserId: student.externalId as string, + roleName: RoleName.STUDENT, + }, + { + externalUserId: teacher.externalId as string, + roleName: RoleName.TEACHER, + }, + ], + }); + const systemId = 'systemId'; + + return { + externalGroupDto, + school, + student, + teacher, + studentRole, + teacherRole, + systemId, + }; + }; + + it('should save a new group', async () => { + const { externalGroupDto, school, student, studentRole, teacher, teacherRole, systemId } = setup(); + + await service.provisionExternalGroup(externalGroupDto, systemId); + + expect(groupService.save).toHaveBeenCalledWith({ + props: { + id: expect.any(String), + name: externalGroupDto.name, + externalSource: { + externalId: externalGroupDto.externalId, + systemId, + }, + type: externalGroupDto.type, + organizationId: school.id, + validFrom: externalGroupDto.from, + validUntil: externalGroupDto.until, + users: [ + { + userId: student.id, + roleId: studentRole.id, + }, + { + userId: teacher.id, + roleId: teacherRole.id, + }, + ], + }, + }); + }); + }); + }); }); diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.ts b/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.ts index 4d4e5446990..cf07810498a 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/service/oidc-provisioning.service.ts @@ -1,40 +1,41 @@ import { Injectable, UnprocessableEntityException } from '@nestjs/common'; -import { - EntityId, - FederalState, - LegacySchoolDo, - RoleReference, - SchoolFeatures, - SchoolYear, - UserDO, -} from '@shared/domain'; +import { EntityId, ExternalSource, FederalState, SchoolFeatures, SchoolYear } from '@shared/domain'; +import { RoleReference } from '@shared/domain/domainobject'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; +import { UserDO } from '@shared/domain/domainobject/user.do'; +import { Logger } from '@src/core/logger'; import { AccountService } from '@src/modules/account/services/account.service'; import { AccountSaveDto } from '@src/modules/account/services/dto'; +import { Group, GroupService, GroupUser } from '@src/modules/group'; import { RoleService } from '@src/modules/role'; import { RoleDto } from '@src/modules/role/service/dto/role.dto'; -import { FederalStateService, LegacySchoolService, SchoolYearService } from '@src/modules/school'; +import { FederalStateService, SchoolService, SchoolYearService } from '@src/modules/school'; import { FederalStateNames } from '@src/modules/school/types'; import { UserService } from '@src/modules/user'; +import { ObjectId } from 'bson'; import CryptoJS from 'crypto-js'; -import { ExternalSchoolDto, ExternalUserDto } from '../../../dto'; +import { ExternalGroupDto, ExternalGroupUserDto, ExternalSchoolDto, ExternalUserDto } from '../../../dto'; +import { SchoolForGroupNotFoundLoggable, UserForGroupNotFoundLoggable } from '../../../loggable'; @Injectable() export class OidcProvisioningService { constructor( private readonly userService: UserService, - private readonly schoolService: LegacySchoolService, + private readonly schoolService: SchoolService, + private readonly groupService: GroupService, private readonly roleService: RoleService, private readonly accountService: AccountService, private readonly schoolYearService: SchoolYearService, - private readonly federalStateService: FederalStateService + private readonly federalStateService: FederalStateService, + private readonly logger: Logger ) {} - async provisionExternalSchool(externalSchool: ExternalSchoolDto, systemId: EntityId): Promise { - const existingSchool: LegacySchoolDo | null = await this.schoolService.getSchoolByExternalId( + async provisionExternalSchool(externalSchool: ExternalSchoolDto, systemId: EntityId): Promise { + const existingSchool: SchoolDO | null = await this.schoolService.getSchoolByExternalId( externalSchool.externalId, systemId ); - let school: LegacySchoolDo; + let school: SchoolDO; if (existingSchool) { school = existingSchool; school.name = externalSchool.name; @@ -50,7 +51,7 @@ export class OidcProvisioningService { FederalStateNames.NIEDERSACHEN ); - school = new LegacySchoolDo({ + school = new SchoolDO({ externalId: externalSchool.externalId, name: externalSchool.name, officialSchoolNumber: externalSchool.officialSchoolNumber, @@ -62,7 +63,7 @@ export class OidcProvisioningService { }); } - const savedSchool: LegacySchoolDo = await this.schoolService.save(school, true); + const savedSchool: SchoolDO = await this.schoolService.save(school, true); return savedSchool; } @@ -117,4 +118,73 @@ export class OidcProvisioningService { return savedUser; } + + async provisionExternalGroup(externalGroup: ExternalGroupDto, systemId: EntityId): Promise { + if (externalGroup.users.length === 0) { + return; + } + + const existingGroup: Group | null = await this.groupService.findByExternalSource( + externalGroup.externalId, + systemId + ); + + let organizationId: string | undefined; + if (externalGroup.externalOrganizationId) { + const existingSchool: SchoolDO | null = await this.schoolService.getSchoolByExternalId( + externalGroup.externalOrganizationId, + systemId + ); + + if (!existingSchool || !existingSchool.id) { + this.logger.info(new SchoolForGroupNotFoundLoggable(externalGroup)); + return; + } + + organizationId = existingSchool.id; + } + + const users: GroupUser[] = await this.getFilteredGroupUsers(externalGroup, systemId); + + const group: Group = new Group({ + id: existingGroup ? existingGroup.id : new ObjectId().toHexString(), + name: externalGroup.name, + externalSource: new ExternalSource({ + externalId: externalGroup.externalId, + systemId, + }), + type: externalGroup.type, + organizationId, + validFrom: externalGroup.from, + validUntil: externalGroup.until, + users, + }); + + await this.groupService.save(group); + } + + private async getFilteredGroupUsers(externalGroup: ExternalGroupDto, systemId: string): Promise { + const users: (GroupUser | null)[] = await Promise.all( + externalGroup.users.map(async (externalGroupUser: ExternalGroupUserDto): Promise => { + const user: UserDO | null = await this.userService.findByExternalId(externalGroupUser.externalUserId, systemId); + const roles: RoleDto[] = await this.roleService.findByNames([externalGroupUser.roleName]); + + if (!user || !user.id || roles.length !== 1 || !roles[0].id) { + this.logger.info(new UserForGroupNotFoundLoggable(externalGroupUser)); + return null; + } + + const groupUser: GroupUser = new GroupUser({ + userId: user.id, + roleId: roles[0].id, + }); + + return groupUser; + }) + ); + + const filteredUsers: GroupUser[] = users.filter((groupUser): groupUser is GroupUser => groupUser !== null); + + return filteredUsers; + } } diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/index.ts b/apps/server/src/modules/provisioning/strategy/sanis/response/index.ts new file mode 100644 index 00000000000..56f70ad0f41 --- /dev/null +++ b/apps/server/src/modules/provisioning/strategy/sanis/response/index.ts @@ -0,0 +1,13 @@ +export * from './sanis.response'; +export * from './sanis-role'; +export * from './sanis-group-role'; +export * from './sanis-group-type'; +export * from './sanis-name-response'; +export * from './sanis-gruppe-response'; +export * from './sanis-gruppen-response'; +export * from './sanis-laufzeit-response'; +export * from './sanis-organisation-response'; +export * from './sanis-personenkontext-response'; +export * from './sanis-gruppenzugehoerigkeit-response'; +export * from './sanis-person-response'; +export * from './sanis-sonstige-gruppenzugehoerige-response'; diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-group-role.ts b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-group-role.ts new file mode 100644 index 00000000000..358333ffe56 --- /dev/null +++ b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-group-role.ts @@ -0,0 +1,9 @@ +export enum SanisGroupRole { + TEACHER = 'Lehr', + STUDENT = 'Lern', + CLASS_LEADER = 'KlLeit', + SUPPORT_TEACHER = 'Foerd', + SCHOOL_SUPPORT = 'SchB', + GROUP_MEMBER = 'GMit', + GROUP_LEADER = 'GLeit', +} diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-group-type.ts b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-group-type.ts new file mode 100644 index 00000000000..1af634d511b --- /dev/null +++ b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-group-type.ts @@ -0,0 +1,5 @@ +export enum SanisGroupType { + CLASS = 'Klasse', + COURSE = 'Kurs', + OTHER = 'Sonstig', +} diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-gruppe-response.ts b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-gruppe-response.ts new file mode 100644 index 00000000000..93d8af0e884 --- /dev/null +++ b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-gruppe-response.ts @@ -0,0 +1,14 @@ +import { SanisGroupType } from './sanis-group-type'; +import { SanisLaufzeitResponse } from './sanis-laufzeit-response'; + +export interface SanisGruppeResponse { + id: string; + + bezeichnung: string; + + typ: SanisGroupType; + + orgid: string; + + laufzeit: SanisLaufzeitResponse; +} diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-gruppen-response.ts b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-gruppen-response.ts new file mode 100644 index 00000000000..8154676b8f2 --- /dev/null +++ b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-gruppen-response.ts @@ -0,0 +1,11 @@ +import { SanisGruppeResponse } from './sanis-gruppe-response'; +import { SanisGruppenzugehoerigkeitResponse } from './sanis-gruppenzugehoerigkeit-response'; +import { SanisSonstigeGruppenzugehoerigeResponse } from './sanis-sonstige-gruppenzugehoerige-response'; + +export interface SanisGruppenResponse { + gruppe: SanisGruppeResponse; + + gruppenzugehoerigkeit: SanisGruppenzugehoerigkeitResponse; + + sonstige_gruppenzugehoerige?: SanisSonstigeGruppenzugehoerigeResponse[]; +} diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-gruppenzugehoerigkeit-response.ts b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-gruppenzugehoerigkeit-response.ts new file mode 100644 index 00000000000..16098edd6bf --- /dev/null +++ b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-gruppenzugehoerigkeit-response.ts @@ -0,0 +1,5 @@ +import { SanisGroupRole } from './sanis-group-role'; + +export interface SanisGruppenzugehoerigkeitResponse { + rollen: SanisGroupRole[]; +} diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-laufzeit-response.ts b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-laufzeit-response.ts new file mode 100644 index 00000000000..ad5ac800cdf --- /dev/null +++ b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-laufzeit-response.ts @@ -0,0 +1,5 @@ +export interface SanisLaufzeitResponse { + von: Date; + + bis: Date; +} diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-name-response.ts b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-name-response.ts new file mode 100644 index 00000000000..fad5c23319a --- /dev/null +++ b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-name-response.ts @@ -0,0 +1,5 @@ +export interface SanisNameResponse { + familienname: string; + + vorname: string; +} diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-organisation-response.ts b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-organisation-response.ts new file mode 100644 index 00000000000..258cde00a50 --- /dev/null +++ b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-organisation-response.ts @@ -0,0 +1,9 @@ +export interface SanisOrganisationResponse { + id: string; + + kennung: string; + + name: string; + + typ: string; +} diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-person-response.ts b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-person-response.ts new file mode 100644 index 00000000000..e34d324b2e7 --- /dev/null +++ b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-person-response.ts @@ -0,0 +1,11 @@ +import { SanisNameResponse } from './sanis-name-response'; + +export interface SanisPersonResponse { + name: SanisNameResponse; + + geschlecht: string; + + lokalisierung: string; + + vertrauensstufe: string; +} diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-personenkontext-response.ts b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-personenkontext-response.ts new file mode 100644 index 00000000000..c7d1252b2da --- /dev/null +++ b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-personenkontext-response.ts @@ -0,0 +1,15 @@ +import { SanisRole } from './sanis-role'; +import { SanisGruppenResponse } from './sanis-gruppen-response'; +import { SanisOrganisationResponse } from './sanis-organisation-response'; + +export interface SanisPersonenkontextResponse { + id: string; + + rolle: SanisRole; + + organisation: SanisOrganisationResponse; + + personenstatus: string; + + gruppen?: SanisGruppenResponse[]; +} diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-role.ts b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-role.ts new file mode 100644 index 00000000000..bd3c71f0826 --- /dev/null +++ b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-role.ts @@ -0,0 +1,6 @@ +export enum SanisRole { + LEHR = 'Lehr', + LERN = 'Lern', + LEIT = 'Leit', + ORGADMIN = 'OrgAdmin', +} diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-sonstige-gruppenzugehoerige-response.ts b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-sonstige-gruppenzugehoerige-response.ts new file mode 100644 index 00000000000..0aa20be24dc --- /dev/null +++ b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis-sonstige-gruppenzugehoerige-response.ts @@ -0,0 +1,6 @@ +import { SanisGroupRole } from './sanis-group-role'; + +export interface SanisSonstigeGruppenzugehoerigeResponse { + ktid: string; + rollen: SanisGroupRole[]; +} diff --git a/apps/server/src/modules/provisioning/strategy/sanis/response/sanis.response.ts b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis.response.ts new file mode 100644 index 00000000000..fb8bb8ec6c8 --- /dev/null +++ b/apps/server/src/modules/provisioning/strategy/sanis/response/sanis.response.ts @@ -0,0 +1,10 @@ +import { SanisPersonResponse } from './sanis-person-response'; +import { SanisPersonenkontextResponse } from './sanis-personenkontext-response'; + +export interface SanisResponse { + pid: string; + + person: SanisPersonResponse; + + personenkontexte: SanisPersonenkontextResponse[]; +} diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts index e931fd700af..d199bfce8e6 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.spec.ts @@ -1,51 +1,89 @@ +import { createMock } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; import { RoleName } from '@shared/domain'; +import { Logger } from '@src/core/logger'; +import { GroupTypes } from '@src/modules/group'; import { UUID } from 'bson'; -import { ExternalSchoolDto, ExternalUserDto } from '../../dto'; -import { SanisResponseMapper } from './sanis-response.mapper'; +import { ExternalGroupDto, ExternalSchoolDto, ExternalUserDto } from '../../dto'; import { + SanisGroupRole, + SanisGroupType, + SanisGruppenResponse, + SanisPersonenkontextResponse, SanisResponse, - SanisResponseName, - SanisResponseOrganisation, - SanisResponsePersonenkontext, SanisRole, -} from './sanis.response'; +} from './response'; +import { SanisResponseMapper } from './sanis-response.mapper'; describe('SanisResponseMapper', () => { + let module: TestingModule; let mapper: SanisResponseMapper; - beforeAll(() => { - mapper = new SanisResponseMapper(); + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + SanisResponseMapper, + { + provide: Logger, + useValue: createMock(), + }, + ], + }).compile(); + + mapper = module.get(SanisResponseMapper); }); const setupSanisResponse = () => { const externalUserId = 'aef1f4fd-c323-466e-962b-a84354c0e713'; const externalSchoolId = 'df66c8e6-cfac-40f7-b35b-0da5d8ee680e'; - const sanisResponse: SanisResponse = new SanisResponse({ + const sanisResponse: SanisResponse = { pid: externalUserId, person: { - name: new SanisResponseName({ + name: { vorname: 'firstName', familienname: 'lastName', - }), + }, geschlecht: 'x', lokalisierung: 'de-de', vertrauensstufe: '', }, personenkontexte: [ - new SanisResponsePersonenkontext({ - id: new UUID(), + { + id: new UUID().toString(), rolle: SanisRole.LERN, - organisation: new SanisResponseOrganisation({ - id: new UUID(externalSchoolId), + organisation: { + id: new UUID(externalSchoolId).toString(), name: 'schoolName', typ: 'SCHULE', kennung: 'NI_123456_NI_ashd3838', - }), + }, personenstatus: '', - email: 'test@te.st', - }), + gruppen: [ + { + gruppe: { + id: new UUID().toString(), + bezeichnung: 'bezeichnung', + typ: SanisGroupType.CLASS, + laufzeit: { + von: new Date(2023, 1, 8), + bis: new Date(2024, 7, 31), + }, + orgid: 'orgid', + }, + gruppenzugehoerigkeit: { + rollen: [SanisGroupRole.TEACHER], + }, + sonstige_gruppenzugehoerige: [ + { + rollen: [SanisGroupRole.STUDENT], + ktid: 'ktid', + }, + ], + }, + ], + }, ], - }); + }; return { externalUserId, @@ -54,7 +92,7 @@ describe('SanisResponseMapper', () => { }; }; - describe('mapToExternalSchoolDto is called', () => { + describe('mapToExternalSchoolDto', () => { describe('when a sanis response is provided', () => { it('should map the response to an ExternalSchoolDto', () => { const { sanisResponse, externalSchoolId } = setupSanisResponse(); @@ -70,7 +108,7 @@ describe('SanisResponseMapper', () => { }); }); - describe('mapToExternalUserDto is called', () => { + describe('mapToExternalUserDto', () => { describe('when a sanis response is provided', () => { it('should map the response to an ExternalUserDto', () => { const { sanisResponse, externalUserId } = setupSanisResponse(); @@ -81,10 +119,128 @@ describe('SanisResponseMapper', () => { externalId: externalUserId, firstName: 'firstName', lastName: 'lastName', - email: 'test@te.st', roles: [RoleName.STUDENT], }); }); }); }); + + describe('mapToExternalGroupDtos', () => { + describe('when no group is given', () => { + const setup = () => { + const { sanisResponse } = setupSanisResponse(); + sanisResponse.personenkontexte[0].gruppen = undefined; + + return { + sanisResponse, + }; + }; + + it('should return undefined', () => { + const { sanisResponse } = setup(); + + const result: ExternalGroupDto[] | undefined = mapper.mapToExternalGroupDtos(sanisResponse); + + expect(result).toBeUndefined(); + }); + }); + + describe('when group type is given', () => { + const setup = () => { + const { sanisResponse } = setupSanisResponse(); + const personenkontext: SanisPersonenkontextResponse = sanisResponse.personenkontexte[0]; + const group: SanisGruppenResponse = personenkontext.gruppen![0]; + + return { + sanisResponse, + group, + personenkontext, + }; + }; + + it('should map the sanis response to external group dtos', () => { + const { sanisResponse, group, personenkontext } = setup(); + + const result: ExternalGroupDto[] | undefined = mapper.mapToExternalGroupDtos(sanisResponse); + + expect(result![0]).toEqual({ + name: group.gruppe.bezeichnung, + type: GroupTypes.CLASS, + externalOrganizationId: group.gruppe.orgid, + from: group.gruppe.laufzeit.von, + until: group.gruppe.laufzeit.bis, + externalId: group.gruppe.id, + users: [ + { + externalUserId: group.sonstige_gruppenzugehoerige![0].ktid, + roleName: RoleName.STUDENT, + }, + { + externalUserId: personenkontext.id, + roleName: RoleName.TEACHER, + }, + ].sort((a, b) => a.externalUserId.localeCompare(b.externalUserId)), + }); + }); + }); + + describe('when no group type is provided', () => { + const setup = () => { + const { sanisResponse } = setupSanisResponse(); + sanisResponse.personenkontexte[0].gruppen![0]!.gruppe.typ = SanisGroupType.OTHER; + + return { + sanisResponse, + }; + }; + + it('should return empty array', () => { + const { sanisResponse } = setup(); + + const result: ExternalGroupDto[] | undefined = mapper.mapToExternalGroupDtos(sanisResponse); + + expect(result).toHaveLength(0); + }); + }); + + describe('when a group role mapping is missing', () => { + const setup = () => { + const { sanisResponse } = setupSanisResponse(); + sanisResponse.personenkontexte[0].gruppen![0]!.sonstige_gruppenzugehoerige![0].rollen = [ + SanisGroupRole.SCHOOL_SUPPORT, + ]; + + return { + sanisResponse, + }; + }; + + it('should return only users with known roles', () => { + const { sanisResponse } = setup(); + + const result: ExternalGroupDto[] | undefined = mapper.mapToExternalGroupDtos(sanisResponse); + + expect(result![0].users).toHaveLength(1); + }); + }); + + describe('when a group has no other participants', () => { + const setup = () => { + const { sanisResponse } = setupSanisResponse(); + sanisResponse.personenkontexte[0].gruppen![0]!.sonstige_gruppenzugehoerige = undefined; + + return { + sanisResponse, + }; + }; + + it('should return the group with only the user', () => { + const { sanisResponse } = setup(); + + const result: ExternalGroupDto[] | undefined = mapper.mapToExternalGroupDtos(sanisResponse); + + expect(result![0].users).toHaveLength(1); + }); + }); + }); }); diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts index 037ed40e4a6..3a0d340427e 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis-response.mapper.ts @@ -1,42 +1,125 @@ import { Injectable } from '@nestjs/common'; import { RoleName } from '@shared/domain'; -import { ExternalSchoolDto, ExternalUserDto } from '../../dto'; -import { SanisResponse, SanisRole } from './sanis.response'; +import { Logger } from '@src/core/logger'; +import { GroupTypes } from '@src/modules/group'; +import { ExternalGroupDto, ExternalGroupUserDto, ExternalSchoolDto, ExternalUserDto } from '../../dto'; +import { GroupRoleUnknownLoggable } from '../../loggable'; +import { + SanisGroupRole, + SanisGroupType, + SanisGruppenResponse, + SanisResponse, + SanisRole, + SanisSonstigeGruppenzugehoerigeResponse, +} from './response'; -const RoleMapping = { +const RoleMapping: Record = { [SanisRole.LEHR]: RoleName.TEACHER, [SanisRole.LERN]: RoleName.STUDENT, [SanisRole.LEIT]: RoleName.ADMINISTRATOR, [SanisRole.ORGADMIN]: RoleName.ADMINISTRATOR, }; +const GroupRoleMapping: Partial> = { + [SanisGroupRole.TEACHER]: RoleName.TEACHER, + [SanisGroupRole.STUDENT]: RoleName.STUDENT, +}; + +const GroupTypeMapping: Partial> = { + [SanisGroupType.CLASS]: GroupTypes.CLASS, +}; + @Injectable() export class SanisResponseMapper { SCHOOLNUMBER_PREFIX_REGEX = /^NI_/; + constructor(private readonly logger: Logger) {} + mapToExternalSchoolDto(source: SanisResponse): ExternalSchoolDto { const officialSchoolNumber: string = source.personenkontexte[0].organisation.kennung.replace( this.SCHOOLNUMBER_PREFIX_REGEX, '' ); - return new ExternalSchoolDto({ + + const mapped = new ExternalSchoolDto({ name: source.personenkontexte[0].organisation.name, externalId: source.personenkontexte[0].organisation.id.toString(), officialSchoolNumber, }); + + return mapped; } mapToExternalUserDto(source: SanisResponse): ExternalUserDto { - return new ExternalUserDto({ + const mapped = new ExternalUserDto({ firstName: source.person.name.vorname, lastName: source.person.name.familienname, - email: source.personenkontexte[0].email, roles: [this.mapSanisRoleToRoleName(source)], externalId: source.pid, }); + + return mapped; } private mapSanisRoleToRoleName(source: SanisResponse): RoleName { return RoleMapping[source.personenkontexte[0].rolle]; } + + mapToExternalGroupDtos(source: SanisResponse): ExternalGroupDto[] | undefined { + const groups: SanisGruppenResponse[] | undefined = source.personenkontexte[0].gruppen; + + if (!groups) { + return undefined; + } + + const mapped: ExternalGroupDto[] = groups + .map((group): ExternalGroupDto | null => { + const groupType: GroupTypes | undefined = GroupTypeMapping[group.gruppe.typ]; + + if (!groupType) { + return null; + } + + const sanisGroupUsers: SanisSonstigeGruppenzugehoerigeResponse[] = [ + ...(group.sonstige_gruppenzugehoerige ?? []), + { + ktid: source.personenkontexte[0].id, + rollen: group.gruppenzugehoerigkeit.rollen, + }, + ].sort((a, b) => a.ktid.localeCompare(b.ktid)); + + const gruppenzugehoerigkeiten: ExternalGroupUserDto[] = sanisGroupUsers + .map((relation): ExternalGroupUserDto | null => this.mapToExternalGroupUser(relation)) + .filter((user): user is ExternalGroupUserDto => user !== null); + + return { + name: group.gruppe.bezeichnung, + type: groupType, + externalOrganizationId: group.gruppe.orgid, + from: group.gruppe.laufzeit?.von, + until: group.gruppe.laufzeit?.bis, + externalId: group.gruppe.id, + users: gruppenzugehoerigkeiten, + }; + }) + .filter((group): group is ExternalGroupDto => group !== null); + + return mapped; + } + + private mapToExternalGroupUser(relation: SanisSonstigeGruppenzugehoerigeResponse): ExternalGroupUserDto | null { + const userRole = GroupRoleMapping[relation.rollen[0]]; + + if (!userRole) { + this.logger.info(new GroupRoleUnknownLoggable(relation)); + return null; + } + + const mapped = new ExternalGroupUserDto({ + roleName: userRole, + externalUserId: relation.ktid, + }); + + return mapped; + } } diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis.response.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis.response.ts deleted file mode 100644 index 305d931eabe..00000000000 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis.response.ts +++ /dev/null @@ -1,76 +0,0 @@ -import { UUID } from 'bson'; - -export enum SanisRole { - LEHR = 'Lehr', - LERN = 'Lern', - LEIT = 'Leit', - ORGADMIN = 'OrgAdmin', -} - -export class SanisResponseName { - constructor(sanisResponseName: SanisResponseName) { - this.familienname = sanisResponseName.familienname; - this.vorname = sanisResponseName.vorname; - } - - familienname: string; - - vorname: string; -} - -export class SanisResponsePersonenkontext { - constructor(sanisResponsePersonenkontext: SanisResponsePersonenkontext) { - this.id = sanisResponsePersonenkontext.id; - this.rolle = sanisResponsePersonenkontext.rolle; - this.organisation = sanisResponsePersonenkontext.organisation; - this.personenstatus = sanisResponsePersonenkontext.personenstatus; - this.email = sanisResponsePersonenkontext.email; - } - - id: UUID; - - rolle: SanisRole; - - organisation: SanisResponseOrganisation; - - personenstatus: string; - - // TODO change if neccessary once we have the proper specification - email: string; -} - -export class SanisResponseOrganisation { - constructor(sanisResponseOrganisation: SanisResponseOrganisation) { - this.id = sanisResponseOrganisation.id; - this.kennung = sanisResponseOrganisation.kennung; - this.name = sanisResponseOrganisation.name; - this.typ = sanisResponseOrganisation.typ; - } - - id: UUID; - - kennung: string; - - name: string; - - typ: string; -} - -export class SanisResponse { - constructor(sanisResponse: SanisResponse) { - this.pid = sanisResponse.pid; - this.person = sanisResponse.person; - this.personenkontexte = sanisResponse.personenkontexte; - } - - pid: string; - - person: { - name: SanisResponseName; - geschlecht: string; - lokalisierung: string; - vertrauensstufe: string; - }; - - personenkontexte: SanisResponsePersonenkontext[]; -} 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 e4b512f1029..b1324cbf74b 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 @@ -1,14 +1,16 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { HttpService } from '@nestjs/axios'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; +import { RoleName } from '@shared/domain'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { setupEntities } from '@shared/testing'; -import { AxiosResponse } from 'axios'; +import { axiosResponseFactory, setupEntities } from '@shared/testing'; +import { GroupTypes } from '@src/modules/group'; import { UUID } from 'bson'; import { of } from 'rxjs'; -import { RoleName } from '@shared/domain'; import { + ExternalGroupDto, ExternalSchoolDto, ExternalUserDto, OauthDataDto, @@ -16,25 +18,14 @@ import { ProvisioningSystemDto, } from '../../dto'; import { OidcProvisioningService } from '../oidc/service/oidc-provisioning.service'; +import { SanisGroupRole, SanisGroupType, SanisGruppenResponse, SanisResponse, SanisRole } from './response'; import { SanisResponseMapper } from './sanis-response.mapper'; -import { - SanisResponse, - SanisResponseName, - SanisResponseOrganisation, - SanisResponsePersonenkontext, - SanisRole, -} from './sanis.response'; import { SanisProvisioningStrategy } from './sanis.strategy'; -const createAxiosResponse = (data: SanisResponse): AxiosResponse => { - return { - data: data ?? {}, - status: 0, - statusText: '', - headers: {}, - config: {}, - }; -}; +const createAxiosResponse = (data: SanisResponse) => + axiosResponseFactory.build({ + data, + }); describe('SanisStrategy', () => { let module: TestingModule; @@ -71,8 +62,60 @@ describe('SanisStrategy', () => { afterEach(() => { jest.resetAllMocks(); + Configuration.set('FEATURE_SANIS_GROUP_PROVISIONING_ENABLED', 'true'); }); + const setupSanisResponse = () => { + return { + pid: 'aef1f4fd-c323-466e-962b-a84354c0e713', + person: { + name: { + vorname: 'Hans', + familienname: 'Peter', + }, + geschlecht: 'any', + lokalisierung: 'sn_ZW', + vertrauensstufe: '0', + }, + personenkontexte: [ + { + id: new UUID().toString(), + rolle: SanisRole.LEIT, + organisation: { + id: new UUID('df66c8e6-cfac-40f7-b35b-0da5d8ee680e').toString(), + name: 'schoolName', + typ: 'SCHULE', + kennung: 'Kennung', + }, + personenstatus: 'dead', + gruppen: [ + { + gruppe: { + id: new UUID().toString(), + bezeichnung: 'bezeichnung', + typ: SanisGroupType.CLASS, + laufzeit: { + von: new Date(2023, 1, 8), + bis: new Date(2024, 7, 31), + }, + orgid: 'orgid', + }, + gruppenzugehoerigkeit: { + rollen: [SanisGroupRole.TEACHER], + }, + sonstige_gruppenzugehoerige: [ + { + rollen: [SanisGroupRole.STUDENT], + ktid: 'ktid', + }, + ], + }, + ], + }, + ], + }; + }; + describe('getType is called', () => { describe('when it is called', () => { it('should return type SANIS', () => { @@ -84,64 +127,58 @@ describe('SanisStrategy', () => { }); describe('getData is called', () => { - const setup = () => { - const provisioningUrl = 'sanisProvisioningUrl'; - const input: OauthDataStrategyInputDto = new OauthDataStrategyInputDto({ - system: new ProvisioningSystemDto({ - systemId: 'systemId', - provisioningStrategy: SystemProvisioningStrategy.SANIS, - provisioningUrl, - }), - idToken: 'sanisIdToken', - accessToken: 'sanisAccessToken', - }); - const sanisResponse: SanisResponse = new SanisResponse({ - pid: 'aef1f4fd-c323-466e-962b-a84354c0e713', - person: { - name: new SanisResponseName({ - vorname: 'Hans', - familienname: 'Peter', + describe('when fetching data from sanis', () => { + const setup = () => { + const provisioningUrl = 'sanisProvisioningUrl'; + const input: OauthDataStrategyInputDto = new OauthDataStrategyInputDto({ + system: new ProvisioningSystemDto({ + systemId: 'systemId', + provisioningStrategy: SystemProvisioningStrategy.SANIS, + provisioningUrl, }), - geschlecht: 'any', - lokalisierung: 'sn_ZW', - vertrauensstufe: '0', - }, - personenkontexte: [ - new SanisResponsePersonenkontext({ - id: new UUID(), - rolle: SanisRole.LEIT, - organisation: new SanisResponseOrganisation({ - id: new UUID('df66c8e6-cfac-40f7-b35b-0da5d8ee680e'), - name: 'schoolName', - typ: 'SCHULE', - kennung: 'Kennung', - }), - personenstatus: 'dead', - email: 'test@te.st', + idToken: 'sanisIdToken', + accessToken: 'sanisAccessToken', + }); + const sanisResponse: SanisResponse = setupSanisResponse(); + const user: ExternalUserDto = new ExternalUserDto({ + externalId: 'externalUserId', + }); + const school: ExternalSchoolDto = new ExternalSchoolDto({ + externalId: 'externalSchoolId', + name: 'schoolName', + }); + const sanisGruppeResponse: SanisGruppenResponse = sanisResponse.personenkontexte[0].gruppen![0]!; + const groups: ExternalGroupDto[] = [ + new ExternalGroupDto({ + name: sanisGruppeResponse.gruppe.bezeichnung, + externalId: sanisGruppeResponse.gruppe.id, + type: GroupTypes.CLASS, + externalOrganizationId: sanisGruppeResponse.gruppe.orgid, + from: sanisGruppeResponse.gruppe.laufzeit.von, + until: sanisGruppeResponse.gruppe.laufzeit.bis, + users: [ + { + externalUserId: sanisResponse.personenkontexte[0].id, + roleName: RoleName.TEACHER, + }, + ], }), - ], - }); - const user: ExternalUserDto = new ExternalUserDto({ - externalId: 'externalUserId', - }); - const school: ExternalSchoolDto = new ExternalSchoolDto({ - externalId: 'externalSchoolId', - name: 'schoolName', - }); + ]; - httpService.get.mockReturnValue(of(createAxiosResponse(sanisResponse))); - mapper.mapToExternalUserDto.mockReturnValue(user); - mapper.mapToExternalSchoolDto.mockReturnValue(school); + httpService.get.mockReturnValue(of(createAxiosResponse(sanisResponse))); + mapper.mapToExternalUserDto.mockReturnValue(user); + mapper.mapToExternalSchoolDto.mockReturnValue(school); + mapper.mapToExternalGroupDtos.mockReturnValue(groups); - return { - input, - provisioningUrl, - user, - school, + return { + input, + provisioningUrl, + user, + school, + groups, + }; }; - }; - describe('when fetching data from sanis', () => { it('should make a Http-GET-Request to the provisioning url of sanis with an access token', async () => { const { input, provisioningUrl } = setup(); @@ -157,7 +194,7 @@ describe('SanisStrategy', () => { }); it('should return the oauth data', async () => { - const { input, user, school } = setup(); + const { input, user, school, groups } = setup(); const result: OauthDataDto = await strategy.getData(input); @@ -165,14 +202,101 @@ describe('SanisStrategy', () => { system: input.system, externalUser: user, externalSchool: school, + externalGroups: groups, }); }); }); + describe('when FEATURE_SANIS_GROUP_PROVISIONING_ENABLED is false', () => { + const setup = () => { + const provisioningUrl = 'sanisProvisioningUrl'; + const input: OauthDataStrategyInputDto = new OauthDataStrategyInputDto({ + system: new ProvisioningSystemDto({ + systemId: 'systemId', + provisioningStrategy: SystemProvisioningStrategy.SANIS, + provisioningUrl, + }), + idToken: 'sanisIdToken', + accessToken: 'sanisAccessToken', + }); + const sanisResponse: SanisResponse = setupSanisResponse(); + const user: ExternalUserDto = new ExternalUserDto({ + externalId: 'externalUserId', + }); + const school: ExternalSchoolDto = new ExternalSchoolDto({ + externalId: 'externalSchoolId', + name: 'schoolName', + }); + + httpService.get.mockReturnValue(of(createAxiosResponse(sanisResponse))); + mapper.mapToExternalUserDto.mockReturnValue(user); + mapper.mapToExternalSchoolDto.mockReturnValue(school); + + Configuration.set('FEATURE_SANIS_GROUP_PROVISIONING_ENABLED', 'false'); + + return { + input, + }; + }; + + it('should not call mapToExternalGroupDtos', async () => { + const { input } = setup(); + + await strategy.getData(input); + + expect(mapper.mapToExternalGroupDtos).not.toHaveBeenCalled(); + }); + }); + describe('when the provided system has no provisioning url', () => { + const setup = () => { + const input: OauthDataStrategyInputDto = new OauthDataStrategyInputDto({ + system: new ProvisioningSystemDto({ + systemId: 'systemId', + provisioningStrategy: SystemProvisioningStrategy.SANIS, + provisioningUrl: undefined, + }), + idToken: 'sanisIdToken', + accessToken: 'sanisAccessToken', + }); + const sanisResponse: SanisResponse = setupSanisResponse(); + const user: ExternalUserDto = new ExternalUserDto({ + externalId: 'externalUserId', + }); + const school: ExternalSchoolDto = new ExternalSchoolDto({ + externalId: 'externalSchoolId', + name: 'schoolName', + }); + const sanisGruppeResponse: SanisGruppenResponse = sanisResponse.personenkontexte[0].gruppen![0]!; + const groups: ExternalGroupDto[] = [ + new ExternalGroupDto({ + name: sanisGruppeResponse.gruppe.bezeichnung, + externalId: sanisGruppeResponse.gruppe.id, + type: GroupTypes.CLASS, + externalOrganizationId: sanisGruppeResponse.gruppe.orgid, + from: sanisGruppeResponse.gruppe.laufzeit.von, + until: sanisGruppeResponse.gruppe.laufzeit.bis, + users: [ + { + externalUserId: sanisResponse.personenkontexte[0].id, + roleName: RoleName.TEACHER, + }, + ], + }), + ]; + + httpService.get.mockReturnValue(of(createAxiosResponse(sanisResponse))); + mapper.mapToExternalUserDto.mockReturnValue(user); + mapper.mapToExternalSchoolDto.mockReturnValue(school); + mapper.mapToExternalGroupDtos.mockReturnValue(groups); + + return { + input, + }; + }; + it('should throw an InternalServerErrorException', async () => { const { input } = setup(); - input.system.provisioningUrl = undefined; const promise: Promise = strategy.getData(input); @@ -181,13 +305,56 @@ describe('SanisStrategy', () => { }); describe('when role from sanis is admin', () => { - it('should add teacher and admin svs role to externalUser', async () => { - const { input } = setup(); + const setup = () => { + const provisioningUrl = 'sanisProvisioningUrl'; + const input: OauthDataStrategyInputDto = new OauthDataStrategyInputDto({ + system: new ProvisioningSystemDto({ + systemId: 'systemId', + provisioningStrategy: SystemProvisioningStrategy.SANIS, + provisioningUrl, + }), + idToken: 'sanisIdToken', + accessToken: 'sanisAccessToken', + }); + const sanisResponse: SanisResponse = setupSanisResponse(); const user = new ExternalUserDto({ externalId: 'externalSchoolId', roles: [RoleName.ADMINISTRATOR], }); + const school: ExternalSchoolDto = new ExternalSchoolDto({ + externalId: 'externalSchoolId', + name: 'schoolName', + }); + const sanisGruppeResponse: SanisGruppenResponse = sanisResponse.personenkontexte[0].gruppen![0]!; + const groups: ExternalGroupDto[] = [ + new ExternalGroupDto({ + name: sanisGruppeResponse.gruppe.bezeichnung, + externalId: sanisGruppeResponse.gruppe.id, + type: GroupTypes.CLASS, + externalOrganizationId: sanisGruppeResponse.gruppe.orgid, + from: sanisGruppeResponse.gruppe.laufzeit.von, + until: sanisGruppeResponse.gruppe.laufzeit.bis, + users: [ + { + externalUserId: sanisResponse.personenkontexte[0].id, + roleName: RoleName.TEACHER, + }, + ], + }), + ]; + + httpService.get.mockReturnValue(of(createAxiosResponse(sanisResponse))); mapper.mapToExternalUserDto.mockReturnValue(user); + mapper.mapToExternalSchoolDto.mockReturnValue(school); + mapper.mapToExternalGroupDtos.mockReturnValue(groups); + + return { + input, + }; + }; + + it('should add teacher and admin svs role to externalUser', async () => { + const { input } = setup(); const result: OauthDataDto = await strategy.getData(input); diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts index fd0a50879ac..a09ae204c69 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts @@ -1,14 +1,21 @@ +import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { HttpService } from '@nestjs/axios'; import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { AxiosRequestConfig, AxiosResponse } from 'axios'; import { firstValueFrom } from 'rxjs'; import { RoleName } from '@shared/domain'; -import { ExternalSchoolDto, ExternalUserDto, OauthDataDto, OauthDataStrategyInputDto } from '../../dto'; +import { + ExternalSchoolDto, + ExternalUserDto, + OauthDataDto, + OauthDataStrategyInputDto, + ExternalGroupDto, +} from '../../dto'; import { OidcProvisioningStrategy } from '../oidc/oidc.strategy'; import { OidcProvisioningService } from '../oidc/service/oidc-provisioning.service'; import { SanisResponseMapper } from './sanis-response.mapper'; -import { SanisResponse } from './sanis.response'; +import { SanisResponse } from './response'; @Injectable() export class SanisProvisioningStrategy extends OidcProvisioningStrategy { @@ -44,11 +51,18 @@ export class SanisProvisioningStrategy extends OidcProvisioningStrategy { const externalSchool: ExternalSchoolDto = this.responseMapper.mapToExternalSchoolDto(axiosResponse.data); + let externalGroups: ExternalGroupDto[] | undefined; + if (Configuration.get('FEATURE_SANIS_GROUP_PROVISIONING_ENABLED')) { + externalGroups = this.responseMapper.mapToExternalGroupDtos(axiosResponse.data); + } + const oauthData: OauthDataDto = new OauthDataDto({ system: input.system, externalSchool, externalUser, + externalGroups, }); + return oauthData; } diff --git a/apps/server/src/modules/pseudonym/entity/pseudonym.entity.ts b/apps/server/src/modules/pseudonym/entity/pseudonym.entity.ts index 6cabaffd0ee..c9c051fa8b3 100644 --- a/apps/server/src/modules/pseudonym/entity/pseudonym.entity.ts +++ b/apps/server/src/modules/pseudonym/entity/pseudonym.entity.ts @@ -1,9 +1,9 @@ import { Entity, Property, Unique } from '@mikro-orm/core'; import { ObjectId } from '@mikro-orm/mongodb'; -import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; import { EntityId } from '@shared/domain'; +import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; -export interface IPseudonymEntityProps { +export interface PseudonymEntityProps { id?: EntityId; pseudonym: string; toolId: ObjectId; @@ -23,7 +23,7 @@ export class PseudonymEntity extends BaseEntityWithTimestamps { @Property() userId: ObjectId; - constructor(props: IPseudonymEntityProps) { + constructor(props: PseudonymEntityProps) { super(); if (props.id != null) { this.id = props.id; diff --git a/apps/server/src/modules/pseudonym/repo/pseudonyms.repo.ts b/apps/server/src/modules/pseudonym/repo/pseudonyms.repo.ts index 4444e910552..c4e40535ddb 100644 --- a/apps/server/src/modules/pseudonym/repo/pseudonyms.repo.ts +++ b/apps/server/src/modules/pseudonym/repo/pseudonyms.repo.ts @@ -1,7 +1,7 @@ +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; import { EntityId, Pseudonym } from '@shared/domain'; -import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; -import { IPseudonymEntityProps, PseudonymEntity } from '../entity'; +import { PseudonymEntity, PseudonymEntityProps } from '../entity'; @Injectable() export class PseudonymsRepo { @@ -46,7 +46,7 @@ export class PseudonymsRepo { .getUnitOfWork() .getById(PseudonymEntity.name, domainObject.id); - const entityProps: IPseudonymEntityProps = this.mapDomainObjectToEntityProperties(domainObject); + const entityProps: PseudonymEntityProps = this.mapDomainObjectToEntityProperties(domainObject); let entity: PseudonymEntity = new PseudonymEntity(entityProps); if (existing) { @@ -79,8 +79,9 @@ export class PseudonymsRepo { }); } - protected mapDomainObjectToEntityProperties(entityDO: Pseudonym): IPseudonymEntityProps { + protected mapDomainObjectToEntityProperties(entityDO: Pseudonym): PseudonymEntityProps { return { + id: entityDO.id, pseudonym: entityDO.pseudonym, toolId: new ObjectId(entityDO.toolId), userId: new ObjectId(entityDO.userId), diff --git a/apps/server/src/modules/pseudonym/service/pseudonym.service.spec.ts b/apps/server/src/modules/pseudonym/service/pseudonym.service.spec.ts index 720580e46ec..80c5b461c67 100644 --- a/apps/server/src/modules/pseudonym/service/pseudonym.service.spec.ts +++ b/apps/server/src/modules/pseudonym/service/pseudonym.service.spec.ts @@ -103,7 +103,7 @@ describe('PseudonymService', () => { describe('when searching by userId and toolId', () => { const setup = () => { - const pseudonym: Pseudonym = pseudonymFactory.buildWithId(); + const pseudonym: Pseudonym = pseudonymFactory.build(); const user: UserDO = userDoFactory.buildWithId(); const externalTool: ExternalTool = externalToolFactory.buildWithId(); @@ -218,7 +218,7 @@ describe('PseudonymService', () => { describe('when the pseudonym exists', () => { const setup = () => { - const pseudonym: Pseudonym = pseudonymFactory.buildWithId(); + const pseudonym: Pseudonym = pseudonymFactory.build(); const user: UserDO = userDoFactory.buildWithId(); const externalTool: ExternalTool = externalToolFactory.buildWithId(); @@ -247,7 +247,7 @@ describe('PseudonymService', () => { describe('when no pseudonym exists yet', () => { const setup = () => { - const pseudonym: Pseudonym = pseudonymFactory.buildWithId(); + const pseudonym: Pseudonym = pseudonymFactory.build(); const user: UserDO = userDoFactory.buildWithId(); const externalTool: ExternalTool = externalToolFactory.buildWithId(); @@ -304,10 +304,10 @@ describe('PseudonymService', () => { describe('when searching by userId', () => { const setup = () => { const user1: UserDO = userDoFactory.buildWithId(); - const pseudonym1: Pseudonym = pseudonymFactory.buildWithId({ userId: user1.id }); - const pseudonym2: Pseudonym = pseudonymFactory.buildWithId({ userId: user1.id }); - const pseudonym3: Pseudonym = pseudonymFactory.buildWithId({ userId: user1.id }); - const pseudonym4: Pseudonym = pseudonymFactory.buildWithId({ userId: user1.id }); + const pseudonym1: Pseudonym = pseudonymFactory.build({ userId: user1.id }); + const pseudonym2: Pseudonym = pseudonymFactory.build({ userId: user1.id }); + const pseudonym3: Pseudonym = pseudonymFactory.build({ userId: user1.id }); + const pseudonym4: Pseudonym = pseudonymFactory.build({ userId: user1.id }); pseudonymRepo.findByUserId.mockResolvedValue([pseudonym1, pseudonym2]); externalToolPseudonymRepo.findByUserId.mockResolvedValue([pseudonym3, pseudonym4]); diff --git a/apps/server/src/modules/school/controller/school.controller.spec.ts b/apps/server/src/modules/school/controller/school.controller.spec.ts index 1f6be74df84..cd41a61edf0 100644 --- a/apps/server/src/modules/school/controller/school.controller.spec.ts +++ b/apps/server/src/modules/school/controller/school.controller.spec.ts @@ -4,14 +4,14 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ICurrentUser } from '@src/modules/authentication'; import { MigrationMapper } from '../mapper/migration.mapper'; import { OauthMigrationDto } from '../uc/dto/oauth-migration.dto'; -import { LegacySchoolUc } from '../uc'; +import { SchoolUc } from '../uc/school.uc'; import { MigrationBody, MigrationResponse, SchoolParams } from './dto'; import { SchoolController } from './school.controller'; describe('School Controller', () => { let module: TestingModule; let controller: SchoolController; - let schoolUc: DeepMocked; + let schoolUc: DeepMocked; let mapper: DeepMocked; beforeAll(async () => { @@ -19,8 +19,8 @@ describe('School Controller', () => { controllers: [SchoolController], providers: [ { - provide: LegacySchoolUc, - useValue: createMock(), + provide: SchoolUc, + useValue: createMock(), }, { provide: MigrationMapper, @@ -29,7 +29,7 @@ describe('School Controller', () => { ], }).compile(); controller = module.get(SchoolController); - schoolUc = module.get(LegacySchoolUc); + schoolUc = module.get(SchoolUc); mapper = module.get(MigrationMapper); }); diff --git a/apps/server/src/modules/school/controller/school.controller.ts b/apps/server/src/modules/school/controller/school.controller.ts index 7db8b370437..e4d3c8c3a7a 100644 --- a/apps/server/src/modules/school/controller/school.controller.ts +++ b/apps/server/src/modules/school/controller/school.controller.ts @@ -10,14 +10,14 @@ import { ICurrentUser } from '@src/modules/authentication'; import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; import { MigrationMapper } from '../mapper/migration.mapper'; import { OauthMigrationDto } from '../uc/dto/oauth-migration.dto'; -import { LegacySchoolUc } from '../uc'; +import { SchoolUc } from '../uc/school.uc'; import { MigrationBody, MigrationResponse, SchoolParams } from './dto'; @ApiTags('School') @Authenticate('jwt') @Controller('school') export class SchoolController { - constructor(private readonly schoolUc: LegacySchoolUc, private readonly migrationMapper: MigrationMapper) {} + constructor(private readonly schoolUc: SchoolUc, private readonly migrationMapper: MigrationMapper) {} @Put(':schoolId/migration') @Authenticate('jwt') diff --git a/apps/server/src/modules/school/school-api.module.ts b/apps/server/src/modules/school/school-api.module.ts index e4acf39aa9e..48917e254e2 100644 --- a/apps/server/src/modules/school/school-api.module.ts +++ b/apps/server/src/modules/school/school-api.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { AuthorizationModule } from '@src/modules/authorization'; import { LoggerModule } from '@src/core/logger'; import { UserLoginMigrationModule } from '@src/modules/user-login-migration'; -import { LegacySchoolUc } from './uc'; +import { SchoolUc } from './uc/school.uc'; import { SchoolModule } from './school.module'; import { SchoolController } from './controller/school.controller'; import { MigrationMapper } from './mapper/migration.mapper'; @@ -10,6 +10,6 @@ import { MigrationMapper } from './mapper/migration.mapper'; @Module({ imports: [SchoolModule, AuthorizationModule, LoggerModule, UserLoginMigrationModule], controllers: [SchoolController], - providers: [LegacySchoolUc, MigrationMapper], + providers: [SchoolUc, MigrationMapper], }) export class SchoolApiModule {} diff --git a/apps/server/src/modules/school/school.module.ts b/apps/server/src/modules/school/school.module.ts index 5afc3087beb..f09ddbff769 100644 --- a/apps/server/src/modules/school/school.module.ts +++ b/apps/server/src/modules/school/school.module.ts @@ -1,20 +1,20 @@ import { Module } from '@nestjs/common'; -import { FederalStateRepo, LegacySchoolRepo } from '@shared/repo'; +import { FederalStateRepo, SchoolRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; import { SchoolYearRepo } from './repo'; -import { FederalStateService, LegacySchoolService, SchoolValidationService, SchoolYearService } from './service'; +import { FederalStateService, SchoolService, SchoolValidationService, SchoolYearService } from './service'; @Module({ imports: [LoggerModule], providers: [ - LegacySchoolRepo, - LegacySchoolService, + SchoolRepo, + SchoolService, SchoolYearService, SchoolYearRepo, FederalStateService, FederalStateRepo, SchoolValidationService, ], - exports: [LegacySchoolService, SchoolYearService, FederalStateService], + exports: [SchoolService, SchoolYearService, FederalStateService], }) export class SchoolModule {} diff --git a/apps/server/src/modules/school/service/index.ts b/apps/server/src/modules/school/service/index.ts index 09f01c98215..f216df3e8e8 100644 --- a/apps/server/src/modules/school/service/index.ts +++ b/apps/server/src/modules/school/service/index.ts @@ -1,4 +1,4 @@ -export * from './legacy-school.service'; +export * from './school.service'; export * from './school-year.service'; export * from './federal-state.service'; export * from './validation'; diff --git a/apps/server/src/modules/school/service/legacy-school.service.ts b/apps/server/src/modules/school/service/legacy-school.service.ts deleted file mode 100644 index 1ed43fd5f07..00000000000 --- a/apps/server/src/modules/school/service/legacy-school.service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { EntityId, LegacySchoolDo, SchoolFeatures } from '@shared/domain'; -import { LegacySchoolRepo } from '@shared/repo'; -import { SchoolValidationService } from './validation'; - -/** - * @deprecated because it uses the deprecated LegacySchoolDo. - */ -@Injectable() -export class LegacySchoolService { - constructor( - private readonly schoolRepo: LegacySchoolRepo, - private readonly schoolValidationService: SchoolValidationService - ) {} - - async hasFeature(schoolId: EntityId, feature: SchoolFeatures): Promise { - const entity: LegacySchoolDo = await this.schoolRepo.findById(schoolId); - return entity.features ? entity.features.includes(feature) : false; - } - - async removeFeature(schoolId: EntityId, feature: SchoolFeatures): Promise { - const school: LegacySchoolDo = await this.schoolRepo.findById(schoolId); - if (school.features && school.features.includes(feature)) { - school.features = school.features.filter((f: SchoolFeatures) => f !== feature); - await this.schoolRepo.save(school); - } - } - - async getSchoolById(id: string): Promise { - const schoolDO: LegacySchoolDo = await this.schoolRepo.findById(id); - - return schoolDO; - } - - async getSchoolByExternalId(externalId: string, systemId: string): Promise { - const schoolDO: LegacySchoolDo | null = await this.schoolRepo.findByExternalId(externalId, systemId); - - return schoolDO; - } - - async getSchoolBySchoolNumber(schoolNumber: string): Promise { - const schoolDO: LegacySchoolDo | null = await this.schoolRepo.findBySchoolNumber(schoolNumber); - - return schoolDO; - } - - async save(school: LegacySchoolDo, validate = false): Promise { - if (validate) { - await this.schoolValidationService.validate(school); - } - - const ret: LegacySchoolDo = await this.schoolRepo.save(school); - - return ret; - } -} diff --git a/apps/server/src/modules/school/service/legacy-school.service.spec.ts b/apps/server/src/modules/school/service/school.service.spec.ts similarity index 80% rename from apps/server/src/modules/school/service/legacy-school.service.spec.ts rename to apps/server/src/modules/school/service/school.service.spec.ts index 57cc3c6f1ee..8d303aa0e5b 100644 --- a/apps/server/src/modules/school/service/legacy-school.service.spec.ts +++ b/apps/server/src/modules/school/service/school.service.spec.ts @@ -1,25 +1,26 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { LegacySchoolDo, SchoolFeatures } from '@shared/domain'; -import { LegacySchoolRepo } from '@shared/repo'; -import { legacySchoolDoFactory, setupEntities } from '@shared/testing'; -import { LegacySchoolService } from './legacy-school.service'; +import { SchoolFeatures } from '@shared/domain'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; +import { SchoolRepo } from '@shared/repo'; +import { schoolDOFactory, setupEntities } from '@shared/testing'; +import { SchoolService } from './school.service'; import { SchoolValidationService } from './validation/school-validation.service'; -describe('LegacySchoolService', () => { +describe('SchoolService', () => { let module: TestingModule; - let schoolService: LegacySchoolService; + let schoolService: SchoolService; - let schoolRepo: DeepMocked; + let schoolRepo: DeepMocked; let schoolValidationService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ providers: [ - LegacySchoolService, + SchoolService, { - provide: LegacySchoolRepo, - useValue: createMock(), + provide: SchoolRepo, + useValue: createMock(), }, { provide: SchoolValidationService, @@ -28,8 +29,8 @@ describe('LegacySchoolService', () => { ], }).compile(); - schoolRepo = module.get(LegacySchoolRepo); - schoolService = module.get(LegacySchoolService); + schoolRepo = module.get(SchoolRepo); + schoolService = module.get(SchoolService); schoolValidationService = module.get(SchoolValidationService); await setupEntities(); @@ -41,7 +42,7 @@ describe('LegacySchoolService', () => { const setupOld = () => { const systems: string[] = ['systemId']; - const schoolSaved: LegacySchoolDo = legacySchoolDoFactory.build({ + const schoolSaved: SchoolDO = schoolDOFactory.build({ id: 'testId', name: 'schoolName', externalId: 'externalId', @@ -49,7 +50,7 @@ describe('LegacySchoolService', () => { systems, features: [SchoolFeatures.VIDEOCONFERENCE], }); - const schoolUnsaved: LegacySchoolDo = legacySchoolDoFactory.build({ name: 'school #2}', systems: [] }); + const schoolUnsaved: SchoolDO = schoolDOFactory.build({ name: 'school #2}', systems: [] }); schoolRepo.findById.mockResolvedValue(schoolSaved); schoolRepo.findByExternalId.mockResolvedValue(schoolSaved); schoolRepo.findBySchoolNumber.mockResolvedValue(schoolSaved); @@ -109,7 +110,7 @@ describe('LegacySchoolService', () => { describe('removeFeature', () => { describe('when given schoolFeature exists on school', () => { const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + const school: SchoolDO = schoolDOFactory.buildWithId({ features: [SchoolFeatures.VIDEOCONFERENCE, SchoolFeatures.OAUTH_PROVISIONING_ENABLED], }); @@ -131,7 +132,7 @@ describe('LegacySchoolService', () => { describe('when school has a feature which should be removed', () => { const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + const school: SchoolDO = schoolDOFactory.buildWithId({ features: [SchoolFeatures.VIDEOCONFERENCE, SchoolFeatures.OAUTH_PROVISIONING_ENABLED], }); @@ -169,9 +170,9 @@ describe('LegacySchoolService', () => { it('should return a do', async () => { const { schoolSavedId } = setupOld(); - const schoolDO: LegacySchoolDo = await schoolService.getSchoolById(schoolSavedId); + const schoolDO: SchoolDO = await schoolService.getSchoolById(schoolSavedId); - expect(schoolDO).toBeInstanceOf(LegacySchoolDo); + expect(schoolDO).toBeInstanceOf(SchoolDO); }); }); }); @@ -188,19 +189,16 @@ describe('LegacySchoolService', () => { it('should return a do', async () => { const { schoolSavedExternalId, systems } = setupOld(); - const schoolDO: LegacySchoolDo | null = await schoolService.getSchoolByExternalId( - schoolSavedExternalId, - systems[0] - ); + const schoolDO: SchoolDO | null = await schoolService.getSchoolByExternalId(schoolSavedExternalId, systems[0]); - expect(schoolDO).toBeInstanceOf(LegacySchoolDo); + expect(schoolDO).toBeInstanceOf(SchoolDO); }); it('should return null', async () => { const { systems } = setupOld(); schoolRepo.findByExternalId.mockResolvedValue(null); - const schoolDO: LegacySchoolDo | null = await schoolService.getSchoolByExternalId('null', systems[0]); + const schoolDO: SchoolDO | null = await schoolService.getSchoolByExternalId('null', systems[0]); expect(schoolDO).toBeNull(); }); @@ -218,14 +216,14 @@ describe('LegacySchoolService', () => { it('should return a do', async () => { const { schoolSavedNumber } = setupOld(); - const schoolDO: LegacySchoolDo | null = await schoolService.getSchoolBySchoolNumber(schoolSavedNumber); + const schoolDO: SchoolDO | null = await schoolService.getSchoolBySchoolNumber(schoolSavedNumber); - expect(schoolDO).toBeInstanceOf(LegacySchoolDo); + expect(schoolDO).toBeInstanceOf(SchoolDO); }); it('should return null', async () => { schoolRepo.findBySchoolNumber.mockResolvedValue(null); - const schoolDO: LegacySchoolDo | null = await schoolService.getSchoolBySchoolNumber('null'); + const schoolDO: SchoolDO | null = await schoolService.getSchoolBySchoolNumber('null'); expect(schoolDO).toBeNull(); }); @@ -234,7 +232,7 @@ describe('LegacySchoolService', () => { describe('save is called', () => { describe('when validation is deactivated', () => { const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.build(); + const school: SchoolDO = schoolDOFactory.build(); schoolRepo.save.mockResolvedValue(school); @@ -254,7 +252,7 @@ describe('LegacySchoolService', () => { it('should return a do', async () => { const { school } = setup(); - const schoolDO: LegacySchoolDo = await schoolService.save(school); + const schoolDO: SchoolDO = await schoolService.save(school); expect(schoolDO).toBeDefined(); }); @@ -271,7 +269,7 @@ describe('LegacySchoolService', () => { describe('when validation is active', () => { describe('when the validation fails', () => { const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.build(); + const school: SchoolDO = schoolDOFactory.build(); schoolRepo.save.mockResolvedValueOnce(school); schoolValidationService.validate.mockRejectedValueOnce(new Error()); @@ -308,7 +306,7 @@ describe('LegacySchoolService', () => { describe('when the validation succeeds', () => { const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.build(); + const school: SchoolDO = schoolDOFactory.build(); schoolRepo.save.mockResolvedValueOnce(school); @@ -328,7 +326,7 @@ describe('LegacySchoolService', () => { it('should return a do', async () => { const { school } = setup(); - const schoolDO: LegacySchoolDo = await schoolService.save(school, true); + const schoolDO: SchoolDO = await schoolService.save(school, true); expect(schoolDO).toBeDefined(); }); diff --git a/apps/server/src/modules/school/service/school.service.ts b/apps/server/src/modules/school/service/school.service.ts new file mode 100644 index 00000000000..becf7d58eeb --- /dev/null +++ b/apps/server/src/modules/school/service/school.service.ts @@ -0,0 +1,54 @@ +import { Injectable } from '@nestjs/common'; +import { EntityId, SchoolFeatures } from '@shared/domain'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; +import { SchoolRepo } from '@shared/repo'; +import { SchoolValidationService } from './validation'; + +@Injectable() +export class SchoolService { + constructor( + private readonly schoolRepo: SchoolRepo, + private readonly schoolValidationService: SchoolValidationService + ) {} + + async hasFeature(schoolId: EntityId, feature: SchoolFeatures): Promise { + const entity: SchoolDO = await this.schoolRepo.findById(schoolId); + return entity.features ? entity.features.includes(feature) : false; + } + + async removeFeature(schoolId: EntityId, feature: SchoolFeatures): Promise { + const school: SchoolDO = await this.schoolRepo.findById(schoolId); + if (school.features && school.features.includes(feature)) { + school.features = school.features.filter((f: SchoolFeatures) => f !== feature); + await this.schoolRepo.save(school); + } + } + + async getSchoolById(id: string): Promise { + const schoolDO: SchoolDO = await this.schoolRepo.findById(id); + + return schoolDO; + } + + async getSchoolByExternalId(externalId: string, systemId: string): Promise { + const schoolDO: SchoolDO | null = await this.schoolRepo.findByExternalId(externalId, systemId); + + return schoolDO; + } + + async getSchoolBySchoolNumber(schoolNumber: string): Promise { + const schoolDO: SchoolDO | null = await this.schoolRepo.findBySchoolNumber(schoolNumber); + + return schoolDO; + } + + async save(school: SchoolDO, validate = false): Promise { + if (validate) { + await this.schoolValidationService.validate(school); + } + + const ret: SchoolDO = await this.schoolRepo.save(school); + + return ret; + } +} diff --git a/apps/server/src/modules/school/service/validation/school-validation.service.spec.ts b/apps/server/src/modules/school/service/validation/school-validation.service.spec.ts index aa01608fb9f..6a283e11374 100644 --- a/apps/server/src/modules/school/service/validation/school-validation.service.spec.ts +++ b/apps/server/src/modules/school/service/validation/school-validation.service.spec.ts @@ -1,8 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { LegacySchoolDo } from '@shared/domain'; -import { LegacySchoolRepo } from '@shared/repo'; -import { legacySchoolDoFactory } from '@shared/testing'; +import { SchoolDO } from '@shared/domain'; +import { SchoolRepo } from '@shared/repo'; +import { schoolDOFactory } from '@shared/testing'; import { SchoolNumberDuplicateLoggableException } from '../../error'; import { SchoolValidationService } from './school-validation.service'; @@ -10,21 +10,21 @@ describe('SchoolValidationService', () => { let module: TestingModule; let service: SchoolValidationService; - let schoolRepo: DeepMocked; + let schoolRepo: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ providers: [ SchoolValidationService, { - provide: LegacySchoolRepo, - useValue: createMock(), + provide: SchoolRepo, + useValue: createMock(), }, ], }).compile(); service = module.get(SchoolValidationService); - schoolRepo = module.get(LegacySchoolRepo); + schoolRepo = module.get(SchoolRepo); }); afterAll(async () => { @@ -35,7 +35,7 @@ describe('SchoolValidationService', () => { describe('isSchoolNumberUnique', () => { describe('when a school has no official school number', () => { const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ officialSchoolNumber: undefined }); + const school: SchoolDO = schoolDOFactory.buildWithId({ officialSchoolNumber: undefined }); return { school, @@ -51,7 +51,7 @@ describe('SchoolValidationService', () => { describe('when a new school is created and the school number is unique', () => { const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ officialSchoolNumber: '1234' }); + const school: SchoolDO = schoolDOFactory.buildWithId({ officialSchoolNumber: '1234' }); schoolRepo.findBySchoolNumber.mockResolvedValue(null); @@ -69,7 +69,7 @@ describe('SchoolValidationService', () => { describe('when an existing school is updated and the school number is unique', () => { const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ officialSchoolNumber: '1234' }); + const school: SchoolDO = schoolDOFactory.buildWithId({ officialSchoolNumber: '1234' }); schoolRepo.findBySchoolNumber.mockResolvedValue(school); @@ -87,8 +87,8 @@ describe('SchoolValidationService', () => { describe('when the school number already exists on another school', () => { const setup = () => { - const newSchool: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ officialSchoolNumber: '1234' }); - const existingSchool: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ officialSchoolNumber: '1234' }); + const newSchool: SchoolDO = schoolDOFactory.buildWithId({ officialSchoolNumber: '1234' }); + const existingSchool: SchoolDO = schoolDOFactory.buildWithId({ officialSchoolNumber: '1234' }); schoolRepo.findBySchoolNumber.mockResolvedValue(existingSchool); diff --git a/apps/server/src/modules/school/service/validation/school-validation.service.ts b/apps/server/src/modules/school/service/validation/school-validation.service.ts index 044e0473ef3..a1dc1a7396d 100644 --- a/apps/server/src/modules/school/service/validation/school-validation.service.ts +++ b/apps/server/src/modules/school/service/validation/school-validation.service.ts @@ -1,24 +1,24 @@ import { Injectable } from '@nestjs/common'; -import { LegacySchoolDo } from '@shared/domain'; -import { LegacySchoolRepo } from '@shared/repo'; +import { SchoolDO } from '@shared/domain'; +import { SchoolRepo } from '@shared/repo'; import { SchoolNumberDuplicateLoggableException } from '../../error'; @Injectable() export class SchoolValidationService { - constructor(private readonly schoolRepo: LegacySchoolRepo) {} + constructor(private readonly schoolRepo: SchoolRepo) {} - public async validate(school: LegacySchoolDo): Promise { + public async validate(school: SchoolDO): Promise { if (!(await this.isSchoolNumberUnique(school))) { throw new SchoolNumberDuplicateLoggableException(school.officialSchoolNumber as string); } } - private async isSchoolNumberUnique(school: LegacySchoolDo): Promise { + private async isSchoolNumberUnique(school: SchoolDO): Promise { if (!school.officialSchoolNumber) { return true; } - const foundSchool: LegacySchoolDo | null = await this.schoolRepo.findBySchoolNumber(school.officialSchoolNumber); + const foundSchool: SchoolDO | null = await this.schoolRepo.findBySchoolNumber(school.officialSchoolNumber); return foundSchool === null || foundSchool.id === school.id; } diff --git a/apps/server/src/modules/school/uc/index.ts b/apps/server/src/modules/school/uc/index.ts deleted file mode 100644 index 97a341c0458..00000000000 --- a/apps/server/src/modules/school/uc/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './legacy-school.uc'; diff --git a/apps/server/src/modules/school/uc/legacy-school.uc.spec.ts b/apps/server/src/modules/school/uc/school.uc.spec.ts similarity index 89% rename from apps/server/src/modules/school/uc/legacy-school.uc.spec.ts rename to apps/server/src/modules/school/uc/school.uc.spec.ts index 5bb583a743d..e293af1d869 100644 --- a/apps/server/src/modules/school/uc/legacy-school.uc.spec.ts +++ b/apps/server/src/modules/school/uc/school.uc.spec.ts @@ -1,11 +1,12 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { LegacySchoolDo, UserLoginMigrationDO } from '@shared/domain'; -import { legacySchoolDoFactory, userLoginMigrationDOFactory } from '@shared/testing/factory'; +import { UserLoginMigrationDO } from '@shared/domain'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; +import { schoolDOFactory, userLoginMigrationDOFactory } from '@shared/testing/factory'; import { AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/school/service'; -import { LegacySchoolUc } from '@src/modules/school/uc'; +import { SchoolService } from '@src/modules/school/service/school.service'; +import { SchoolUc } from '@src/modules/school/uc/school.uc'; import { SchoolMigrationService, UserLoginMigrationRevertService, @@ -13,11 +14,11 @@ import { } from '@src/modules/user-login-migration'; import { OauthMigrationDto } from './dto/oauth-migration.dto'; -describe('LegacySchoolUc', () => { +describe('SchoolUc', () => { let module: TestingModule; - let schoolUc: LegacySchoolUc; + let schoolUc: SchoolUc; - let schoolService: DeepMocked; + let schoolService: DeepMocked; let authService: DeepMocked; let schoolMigrationService: DeepMocked; let userLoginMigrationService: DeepMocked; @@ -26,10 +27,10 @@ describe('LegacySchoolUc', () => { beforeAll(async () => { module = await Test.createTestingModule({ providers: [ - LegacySchoolUc, + SchoolUc, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolService, + useValue: createMock(), }, { provide: AuthorizationService, @@ -50,9 +51,9 @@ describe('LegacySchoolUc', () => { ], }).compile(); - schoolService = module.get(LegacySchoolService); + schoolService = module.get(SchoolService); authService = module.get(AuthorizationService); - schoolUc = module.get(LegacySchoolUc); + schoolUc = module.get(SchoolUc); schoolMigrationService = module.get(SchoolMigrationService); userLoginMigrationService = module.get(UserLoginMigrationService); userLoginMigrationRevertService = module.get(UserLoginMigrationRevertService); @@ -69,7 +70,7 @@ describe('LegacySchoolUc', () => { describe('setMigration is called', () => { describe('when first starting the migration', () => { const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); const userLoginMigration: UserLoginMigrationDO = new UserLoginMigrationDO({ schoolId: 'schoolId', targetSystemId: 'targetSystemId', @@ -97,7 +98,7 @@ describe('LegacySchoolUc', () => { describe('when closing a migration', () => { describe('when there were migrated users', () => { const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); const userLoginMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.build({ closedAt: undefined, }); @@ -128,7 +129,7 @@ describe('LegacySchoolUc', () => { describe('when there were no users migrated', () => { const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); const userLoginMigration: UserLoginMigrationDO = userLoginMigrationDOFactory.build({ closedAt: undefined, }); @@ -162,7 +163,7 @@ describe('LegacySchoolUc', () => { describe('when restarting a migration', () => { const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); const userLoginMigration: UserLoginMigrationDO = new UserLoginMigrationDO({ schoolId: 'schoolId', targetSystemId: 'targetSystemId', @@ -193,7 +194,7 @@ describe('LegacySchoolUc', () => { describe('when trying to start a finished migration after the grace period', () => { const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); const userLoginMigration: UserLoginMigrationDO = new UserLoginMigrationDO({ schoolId: 'schoolId', targetSystemId: 'targetSystemId', @@ -229,7 +230,7 @@ describe('LegacySchoolUc', () => { describe('getMigration is called', () => { describe('when the school has a migration', () => { const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); const userLoginMigration: UserLoginMigrationDO = new UserLoginMigrationDO({ schoolId: 'schoolId', targetSystemId: 'targetSystemId', @@ -261,7 +262,7 @@ describe('LegacySchoolUc', () => { describe('when the school has no migration', () => { const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); userLoginMigrationService.findMigrationBySchool.mockResolvedValue(null); schoolService.getSchoolById.mockResolvedValue(school); diff --git a/apps/server/src/modules/school/uc/legacy-school.uc.ts b/apps/server/src/modules/school/uc/school.uc.ts similarity index 88% rename from apps/server/src/modules/school/uc/legacy-school.uc.ts rename to apps/server/src/modules/school/uc/school.uc.ts index 57a004a441e..0fc424938a2 100644 --- a/apps/server/src/modules/school/uc/legacy-school.uc.ts +++ b/apps/server/src/modules/school/uc/school.uc.ts @@ -1,21 +1,18 @@ import { Injectable } from '@nestjs/common'; -import { Permission, LegacySchoolDo, UserLoginMigrationDO } from '@shared/domain'; +import { Permission, SchoolDO, UserLoginMigrationDO } from '@shared/domain'; import { Action, AuthorizableReferenceType, AuthorizationService } from '@src/modules/authorization'; import { SchoolMigrationService, UserLoginMigrationRevertService, UserLoginMigrationService, } from '@src/modules/user-login-migration'; -import { LegacySchoolService } from '../service'; +import { SchoolService } from '../service'; import { OauthMigrationDto } from './dto/oauth-migration.dto'; -/** - * @deprecated because it uses the deprecated LegacySchoolService. - */ @Injectable() -export class LegacySchoolUc { +export class SchoolUc { constructor( - private readonly schoolService: LegacySchoolService, + private readonly schoolService: SchoolService, private readonly authService: AuthorizationService, private readonly schoolMigrationService: SchoolMigrationService, private readonly userLoginMigrationService: UserLoginMigrationService, @@ -61,7 +58,7 @@ export class LegacySchoolUc { await this.schoolMigrationService.unmarkOutdatedUsers(schoolId); } - const school: LegacySchoolDo = await this.schoolService.getSchoolById(schoolId); + const school: SchoolDO = await this.schoolService.getSchoolById(schoolId); const migrationDto: OauthMigrationDto = new OauthMigrationDto({ oauthMigrationPossible: !updatedUserLoginMigration.closedAt ? updatedUserLoginMigration.startedAt : undefined, @@ -84,7 +81,7 @@ export class LegacySchoolUc { schoolId ); - const school: LegacySchoolDo = await this.schoolService.getSchoolById(schoolId); + const school: SchoolDO = await this.schoolService.getSchoolById(schoolId); const migrationDto: OauthMigrationDto = new OauthMigrationDto({ oauthMigrationPossible: diff --git a/apps/server/src/modules/server/server.module.ts b/apps/server/src/modules/server/server.module.ts index 5a5826843e7..ad338bae642 100644 --- a/apps/server/src/modules/server/server.module.ts +++ b/apps/server/src/modules/server/server.module.ts @@ -16,6 +16,7 @@ import { AuthenticationApiModule } from '@src/modules/authentication/authenticat import { BoardApiModule } from '@src/modules/board/board-api.module'; import { CollaborativeStorageModule } from '@src/modules/collaborative-storage'; import { FilesStorageClientModule } from '@src/modules/files-storage-client'; +import { GroupApiModule } from '@src/modules/group/group-api.module'; import { LearnroomApiModule } from '@src/modules/learnroom/learnroom-api.module'; import { LessonApiModule } from '@src/modules/lesson/lesson-api.module'; import { NewsModule } from '@src/modules/news'; @@ -34,6 +35,7 @@ import { VideoConferenceApiModule } from '@src/modules/video-conference/video-co import connectRedis from 'connect-redis'; import session from 'express-session'; import { RedisClient } from 'redis'; +import { TeamsApiModule } from '@src/modules/teams/teams-api.module'; import { ServerController } from './controller/server.controller'; import { serverConfig } from './server.config'; @@ -70,6 +72,8 @@ const serverModules = [ ToolApiModule, UserLoginMigrationApiModule, BoardApiModule, + GroupApiModule, + TeamsApiModule, ]; export const defaultMikroOrmOptions: MikroOrmModuleSyncOptions = { diff --git a/apps/server/src/modules/teams/service/team.service.spec.ts b/apps/server/src/modules/teams/service/team.service.spec.ts index d9dbe6e8457..1406f90e0b9 100644 --- a/apps/server/src/modules/teams/service/team.service.spec.ts +++ b/apps/server/src/modules/teams/service/team.service.spec.ts @@ -1,9 +1,7 @@ import { DeepMocked, createMock } from '@golevelup/ts-jest'; -import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { TeamsRepo } from '@shared/repo'; -import { setupEntities, teamFactory, teamUserFactory, userDoFactory } from '@shared/testing'; -import { EntityId, UserDO } from '@shared/domain'; +import { setupEntities, teamFactory, teamUserFactory } from '@shared/testing'; import { TeamService } from './team.service'; describe('TeamService', () => { @@ -37,24 +35,39 @@ describe('TeamService', () => { await module.close(); }); - describe('deleteUserDataFromTeams', () => { - describe('when user is missing', () => { + describe('findUserDataFromTeams', () => { + describe('when finding by userId', () => { const setup = () => { - const user: UserDO = userDoFactory.build({ id: undefined }); - const userId = user.id as EntityId; + const teamUser = teamUserFactory.buildWithId(); + const team1 = teamFactory.withTeamUser([teamUser]).build(); + const team2 = teamFactory.withTeamUser([teamUser]).build(); + + teamsRepo.findByUserId.mockResolvedValue([team1, team2]); return { - userId, + teamUser, }; }; - it('should throw an error', async () => { - const { userId } = setup(); + it('should call teamsRepo.findByUserId', async () => { + const { teamUser } = setup(); + + await service.findUserDataFromTeams(teamUser.user.id); - await expect(service.deleteUserDataFromTeams(userId)).rejects.toThrowError(InternalServerErrorException); + expect(teamsRepo.findByUserId).toBeCalledWith(teamUser.user.id); + }); + + it('should return array of two teams with user', async () => { + const { teamUser } = setup(); + + const result = await service.findUserDataFromTeams(teamUser.user.id); + + expect(result.length).toEqual(2); }); }); + }); + describe('deleteUserDataFromTeams', () => { describe('when deleting by userId', () => { const setup = () => { const teamUser = teamUserFactory.buildWithId(); diff --git a/apps/server/src/modules/teams/service/team.service.ts b/apps/server/src/modules/teams/service/team.service.ts index eb478e35933..364d6b73e57 100644 --- a/apps/server/src/modules/teams/service/team.service.ts +++ b/apps/server/src/modules/teams/service/team.service.ts @@ -1,4 +1,4 @@ -import { Injectable, InternalServerErrorException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; import { EntityId, TeamEntity } from '@shared/domain'; import { TeamsRepo } from '@shared/repo'; @@ -6,23 +6,21 @@ import { TeamsRepo } from '@shared/repo'; export class TeamService { constructor(private readonly teamsRepo: TeamsRepo) {} - public async deleteUserDataFromTeams(userId: EntityId): Promise { - if (!userId) { - throw new InternalServerErrorException('User id is missing'); - } + public async findUserDataFromTeams(userId: EntityId): Promise { + const teams = await this.teamsRepo.findByUserId(userId); + return teams; + } + + public async deleteUserDataFromTeams(userId: EntityId): Promise { const teams = await this.teamsRepo.findByUserId(userId); - const updatedTeams: TeamEntity[] = teams.map((team: TeamEntity) => { - return { - ...team, - userIds: team.userIds.filter((u) => u.userId.id !== userId), - teamUsers: team.userIds.filter((u) => u.userId.id !== userId), - }; + teams.forEach((team) => { + team.userIds = team.userIds.filter((u) => u.userId.id !== userId); }); - await this.teamsRepo.save(updatedTeams); + await this.teamsRepo.save(teams); - return updatedTeams.length; + return teams.length; } } diff --git a/apps/server/src/modules/teams/teams-api.module.ts b/apps/server/src/modules/teams/teams-api.module.ts new file mode 100644 index 00000000000..5fc620fe76a --- /dev/null +++ b/apps/server/src/modules/teams/teams-api.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; +import { TeamsModule } from '@src/modules/teams/teams.module'; + +@Module({ + imports: [TeamsModule], + providers: [], + controllers: [], + exports: [], +}) +export class TeamsApiModule {} diff --git a/apps/server/src/modules/teams/uc/index.ts b/apps/server/src/modules/teams/uc/index.ts deleted file mode 100644 index fe02328cb41..00000000000 --- a/apps/server/src/modules/teams/uc/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from './team.uc'; diff --git a/apps/server/src/modules/teams/uc/team.uc.spec.ts b/apps/server/src/modules/teams/uc/team.uc.spec.ts deleted file mode 100644 index 3d2d8372fef..00000000000 --- a/apps/server/src/modules/teams/uc/team.uc.spec.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Test, TestingModule } from '@nestjs/testing'; -import { setupEntities, teamUserFactory } from '@shared/testing'; -import { TeamService } from '../service'; -import { TeamUC } from './team.uc'; - -describe('TeamUC', () => { - let teamUC: TeamUC; - let module: TestingModule; - - let teamService: DeepMocked; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - TeamUC, - { - provide: TeamService, - useValue: createMock(), - }, - ], - }).compile(); - teamUC = module.get(TeamUC); - teamService = module.get(TeamService); - - await setupEntities(); - }); - - afterAll(async () => { - await module.close(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should be defined', () => { - expect(teamUC).toBeDefined(); - }); - - it('should call teamService.deleteUserDataFromTeams', async () => { - const teamUser = teamUserFactory.build(); - - await teamUC.deleteUserData(teamUser.user.id); - - expect(teamService.deleteUserDataFromTeams).toBeCalledWith(teamUser.user.id); - }); - - it('should update teams without deleted user', async () => { - const teamUser = teamUserFactory.build(); - - teamService.deleteUserDataFromTeams.mockResolvedValue(3); - - const result = await teamUC.deleteUserData(teamUser.user.id); - - expect(result).toEqual(3); - }); -}); diff --git a/apps/server/src/modules/teams/uc/team.uc.ts b/apps/server/src/modules/teams/uc/team.uc.ts deleted file mode 100644 index ad5c735ae00..00000000000 --- a/apps/server/src/modules/teams/uc/team.uc.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { EntityId } from '@shared/domain'; -import { TeamService } from '../service'; - -@Injectable() -export class TeamUC { - constructor(private readonly teamService: TeamService) {} - - async deleteUserData(userId: EntityId): Promise { - const updatedTeams = await this.teamService.deleteUserDataFromTeams(userId); - - return updatedTeams; - } -} diff --git a/apps/server/src/modules/tool/common/uc/tool-permission-helper.ts b/apps/server/src/modules/tool/common/uc/tool-permission-helper.ts index d869ce08d50..6e47f6db97c 100644 --- a/apps/server/src/modules/tool/common/uc/tool-permission-helper.ts +++ b/apps/server/src/modules/tool/common/uc/tool-permission-helper.ts @@ -1,7 +1,7 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; -import { EntityId, LegacySchoolDo, User } from '@shared/domain'; +import { EntityId, SchoolDO, User } from '@shared/domain'; import { AuthorizableReferenceType, AuthorizationContext, AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { ContextExternalTool } from '../../context-external-tool/domain'; import { SchoolExternalTool } from '../../school-external-tool/domain'; import { ContextTypeMapper } from '../mapper'; @@ -10,7 +10,7 @@ import { ContextTypeMapper } from '../mapper'; export class ToolPermissionHelper { constructor( @Inject(forwardRef(() => AuthorizationService)) private authorizationService: AuthorizationService, - private readonly schoolService: LegacySchoolService + private readonly schoolService: SchoolService ) {} // TODO build interface to get contextDO by contextType @@ -42,7 +42,7 @@ export class ToolPermissionHelper { context: AuthorizationContext ): Promise { const user: User = await this.authorizationService.getUserWithPermissions(userId); - const school: LegacySchoolDo = await this.schoolService.getSchoolById(schoolExternalTool.schoolId); + const school: SchoolDO = await this.schoolService.getSchoolById(schoolExternalTool.schoolId); this.authorizationService.checkPermission(user, school, context); } } diff --git a/apps/server/src/modules/tool/common/uc/tool-permissions-helper.spec.ts b/apps/server/src/modules/tool/common/uc/tool-permissions-helper.spec.ts index 2aa60622e57..a42c1c5cbde 100644 --- a/apps/server/src/modules/tool/common/uc/tool-permissions-helper.spec.ts +++ b/apps/server/src/modules/tool/common/uc/tool-permissions-helper.spec.ts @@ -1,14 +1,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { - contextExternalToolFactory, - legacySchoolDoFactory, - schoolExternalToolFactory, - setupEntities, -} from '@shared/testing'; -import { Permission, LegacySchoolDo } from '@shared/domain'; +import { contextExternalToolFactory, schoolDOFactory, schoolExternalToolFactory, setupEntities } from '@shared/testing'; +import { Permission, SchoolDO } from '@shared/domain'; import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { ContextExternalTool } from '../../context-external-tool/domain'; import { ToolPermissionHelper } from './tool-permission-helper'; import { SchoolExternalTool } from '../../school-external-tool/domain'; @@ -18,7 +13,7 @@ describe('ToolPermissionHelper', () => { let helper: ToolPermissionHelper; let authorizationService: DeepMocked; - let schoolService: DeepMocked; + let schoolService: DeepMocked; beforeAll(async () => { await setupEntities(); @@ -30,15 +25,15 @@ describe('ToolPermissionHelper', () => { useValue: createMock(), }, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolService, + useValue: createMock(), }, ], }).compile(); helper = module.get(ToolPermissionHelper); authorizationService = module.get(AuthorizationService); - schoolService = module.get(LegacySchoolService); + schoolService = module.get(SchoolService); }); afterAll(async () => { @@ -84,7 +79,7 @@ describe('ToolPermissionHelper', () => { const userId = 'userId'; const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId(); const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.SCHOOL_TOOL_ADMIN]); - const school: LegacySchoolDo = legacySchoolDoFactory.build({ id: schoolExternalTool.schoolId }); + const school: SchoolDO = schoolDOFactory.build({ id: schoolExternalTool.schoolId }); schoolService.getSchoolById.mockResolvedValue(school); diff --git a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-authorizable.service.spec.ts b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-authorizable.service.spec.ts index a0698e904a9..96828243654 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-authorizable.service.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-authorizable.service.spec.ts @@ -1,4 +1,4 @@ -import { contextExternalToolFactory, legacySchoolDoFactory, schoolExternalToolFactory } from '@shared/testing'; +import { contextExternalToolFactory, schoolDOFactory, schoolExternalToolFactory } from '@shared/testing'; import { Test, TestingModule } from '@nestjs/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ContextExternalToolRepo } from '@shared/repo'; @@ -38,7 +38,7 @@ describe('ContextExternalToolAuthorizableService', () => { describe('findById', () => { describe('when id is given', () => { const setup = () => { - const schoolId: string = legacySchoolDoFactory.buildWithId().id as string; + const schoolId: string = schoolDOFactory.buildWithId().id as string; const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ schoolId, }); diff --git a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.spec.ts b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.spec.ts index 28cb093ae2d..1f2a1496fac 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.spec.ts @@ -4,7 +4,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ContextExternalToolRepo } from '@shared/repo'; import { contextExternalToolFactory, - legacySchoolDoFactory, + schoolDOFactory, schoolExternalToolFactory, } from '@shared/testing/factory/domainobject'; import { AuthorizationService } from '@src/modules/authorization'; @@ -133,7 +133,7 @@ describe('ContextExternalToolService', () => { describe('getContextExternalToolById', () => { describe('when contextExternalToolId is given', () => { const setup = () => { - const schoolId: string = legacySchoolDoFactory.buildWithId().id as string; + const schoolId: string = schoolDOFactory.buildWithId().id as string; const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ schoolId, }); diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-logo-service.spec.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-logo-service.spec.ts index 1c016b34bc3..57acd50122f 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-logo-service.spec.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-logo-service.spec.ts @@ -1,23 +1,22 @@ -import { of, throwError } from 'rxjs'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { HttpService } from '@nestjs/axios'; import { HttpException, HttpStatus } from '@nestjs/common'; -import { AxiosResponse } from 'axios'; import { Test, TestingModule } from '@nestjs/testing'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { HttpService } from '@nestjs/axios'; +import { axiosResponseFactory, externalToolFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { externalToolFactory } from '@shared/testing'; +import { of, throwError } from 'rxjs'; +import { IToolFeatures, ToolFeatures } from '../../tool-config'; +import { ExternalTool } from '../domain'; +import { ExternalToolLogo } from '../domain/external-tool-logo'; import { + ExternalToolLogoFetchFailedLoggableException, ExternalToolLogoFetchedLoggable, ExternalToolLogoNotFoundLoggableException, ExternalToolLogoSizeExceededLoggableException, - ExternalToolLogoFetchFailedLoggableException, ExternalToolLogoWrongFileTypeLoggableException, } from '../loggable'; import { ExternalToolLogoService } from './external-tool-logo.service'; -import { ExternalTool } from '../domain'; -import { IToolFeatures, ToolFeatures } from '../../tool-config'; import { ExternalToolService } from './external-tool.service'; -import { ExternalToolLogo } from '../domain/external-tool-logo'; describe('ExternalToolLogoService', () => { let module: TestingModule; @@ -195,13 +194,13 @@ describe('ExternalToolLogoService', () => { const logoBuffer: Buffer = Buffer.from(base64Logo, 'base64'); httpService.get.mockReturnValue( - of({ - data: logoBuffer, - status: HttpStatus.OK, - statusText: 'OK', - headers: {}, - config: {}, - } as AxiosResponse) + of( + axiosResponseFactory.build({ + data: logoBuffer, + status: HttpStatus.OK, + statusText: 'OK', + }) + ) ); const logoUrl = 'https://logo.com/'; diff --git a/apps/server/src/modules/tool/tool-launch/service/strategy/abstract-launch.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/strategy/abstract-launch.strategy.spec.ts index b2d9c898171..10f0e19e639 100644 --- a/apps/server/src/modules/tool/tool-launch/service/strategy/abstract-launch.strategy.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/strategy/abstract-launch.strategy.spec.ts @@ -2,18 +2,18 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { Course, EntityId, LegacySchoolDo } from '@shared/domain'; +import { Course, EntityId, SchoolDO } from '@shared/domain'; import { CourseRepo } from '@shared/repo'; import { contextExternalToolFactory, courseFactory, customParameterFactory, externalToolFactory, - legacySchoolDoFactory, + schoolDOFactory, schoolExternalToolFactory, setupEntities, } from '@shared/testing'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { MissingToolParameterValueLoggableException, ParameterTypeNotImplementedLoggableException } from '../../error'; import { LaunchRequestMethod, @@ -73,7 +73,7 @@ describe('AbstractLaunchStrategy', () => { let module: TestingModule; let launchStrategy: TestLaunchStrategy; - let schoolService: DeepMocked; + let schoolService: DeepMocked; let courseRepo: DeepMocked; beforeAll(async () => { @@ -83,8 +83,8 @@ describe('AbstractLaunchStrategy', () => { providers: [ TestLaunchStrategy, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolService, + useValue: createMock(), }, { provide: CourseRepo, @@ -94,7 +94,7 @@ describe('AbstractLaunchStrategy', () => { }).compile(); launchStrategy = module.get(TestLaunchStrategy); - schoolService = module.get(LegacySchoolService); + schoolService = module.get(SchoolService); courseRepo = module.get(CourseRepo); }); @@ -184,7 +184,7 @@ describe('AbstractLaunchStrategy', () => { }); // Other - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId( + const school: SchoolDO = schoolDOFactory.buildWithId( { officialSchoolNumber: '1234', }, @@ -369,7 +369,7 @@ describe('AbstractLaunchStrategy', () => { parameters: [], }); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + const school: SchoolDO = schoolDOFactory.buildWithId({ officialSchoolNumber: undefined, }); diff --git a/apps/server/src/modules/tool/tool-launch/service/strategy/abstract-launch.strategy.ts b/apps/server/src/modules/tool/tool-launch/service/strategy/abstract-launch.strategy.ts index 2b85ab53922..b2fe6f94446 100644 --- a/apps/server/src/modules/tool/tool-launch/service/strategy/abstract-launch.strategy.ts +++ b/apps/server/src/modules/tool/tool-launch/service/strategy/abstract-launch.strategy.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { Course, EntityId, LegacySchoolDo } from '@shared/domain'; +import { Course, EntityId, SchoolDO } from '@shared/domain'; import { CourseRepo } from '@shared/repo'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { URLSearchParams } from 'url'; import { CustomParameterLocation, @@ -21,7 +21,7 @@ import { ExternalTool } from '../../../external-tool/domain'; @Injectable() export abstract class AbstractLaunchStrategy implements IToolLaunchStrategy { - constructor(private readonly schoolService: LegacySchoolService, private readonly courseRepo: CourseRepo) {} + constructor(private readonly schoolService: SchoolService, private readonly courseRepo: CourseRepo) {} public async createLaunchData(userId: EntityId, data: IToolLaunchParams): Promise { const launchData: ToolLaunchData = this.buildToolLaunchDataFromExternalTool(data.externalTool); @@ -226,7 +226,7 @@ export abstract class AbstractLaunchStrategy implements IToolLaunchStrategy { ); } case CustomParameterType.AUTO_SCHOOLNUMBER: { - const school: LegacySchoolDo = await this.schoolService.getSchoolById(schoolExternalTool.schoolId); + const school: SchoolDO = await this.schoolService.getSchoolById(schoolExternalTool.schoolId); return school.officialSchoolNumber; } diff --git a/apps/server/src/modules/tool/tool-launch/service/strategy/basic-tool-launch.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/strategy/basic-tool-launch.strategy.spec.ts index 3c9ba5d47e2..4aa1314ce41 100644 --- a/apps/server/src/modules/tool/tool-launch/service/strategy/basic-tool-launch.strategy.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/strategy/basic-tool-launch.strategy.spec.ts @@ -2,7 +2,7 @@ import { createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { CourseRepo } from '@shared/repo'; import { contextExternalToolFactory, externalToolFactory, schoolExternalToolFactory } from '@shared/testing'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { LaunchRequestMethod, PropertyData, PropertyLocation } from '../../types'; import { BasicToolLaunchStrategy } from './basic-tool-launch.strategy'; import { IToolLaunchParams } from './tool-launch-params.interface'; @@ -19,8 +19,8 @@ describe('BasicToolLaunchStrategy', () => { providers: [ BasicToolLaunchStrategy, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolService, + useValue: createMock(), }, { provide: CourseRepo, diff --git a/apps/server/src/modules/tool/tool-launch/service/strategy/lti11-tool-launch.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/strategy/lti11-tool-launch.strategy.spec.ts index 1b9c19b7aa9..5a58fe373b5 100644 --- a/apps/server/src/modules/tool/tool-launch/service/strategy/lti11-tool-launch.strategy.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/strategy/lti11-tool-launch.strategy.spec.ts @@ -11,7 +11,7 @@ import { } from '@shared/testing'; import { pseudonymFactory } from '@shared/testing/factory/domainobject/pseudonym.factory'; import { PseudonymService } from '@src/modules/pseudonym/service'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { UserService } from '@src/modules/user'; import { ObjectId } from 'bson'; import { Authorization } from 'oauth-1.0a'; @@ -49,8 +49,8 @@ describe('Lti11ToolLaunchStrategy', () => { useValue: createMock(), }, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolService, + useValue: createMock(), }, { provide: CourseRepo, @@ -348,7 +348,7 @@ describe('Lti11ToolLaunchStrategy', () => { const user: UserDO = userDoFactory.buildWithId(); - const pseudonym: Pseudonym = pseudonymFactory.buildWithId(); + const pseudonym: Pseudonym = pseudonymFactory.build(); userService.findById.mockResolvedValue(user); pseudonymService.findOrCreatePseudonym.mockResolvedValue(pseudonym); diff --git a/apps/server/src/modules/tool/tool-launch/service/strategy/lti11-tool-launch.strategy.ts b/apps/server/src/modules/tool/tool-launch/service/strategy/lti11-tool-launch.strategy.ts index af97d2ba7c2..7038765f06b 100644 --- a/apps/server/src/modules/tool/tool-launch/service/strategy/lti11-tool-launch.strategy.ts +++ b/apps/server/src/modules/tool/tool-launch/service/strategy/lti11-tool-launch.strategy.ts @@ -3,7 +3,7 @@ import { EntityId, LtiPrivacyPermission, Pseudonym, RoleName, UserDO } from '@sh import { RoleReference } from '@shared/domain/domainobject'; import { CourseRepo } from '@shared/repo'; import { PseudonymService } from '@src/modules/pseudonym'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { UserService } from '@src/modules/user'; import { Authorization } from 'oauth-1.0a'; import { LtiRole } from '../../../common/enum'; @@ -20,7 +20,7 @@ export class Lti11ToolLaunchStrategy extends AbstractLaunchStrategy { private readonly userService: UserService, private readonly pseudonymService: PseudonymService, private readonly lti11EncryptionService: Lti11EncryptionService, - schoolService: LegacySchoolService, + schoolService: SchoolService, courseRepo: CourseRepo ) { super(schoolService, courseRepo); diff --git a/apps/server/src/modules/tool/tool-launch/service/strategy/oauth2-tool-launch.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/strategy/oauth2-tool-launch.strategy.spec.ts index 488ac87c444..021e9c93ba3 100644 --- a/apps/server/src/modules/tool/tool-launch/service/strategy/oauth2-tool-launch.strategy.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/strategy/oauth2-tool-launch.strategy.spec.ts @@ -2,7 +2,7 @@ import { createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { CourseRepo } from '@shared/repo'; import { contextExternalToolFactory, externalToolFactory, schoolExternalToolFactory } from '@shared/testing'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { LaunchRequestMethod, PropertyData } from '../../types'; import { OAuth2ToolLaunchStrategy } from './oauth2-tool-launch.strategy'; import { IToolLaunchParams } from './tool-launch-params.interface'; @@ -19,8 +19,8 @@ describe('OAuth2ToolLaunchStrategy', () => { providers: [ OAuth2ToolLaunchStrategy, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolService, + useValue: createMock(), }, { provide: CourseRepo, diff --git a/apps/server/src/modules/user-import/controller/import-user.controller.spec.ts b/apps/server/src/modules/user-import/controller/import-user.controller.spec.ts index d6358d314ae..386a2e874b8 100644 --- a/apps/server/src/modules/user-import/controller/import-user.controller.spec.ts +++ b/apps/server/src/modules/user-import/controller/import-user.controller.spec.ts @@ -2,7 +2,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ImportUserRepo, SystemRepo, UserRepo } from '@shared/repo'; import { AccountService } from '@src/modules/account/services/account.service'; import { AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { LoggerModule } from '@src/core/logger'; import { ConfigModule } from '@nestjs/config'; import { UserImportUc } from '../uc/user-import.uc'; @@ -34,7 +34,7 @@ describe('ImportUserController', () => { useValue: {}, }, { - provide: LegacySchoolService, + provide: SchoolService, useValue: {}, }, { 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 c2f1838e991..66ce2c32fef 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 @@ -2,12 +2,10 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons'; import { ObjectId } from '@mikro-orm/mongodb'; import { BadRequestException, ForbiddenException } from '@nestjs/common'; -import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { UserAlreadyAssignedToImportUserError } from '@shared/common'; import { ImportUser, - LegacySchoolDo, MatchCreator, MatchCreatorScope, Permission, @@ -16,14 +14,16 @@ import { System, User, } from '@shared/domain'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; import { MongoMemoryDatabaseModule } from '@shared/infra/database'; import { ImportUserRepo, SystemRepo, UserRepo } from '@shared/repo'; import { federalStateFactory, importUserFactory, schoolFactory, userFactory } from '@shared/testing'; import { systemFactory } from '@shared/testing/factory/system.factory'; -import { LoggerModule } from '@src/core/logger'; import { AccountService } from '@src/modules/account/services/account.service'; import { AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '../../school'; +import { LoggerModule } from '@src/core/logger'; +import { ConfigModule } from '@nestjs/config'; +import { SchoolService } from '../../school'; import { LdapAlreadyPersistedException, MigrationAlreadyActivatedException, @@ -37,7 +37,7 @@ describe('[ImportUserModule]', () => { let uc: UserImportUc; let accountService: DeepMocked; let importUserRepo: DeepMocked; - let schoolService: DeepMocked; + let schoolService: DeepMocked; let systemRepo: DeepMocked; let userRepo: DeepMocked; let authorizationService: DeepMocked; @@ -61,8 +61,8 @@ describe('[ImportUserModule]', () => { useValue: createMock(), }, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolService, + useValue: createMock(), }, { provide: SystemRepo, @@ -81,7 +81,7 @@ describe('[ImportUserModule]', () => { uc = module.get(UserImportUc); // TODO UserRepo not available in UserUc?! accountService = module.get(AccountService); importUserRepo = module.get(ImportUserRepo); - schoolService = module.get(LegacySchoolService); + schoolService = module.get(SchoolService); systemRepo = module.get(SystemRepo); userRepo = module.get(UserRepo); authorizationService = module.get(AuthorizationService); @@ -114,7 +114,7 @@ describe('[ImportUserModule]', () => { }); }; - const createMockSchoolDo = (school?: School): LegacySchoolDo => { + const createMockSchoolDo = (school?: School): SchoolDO => { const name = school ? school.name : 'testSchool'; const id = school ? school.id : 'someId'; const features = school ? school.features ?? [SchoolFeatures.LDAP_UNIVENTION_MIGRATION] : []; @@ -126,7 +126,7 @@ describe('[ImportUserModule]', () => { school && school.systems.isInitialized() ? school.systems.getItems().map((system: System) => system.id) : []; const federalState = school ? school.federalState : federalStateFactory.build(); - return new LegacySchoolDo({ + return new SchoolDO({ id, name, features, @@ -635,11 +635,9 @@ describe('[ImportUserModule]', () => { }); it('Should save school params', async () => { schoolServiceSaveSpy.mockRestore(); - schoolServiceSaveSpy = schoolService.save.mockImplementation((schoolDo: LegacySchoolDo) => - Promise.resolve(schoolDo) - ); + schoolServiceSaveSpy = schoolService.save.mockImplementation((schoolDo: SchoolDO) => Promise.resolve(schoolDo)); await uc.startSchoolInUserMigration(currentUser.id); - const schoolParams: LegacySchoolDo = { ...createMockSchoolDo(school) }; + const schoolParams: SchoolDO = { ...createMockSchoolDo(school) }; schoolParams.inUserMigration = true; schoolParams.externalId = 'foo'; schoolParams.inMaintenanceSince = currentDate; 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 7121b2c7b27..25c002601ef 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 @@ -1,4 +1,3 @@ -import { Configuration } from '@hpi-schul-cloud/commons'; import { BadRequestException, ForbiddenException, Injectable, InternalServerErrorException } from '@nestjs/common'; import { UserAlreadyAssignedToImportUserError } from '@shared/common'; import { @@ -7,9 +6,8 @@ import { EntityId, IFindOptions, IImportUserScope, - ImportUser, INameMatch, - LegacySchoolDo, + ImportUser, MatchCreator, MatchCreatorScope, Permission, @@ -17,12 +15,15 @@ import { System, User, } from '@shared/domain'; + +import { Configuration } from '@hpi-schul-cloud/commons'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; import { ImportUserRepo, SystemRepo, UserRepo } from '@shared/repo'; import { Logger } from '@src/core/logger'; import { AccountService } from '@src/modules/account/services/account.service'; import { AccountDto } from '@src/modules/account/services/dto/account.dto'; import { AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { MigrationMayBeCompleted, MigrationMayNotBeCompleted, @@ -48,7 +49,7 @@ export class UserImportUc { private readonly accountService: AccountService, private readonly importUserRepo: ImportUserRepo, private readonly authorizationService: AuthorizationService, - private readonly schoolService: LegacySchoolService, + private readonly schoolService: SchoolService, private readonly systemRepo: SystemRepo, private readonly userRepo: UserRepo, private readonly logger: Logger @@ -56,7 +57,7 @@ export class UserImportUc { this.logger.setContext(UserImportUc.name); } - private checkFeatureEnabled(school: LegacySchoolDo): void | never { + private checkFeatureEnabled(school: SchoolDO): void | never { const enabled = Configuration.get('FEATURE_USER_MIGRATION_ENABLED') as boolean; const isLdapPilotSchool = school.features && school.features.includes(SchoolFeatures.LDAP_UNIVENTION_MIGRATION); if (!enabled && !isLdapPilotSchool) { @@ -78,7 +79,7 @@ export class UserImportUc { options?: IFindOptions ): Promise> { const currentUser = await this.getCurrentUser(currentUserId, Permission.SCHOOL_IMPORT_USERS_VIEW); - const school: LegacySchoolDo = await this.schoolService.getSchoolById(currentUser.school.id); + const school: SchoolDO = await this.schoolService.getSchoolById(currentUser.school.id); this.checkFeatureEnabled(school); // TODO Change ImportUserRepo to DO to fix this workaround const countedImportUsers = await this.importUserRepo.findImportUsers(currentUser.school, query, options); @@ -94,7 +95,7 @@ export class UserImportUc { */ async setMatch(currentUserId: EntityId, importUserId: EntityId, userMatchId: EntityId): Promise { const currentUser = await this.getCurrentUser(currentUserId, Permission.SCHOOL_IMPORT_USERS_UPDATE); - const school: LegacySchoolDo = await this.schoolService.getSchoolById(currentUser.school.id); + const school: SchoolDO = await this.schoolService.getSchoolById(currentUser.school.id); this.checkFeatureEnabled(school); const importUser = await this.importUserRepo.findById(importUserId); const userMatch = await this.userRepo.findById(userMatchId, true); @@ -119,7 +120,7 @@ export class UserImportUc { async removeMatch(currentUserId: EntityId, importUserId: EntityId): Promise { const currentUser = await this.getCurrentUser(currentUserId, Permission.SCHOOL_IMPORT_USERS_UPDATE); - const school: LegacySchoolDo = await this.schoolService.getSchoolById(currentUser.school.id); + const school: SchoolDO = await this.schoolService.getSchoolById(currentUser.school.id); this.checkFeatureEnabled(school); const importUser = await this.importUserRepo.findById(importUserId); // check same school @@ -136,7 +137,7 @@ export class UserImportUc { async updateFlag(currentUserId: EntityId, importUserId: EntityId, flagged: boolean): Promise { const currentUser = await this.getCurrentUser(currentUserId, Permission.SCHOOL_IMPORT_USERS_UPDATE); - const school: LegacySchoolDo = await this.schoolService.getSchoolById(currentUser.school.id); + const school: SchoolDO = await this.schoolService.getSchoolById(currentUser.school.id); this.checkFeatureEnabled(school); const importUser = await this.importUserRepo.findById(importUserId); @@ -167,7 +168,7 @@ export class UserImportUc { options?: IFindOptions ): Promise> { const currentUser = await this.getCurrentUser(currentUserId, Permission.SCHOOL_IMPORT_USERS_VIEW); - const school: LegacySchoolDo = await this.schoolService.getSchoolById(currentUser.school.id); + const school: SchoolDO = await this.schoolService.getSchoolById(currentUser.school.id); this.checkFeatureEnabled(school); // TODO Change to UserService to fix this workaround const unmatchedCountedUsers = await this.userRepo.findWithoutImportUser(currentUser.school, query, options); @@ -176,7 +177,7 @@ export class UserImportUc { async saveAllUsersMatches(currentUserId: EntityId): Promise { const currentUser = await this.getCurrentUser(currentUserId, Permission.SCHOOL_IMPORT_USERS_MIGRATE); - const school: LegacySchoolDo = await this.schoolService.getSchoolById(currentUser.school.id); + const school: SchoolDO = await this.schoolService.getSchoolById(currentUser.school.id); this.checkFeatureEnabled(school); const filters: IImportUserScope = { matches: [MatchCreatorScope.MANUAL, MatchCreatorScope.AUTO] }; // TODO batch/paginated import? @@ -218,7 +219,7 @@ export class UserImportUc { private async endSchoolInUserMigration(currentUserId: EntityId): Promise { const currentUser = await this.getCurrentUser(currentUserId, Permission.SCHOOL_IMPORT_USERS_MIGRATE); - const school: LegacySchoolDo = await this.schoolService.getSchoolById(currentUser.school.id); + const school: SchoolDO = await this.schoolService.getSchoolById(currentUser.school.id); this.checkFeatureEnabled(school); if (!school.externalId || school.inUserMigration !== true || !school.inMaintenanceSince) { this.logger.warning(new MigrationMayBeCompleted(school.inUserMigration)); @@ -230,7 +231,7 @@ export class UserImportUc { async startSchoolInUserMigration(currentUserId: EntityId, useCentralLdap = true): Promise { const currentUser = await this.getCurrentUser(currentUserId, Permission.SCHOOL_IMPORT_USERS_MIGRATE); - const school: LegacySchoolDo = await this.schoolService.getSchoolById(currentUser.school.id); + const school: SchoolDO = await this.schoolService.getSchoolById(currentUser.school.id); this.logger.notice(new SchoolInUserMigrationStartLoggable(currentUserId, school.name, useCentralLdap)); this.checkFeatureEnabled(school); this.checkSchoolNumber(school, useCentralLdap); @@ -252,7 +253,7 @@ export class UserImportUc { 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); + const school: SchoolDO = await this.schoolService.getSchoolById(currentUser.school.id); this.checkFeatureEnabled(school); if (school.inUserMigration !== false || !school.inMaintenanceSince || !school.externalId) { this.logger.warning(new MigrationMayNotBeCompleted(school.inUserMigration)); @@ -270,10 +271,7 @@ export class UserImportUc { return currentUser; } - private async updateUserAndAccount( - importUser: ImportUser, - school: LegacySchoolDo - ): Promise<[User, Account] | undefined> { + private async updateUserAndAccount(importUser: ImportUser, school: SchoolDO): Promise<[User, Account] | undefined> { if (!importUser.user || !importUser.loginName || !school.externalId) { return; } @@ -298,7 +296,7 @@ export class UserImportUc { return system; } - private async checkNoExistingLdapBeforeStart(school: LegacySchoolDo): Promise { + private async checkNoExistingLdapBeforeStart(school: SchoolDO): Promise { if (school.systems && school.systems?.length > 0) { for (const systemId of school.systems) { // very unusual to have more than 1 system @@ -311,13 +309,13 @@ export class UserImportUc { } } - private checkSchoolNumber(school: LegacySchoolDo, useCentralLdap: boolean): void | never { + private checkSchoolNumber(school: SchoolDO, useCentralLdap: boolean): void | never { if (useCentralLdap && !school.officialSchoolNumber) { throw new MissingSchoolNumberException(); } } - private checkSchoolNotInMigration(school: LegacySchoolDo): void | never { + private checkSchoolNotInMigration(school: SchoolDO): void | never { if (school.inUserMigration !== undefined && school.inUserMigration !== null) { throw new MigrationAlreadyActivatedException(); } diff --git a/apps/server/src/modules/user-import/user-import.module.ts b/apps/server/src/modules/user-import/user-import.module.ts index ca2bae05883..af5e4e89fa3 100644 --- a/apps/server/src/modules/user-import/user-import.module.ts +++ b/apps/server/src/modules/user-import/user-import.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { ImportUserRepo, LegacySchoolRepo, SystemRepo, UserRepo } from '@shared/repo'; +import { ImportUserRepo, SchoolRepo, SystemRepo, UserRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; import { AccountModule } from '../account'; import { AuthorizationModule } from '../authorization'; @@ -10,7 +10,7 @@ import { UserImportUc } from './uc/user-import.uc'; @Module({ imports: [LoggerModule, AccountModule, SchoolModule, AuthorizationModule], controllers: [ImportUserController], - providers: [UserImportUc, ImportUserRepo, LegacySchoolRepo, SystemRepo, UserRepo], + providers: [UserImportUc, ImportUserRepo, SchoolRepo, SystemRepo, UserRepo], exports: [], }) /** 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 24dfd650bd8..9193d58d4fa 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 @@ -16,12 +16,12 @@ import { } from '@shared/testing'; import { JwtTestFactory } from '@shared/testing/factory/jwt.test.factory'; import { OauthTokenResponse } from '@src/modules/oauth/service/dto'; -import { SanisResponse, SanisRole } from '@src/modules/provisioning'; import { ServerTestModule } from '@src/modules/server'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { UUID } from 'bson'; import { Response } from 'supertest'; +import { SanisResponse, SanisRole } from '@src/modules/provisioning/strategy/sanis/response'; import { UserLoginMigrationResponse } from '../dto'; import { Oauth2MigrationParams } from '../dto/oauth2-migration.params'; @@ -423,16 +423,15 @@ describe('UserLoginMigrationController (API)', () => { }, personenkontexte: [ { - id: new UUID('aef1f4fd-c323-466e-962b-a84354c0e713'), + id: new UUID('aef1f4fd-c323-466e-962b-a84354c0e713').toString(), rolle: SanisRole.LEHR, organisation: { - id: new UUID('aef1f4fd-c323-466e-962b-a84354c0e713'), + id: new UUID('aef1f4fd-c323-466e-962b-a84354c0e713').toString(), kennung: officialSchoolNumber, name: 'schulName', typ: 'not necessary', }, personenstatus: 'not necessary', - email: 'email', }, ], }); diff --git a/apps/server/src/modules/user-login-migration/service/migration-check.service.spec.ts b/apps/server/src/modules/user-login-migration/service/migration-check.service.spec.ts index c21be1bddb2..c5092c9d393 100644 --- a/apps/server/src/modules/user-login-migration/service/migration-check.service.spec.ts +++ b/apps/server/src/modules/user-login-migration/service/migration-check.service.spec.ts @@ -1,9 +1,9 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { LegacySchoolDo, UserDO, UserLoginMigrationDO } from '@shared/domain'; +import { SchoolDO, UserDO, UserLoginMigrationDO } from '@shared/domain'; import { UserLoginMigrationRepo } from '@shared/repo'; -import { legacySchoolDoFactory, userDoFactory } from '@shared/testing'; -import { LegacySchoolService } from '@src/modules/school'; +import { schoolDOFactory, userDoFactory } from '@shared/testing'; +import { SchoolService } from '@src/modules/school'; import { UserService } from '@src/modules/user'; import { MigrationCheckService } from './migration-check.service'; @@ -12,7 +12,7 @@ describe('MigrationCheckService', () => { let service: MigrationCheckService; let userService: DeepMocked; - let schoolService: DeepMocked; + let schoolService: DeepMocked; let userLoginMigrationRepo: DeepMocked; beforeAll(async () => { @@ -24,8 +24,8 @@ describe('MigrationCheckService', () => { useValue: createMock(), }, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolService, + useValue: createMock(), }, { provide: UserLoginMigrationRepo, @@ -36,7 +36,7 @@ describe('MigrationCheckService', () => { service = module.get(MigrationCheckService); userService = module.get(UserService); - schoolService = module.get(LegacySchoolService); + schoolService = module.get(SchoolService); userLoginMigrationRepo = module.get(UserLoginMigrationRepo); }); @@ -62,7 +62,7 @@ describe('MigrationCheckService', () => { describe('when a non-migrating school was found', () => { const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); const user: UserDO = userDoFactory.buildWithId(); schoolService.getSchoolBySchoolNumber.mockResolvedValue(school); @@ -81,7 +81,7 @@ describe('MigrationCheckService', () => { describe('when a migrating school was found but no user', () => { const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); const userLoginMigration: UserLoginMigrationDO = new UserLoginMigrationDO({ schoolId: school.id as string, targetSystemId: 'targetSystemId', @@ -103,7 +103,7 @@ describe('MigrationCheckService', () => { describe('when a migrating school and a user that has not migrated were found', () => { const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); const user: UserDO = userDoFactory.buildWithId({ lastLoginSystemChange: undefined }); const userLoginMigration: UserLoginMigrationDO = new UserLoginMigrationDO({ schoolId: school.id as string, @@ -126,7 +126,7 @@ describe('MigrationCheckService', () => { describe('when a migrating school and a user that has migrated were found', () => { const setup = () => { - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); const user: UserDO = userDoFactory.buildWithId({ lastLoginSystemChange: new Date('2023-03-04') }); const userLoginMigration: UserLoginMigrationDO = new UserLoginMigrationDO({ schoolId: school.id as string, diff --git a/apps/server/src/modules/user-login-migration/service/migration-check.service.ts b/apps/server/src/modules/user-login-migration/service/migration-check.service.ts index ec39a619ef7..d2bfc6cc065 100644 --- a/apps/server/src/modules/user-login-migration/service/migration-check.service.ts +++ b/apps/server/src/modules/user-login-migration/service/migration-check.service.ts @@ -1,19 +1,21 @@ import { Injectable } from '@nestjs/common'; -import { EntityId, LegacySchoolDo, UserDO, UserLoginMigrationDO } from '@shared/domain'; +import { EntityId, UserLoginMigrationDO } from '@shared/domain'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; +import { UserDO } from '@shared/domain/domainobject/user.do'; import { UserLoginMigrationRepo } from '@shared/repo'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { UserService } from '@src/modules/user'; @Injectable() export class MigrationCheckService { constructor( private readonly userService: UserService, - private readonly schoolService: LegacySchoolService, + private readonly schoolService: SchoolService, private readonly userLoginMigrationRepo: UserLoginMigrationRepo ) {} async shouldUserMigrate(externalUserId: string, systemId: EntityId, officialSchoolNumber: string): Promise { - const school: LegacySchoolDo | null = await this.schoolService.getSchoolBySchoolNumber(officialSchoolNumber); + const school: SchoolDO | null = await this.schoolService.getSchoolBySchoolNumber(officialSchoolNumber); if (school && school.id) { const userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationRepo.findBySchoolId( diff --git a/apps/server/src/modules/user-login-migration/service/school-migration.service.spec.ts b/apps/server/src/modules/user-login-migration/service/school-migration.service.spec.ts index dad2d2451aa..df40d7ff6bb 100644 --- a/apps/server/src/modules/user-login-migration/service/school-migration.service.spec.ts +++ b/apps/server/src/modules/user-login-migration/service/school-migration.service.spec.ts @@ -3,12 +3,16 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ValidationError } from '@shared/common'; -import { LegacySchoolDo, Page, UserDO, UserLoginMigrationDO } from '@shared/domain'; +import { UserLoginMigrationDO } from '@shared/domain'; +import { Page } from '@shared/domain/domainobject/page'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; +import { UserDO } from '@shared/domain/domainobject/user.do'; import { UserLoginMigrationRepo } from '@shared/repo/userloginmigration/user-login-migration.repo'; -import { legacySchoolDoFactory, setupEntities, userDoFactory, userLoginMigrationDOFactory } from '@shared/testing'; +import { setupEntities, userDoFactory, userLoginMigrationDOFactory } from '@shared/testing'; +import { schoolDOFactory } from '@shared/testing/factory/domainobject/school.factory'; import { LegacyLogger } from '@src/core/logger'; import { ICurrentUser } from '@src/modules/authentication'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { UserService } from '@src/modules/user'; import { OAuthMigrationError } from '@src/modules/user-login-migration/error/oauth-migration.error'; import { SchoolMigrationService } from './school-migration.service'; @@ -18,7 +22,7 @@ describe('SchoolMigrationService', () => { let service: SchoolMigrationService; let userService: DeepMocked; - let schoolService: DeepMocked; + let schoolService: DeepMocked; let userLoginMigrationRepo: DeepMocked; beforeAll(async () => { @@ -28,8 +32,8 @@ describe('SchoolMigrationService', () => { providers: [ SchoolMigrationService, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolService, + useValue: createMock(), }, { provide: UserService, @@ -47,7 +51,7 @@ describe('SchoolMigrationService', () => { }).compile(); service = module.get(SchoolMigrationService); - schoolService = module.get(LegacySchoolService); + schoolService = module.get(SchoolService); userService = module.get(UserService); userLoginMigrationRepo = module.get(UserLoginMigrationRepo); @@ -118,7 +122,7 @@ describe('SchoolMigrationService', () => { describe('schoolToMigrate is called', () => { describe('when school number is missing', () => { const setup = () => { - const schoolDO: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + const schoolDO: SchoolDO = schoolDOFactory.buildWithId({ id: 'schoolId', name: 'schoolName', officialSchoolNumber: 'officialSchoolNumber', @@ -155,7 +159,7 @@ describe('SchoolMigrationService', () => { describe('when school could not be found with official school number', () => { const setup = () => { - const schoolDO: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + const schoolDO: SchoolDO = schoolDOFactory.buildWithId({ id: 'schoolId', name: 'schoolName', officialSchoolNumber: 'officialSchoolNumber', @@ -192,7 +196,7 @@ describe('SchoolMigrationService', () => { describe('when current users school not match with school of to migrate user ', () => { const setup = () => { - const schoolDO: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + const schoolDO: SchoolDO = schoolDOFactory.buildWithId({ id: 'schoolId', name: 'schoolName', officialSchoolNumber: 'officialSchoolNumber', @@ -232,7 +236,7 @@ describe('SchoolMigrationService', () => { describe('when school was already migrated', () => { const setup = () => { - const schoolDO: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + const schoolDO: SchoolDO = schoolDOFactory.buildWithId({ id: 'schoolId', name: 'schoolName', officialSchoolNumber: 'officialSchoolNumber', @@ -255,7 +259,7 @@ describe('SchoolMigrationService', () => { schoolService.getSchoolById.mockResolvedValue(schoolDO); schoolService.getSchoolBySchoolNumber.mockResolvedValue(schoolDO); - const result: LegacySchoolDo | null = await service.schoolToMigrate( + const result: SchoolDO | null = await service.schoolToMigrate( currentUserId, externalId, schoolDO.officialSchoolNumber @@ -267,7 +271,7 @@ describe('SchoolMigrationService', () => { describe('when school has to be migrated', () => { const setup = () => { - const schoolDO: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + const schoolDO: SchoolDO = schoolDOFactory.buildWithId({ id: 'schoolId', name: 'schoolName', officialSchoolNumber: 'officialSchoolNumber', @@ -289,7 +293,7 @@ describe('SchoolMigrationService', () => { schoolService.getSchoolBySchoolNumber.mockResolvedValue(schoolDO); userService.findById.mockResolvedValue(userDO); - const result: LegacySchoolDo | null = await service.schoolToMigrate( + const result: SchoolDO | null = await service.schoolToMigrate( currentUserId, 'newExternalId', schoolDO.officialSchoolNumber @@ -303,7 +307,7 @@ describe('SchoolMigrationService', () => { describe('migrateSchool is called', () => { describe('when school will be migrated', () => { const setup = () => { - const schoolDO: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + const schoolDO: SchoolDO = schoolDOFactory.buildWithId({ id: 'schoolId', name: 'schoolName', officialSchoolNumber: 'officialSchoolNumber', @@ -325,7 +329,7 @@ describe('SchoolMigrationService', () => { await service.migrateSchool(newExternalId, schoolDO, targetSystemId); expect(schoolService.save).toHaveBeenCalledWith( - expect.objectContaining>({ + expect.objectContaining>({ systems: [targetSystemId], previousExternalId: firstExternalId, externalId: newExternalId, @@ -341,7 +345,7 @@ describe('SchoolMigrationService', () => { await service.migrateSchool('newExternalId', schoolDO, targetSystemId); expect(schoolService.save).toHaveBeenCalledWith( - expect.objectContaining>({ + expect.objectContaining>({ systems: ['existingSystem', targetSystemId], }) ); @@ -356,7 +360,7 @@ describe('SchoolMigrationService', () => { await service.migrateSchool('newExternalId', schoolDO, targetSystemId); expect(schoolService.save).toHaveBeenCalledWith( - expect.objectContaining>({ + expect.objectContaining>({ systems: [targetSystemId], }) ); 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 79e5a05e0d9..2636e35c86c 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 @@ -1,9 +1,9 @@ import { Injectable, UnprocessableEntityException } from '@nestjs/common'; import { ValidationError } from '@shared/common'; -import { Page, LegacySchoolDo, UserDO, UserLoginMigrationDO } from '@shared/domain'; +import { Page, SchoolDO, UserDO, UserLoginMigrationDO } from '@shared/domain'; import { UserLoginMigrationRepo } from '@shared/repo'; import { LegacyLogger } from '@src/core/logger'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { UserService } from '@src/modules/user'; import { performance } from 'perf_hooks'; import { OAuthMigrationError } from '../error'; @@ -11,7 +11,7 @@ import { OAuthMigrationError } from '../error'; @Injectable() export class SchoolMigrationService { constructor( - private readonly schoolService: LegacySchoolService, + private readonly schoolService: SchoolService, private readonly logger: LegacyLogger, private readonly userService: UserService, private readonly userLoginMigrationRepo: UserLoginMigrationRepo @@ -25,8 +25,8 @@ export class SchoolMigrationService { } } - async migrateSchool(externalId: string, existingSchool: LegacySchoolDo, targetSystemId: string): Promise { - const schoolDOCopy: LegacySchoolDo = new LegacySchoolDo({ ...existingSchool }); + async migrateSchool(externalId: string, existingSchool: SchoolDO, targetSystemId: string): Promise { + const schoolDOCopy: SchoolDO = new SchoolDO({ ...existingSchool }); try { await this.doMigration(externalId, existingSchool, targetSystemId); @@ -44,7 +44,7 @@ export class SchoolMigrationService { currentUserId: string, externalId: string, officialSchoolNumber: string | undefined - ): Promise { + ): Promise { if (!officialSchoolNumber) { throw new OAuthMigrationError( 'Official school number from target migration system is missing', @@ -54,13 +54,11 @@ export class SchoolMigrationService { const userDO: UserDO | null = await this.userService.findById(currentUserId); if (userDO) { - const schoolDO: LegacySchoolDo = await this.schoolService.getSchoolById(userDO.schoolId); + const schoolDO: SchoolDO = await this.schoolService.getSchoolById(userDO.schoolId); this.checkOfficialSchoolNumbersMatch(schoolDO, officialSchoolNumber); } - const existingSchool: LegacySchoolDo | null = await this.schoolService.getSchoolBySchoolNumber( - officialSchoolNumber - ); + const existingSchool: SchoolDO | null = await this.schoolService.getSchoolBySchoolNumber(officialSchoolNumber); if (!existingSchool) { throw new OAuthMigrationError( @@ -127,7 +125,7 @@ export class SchoolMigrationService { this.logger.warn(`restartMigration for schoolId ${schoolId} took ${endTime - startTime} milliseconds`); } - private async doMigration(externalId: string, schoolDO: LegacySchoolDo, targetSystemId: string): Promise { + private async doMigration(externalId: string, schoolDO: SchoolDO, targetSystemId: string): Promise { if (schoolDO.systems) { schoolDO.systems.push(targetSystemId); } else { @@ -138,13 +136,13 @@ export class SchoolMigrationService { await this.schoolService.save(schoolDO); } - private async rollbackMigration(originalSchoolDO: LegacySchoolDo) { + private async rollbackMigration(originalSchoolDO: SchoolDO) { if (originalSchoolDO) { await this.schoolService.save(originalSchoolDO); } } - private checkOfficialSchoolNumbersMatch(schoolDO: LegacySchoolDo, officialExternalSchoolNumber: string): void { + private checkOfficialSchoolNumbersMatch(schoolDO: SchoolDO, officialExternalSchoolNumber: string): void { if (schoolDO.officialSchoolNumber !== officialExternalSchoolNumber) { throw new OAuthMigrationError( 'Current users school is not the same as school found by official school number from target migration system', diff --git a/apps/server/src/modules/user-login-migration/service/user-login-migration-revert.service.spec.ts b/apps/server/src/modules/user-login-migration/service/user-login-migration-revert.service.spec.ts index cf680799ca1..66b13122d13 100644 --- a/apps/server/src/modules/user-login-migration/service/user-login-migration-revert.service.spec.ts +++ b/apps/server/src/modules/user-login-migration/service/user-login-migration-revert.service.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { SchoolFeatures } from '@shared/domain'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { setupEntities, userLoginMigrationDOFactory } from '@shared/testing'; import { UserLoginMigrationRevertService } from './user-login-migration-revert.service'; import { UserLoginMigrationService } from './user-login-migration.service'; @@ -10,7 +10,7 @@ describe('UserLoginMigrationRevertService', () => { let module: TestingModule; let service: UserLoginMigrationRevertService; - let schoolService: DeepMocked; + let schoolService: DeepMocked; let userLoginMigrationService: DeepMocked; beforeAll(async () => { @@ -24,14 +24,14 @@ describe('UserLoginMigrationRevertService', () => { useValue: createMock(), }, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolService, + useValue: createMock(), }, ], }).compile(); service = module.get(UserLoginMigrationRevertService); - schoolService = module.get(LegacySchoolService); + schoolService = module.get(SchoolService); userLoginMigrationService = module.get(UserLoginMigrationService); }); diff --git a/apps/server/src/modules/user-login-migration/service/user-login-migration-revert.service.ts b/apps/server/src/modules/user-login-migration/service/user-login-migration-revert.service.ts index 41d4234bc70..105f546fc0c 100644 --- a/apps/server/src/modules/user-login-migration/service/user-login-migration-revert.service.ts +++ b/apps/server/src/modules/user-login-migration/service/user-login-migration-revert.service.ts @@ -1,13 +1,13 @@ import { Injectable } from '@nestjs/common'; import { SchoolFeatures, UserLoginMigrationDO } from '@shared/domain'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { UserLoginMigrationService } from './user-login-migration.service'; @Injectable() export class UserLoginMigrationRevertService { constructor( private readonly userLoginMigrationService: UserLoginMigrationService, - private readonly schoolService: LegacySchoolService + private readonly schoolService: SchoolService ) {} async revertUserLoginMigration(userLoginMigration: UserLoginMigrationDO): Promise { diff --git a/apps/server/src/modules/user-login-migration/service/user-login-migration.service.spec.ts b/apps/server/src/modules/user-login-migration/service/user-login-migration.service.spec.ts index bec32e35a3d..44d402fa46f 100644 --- a/apps/server/src/modules/user-login-migration/service/user-login-migration.service.spec.ts +++ b/apps/server/src/modules/user-login-migration/service/user-login-migration.service.spec.ts @@ -3,10 +3,10 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { ObjectId } from '@mikro-orm/mongodb'; import { InternalServerErrorException, UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { EntityId, LegacySchoolDo, SchoolFeatures, UserDO, UserLoginMigrationDO } from '@shared/domain'; +import { EntityId, SchoolDO, SchoolFeatures, UserDO, UserLoginMigrationDO } from '@shared/domain'; import { UserLoginMigrationRepo } from '@shared/repo'; -import { legacySchoolDoFactory, userDoFactory, userLoginMigrationDOFactory } from '@shared/testing'; -import { LegacySchoolService } from '@src/modules/school'; +import { schoolDOFactory, userDoFactory, userLoginMigrationDOFactory } from '@shared/testing'; +import { SchoolService } from '@src/modules/school'; import { SystemService } from '@src/modules/system'; import { SystemDto } from '@src/modules/system/service'; import { UserService } from '@src/modules/user'; @@ -19,7 +19,7 @@ describe('UserLoginMigrationService', () => { let service: UserLoginMigrationService; let userService: DeepMocked; - let schoolService: DeepMocked; + let schoolService: DeepMocked; let systemService: DeepMocked; let userLoginMigrationRepo: DeepMocked; let schoolMigrationService: DeepMocked; @@ -41,8 +41,8 @@ describe('UserLoginMigrationService', () => { useValue: createMock(), }, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolService, + useValue: createMock(), }, { provide: SystemService, @@ -61,7 +61,7 @@ describe('UserLoginMigrationService', () => { service = module.get(UserLoginMigrationService); userService = module.get(UserService); - schoolService = module.get(LegacySchoolService); + schoolService = module.get(SchoolService); systemService = module.get(SystemService); userLoginMigrationRepo = module.get(UserLoginMigrationRepo); schoolMigrationService = module.get(SchoolMigrationService); @@ -165,7 +165,7 @@ describe('UserLoginMigrationService', () => { describe('when the school has no systems', () => { const setup = () => { const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(undefined, schoolId); + const school: SchoolDO = schoolDOFactory.buildWithId(undefined, schoolId); const targetSystemId: EntityId = new ObjectId().toHexString(); const system: SystemDto = new SystemDto({ @@ -212,7 +212,7 @@ describe('UserLoginMigrationService', () => { }); const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ systems: [sourceSystemId] }, schoolId); + const school: SchoolDO = schoolDOFactory.buildWithId({ systems: [sourceSystemId] }, schoolId); schoolService.getSchoolById.mockResolvedValue(school); systemService.findByType.mockResolvedValue([system]); @@ -246,7 +246,7 @@ describe('UserLoginMigrationService', () => { describe('when the school has a feature', () => { const setup = () => { const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(undefined, schoolId); + const school: SchoolDO = schoolDOFactory.buildWithId(undefined, schoolId); const targetSystemId: EntityId = new ObjectId().toHexString(); const system: SystemDto = new SystemDto({ @@ -274,7 +274,7 @@ describe('UserLoginMigrationService', () => { await service.setMigration(schoolId, true, undefined, undefined); expect(schoolService.save).toHaveBeenCalledWith( - expect.objectContaining>({ + expect.objectContaining>({ features: [existingFeature, SchoolFeatures.OAUTH_PROVISIONING_ENABLED], }) ); @@ -284,7 +284,7 @@ describe('UserLoginMigrationService', () => { describe('when the school has no features yet', () => { const setup = () => { const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ features: undefined }, schoolId); + const school: SchoolDO = schoolDOFactory.buildWithId({ features: undefined }, schoolId); const targetSystemId: EntityId = new ObjectId().toHexString(); const system: SystemDto = new SystemDto({ @@ -310,7 +310,7 @@ describe('UserLoginMigrationService', () => { await service.setMigration(schoolId, true, undefined, undefined); expect(schoolService.save).toHaveBeenCalledWith( - expect.objectContaining>({ + expect.objectContaining>({ features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], }) ); @@ -320,7 +320,7 @@ describe('UserLoginMigrationService', () => { describe('when modifying a migration that does not exist on the school', () => { const setup = () => { const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(undefined, schoolId); + const school: SchoolDO = schoolDOFactory.buildWithId(undefined, schoolId); schoolService.getSchoolById.mockResolvedValue(school); userLoginMigrationRepo.findBySchoolId.mockResolvedValue(null); @@ -343,7 +343,7 @@ describe('UserLoginMigrationService', () => { describe('when creating a new migration but the SANIS system does not exist', () => { const setup = () => { const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(undefined, schoolId); + const school: SchoolDO = schoolDOFactory.buildWithId(undefined, schoolId); schoolService.getSchoolById.mockResolvedValue(school); systemService.findByType.mockResolvedValue([]); @@ -368,7 +368,7 @@ describe('UserLoginMigrationService', () => { describe('when restarting the migration', () => { const setup = () => { const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(undefined, schoolId); + const school: SchoolDO = schoolDOFactory.buildWithId(undefined, schoolId); const targetSystemId: EntityId = new ObjectId().toHexString(); const system: SystemDto = new SystemDto({ @@ -415,7 +415,7 @@ describe('UserLoginMigrationService', () => { describe('when setting the migration to mandatory', () => { const setup = () => { const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(undefined, schoolId); + const school: SchoolDO = schoolDOFactory.buildWithId(undefined, schoolId); const targetSystemId: EntityId = new ObjectId().toHexString(); const system: SystemDto = new SystemDto({ @@ -459,7 +459,7 @@ describe('UserLoginMigrationService', () => { describe('when setting the migration back to optional', () => { const setup = () => { const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(undefined, schoolId); + const school: SchoolDO = schoolDOFactory.buildWithId(undefined, schoolId); const targetSystemId: EntityId = new ObjectId().toHexString(); const system: SystemDto = new SystemDto({ @@ -504,7 +504,7 @@ describe('UserLoginMigrationService', () => { describe('when closing the migration', () => { const setup = () => { const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(undefined, schoolId); + const school: SchoolDO = schoolDOFactory.buildWithId(undefined, schoolId); const targetSystemId: EntityId = new ObjectId().toHexString(); const system: SystemDto = new SystemDto({ @@ -531,6 +531,17 @@ describe('UserLoginMigrationService', () => { }; }; + it('should call schoolService.removeFeature', async () => { + const { schoolId } = setup(); + + await service.setMigration(schoolId, undefined, undefined, true); + + expect(schoolService.removeFeature).toHaveBeenCalledWith( + schoolId, + SchoolFeatures.ENABLE_LDAP_SYNC_DURING_MIGRATION + ); + }); + it('should save the UserLoginMigration with close date and finish date', async () => { const { schoolId, userLoginMigration } = setup(); const expected: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ @@ -551,7 +562,7 @@ describe('UserLoginMigrationService', () => { describe('when schoolId is given', () => { const setup = () => { const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(undefined, schoolId); + const school: SchoolDO = schoolDOFactory.buildWithId(undefined, schoolId); const targetSystemId: EntityId = new ObjectId().toHexString(); const system: SystemDto = new SystemDto({ @@ -614,7 +625,7 @@ describe('UserLoginMigrationService', () => { }); const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ systems: [sourceSystemId] }, schoolId); + const school: SchoolDO = schoolDOFactory.buildWithId({ systems: [sourceSystemId] }, schoolId); schoolService.getSchoolById.mockResolvedValue(school); systemService.findByType.mockResolvedValue([system]); @@ -647,7 +658,7 @@ describe('UserLoginMigrationService', () => { describe('when the school has schoolfeatures', () => { const setup = () => { const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(undefined, schoolId); + const school: SchoolDO = schoolDOFactory.buildWithId(undefined, schoolId); const targetSystemId: EntityId = new ObjectId().toHexString(); const system: SystemDto = new SystemDto({ @@ -674,7 +685,7 @@ describe('UserLoginMigrationService', () => { await service.startMigration(schoolId); expect(schoolService.save).toHaveBeenCalledWith( - expect.objectContaining>({ + expect.objectContaining>({ features: [existingFeature, SchoolFeatures.OAUTH_PROVISIONING_ENABLED], }) ); @@ -684,7 +695,7 @@ describe('UserLoginMigrationService', () => { describe('when the school has no features yet', () => { const setup = () => { const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ features: undefined }, schoolId); + const school: SchoolDO = schoolDOFactory.buildWithId({ features: undefined }, schoolId); const targetSystemId: EntityId = new ObjectId().toHexString(); const system: SystemDto = new SystemDto({ @@ -708,7 +719,7 @@ describe('UserLoginMigrationService', () => { await service.startMigration(schoolId); expect(schoolService.save).toHaveBeenCalledWith( - expect.objectContaining>({ + expect.objectContaining>({ features: [SchoolFeatures.OAUTH_PROVISIONING_ENABLED], }) ); @@ -718,7 +729,7 @@ describe('UserLoginMigrationService', () => { describe('when creating a new migration but the SANIS system does not exist', () => { const setup = () => { const schoolId: EntityId = new ObjectId().toHexString(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(undefined, schoolId); + const school: SchoolDO = schoolDOFactory.buildWithId(undefined, schoolId); schoolService.getSchoolById.mockResolvedValue(school); systemService.findByType.mockResolvedValue([]); @@ -792,76 +803,6 @@ describe('UserLoginMigrationService', () => { }); }); - describe('restartMigration', () => { - describe('when migration restart was successfully', () => { - const setup = () => { - const schoolId: EntityId = new ObjectId().toHexString(); - - const targetSystemId: EntityId = new ObjectId().toHexString(); - - const userLoginMigrationDO: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - targetSystemId, - schoolId, - startedAt: mockedDate, - }); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(userLoginMigrationDO); - schoolMigrationService.unmarkOutdatedUsers.mockResolvedValue(); - userLoginMigrationRepo.save.mockResolvedValue(userLoginMigrationDO); - - return { - schoolId, - targetSystemId, - userLoginMigrationDO, - }; - }; - - it('should call save the user login migration', async () => { - const { schoolId, userLoginMigrationDO } = setup(); - - await service.restartMigration(schoolId); - - expect(userLoginMigrationRepo.save).toHaveBeenCalledWith(userLoginMigrationDO); - }); - - it('should call unmark the outdated users from this migration', async () => { - const { schoolId } = setup(); - - await service.restartMigration(schoolId); - - expect(schoolMigrationService.unmarkOutdatedUsers).toHaveBeenCalledWith(schoolId); - }); - }); - - describe('when migration could not be found', () => { - const setup = () => { - const schoolId: EntityId = new ObjectId().toHexString(); - - const targetSystemId: EntityId = new ObjectId().toHexString(); - - const userLoginMigrationDO: UserLoginMigrationDO = userLoginMigrationDOFactory.buildWithId({ - targetSystemId, - schoolId, - startedAt: mockedDate, - }); - userLoginMigrationRepo.findBySchoolId.mockResolvedValue(null); - - return { - schoolId, - targetSystemId, - userLoginMigrationDO, - }; - }; - - it('should throw ModifyUserLoginMigrationLoggableException ', async () => { - const { schoolId } = setup(); - - const func = async () => service.restartMigration(schoolId); - - await expect(func).rejects.toThrow(UserLoginMigrationNotFoundLoggableException); - }); - }); - }); - describe('deleteUserLoginMigration', () => { describe('when a userLoginMigration is given', () => { const setup = () => { @@ -1075,6 +1016,17 @@ describe('UserLoginMigrationService', () => { }; }; + it('should call schoolService.removeFeature', async () => { + const { schoolId } = setup(); + + await service.closeMigration(schoolId); + + expect(schoolService.removeFeature).toHaveBeenCalledWith( + schoolId, + SchoolFeatures.ENABLE_LDAP_SYNC_DURING_MIGRATION + ); + }); + it('should save the closed user login migration', async () => { const { schoolId, closedUserLoginMigration } = setup(); diff --git a/apps/server/src/modules/user-login-migration/service/user-login-migration.service.ts b/apps/server/src/modules/user-login-migration/service/user-login-migration.service.ts index c094e193d5b..1f48b814247 100644 --- a/apps/server/src/modules/user-login-migration/service/user-login-migration.service.ts +++ b/apps/server/src/modules/user-login-migration/service/user-login-migration.service.ts @@ -1,8 +1,8 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { Injectable, InternalServerErrorException, UnprocessableEntityException } from '@nestjs/common'; -import { EntityId, LegacySchoolDo, SchoolFeatures, SystemTypeEnum, UserDO, UserLoginMigrationDO } from '@shared/domain'; +import { EntityId, SchoolDO, SchoolFeatures, SystemTypeEnum, UserDO, UserLoginMigrationDO } from '@shared/domain'; import { UserLoginMigrationRepo } from '@shared/repo'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { SystemDto, SystemService } from '@src/modules/system'; import { UserService } from '@src/modules/user'; import { UserLoginMigrationNotFoundLoggableException } from '../error'; @@ -13,7 +13,7 @@ export class UserLoginMigrationService { constructor( private readonly userService: UserService, private readonly userLoginMigrationRepo: UserLoginMigrationRepo, - private readonly schoolService: LegacySchoolService, + private readonly schoolService: SchoolService, private readonly systemService: SystemService, private readonly schoolMigrationService: SchoolMigrationService ) {} @@ -32,7 +32,7 @@ export class UserLoginMigrationService { oauthMigrationMandatory?: boolean, oauthMigrationFinished?: boolean ): Promise { - const schoolDo: LegacySchoolDo = await this.schoolService.getSchoolById(schoolId); + const schoolDo: SchoolDO = await this.schoolService.getSchoolById(schoolId); const existingUserLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationRepo.findBySchoolId( schoolId @@ -71,11 +71,16 @@ export class UserLoginMigrationService { const savedMigration: UserLoginMigrationDO = await this.userLoginMigrationRepo.save(userLoginMigration); + if (oauthMigrationFinished !== undefined) { + // this would throw an error when executed before the userLoginMigrationRepo.save method. + await this.schoolService.removeFeature(schoolId, SchoolFeatures.ENABLE_LDAP_SYNC_DURING_MIGRATION); + } + return savedMigration; } async startMigration(schoolId: string): Promise { - const schoolDo: LegacySchoolDo = await this.schoolService.getSchoolById(schoolId); + const schoolDo: SchoolDO = await this.schoolService.getSchoolById(schoolId); const userLoginMigrationDO: UserLoginMigrationDO = await this.createNewMigration(schoolDo); @@ -128,6 +133,8 @@ export class UserLoginMigrationService { throw new UserLoginMigrationNotFoundLoggableException(schoolId); } + await this.schoolService.removeFeature(schoolId, SchoolFeatures.ENABLE_LDAP_SYNC_DURING_MIGRATION); + const now: Date = new Date(); const gracePeriodDuration: number = Configuration.get('MIGRATION_END_GRACE_PERIOD_MS') as number; @@ -139,7 +146,7 @@ export class UserLoginMigrationService { return userLoginMigration; } - private async createNewMigration(school: LegacySchoolDo): Promise { + private async createNewMigration(school: SchoolDO): Promise { const oauthSystems: SystemDto[] = await this.systemService.findByType(SystemTypeEnum.OAUTH); const sanisSystem: SystemDto | undefined = oauthSystems.find((system: SystemDto) => system.alias === 'SANIS'); @@ -171,7 +178,7 @@ export class UserLoginMigrationService { return userLoginMigration; } - private enableOauthMigrationFeature(schoolDo: LegacySchoolDo) { + private enableOauthMigrationFeature(schoolDo: SchoolDO) { if (schoolDo.features && !schoolDo.features.includes(SchoolFeatures.OAUTH_PROVISIONING_ENABLED)) { schoolDo.features.push(SchoolFeatures.OAUTH_PROVISIONING_ENABLED); } else { diff --git a/apps/server/src/modules/user-login-migration/service/user-migration.service.spec.ts b/apps/server/src/modules/user-login-migration/service/user-migration.service.spec.ts index 5d94187702a..c4729ec1bc9 100644 --- a/apps/server/src/modules/user-login-migration/service/user-migration.service.spec.ts +++ b/apps/server/src/modules/user-login-migration/service/user-migration.service.spec.ts @@ -4,12 +4,12 @@ import { IConfig } from '@hpi-schul-cloud/commons/lib/interfaces/IConfig'; import { ObjectId } from '@mikro-orm/mongodb'; import { BadRequestException, NotFoundException, UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { LegacySchoolDo, RoleName, UserDO } from '@shared/domain'; -import { legacySchoolDoFactory, setupEntities, userDoFactory } from '@shared/testing'; +import { RoleName, SchoolDO, UserDO } from '@shared/domain'; +import { schoolDOFactory, setupEntities, userDoFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { AccountService } from '@src/modules/account/services/account.service'; import { AccountDto, AccountSaveDto } from '@src/modules/account/services/dto'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { SystemService } from '@src/modules/system'; import { OauthConfigDto } from '@src/modules/system/service/dto/oauth-config.dto'; import { SystemDto } from '@src/modules/system/service/dto/system.dto'; @@ -24,7 +24,7 @@ describe('UserMigrationService', () => { let configBefore: IConfig; let logger: LegacyLogger; - let schoolService: DeepMocked; + let schoolService: DeepMocked; let systemService: DeepMocked; let userService: DeepMocked; let accountService: DeepMocked; @@ -43,8 +43,8 @@ describe('UserMigrationService', () => { providers: [ UserMigrationService, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolService, + useValue: createMock(), }, { provide: SystemService, @@ -66,7 +66,7 @@ describe('UserMigrationService', () => { }).compile(); service = module.get(UserMigrationService); - schoolService = module.get(LegacySchoolService); + schoolService = module.get(SchoolService); systemService = module.get(SystemService); userService = module.get(UserService); accountService = module.get(AccountService); @@ -89,7 +89,7 @@ describe('UserMigrationService', () => { describe('when finding the migration systems', () => { const setup = () => { const officialSchoolNumber = '3'; - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ name: 'schoolName', officialSchoolNumber }); + const school: SchoolDO = schoolDOFactory.buildWithId({ name: 'schoolName', officialSchoolNumber }); schoolService.getSchoolBySchoolNumber.mockResolvedValue(school); diff --git a/apps/server/src/modules/user-login-migration/service/user-migration.service.ts b/apps/server/src/modules/user-login-migration/service/user-migration.service.ts index d2611b25b72..2de31471a85 100644 --- a/apps/server/src/modules/user-login-migration/service/user-migration.service.ts +++ b/apps/server/src/modules/user-login-migration/service/user-migration.service.ts @@ -1,11 +1,11 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { BadRequestException, Injectable, NotFoundException, UnprocessableEntityException } from '@nestjs/common'; -import { LegacySchoolDo } from '@shared/domain'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; import { UserDO } from '@shared/domain/domainobject/user.do'; import { LegacyLogger } from '@src/core/logger'; import { AccountService } from '@src/modules/account/services/account.service'; import { AccountDto } from '@src/modules/account/services/dto'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { SystemDto, SystemService } from '@src/modules/system/service'; import { UserService } from '@src/modules/user'; import { EntityId } from '@src/shared/domain/types'; @@ -29,7 +29,7 @@ export class UserMigrationService { private readonly loginUrl: string = '/login'; constructor( - private readonly schoolService: LegacySchoolService, + private readonly schoolService: SchoolService, private readonly systemService: SystemService, private readonly userService: UserService, private readonly logger: LegacyLogger, @@ -40,7 +40,7 @@ export class UserMigrationService { } async getMigrationConsentPageRedirect(officialSchoolNumber: string, originSystemId: string): Promise { - const school: LegacySchoolDo | null = await this.schoolService.getSchoolBySchoolNumber(officialSchoolNumber); + const school: SchoolDO | null = await this.schoolService.getSchoolBySchoolNumber(officialSchoolNumber); if (!school || !school.id) { throw new NotFoundException(`School with offical school number ${officialSchoolNumber} does not exist.`); diff --git a/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.spec.ts b/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.spec.ts index e2c9218322b..74111af89d7 100644 --- a/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.spec.ts +++ b/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.spec.ts @@ -1,11 +1,11 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { Permission, LegacySchoolDo, User, UserLoginMigrationDO } from '@shared/domain'; -import { legacySchoolDoFactory, setupEntities, userFactory, userLoginMigrationDOFactory } from '@shared/testing'; +import { Permission, SchoolDO, User, UserLoginMigrationDO } from '@shared/domain'; +import { schoolDOFactory, setupEntities, userFactory, userLoginMigrationDOFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; import { AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { UserLoginMigrationGracePeriodExpiredLoggableException, UserLoginMigrationNotFoundLoggableException, @@ -19,7 +19,7 @@ describe('RestartUserLoginMigrationUc', () => { let userLoginMigrationService: DeepMocked; let authorizationService: DeepMocked; - let schoolService: DeepMocked; + let schoolService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -34,8 +34,8 @@ describe('RestartUserLoginMigrationUc', () => { useValue: createMock(), }, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolService, + useValue: createMock(), }, { provide: Logger, @@ -47,7 +47,7 @@ describe('RestartUserLoginMigrationUc', () => { uc = module.get(RestartUserLoginMigrationUc); userLoginMigrationService = module.get(UserLoginMigrationService); authorizationService = module.get(AuthorizationService); - schoolService = module.get(LegacySchoolService); + schoolService = module.get(SchoolService); await setupEntities(); }); @@ -70,7 +70,7 @@ describe('RestartUserLoginMigrationUc', () => { const user: User = userFactory.buildWithId(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); @@ -115,7 +115,7 @@ describe('RestartUserLoginMigrationUc', () => { const user: User = userFactory.buildWithId(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); @@ -157,7 +157,7 @@ describe('RestartUserLoginMigrationUc', () => { const setup = () => { const user: User = userFactory.buildWithId(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); @@ -179,7 +179,7 @@ describe('RestartUserLoginMigrationUc', () => { const setup = () => { const user: User = userFactory.buildWithId(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); @@ -207,7 +207,7 @@ describe('RestartUserLoginMigrationUc', () => { const user: User = userFactory.buildWithId(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); diff --git a/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.ts b/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.ts index 88764a756ae..a515755830a 100644 --- a/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.ts +++ b/apps/server/src/modules/user-login-migration/uc/restart-user-login-migration.uc.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; -import { Permission, LegacySchoolDo, User, UserLoginMigrationDO } from '@shared/domain'; +import { Permission, SchoolDO, User, UserLoginMigrationDO } from '@shared/domain'; import { Logger } from '@src/core/logger'; import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { UserLoginMigrationGracePeriodExpiredLoggableException, UserLoginMigrationNotFoundLoggableException, @@ -15,7 +15,7 @@ export class RestartUserLoginMigrationUc { constructor( private readonly userLoginMigrationService: UserLoginMigrationService, private readonly authorizationService: AuthorizationService, - private readonly schoolService: LegacySchoolService, + private readonly schoolService: SchoolService, private readonly logger: Logger ) { this.logger.setContext(RestartUserLoginMigrationUc.name); @@ -48,7 +48,7 @@ export class RestartUserLoginMigrationUc { async checkPermission(userId: string, schoolId: string): Promise { const user: User = await this.authorizationService.getUserWithPermissions(userId); - const school: LegacySchoolDo = await this.schoolService.getSchoolById(schoolId); + const school: SchoolDO = await this.schoolService.getSchoolById(schoolId); const context: AuthorizationContext = AuthorizationContextBuilder.write([Permission.USER_LOGIN_MIGRATION_ADMIN]); this.authorizationService.checkPermission(user, school, context); diff --git a/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.spec.ts b/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.spec.ts index ab937d679fd..24207e57370 100644 --- a/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.spec.ts +++ b/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.spec.ts @@ -1,11 +1,11 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { Permission, LegacySchoolDo, User, UserLoginMigrationDO } from '@shared/domain'; -import { legacySchoolDoFactory, setupEntities, userFactory, userLoginMigrationDOFactory } from '@shared/testing'; +import { Permission, SchoolDO, User, UserLoginMigrationDO } from '@shared/domain'; +import { schoolDOFactory, setupEntities, userFactory, userLoginMigrationDOFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; import { AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { SchoolNumberMissingLoggableException, UserLoginMigrationAlreadyClosedLoggableException } from '../error'; import { UserLoginMigrationService } from '../service'; import { StartUserLoginMigrationUc } from './start-user-login-migration.uc'; @@ -16,7 +16,7 @@ describe('StartUserLoginMigrationUc', () => { let userLoginMigrationService: DeepMocked; let authorizationService: DeepMocked; - let schoolService: DeepMocked; + let schoolService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -31,8 +31,8 @@ describe('StartUserLoginMigrationUc', () => { useValue: createMock(), }, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolService, + useValue: createMock(), }, { provide: Logger, @@ -44,7 +44,7 @@ describe('StartUserLoginMigrationUc', () => { uc = module.get(StartUserLoginMigrationUc); userLoginMigrationService = module.get(UserLoginMigrationService); authorizationService = module.get(AuthorizationService); - schoolService = module.get(LegacySchoolService); + schoolService = module.get(SchoolService); await setupEntities(); }); @@ -64,7 +64,7 @@ describe('StartUserLoginMigrationUc', () => { const user: User = userFactory.buildWithId(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); @@ -109,7 +109,7 @@ describe('StartUserLoginMigrationUc', () => { const user: User = userFactory.buildWithId(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); @@ -139,7 +139,7 @@ describe('StartUserLoginMigrationUc', () => { const setup = () => { const user: User = userFactory.buildWithId(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); @@ -161,7 +161,7 @@ describe('StartUserLoginMigrationUc', () => { const setup = () => { const user: User = userFactory.buildWithId(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ officialSchoolNumber: undefined }); + const school: SchoolDO = schoolDOFactory.buildWithId({ officialSchoolNumber: undefined }); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); @@ -184,7 +184,7 @@ describe('StartUserLoginMigrationUc', () => { const user: User = userFactory.buildWithId(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); diff --git a/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.ts b/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.ts index 5949855f42e..d97fe1d0ff8 100644 --- a/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.ts +++ b/apps/server/src/modules/user-login-migration/uc/start-user-login-migration.uc.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; -import { Permission, LegacySchoolDo, User, UserLoginMigrationDO } from '@shared/domain'; +import { Permission, SchoolDO, User, UserLoginMigrationDO } from '@shared/domain'; import { Logger } from '@src/core/logger'; import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { SchoolNumberMissingLoggableException, UserLoginMigrationAlreadyClosedLoggableException } from '../error'; import { UserLoginMigrationStartLoggable } from '../loggable'; import { UserLoginMigrationService } from '../service'; @@ -12,7 +12,7 @@ export class StartUserLoginMigrationUc { constructor( private readonly userLoginMigrationService: UserLoginMigrationService, private readonly authorizationService: AuthorizationService, - private readonly schoolService: LegacySchoolService, + private readonly schoolService: SchoolService, private readonly logger: Logger ) { this.logger.setContext(StartUserLoginMigrationUc.name); @@ -43,7 +43,7 @@ export class StartUserLoginMigrationUc { async checkPreconditions(userId: string, schoolId: string): Promise { const user: User = await this.authorizationService.getUserWithPermissions(userId); - const school: LegacySchoolDo = await this.schoolService.getSchoolById(schoolId); + const school: SchoolDO = await this.schoolService.getSchoolById(schoolId); const context: AuthorizationContext = AuthorizationContextBuilder.write([Permission.USER_LOGIN_MIGRATION_ADMIN]); this.authorizationService.checkPermission(user, school, context); diff --git a/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.spec.ts b/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.spec.ts index edd1f455aa4..99cd16eeeba 100644 --- a/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.spec.ts +++ b/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.spec.ts @@ -1,11 +1,11 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { Permission, LegacySchoolDo, User, UserLoginMigrationDO } from '@shared/domain'; -import { legacySchoolDoFactory, setupEntities, userFactory, userLoginMigrationDOFactory } from '@shared/testing'; +import { Permission, SchoolDO, User, UserLoginMigrationDO } from '@shared/domain'; +import { schoolDOFactory, setupEntities, userFactory, userLoginMigrationDOFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; import { AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { UserLoginMigrationAlreadyClosedLoggableException, UserLoginMigrationGracePeriodExpiredLoggableException, @@ -20,7 +20,7 @@ describe('ToggleUserLoginMigrationUc', () => { let userLoginMigrationService: DeepMocked; let authorizationService: DeepMocked; - let schoolService: DeepMocked; + let schoolService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -35,8 +35,8 @@ describe('ToggleUserLoginMigrationUc', () => { useValue: createMock(), }, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolService, + useValue: createMock(), }, { provide: Logger, @@ -48,7 +48,7 @@ describe('ToggleUserLoginMigrationUc', () => { uc = module.get(ToggleUserLoginMigrationUc); userLoginMigrationService = module.get(UserLoginMigrationService); authorizationService = module.get(AuthorizationService); - schoolService = module.get(LegacySchoolService); + schoolService = module.get(SchoolService); await setupEntities(); }); @@ -71,7 +71,7 @@ describe('ToggleUserLoginMigrationUc', () => { const user: User = userFactory.buildWithId(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); @@ -119,7 +119,7 @@ describe('ToggleUserLoginMigrationUc', () => { const user: User = userFactory.buildWithId(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); @@ -162,7 +162,7 @@ describe('ToggleUserLoginMigrationUc', () => { const setup = () => { const user: User = userFactory.buildWithId(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); @@ -184,7 +184,7 @@ describe('ToggleUserLoginMigrationUc', () => { const setup = () => { const user: User = userFactory.buildWithId(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); @@ -212,7 +212,7 @@ describe('ToggleUserLoginMigrationUc', () => { const user: User = userFactory.buildWithId(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); @@ -236,7 +236,7 @@ describe('ToggleUserLoginMigrationUc', () => { const user: User = userFactory.buildWithId(); - const school: LegacySchoolDo = legacySchoolDoFactory.buildWithId(); + const school: SchoolDO = schoolDOFactory.buildWithId(); authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); schoolService.getSchoolById.mockResolvedValueOnce(school); diff --git a/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.ts b/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.ts index 06bd00323a5..d54db5c3ff1 100644 --- a/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.ts +++ b/apps/server/src/modules/user-login-migration/uc/toggle-user-login-migration.uc.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { Permission, LegacySchoolDo, User, UserLoginMigrationDO } from '@shared/domain'; +import { Permission, SchoolDO, User, UserLoginMigrationDO } from '@shared/domain'; import { Logger } from '@src/core/logger'; import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { UserLoginMigrationAlreadyClosedLoggableException, UserLoginMigrationGracePeriodExpiredLoggableException, @@ -16,7 +16,7 @@ export class ToggleUserLoginMigrationUc { constructor( private readonly userLoginMigrationService: UserLoginMigrationService, private readonly authorizationService: AuthorizationService, - private readonly schoolService: LegacySchoolService, + private readonly schoolService: SchoolService, private readonly logger: Logger ) {} @@ -50,7 +50,7 @@ export class ToggleUserLoginMigrationUc { async checkPermission(userId: string, schoolId: string): Promise { const user: User = await this.authorizationService.getUserWithPermissions(userId); - const school: LegacySchoolDo = await this.schoolService.getSchoolById(schoolId); + const school: SchoolDO = await this.schoolService.getSchoolById(schoolId); const context: AuthorizationContext = AuthorizationContextBuilder.write([Permission.USER_LOGIN_MIGRATION_ADMIN]); this.authorizationService.checkPermission(user, school, context); 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 6f6bb0f4a9a..2ca93a14c1f 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 @@ -2,10 +2,10 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; -import { Page, Permission, LegacySchoolDo, System, User, UserLoginMigrationDO } from '@shared/domain'; +import { Page, Permission, SchoolDO, System, User, UserLoginMigrationDO } from '@shared/domain'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { - legacySchoolDoFactory, + schoolDOFactory, setupEntities, systemFactory, userFactory, @@ -18,7 +18,7 @@ import { OAuthTokenDto } from '@src/modules/oauth'; import { OAuthService } from '@src/modules/oauth/service/oauth.service'; import { ProvisioningService } from '@src/modules/provisioning'; import { ExternalSchoolDto, ExternalUserDto, OauthDataDto, ProvisioningSystemDto } from '@src/modules/provisioning/dto'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { Oauth2MigrationParams } from '../controller/dto/oauth2-migration.params'; import { OAuthMigrationError, SchoolMigrationError, UserLoginMigrationError } from '../error'; import { PageTypes } from '../interface/page-types.enum'; @@ -74,8 +74,8 @@ describe('UserLoginMigrationUc', () => { useValue: createMock(), }, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolService, + useValue: createMock(), }, { provide: LegacyLogger, @@ -304,7 +304,7 @@ describe('UserLoginMigrationUc', () => { .withOauthConfig() .buildWithId({ provisioningStrategy: SystemProvisioningStrategy.SANIS }); - const schoolDO: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + const schoolDO: SchoolDO = schoolDOFactory.buildWithId({ systems: [sourceSystem.id], officialSchoolNumber: 'officialSchoolNumber', externalId: 'oldSchoolExternalId', @@ -419,7 +419,7 @@ describe('UserLoginMigrationUc', () => { .withOauthConfig() .buildWithId({ provisioningStrategy: SystemProvisioningStrategy.SANIS }); - const schoolDO: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + const schoolDO: SchoolDO = schoolDOFactory.buildWithId({ systems: [sourceSystem.id], officialSchoolNumber: 'officialSchoolNumber', externalId: 'oldSchoolExternalId', @@ -483,7 +483,7 @@ describe('UserLoginMigrationUc', () => { .withOauthConfig() .buildWithId({ provisioningStrategy: SystemProvisioningStrategy.SANIS }); - const schoolDO: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + const schoolDO: SchoolDO = schoolDOFactory.buildWithId({ systems: [sourceSystem.id], officialSchoolNumber: 'officialSchoolNumber', externalId: 'oldSchoolExternalId', @@ -619,7 +619,7 @@ describe('UserLoginMigrationUc', () => { .withOauthConfig() .buildWithId({ provisioningStrategy: SystemProvisioningStrategy.SANIS }); - const schoolDO: LegacySchoolDo = legacySchoolDoFactory.buildWithId({ + const schoolDO: SchoolDO = schoolDOFactory.buildWithId({ systems: [sourceSystem.id], officialSchoolNumber: 'officialSchoolNumber', externalId: 'oldSchoolExternalId', 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 bfbce03c9ca..121e236cea8 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,6 +1,6 @@ import { ForbiddenException, Injectable } from '@nestjs/common'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; -import { EntityId, Page, Permission, LegacySchoolDo, User, UserLoginMigrationDO } from '@shared/domain'; +import { EntityId, Page, Permission, SchoolDO, User, UserLoginMigrationDO } from '@shared/domain'; import { LegacyLogger } from '@src/core/logger'; import { AuthenticationService } from '@src/modules/authentication/services/authentication.service'; import { Action, AuthorizationService } from '@src/modules/authorization'; @@ -95,7 +95,7 @@ export class UserLoginMigrationUc { this.logMigrationInformation(currentUserId, undefined, data, targetSystemId); if (data.externalSchool) { - let schoolToMigrate: LegacySchoolDo | null; + let schoolToMigrate: SchoolDO | null; // TODO: N21-820 after fully switching to the new client login flow, try/catch will be obsolete and schoolToMigrate should throw correct errors try { schoolToMigrate = await this.schoolMigrationService.schoolToMigrate( @@ -157,7 +157,7 @@ export class UserLoginMigrationUc { text?: string, oauthData?: OauthDataDto, targetSystemId?: string, - school?: LegacySchoolDo + school?: SchoolDO ) { let message = `MIGRATION (userId: ${userId}): ${text ?? ''}`; if (!school && oauthData) { diff --git a/apps/server/src/modules/video-conference/bbb/bbb.service.spec.ts b/apps/server/src/modules/video-conference/bbb/bbb.service.spec.ts index 7f611c66eae..1731d10ff8e 100644 --- a/apps/server/src/modules/video-conference/bbb/bbb.service.spec.ts +++ b/apps/server/src/modules/video-conference/bbb/bbb.service.spec.ts @@ -3,11 +3,12 @@ import { HttpService } from '@nestjs/axios'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ConverterUtil } from '@shared/common'; +import { axiosResponseFactory } from '@shared/testing'; +import { ErrorUtils } from '@src/core/error/utils'; import { AxiosResponse } from 'axios'; import crypto, { Hash } from 'crypto'; import { of } from 'rxjs'; import { URLSearchParams } from 'url'; -import { ErrorUtils } from '@src/core/error/utils'; import { BbbSettings, IBbbSettings } from './bbb-settings.interface'; import { BBBService } from './bbb.service'; import { BBBBaseMeetingConfig, BBBCreateConfig, BBBJoinConfig, BBBRole, GuestPolicy } from './request'; @@ -78,15 +79,10 @@ const createBBBJoinConfig = (): BBBJoinConfig => { }; type BBBResponseType = BBBCreateResponse | BBBMeetingInfoResponse | BBBBaseResponse; -const createAxiosResponse = (data: BBBResponse): AxiosResponse> => { - return { - data: data ?? {}, - status: 0, - statusText: '', - headers: {}, - config: {}, - }; -}; +const createAxiosResponse = (data: BBBResponse) => + axiosResponseFactory.build({ + data, + }); class BBBServiceTest extends BBBService { public superToParams(object: BBBCreateConfig | BBBBaseMeetingConfig): URLSearchParams { diff --git a/apps/server/src/modules/video-conference/service/video-conference.service.spec.ts b/apps/server/src/modules/video-conference/service/video-conference.service.spec.ts index 34441abe2f7..9136724d3d7 100644 --- a/apps/server/src/modules/video-conference/service/video-conference.service.spec.ts +++ b/apps/server/src/modules/video-conference/service/video-conference.service.spec.ts @@ -19,7 +19,7 @@ import { AuthorizationContextBuilder, AuthorizationService, } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { UserService } from '@src/modules/user'; import { courseFactory, roleFactory, setupEntities, userDoFactory } from '@shared/testing'; import { videoConferenceDOFactory } from '@shared/testing/factory/video-conference.do.factory'; @@ -39,7 +39,7 @@ describe('VideoConferenceService', () => { let courseService: DeepMocked; let calendarService: DeepMocked; let authorizationService: DeepMocked; - let schoolService: DeepMocked; + let schoolService: DeepMocked; let teamsRepo: DeepMocked; let userService: DeepMocked; let videoConferenceRepo: DeepMocked; @@ -68,8 +68,8 @@ describe('VideoConferenceService', () => { useValue: createMock(), }, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolService, + useValue: createMock(), }, { provide: TeamsRepo, @@ -90,7 +90,7 @@ describe('VideoConferenceService', () => { courseService = module.get(CourseService); calendarService = module.get(CalendarService); authorizationService = module.get(AuthorizationService); - schoolService = module.get(LegacySchoolService); + schoolService = module.get(SchoolService); teamsRepo = module.get(TeamsRepo); userService = module.get(UserService); videoConferenceRepo = module.get(VideoConferenceRepo); diff --git a/apps/server/src/modules/video-conference/service/video-conference.service.ts b/apps/server/src/modules/video-conference/service/video-conference.service.ts index 55b24036ccc..eec64d1bd4a 100644 --- a/apps/server/src/modules/video-conference/service/video-conference.service.ts +++ b/apps/server/src/modules/video-conference/service/video-conference.service.ts @@ -22,7 +22,7 @@ import { AuthorizationService, } from '@src/modules/authorization'; import { CourseService } from '@src/modules/learnroom/service/course.service'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school'; import { UserService } from '@src/modules/user'; import { BBBRole } from '../bbb'; import { ErrorStatus } from '../error/error-status.enum'; @@ -37,7 +37,7 @@ export class VideoConferenceService { private readonly courseService: CourseService, private readonly calendarService: CalendarService, private readonly authorizationService: AuthorizationService, - private readonly schoolService: LegacySchoolService, + private readonly schoolService: SchoolService, private readonly teamsRepo: TeamsRepo, private readonly userService: UserService, private readonly videoConferenceRepo: VideoConferenceRepo diff --git a/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.spec.ts b/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.spec.ts index aaa657af03a..5d12520032b 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.spec.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.spec.ts @@ -19,7 +19,7 @@ import { CalendarEventDto } from '@shared/infra/calendar/dto/calendar-event.dto' import { TeamsRepo, VideoConferenceRepo } from '@shared/repo'; import { roleFactory, setupEntities, userDoFactory } from '@shared/testing'; import { teamFactory } from '@shared/testing/factory/team.factory'; -import { AuthorizationService, LegacySchoolService, UserService } from '@src/modules'; +import { AuthorizationService, SchoolService, UserService } from '@src/modules'; import { ICurrentUser } from '@src/modules/authentication'; import { CourseService } from '@src/modules/learnroom/service/course.service'; import { IScopeInfo, VideoConference, VideoConferenceJoin, VideoConferenceState } from './dto'; @@ -69,7 +69,7 @@ describe('VideoConferenceUc', () => { let courseService: DeepMocked; let userService: DeepMocked; let calendarService: DeepMocked; - let schoolService: DeepMocked; + let schoolService: DeepMocked; const hostUrl = 'https://localhost:4000'; const course: Course = { id: 'courseId', name: 'courseName' } as Course; @@ -142,13 +142,13 @@ describe('VideoConferenceUc', () => { useValue: createMock(), }, { - provide: LegacySchoolService, - useValue: createMock(), + provide: SchoolService, + useValue: createMock(), }, ], }).compile(); useCase = module.get(VideoConferenceDeprecatedUcSpec); - schoolService = module.get(LegacySchoolService); + schoolService = module.get(SchoolService); authorizationService = module.get(AuthorizationService); courseService = module.get(CourseService); calendarService = module.get(CalendarService); diff --git a/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.ts b/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.ts index 023de7db7ce..931e4810cfb 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-deprecated.uc.ts @@ -24,7 +24,7 @@ import { AuthorizationContextBuilder, AuthorizationService, } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/school'; +import { SchoolService } from '@src/modules/school/service/school.service'; import { CourseService } from '@src/modules/learnroom/service/course.service'; import { UserService } from '@src/modules/user'; import { IScopeInfo, VideoConference, VideoConferenceInfo, VideoConferenceJoin, VideoConferenceState } from './dto'; @@ -68,7 +68,7 @@ export class VideoConferenceDeprecatedUc { private readonly courseService: CourseService, private readonly userService: UserService, private readonly calendarService: CalendarService, - private readonly schoolService: LegacySchoolService + private readonly schoolService: SchoolService ) { this.hostURL = Configuration.get('HOST') as string; } diff --git a/apps/server/src/modules/video-conference/uc/video-conference-join.uc.spec.ts b/apps/server/src/modules/video-conference/uc/video-conference-join.uc.spec.ts index 1d688314f71..0f46fcde450 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-join.uc.spec.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-join.uc.spec.ts @@ -158,43 +158,106 @@ describe('VideoConferenceJoinUc', () => { }); describe('and waiting room is enabled', () => { - const setup = () => { - const user: UserDO = userDoFactory.buildWithId(); - const currentUserId: string = user.id as string; - - const scope = { scope: VideoConferenceScope.COURSE, id: new ObjectId().toHexString() }; - const options: VideoConferenceOptions = { - everyAttendeeJoinsMuted: true, - everybodyJoinsAsModerator: true, - moderatorMustApproveJoinRequests: true, + describe('and everybodyJoinsAsModerator is true', () => { + const setup = () => { + const user: UserDO = userDoFactory.buildWithId(); + const currentUserId: string = user.id as string; + + const scope = { scope: VideoConferenceScope.COURSE, id: new ObjectId().toHexString() }; + const options: VideoConferenceOptions = { + everyAttendeeJoinsMuted: true, + everybodyJoinsAsModerator: true, + moderatorMustApproveJoinRequests: true, + }; + const videoConference: VideoConferenceDO = videoConferenceDOFactory.build({ options }); + + const bbbJoinResponse: BBBResponse = { + response: { + url: 'url', + }, + } as BBBResponse; + + userService.findById.mockResolvedValue(user); + videoConferenceService.getUserRoleAndGuestStatusByUserIdForBbb.mockResolvedValue({ + role: BBBRole.VIEWER, + isGuest: true, + }); + videoConferenceService.sanitizeString.mockReturnValue(`${user.firstName} ${user.lastName}`); + bbbService.join.mockResolvedValue(bbbJoinResponse.response.url); + videoConferenceService.findVideoConferenceByScopeIdAndScope.mockResolvedValue(videoConference); + + return { user, currentUserId, scope, options, bbbJoinResponse }; }; - const videoConference: VideoConferenceDO = videoConferenceDOFactory.build({ options }); - const bbbJoinResponse: BBBResponse = { - response: { - url: 'url', - }, - } as BBBResponse; + it('should return a video conference join with url from bbb', async () => { + const { currentUserId, scope, bbbJoinResponse } = setup(); - userService.findById.mockResolvedValue(user); - videoConferenceService.getUserRoleAndGuestStatusByUserIdForBbb.mockResolvedValue({ - role: BBBRole.VIEWER, - isGuest: true, + const result: VideoConferenceJoin = await uc.join(currentUserId, scope); + + expect(result).toEqual( + expect.objectContaining>({ url: bbbJoinResponse.response.url }) + ); }); - bbbService.join.mockResolvedValue(bbbJoinResponse.response.url); - videoConferenceService.findVideoConferenceByScopeIdAndScope.mockResolvedValue(videoConference); - return { user, currentUserId, scope, options, bbbJoinResponse }; - }; + it('should call join with guest true', async () => { + const { currentUserId, scope, user } = setup(); + + await uc.join(currentUserId, scope); - it('should return a video conference join with url from bbb', async () => { - const { currentUserId, scope, bbbJoinResponse } = setup(); + expect(bbbService.join).toHaveBeenCalledWith({ + fullName: `${user.firstName} ${user.lastName}`, + meetingID: scope.id, + role: BBBRole.VIEWER, + userID: currentUserId, + guest: true, + }); + }); + }); + + describe('and everybodyJoinsAsModerator is false', () => { + const setup = () => { + const user: UserDO = userDoFactory.buildWithId(); + const currentUserId: string = user.id as string; + + const scope = { scope: VideoConferenceScope.COURSE, id: new ObjectId().toHexString() }; + const options: VideoConferenceOptions = { + everyAttendeeJoinsMuted: true, + everybodyJoinsAsModerator: false, + moderatorMustApproveJoinRequests: true, + }; + const videoConference: VideoConferenceDO = videoConferenceDOFactory.build({ options }); + + const bbbJoinResponse: BBBResponse = { + response: { + url: 'url', + }, + } as BBBResponse; + + userService.findById.mockResolvedValue(user); + videoConferenceService.getUserRoleAndGuestStatusByUserIdForBbb.mockResolvedValue({ + role: BBBRole.VIEWER, + isGuest: false, + }); + videoConferenceService.sanitizeString.mockReturnValue(`${user.firstName} ${user.lastName}`); + bbbService.join.mockResolvedValue(bbbJoinResponse.response.url); + videoConferenceService.findVideoConferenceByScopeIdAndScope.mockResolvedValue(videoConference); + + return { user, currentUserId, scope, options, bbbJoinResponse }; + }; - const result: VideoConferenceJoin = await uc.join(currentUserId, scope); + it('should call join with guest true', async () => { + const { currentUserId, scope, user } = setup(); - expect(result).toEqual( - expect.objectContaining>({ url: bbbJoinResponse.response.url }) - ); + await uc.join(currentUserId, scope); + + expect(bbbService.join).toHaveBeenCalledWith({ + fullName: `${user.firstName} ${user.lastName}`, + meetingID: scope.id, + role: BBBRole.VIEWER, + userID: currentUserId, + guest: true, + }); + }); }); }); }); @@ -223,10 +286,11 @@ describe('VideoConferenceJoinUc', () => { role: BBBRole.VIEWER, isGuest: false, }); + videoConferenceService.sanitizeString.mockReturnValue(`${user.firstName} ${user.lastName}`); bbbService.join.mockResolvedValue(bbbJoinResponse.response.url); videoConferenceService.findVideoConferenceByScopeIdAndScope.mockResolvedValue(videoConference); - return { currentUserId, scope, bbbJoinResponse }; + return { currentUserId, scope, bbbJoinResponse, user }; }; it('should return a video conference join with url from bbb', async () => { @@ -250,6 +314,20 @@ describe('VideoConferenceJoinUc', () => { expect.objectContaining>({ role: BBBRole.MODERATOR }) ); }); + + it('should call join with guest false', async () => { + const { currentUserId, scope, user } = setup(); + + await uc.join(currentUserId, scope); + + expect(bbbService.join).toHaveBeenCalledWith({ + fullName: `${user.firstName} ${user.lastName}`, + meetingID: scope.id, + role: BBBRole.MODERATOR, + userID: currentUserId, + guest: false, + }); + }); }); }); }); diff --git a/apps/server/src/modules/video-conference/uc/video-conference-join.uc.ts b/apps/server/src/modules/video-conference/uc/video-conference-join.uc.ts index fbd5f6bc9b8..5728a4b0da2 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-join.uc.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-join.uc.ts @@ -43,6 +43,13 @@ export class VideoConferenceJoinUc { joinBuilder.withRole(BBBRole.MODERATOR); } + if ( + videoConference.options.moderatorMustApproveJoinRequests && + !videoConference.options.everybodyJoinsAsModerator + ) { + joinBuilder.asGuest(true); + } + if (!videoConference.options.moderatorMustApproveJoinRequests && isGuest) { throw new ForbiddenException( ErrorStatus.GUESTS_CANNOT_JOIN_CONFERENCE, diff --git a/apps/server/src/shared/domain/domainobject/external-source.ts b/apps/server/src/shared/domain/domainobject/external-source.ts new file mode 100644 index 00000000000..c29c9f5ae24 --- /dev/null +++ b/apps/server/src/shared/domain/domainobject/external-source.ts @@ -0,0 +1,10 @@ +export class ExternalSource { + externalId: string; + + systemId: string; + + constructor(props: ExternalSource) { + this.externalId = props.externalId; + this.systemId = props.systemId; + } +} diff --git a/apps/server/src/shared/domain/domainobject/index.ts b/apps/server/src/shared/domain/domainobject/index.ts index 6459619c2e6..b8c862ad23b 100644 --- a/apps/server/src/shared/domain/domainobject/index.ts +++ b/apps/server/src/shared/domain/domainobject/index.ts @@ -3,8 +3,9 @@ export * from './pseudonym.do'; export * from './video-conference.do'; export * from './board'; export * from './user-login-migration.do'; -export * from './legacy-school.do'; +export * from './school.do'; export * from './user.do'; export * from './page'; export * from './role-reference'; export * from './ltitool.do'; +export * from './external-source'; diff --git a/apps/server/src/shared/domain/domainobject/legacy-school.do.ts b/apps/server/src/shared/domain/domainobject/school.do.ts similarity index 88% rename from apps/server/src/shared/domain/domainobject/legacy-school.do.ts rename to apps/server/src/shared/domain/domainobject/school.do.ts index 5ac496a04c4..6ea0cffb7a4 100644 --- a/apps/server/src/shared/domain/domainobject/legacy-school.do.ts +++ b/apps/server/src/shared/domain/domainobject/school.do.ts @@ -2,10 +2,7 @@ import { FederalState, SchoolFeatures, SchoolYear } from '@shared/domain/entity' import { EntityId } from '@shared/domain/types'; import { BaseDO } from './base.do'; -/** - * @deprecated because it extends the deprecated BaseDO. - */ -export class LegacySchoolDo extends BaseDO { +export class SchoolDO extends BaseDO { externalId?: string; inMaintenanceSince?: Date; @@ -30,7 +27,7 @@ export class LegacySchoolDo extends BaseDO { // TODO: N21-990 Refactoring: Create domain objects for schoolYear and federalState federalState: FederalState; - constructor(params: LegacySchoolDo) { + constructor(params: SchoolDO) { super(); this.id = params.id; this.externalId = params.externalId; diff --git a/apps/server/src/shared/domain/entity/all-entities.ts b/apps/server/src/shared/domain/entity/all-entities.ts index f188b5d023e..d025133f6b0 100644 --- a/apps/server/src/shared/domain/entity/all-entities.ts +++ b/apps/server/src/shared/domain/entity/all-entities.ts @@ -1,8 +1,10 @@ -import { ShareToken } from '@src/modules/sharing/entity/share-token.entity'; +import { GroupEntity } from '@src/modules/group/entity'; import { ExternalToolPseudonymEntity, PseudonymEntity } from '@src/modules/pseudonym/entity'; -import { ExternalToolEntity } from '@src/modules/tool/external-tool/entity'; +import { ShareToken } from '@src/modules/sharing/entity/share-token.entity'; import { ContextExternalToolEntity } from '@src/modules/tool/context-external-tool/entity'; +import { ExternalToolEntity } from '@src/modules/tool/external-tool/entity'; import { SchoolExternalToolEntity } from '@src/modules/tool/school-external-tool/entity'; +import { ClassEntity } from '@src/modules/class/entity'; import { Account } from './account.entity'; import { CardNode, @@ -23,8 +25,8 @@ import { ImportUser } from './import-user.entity'; import { Board, BoardElement, - ColumnBoardTarget, ColumnboardBoardElement, + ColumnBoardTarget, LessonBoardElement, TaskBoardElement, } from './legacy-board'; @@ -54,6 +56,7 @@ export const ALL_ENTITIES = [ ColumnBoardNode, ColumnBoardTarget, ColumnNode, + ClassEntity, FileElementNode, RichTextElementNode, SubmissionContainerElementNode, @@ -94,4 +97,5 @@ export const ALL_ENTITIES = [ User, UserLoginMigration, VideoConference, + GroupEntity, ]; diff --git a/apps/server/src/shared/domain/entity/external-source.entity.ts b/apps/server/src/shared/domain/entity/external-source.entity.ts new file mode 100644 index 00000000000..fabe2f03d83 --- /dev/null +++ b/apps/server/src/shared/domain/entity/external-source.entity.ts @@ -0,0 +1,22 @@ +import { Embeddable, ManyToOne, Property } from '@mikro-orm/core'; +import { System } from './system.entity'; + +export interface ExternalSourceEntityProps { + externalId: string; + + system: System; +} + +@Embeddable() +export class ExternalSourceEntity { + @Property() + externalId: string; + + @ManyToOne(() => System) + system: System; + + constructor(props: ExternalSourceEntityProps) { + this.externalId = props.externalId; + this.system = props.system; + } +} diff --git a/apps/server/src/shared/domain/entity/index.ts b/apps/server/src/shared/domain/entity/index.ts index 2893631fcbf..5c8ff64a9de 100644 --- a/apps/server/src/shared/domain/entity/index.ts +++ b/apps/server/src/shared/domain/entity/index.ts @@ -25,3 +25,4 @@ export * from './team.entity'; export * from './user-login-migration.entity'; export * from './user.entity'; export * from './video-conference.entity'; +export * from './external-source.entity'; diff --git a/apps/server/src/shared/domain/rules/index.ts b/apps/server/src/shared/domain/rules/index.ts index 888b2ee8501..ea72f8b4d0a 100644 --- a/apps/server/src/shared/domain/rules/index.ts +++ b/apps/server/src/shared/domain/rules/index.ts @@ -4,7 +4,7 @@ import { CourseGroupRule } from './course-group.rule'; import { CourseRule } from './course.rule'; import { LessonRule } from './lesson.rule'; import { SchoolExternalToolRule } from './school-external-tool.rule'; -import { LegacySchoolRule } from './legacy-school.rule'; +import { SchoolRule } from './school.rule'; import { SubmissionRule } from './submission.rule'; import { TaskRule } from './task.rule'; import { TeamRule } from './team.rule'; @@ -16,7 +16,7 @@ export * from './course-group.rule'; export * from './course.rule'; export * from './lesson.rule'; export * from './school-external-tool.rule'; -export * from './legacy-school.rule'; +export * from './school.rule'; export * from './submission.rule'; export * from './task.rule'; export * from './team.rule'; @@ -27,7 +27,7 @@ export const ALL_RULES = [ LessonRule, CourseRule, CourseGroupRule, - LegacySchoolRule, + SchoolRule, SubmissionRule, TaskRule, TeamRule, diff --git a/apps/server/src/shared/domain/rules/legacy-school.rule.spec.ts b/apps/server/src/shared/domain/rules/legacy-school.rule.spec.ts deleted file mode 100644 index c547f772de5..00000000000 --- a/apps/server/src/shared/domain/rules/legacy-school.rule.spec.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { Permission } from '@shared/domain/interface'; -import { roleFactory, legacySchoolDoFactory, setupEntities, userFactory } from '@shared/testing'; -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; -import { Action } from '@src/modules/authorization/types'; -import { ObjectID } from 'bson'; -import { LegacySchoolRule } from './legacy-school.rule'; - -describe('LegacySchoolRule', () => { - let service: LegacySchoolRule; - let authorizationHelper: AuthorizationHelper; - const permissionA = 'a' as Permission; - const permissionB = 'b' as Permission; - const permissionC = 'c' as Permission; - - beforeAll(async () => { - await setupEntities(); - - const module: TestingModule = await Test.createTestingModule({ - providers: [AuthorizationHelper, LegacySchoolRule], - }).compile(); - - service = await module.get(LegacySchoolRule); - authorizationHelper = await module.get(AuthorizationHelper); - }); - - const setupSchoolAndUser = () => { - const school = legacySchoolDoFactory.build({ id: new ObjectID().toString() }); - const role = roleFactory.build({ permissions: [permissionA, permissionB] }); - const user = userFactory.build({ - roles: [role], - school: { id: school.id }, - }); - - return { school, user }; - }; - - it('should call hasAllPermissions on AuthorizationHelper', () => { - const { school, user } = setupSchoolAndUser(); - const spy = jest.spyOn(authorizationHelper, 'hasAllPermissions'); - - service.hasPermission(user, school, { action: Action.read, requiredPermissions: [] }); - - expect(spy).toBeCalledWith(user, []); - }); - - it('should return "true" if user in scope', () => { - const { school, user } = setupSchoolAndUser(); - - const res = service.hasPermission(user, school, { action: Action.read, requiredPermissions: [] }); - - expect(res).toBe(true); - }); - - it('should return "false" if user has not permission', () => { - const { school, user } = setupSchoolAndUser(); - - const res = service.hasPermission(user, school, { action: Action.read, requiredPermissions: [permissionC] }); - - expect(res).toBe(false); - }); - - it('should return "false" if user has not same school', () => { - const { user } = setupSchoolAndUser(); - const school = legacySchoolDoFactory.build(); - - const res = service.hasPermission(user, school, { action: Action.read, requiredPermissions: [permissionA] }); - - expect(res).toBe(false); - }); -}); diff --git a/apps/server/src/shared/domain/rules/legacy-school.rule.ts b/apps/server/src/shared/domain/rules/legacy-school.rule.ts deleted file mode 100644 index 5068d327c35..00000000000 --- a/apps/server/src/shared/domain/rules/legacy-school.rule.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Injectable } from '@nestjs/common'; -import { BaseDO, LegacySchoolDo } from '@shared/domain'; -import { User } from '@shared/domain/entity'; -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; -import { AuthorizationContext, Rule } from '@src/modules/authorization/types'; -import { AuthorizableObject } from '../domain-object'; - -/** - * @deprecated because it uses the deprecated LegacySchoolDo. - */ -@Injectable() -export class LegacySchoolRule implements Rule { - constructor(private readonly authorizationHelper: AuthorizationHelper) {} - - public isApplicable(user: User, object: AuthorizableObject | BaseDO): boolean { - const isMatched = object instanceof LegacySchoolDo; - - return isMatched; - } - - public hasPermission(user: User, entity: LegacySchoolDo, context: AuthorizationContext): boolean { - const hasPermission = - this.authorizationHelper.hasAllPermissions(user, context.requiredPermissions) && user.school.id === entity.id; - - return hasPermission; - } -} diff --git a/apps/server/src/shared/domain/rules/school.rule.spec.ts b/apps/server/src/shared/domain/rules/school.rule.spec.ts new file mode 100644 index 00000000000..74e017ce4a4 --- /dev/null +++ b/apps/server/src/shared/domain/rules/school.rule.spec.ts @@ -0,0 +1,64 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; +import { Role, School, User } from '@shared/domain/entity'; +import { Permission } from '@shared/domain/interface'; +import { roleFactory, schoolDOFactory, schoolFactory, setupEntities, userFactory } from '@shared/testing'; +import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; +import { Action } from '@src/modules/authorization/types'; +import { SchoolRule } from './school.rule'; + +describe('SchoolRule', () => { + let service: SchoolRule; + let authorizationHelper: AuthorizationHelper; + let user: User; + let entity: School | SchoolDO; + let role: Role; + const permissionA = 'a' as Permission; + const permissionB = 'b' as Permission; + const permissionC = 'c' as Permission; + + beforeAll(async () => { + await setupEntities(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [AuthorizationHelper, SchoolRule], + }).compile(); + + service = await module.get(SchoolRule); + authorizationHelper = await module.get(AuthorizationHelper); + }); + + beforeEach(() => { + role = roleFactory.build({ permissions: [permissionA, permissionB] }); + user = userFactory.build({ roles: [role] }); + }); + + it('should call hasAllPermissions on AuthorizationHelper', () => { + entity = schoolFactory.build(); + user = userFactory.build({ roles: [role], school: entity }); + const spy = jest.spyOn(authorizationHelper, 'hasAllPermissions'); + service.hasPermission(user, entity, { action: Action.read, requiredPermissions: [] }); + expect(spy).toBeCalledWith(user, []); + }); + + it('should return "true" if user in scope', () => { + entity = schoolFactory.build(); + user = userFactory.build({ roles: [role], school: entity }); + const res = service.hasPermission(user, entity, { action: Action.read, requiredPermissions: [] }); + expect(res).toBe(true); + }); + + it('should return "false" if user has not permission', () => { + entity = schoolFactory.build(); + user = userFactory.build({ roles: [role], school: entity }); + const res = service.hasPermission(user, entity, { action: Action.read, requiredPermissions: [permissionC] }); + expect(res).toBe(false); + }); + + it('should return "false" if user has not some school', () => { + entity = schoolDOFactory.build({ name: 'testschool', id: 'invalidId' }); + user = userFactory.build({ roles: [role] }); + const res = service.hasPermission(user, entity, { action: Action.read, requiredPermissions: [permissionA] }); + expect(res).toBe(false); + }); +}); diff --git a/apps/server/src/shared/domain/rules/school.rule.ts b/apps/server/src/shared/domain/rules/school.rule.ts new file mode 100644 index 00000000000..358d549885c --- /dev/null +++ b/apps/server/src/shared/domain/rules/school.rule.ts @@ -0,0 +1,23 @@ +import { Injectable } from '@nestjs/common'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; +import { School, User } from '@shared/domain/entity'; +import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; +import { AuthorizationContext, Rule } from '@src/modules/authorization/types'; + +@Injectable() +export class SchoolRule implements Rule { + constructor(private readonly authorizationHelper: AuthorizationHelper) {} + + public isApplicable(user: User, entity: School | SchoolDO): boolean { + const isMatched: boolean = entity instanceof School || entity instanceof SchoolDO; + + return isMatched; + } + + public hasPermission(user: User, entity: School | SchoolDO, context: AuthorizationContext): boolean { + const hasPermission: boolean = + this.authorizationHelper.hasAllPermissions(user, context.requiredPermissions) && user.school.id === entity.id; + + return hasPermission; + } +} diff --git a/apps/server/src/shared/infra/cache/cache.module.ts b/apps/server/src/shared/infra/cache/cache.module.ts index ed6aad058b7..897e93926d1 100644 --- a/apps/server/src/shared/infra/cache/cache.module.ts +++ b/apps/server/src/shared/infra/cache/cache.module.ts @@ -1,6 +1,6 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { CacheModule, Module } from '@nestjs/common'; -import { CacheModuleOptions } from '@nestjs/common/cache/interfaces/cache-module.interface'; +import { CacheModule, CacheModuleOptions } from '@nestjs/cache-manager'; +import { Module } from '@nestjs/common'; import { LegacyLogger, LoggerModule } from '@src/core/logger'; import { create } from 'cache-manager-redis-store'; import { RedisClient } from 'redis'; diff --git a/apps/server/src/shared/infra/calendar/service/calendar.service.spec.ts b/apps/server/src/shared/infra/calendar/service/calendar.service.spec.ts index 35eb1fc53b8..43ca5ad06d4 100644 --- a/apps/server/src/shared/infra/calendar/service/calendar.service.spec.ts +++ b/apps/server/src/shared/infra/calendar/service/calendar.service.spec.ts @@ -6,6 +6,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { CalendarEventDto, CalendarService } from '@shared/infra/calendar'; import { ICalendarEvent } from '@shared/infra/calendar/interface/calendar-event.interface'; import { CalendarMapper } from '@shared/infra/calendar/mapper/calendar.mapper'; +import { axiosResponseFactory } from '@shared/testing'; import { AxiosResponse } from 'axios'; import { of, throwError } from 'rxjs'; @@ -65,13 +66,9 @@ describe('CalendarServiceSpec', () => { }, ], }; - const axiosResponse: AxiosResponse = { + const axiosResponse: AxiosResponse = axiosResponseFactory.build({ data: event, - status: 0, - statusText: 'statusText', - headers: {}, - config: {}, - }; + }); httpService.get.mockReturnValue(of(axiosResponse)); calendarMapper.mapToDto.mockReturnValue({ title, teamId }); diff --git a/apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.client.spec.ts b/apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.client.spec.ts index e6e4e46b718..5aa4cb8b09d 100644 --- a/apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.client.spec.ts +++ b/apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.client.spec.ts @@ -1,16 +1,17 @@ -import { Test, TestingModule } from '@nestjs/testing'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; import { HttpService } from '@nestjs/axios'; +import { NotFoundException, NotImplementedException, UnprocessableEntityException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { NextcloudClient } from '@shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.client'; +import { axiosResponseFactory } from '@shared/testing'; +import { LegacyLogger } from '@src/core/logger'; import { AxiosResponse } from 'axios'; import { Observable, of } from 'rxjs'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { LegacyLogger } from '@src/core/logger'; -import { NextcloudClient } from '@shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.client'; -import { NotFoundException, NotImplementedException, UnprocessableEntityException } from '@nestjs/common'; -import { ObjectId } from '@mikro-orm/mongodb'; import { + GroupUsers, GroupfoldersCreated, GroupfoldersFolder, - GroupUsers, Meta, NextcloudGroups, OcsResponse, @@ -42,15 +43,10 @@ function createOcsResponse(data: T, meta: Meta = defaultMetadata): }; } -function createAxiosResponse(data: T): AxiosResponse { - return { +const createAxiosResponse = (data: unknown) => + axiosResponseFactory.build({ data, - status: 0, - statusText: '', - headers: {}, - config: {}, - }; -} + }); function createObservable(data: T): Observable> { return of(createAxiosResponse(data)); diff --git a/apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.spec.ts b/apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.spec.ts index c9128ba780e..8efd3ff9a7d 100644 --- a/apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.spec.ts +++ b/apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.spec.ts @@ -1,8 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { LtiToolDO } from '@shared/domain/domainobject/ltitool.do'; import { LtiPrivacyPermission, LtiRoleType, Pseudonym, RoleName, User, UserDO } from '@shared/domain'; +import { LtiToolDO } from '@shared/domain/domainobject/ltitool.do'; import { TeamRolePermissionsDto } from '@shared/infra/collaborative-storage/dto/team-role-permissions.dto'; import { NextcloudClient } from '@shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.client'; import { NextcloudStrategy } from '@shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy'; @@ -10,8 +10,8 @@ import { LtiToolRepo } from '@shared/repo'; import { ltiToolDOFactory, pseudonymFactory, setupEntities, userDoFactory, userFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { TeamDto, TeamUserDto } from '@src/modules/collaborative-storage/services/dto/team.dto'; -import { ExternalToolService } from '@src/modules/tool/external-tool/service'; import { PseudonymService } from '@src/modules/pseudonym'; +import { ExternalToolService } from '@src/modules/tool/external-tool/service'; import { UserService } from '@src/modules/user'; class NextcloudStrategySpec extends NextcloudStrategy { @@ -385,7 +385,7 @@ describe('NextCloudStrategy', () => { const userDo: UserDO = userDoFactory.build({ id: user.id }); const teamUsers: TeamUserDto[] = [{ userId: user.id, schoolId: user.school.id, roleId: user.roles[0].id }]; - const pseudonym: Pseudonym = pseudonymFactory.buildWithId({ + const pseudonym: Pseudonym = pseudonymFactory.build({ userId: user.id, toolId: nextcloudTool.id as string, pseudonym: `ps${user.id}`, @@ -465,7 +465,7 @@ describe('NextCloudStrategy', () => { const user: User = userFactory.withRoleByName(RoleName.TEAMMEMBER).buildWithId(); const teamUsers: TeamUserDto[] = []; - const pseudonym: Pseudonym = pseudonymFactory.buildWithId({ + const pseudonym: Pseudonym = pseudonymFactory.build({ userId: user.id, toolId: nextcloudTool.id as string, pseudonym: `ps${user.id}`, @@ -531,7 +531,7 @@ describe('NextCloudStrategy', () => { { userId: 'invalidId', schoolId: 'someSchool', roleId: 'someRole' }, ]; - const pseudonym: Pseudonym = pseudonymFactory.buildWithId({ + const pseudonym: Pseudonym = pseudonymFactory.build({ userId: user.id, toolId: nextcloudTool.id as string, pseudonym: `ps${user.id}`, @@ -570,7 +570,7 @@ describe('NextCloudStrategy', () => { const user: User = userFactory.withRoleByName(RoleName.TEAMMEMBER).buildWithId(); const teamUsers: TeamUserDto[] = [{ userId: user.id, schoolId: user.school.id, roleId: user.roles[0].id }]; - const pseudonym: Pseudonym = pseudonymFactory.buildWithId({ + const pseudonym: Pseudonym = pseudonymFactory.build({ userId: user.id, toolId: nextcloudTool.id as string, pseudonym: `ps${user.id}`, diff --git a/apps/server/src/shared/infra/console/console-writer/console-writer.service.spec.ts b/apps/server/src/shared/infra/console/console-writer/console-writer.service.spec.ts index eaace67cd61..09cbb79c9ff 100644 --- a/apps/server/src/shared/infra/console/console-writer/console-writer.service.spec.ts +++ b/apps/server/src/shared/infra/console/console-writer/console-writer.service.spec.ts @@ -20,14 +20,4 @@ describe('ConsoleWriterService', () => { it('should be defined', () => { expect(service).toBeDefined(); }); - describe('when using info on console writer', () => { - it('should call spinner info with same input text', () => { - // eslint-disable-next-line @typescript-eslint/dot-notation - const spinnerSpy = jest.spyOn(service['spinner'], 'info'); - const someRandomText = 'random text'; - service.info(someRandomText); - expect(spinnerSpy).toHaveBeenCalledWith(someRandomText); - spinnerSpy.mockReset(); - }); - }); }); diff --git a/apps/server/src/shared/infra/console/console-writer/console-writer.service.ts b/apps/server/src/shared/infra/console/console-writer/console-writer.service.ts index 62aadb9ba4d..f02f7b8785d 100644 --- a/apps/server/src/shared/infra/console/console-writer/console-writer.service.ts +++ b/apps/server/src/shared/infra/console/console-writer/console-writer.service.ts @@ -1,15 +1,9 @@ import { Injectable } from '@nestjs/common'; -import { createSpinner } from 'nestjs-console'; -import ora from 'ora'; @Injectable() -/** - * Console writer service using ora spinner internally. - */ export class ConsoleWriterService { - private spinner: ora.Ora = createSpinner(); - info(text: string): void { - this.spinner.info(text); + // eslint-disable-next-line no-console + console.info('Info:', text); } } diff --git a/apps/server/src/shared/infra/oauth-provider/hydra/hydra.adapter.spec.ts b/apps/server/src/shared/infra/oauth-provider/hydra/hydra.adapter.spec.ts index 6357ba1f7fa..0e244b37d16 100644 --- a/apps/server/src/shared/infra/oauth-provider/hydra/hydra.adapter.spec.ts +++ b/apps/server/src/shared/infra/oauth-provider/hydra/hydra.adapter.spec.ts @@ -1,8 +1,7 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { HydraAdapter } from '@shared/infra/oauth-provider/hydra/hydra.adapter'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { HttpService } from '@nestjs/axios'; -import { AxiosRequestConfig, AxiosRequestHeaders, AxiosResponse, Method } from 'axios'; +import { Test, TestingModule } from '@nestjs/testing'; import { AcceptConsentRequestBody, AcceptLoginRequestBody, @@ -13,9 +12,11 @@ import { ProviderRedirectResponse, RejectRequestBody, } from '@shared/infra/oauth-provider/dto'; -import { of } from 'rxjs'; -import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { ProviderConsentSessionResponse } from '@shared/infra/oauth-provider/dto/response/consent-session.response'; +import { HydraAdapter } from '@shared/infra/oauth-provider/hydra/hydra.adapter'; +import { axiosResponseFactory } from '@shared/testing'; +import { AxiosRequestConfig, Method, RawAxiosRequestHeaders } from 'axios'; +import { of } from 'rxjs'; import resetAllMocks = jest.resetAllMocks; class HydraAdapterSpec extends HydraAdapter { @@ -23,21 +24,16 @@ class HydraAdapterSpec extends HydraAdapter { method: Method, url: string, data?: unknown, - additionalHeaders: AxiosRequestHeaders = {} + additionalHeaders?: RawAxiosRequestHeaders ): Promise { return super.request(method, url, data, additionalHeaders); } } -const createAxiosResponse = (data: T): AxiosResponse => { - return { +const createAxiosResponse = (data: T) => + axiosResponseFactory.build({ data, - status: 200, - statusText: '', - headers: {}, - config: {}, - }; -}; + }); describe('HydraService', () => { let module: TestingModule; diff --git a/apps/server/src/shared/infra/oauth-provider/hydra/hydra.adapter.ts b/apps/server/src/shared/infra/oauth-provider/hydra/hydra.adapter.ts index 0b325640693..f554a15abd3 100644 --- a/apps/server/src/shared/infra/oauth-provider/hydra/hydra.adapter.ts +++ b/apps/server/src/shared/infra/oauth-provider/hydra/hydra.adapter.ts @@ -1,9 +1,9 @@ -import { Injectable } from '@nestjs/common'; -import { HttpService } from '@nestjs/axios'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { AxiosRequestHeaders, AxiosResponse, Method } from 'axios'; -import { firstValueFrom, Observable } from 'rxjs'; +import { HttpService } from '@nestjs/axios'; +import { Injectable } from '@nestjs/common'; +import { AxiosResponse, Method, RawAxiosRequestHeaders } from 'axios'; import QueryString from 'qs'; +import { Observable, firstValueFrom } from 'rxjs'; import { URL } from 'url'; import { AcceptConsentRequestBody, @@ -15,8 +15,8 @@ import { ProviderRedirectResponse, RejectRequestBody, } from '../dto'; -import { OauthProviderService } from '../oauth-provider.service'; import { ProviderConsentSessionResponse } from '../dto/response/consent-session.response'; +import { OauthProviderService } from '../oauth-provider.service'; @Injectable() export class HydraAdapter extends OauthProviderService { @@ -158,7 +158,7 @@ export class HydraAdapter extends OauthProviderService { method: Method, url: string, data?: unknown, - additionalHeaders: AxiosRequestHeaders = {} + additionalHeaders: RawAxiosRequestHeaders = {} ): Promise { const observable: Observable> = this.httpService.request({ url, diff --git a/apps/server/src/shared/infra/s3-client/README.md b/apps/server/src/shared/infra/s3-client/README.md new file mode 100644 index 00000000000..0170145342b --- /dev/null +++ b/apps/server/src/shared/infra/s3-client/README.md @@ -0,0 +1,41 @@ +# S3 client module + +This module allows to connect to the S3 storage with our abstraction layer. + +## how to use + +You need to create a unique connection token and set it as the connection name in S3 configuration. And you must use this token, when injecting the S3 client into your service. This is **very important**, because multiple modules could potentially use the S3 client with different configurations. + +The S3ClientModule.register method awaits an array of S3 configurations. Also you can create many connections to different S3 providers and buckets. + +```ts +// your.config.ts +export const YOUR_S3_UNIQ_CONNECTION_TOKEN = "YOUR_S3_UNIQ_CONNECTION_TOKEN"; + +const s3Config: S3Config = { + connectionName: YOUR_S3_UNIQ_CONNECTION_TOKEN, // Important! + endpoint: "", + region: "", + bucket: "", + accessKeyId: "", + secretAccessKey: "", +}; + +// your.service.ts + +@Injectable() +export class FilesStorageService { + constructor( + @Inject(YOUR_S3_UNIQ_CONNECTION_TOKEN) // Important! + private readonly storageClient: S3ClientAdapter) +} + +// your.module.ts +@Module({ + imports: [S3ClientModule.register([s3Config]),] + providers: [YourService] +}) + +export class YourModule {} + +``` diff --git a/apps/server/src/shared/infra/s3-client/interface/index.ts b/apps/server/src/shared/infra/s3-client/interface/index.ts index 1d6de75f7f1..dc4b76ad922 100644 --- a/apps/server/src/shared/infra/s3-client/interface/index.ts +++ b/apps/server/src/shared/infra/s3-client/interface/index.ts @@ -1,6 +1,7 @@ import { Readable } from 'stream'; export interface S3Config { + connectionName: string; endpoint: string; region: string; bucket: string; diff --git a/apps/server/src/shared/infra/s3-client/s3-client.module.spec.ts b/apps/server/src/shared/infra/s3-client/s3-client.module.spec.ts new file mode 100644 index 00000000000..39ebb4f7478 --- /dev/null +++ b/apps/server/src/shared/infra/s3-client/s3-client.module.spec.ts @@ -0,0 +1,90 @@ +import { createMock } from '@golevelup/ts-jest'; +import { Inject } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { LegacyLogger } from '@src/core/logger'; +import { S3ClientAdapter } from './s3-client.adapter'; +import { S3ClientModule } from './s3-client.module'; + +const connectionOne = 'connectionOne'; +const connectionTwo = 'connectionTwo'; + +class OneService { + constructor(@Inject(connectionOne) public s3client: S3ClientAdapter) {} +} + +describe('S3ClientModule', () => { + let module: TestingModule; + const s3ClientConfigOne = { + connectionName: connectionOne, + endpoint: 'endpoint-1', + region: 'region-eu-2', + bucket: 'bucket-1', + accessKeyId: 'accessKeyId-1', + secretAccessKey: 'secretAccessKey-1', + }; + const s3ClientConfigTwo = { + connectionName: connectionTwo, + endpoint: 'endpoint-2', + region: 'region-eu-2', + bucket: 'bucket-2', + accessKeyId: 'accessKeyId-2', + secretAccessKey: 'secretAccessKey-2', + }; + + let s3ClientAdapterOne: S3ClientAdapter; + let s3ClientAdapterTwo: S3ClientAdapter; + let serviceOne: OneService; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + S3ClientModule.register([s3ClientConfigOne, s3ClientConfigTwo]), + ConfigModule.forRoot({ ignoreEnvFile: true, ignoreEnvVars: true, isGlobal: true }), + ], + providers: [ + { + provide: LegacyLogger, + useValue: createMock(), + }, + OneService, + ], + }).compile(); + + s3ClientAdapterOne = module.get(connectionOne); + s3ClientAdapterTwo = module.get(connectionTwo); + serviceOne = module.get(OneService); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('when connectionOne is initialized with register method', () => { + it('should be defined', () => { + expect(s3ClientAdapterOne).toBeDefined(); + }); + + it('should has correctly connection', () => { + expect(s3ClientAdapterOne.config).toBe(s3ClientConfigOne); + }); + }); + + describe('when connectionTwo is initialized with register method', () => { + it('should be defined', () => { + expect(s3ClientAdapterTwo).toBeDefined(); + }); + + it('should has correctly connection', () => { + expect(s3ClientAdapterTwo.config).toBe(s3ClientConfigTwo); + }); + }); + + describe('OneService', () => { + describe('when connectionOne is injected', () => { + it('should has injected s3ClientAdapterOne', () => { + expect(serviceOne.s3client).toBe(s3ClientAdapterOne); + }); + }); + }); +}); diff --git a/apps/server/src/shared/infra/s3-client/s3-client.module.ts b/apps/server/src/shared/infra/s3-client/s3-client.module.ts index c2d08963106..d4a366a9771 100644 --- a/apps/server/src/shared/infra/s3-client/s3-client.module.ts +++ b/apps/server/src/shared/infra/s3-client/s3-client.module.ts @@ -1,44 +1,41 @@ import { S3Client } from '@aws-sdk/client-s3'; import { DynamicModule, Module } from '@nestjs/common'; -import { LoggerModule } from '@src/core/logger'; -import { S3_CLIENT, S3_CONFIG } from './constants'; +import { LegacyLogger, LoggerModule } from '@src/core/logger'; import { S3Config } from './interface'; import { S3ClientAdapter } from './s3-client.adapter'; -@Module({ - imports: [LoggerModule], - providers: [S3ClientAdapter], -}) +const createS3ClientAdapter = (config: S3Config, legacyLogger: LegacyLogger) => { + const { region, accessKeyId, secretAccessKey, endpoint } = config; + + const s3Client = new S3Client({ + region, + credentials: { + accessKeyId, + secretAccessKey, + }, + endpoint, + forcePathStyle: true, + tls: true, + }); + return new S3ClientAdapter(s3Client, config, legacyLogger); +}; + +@Module({}) export class S3ClientModule { - static register(options: S3Config): DynamicModule { - const providers = [ - { - provide: S3_CONFIG, - useValue: options, - }, + static register(configs: S3Config[]): DynamicModule { + const providers = configs.flatMap((config) => [ { - provide: S3_CLIENT, - useFactory: (config: S3Config) => { - const { region, accessKeyId, secretAccessKey, endpoint } = config; - return new S3Client({ - region, - credentials: { - accessKeyId, - secretAccessKey, - }, - endpoint, - forcePathStyle: true, - tls: true, - }); - }, - inject: [S3_CONFIG], + provide: config.connectionName, + useFactory: (logger: LegacyLogger) => createS3ClientAdapter(config, logger), + inject: [LegacyLogger], }, - ]; + ]); return { module: S3ClientModule, + imports: [LoggerModule], providers, - exports: [S3ClientAdapter], + exports: providers, }; } } diff --git a/apps/server/src/shared/repo/school/index.ts b/apps/server/src/shared/repo/school/index.ts index 602b003d013..89e46f27b72 100644 --- a/apps/server/src/shared/repo/school/index.ts +++ b/apps/server/src/shared/repo/school/index.ts @@ -1 +1 @@ -export * from './legacy-school.repo'; +export * from './school.repo'; diff --git a/apps/server/src/shared/repo/school/legacy-school.repo.integration.spec.ts b/apps/server/src/shared/repo/school/school.repo.integration.spec.ts similarity index 87% rename from apps/server/src/shared/repo/school/legacy-school.repo.integration.spec.ts rename to apps/server/src/shared/repo/school/school.repo.integration.spec.ts index 5cd81119d20..ab4fd5e1f17 100644 --- a/apps/server/src/shared/repo/school/legacy-school.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/school/school.repo.integration.spec.ts @@ -5,7 +5,6 @@ import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ISchoolProperties, - LegacySchoolDo, School, SchoolRolePermission, SchoolRoles, @@ -13,34 +12,32 @@ import { System, UserLoginMigration, } from '@shared/domain'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; import { MongoMemoryDatabaseModule } from '@shared/infra/database'; -import { - legacySchoolDoFactory, - schoolFactory, - schoolYearFactory, - systemFactory, - userLoginMigrationFactory, -} from '@shared/testing'; +import { schoolFactory, systemFactory } from '@shared/testing'; +import { schoolDOFactory } from '@shared/testing/factory/domainobject/school.factory'; +import { schoolYearFactory } from '@shared/testing/factory/schoolyear.factory'; import { LegacyLogger } from '@src/core/logger'; -import { LegacySchoolRepo } from '..'; +import { userLoginMigrationFactory } from '@shared/testing/factory/user-login-migration.factory'; +import { SchoolRepo } from '..'; -describe('LegacySchoolRepo', () => { +describe('SchoolRepo', () => { let module: TestingModule; - let repo: LegacySchoolRepo; + let repo: SchoolRepo; let em: EntityManager; beforeAll(async () => { module = await Test.createTestingModule({ imports: [MongoMemoryDatabaseModule.forRoot()], providers: [ - LegacySchoolRepo, + SchoolRepo, { provide: LegacyLogger, useValue: createMock(), }, ], }).compile(); - repo = module.get(LegacySchoolRepo); + repo = module.get(SchoolRepo); em = module.get(EntityManager); }); @@ -62,7 +59,7 @@ describe('LegacySchoolRepo', () => { describe('save is called', () => { describe('when saving only required fields', () => { function setupDO() { - const domainObject: LegacySchoolDo = legacySchoolDoFactory.build(); + const domainObject: SchoolDO = schoolDOFactory.build(); return { domainObject, }; @@ -73,7 +70,7 @@ describe('LegacySchoolRepo', () => { const { id, ...expected } = domainObject; expected.systems = []; - const result: LegacySchoolDo = await repo.save(domainObject); + const result: SchoolDO = await repo.save(domainObject); expect(result).toMatchObject(expected); expect(result.id).toBeDefined(); @@ -120,7 +117,7 @@ describe('LegacySchoolRepo', () => { await em.persistAndFlush(schoolEntity); - const result: LegacySchoolDo | null = await repo.findByExternalId( + const result: SchoolDO | null = await repo.findByExternalId( schoolEntity.externalId as string, schoolEntity.systems[0].id ); @@ -129,7 +126,7 @@ describe('LegacySchoolRepo', () => { }); it('should return null when no school is found', async () => { - const result: LegacySchoolDo | null = await repo.findByExternalId( + const result: SchoolDO | null = await repo.findByExternalId( new ObjectId().toHexString(), new ObjectId().toHexString() ); @@ -144,13 +141,13 @@ describe('LegacySchoolRepo', () => { await em.persistAndFlush(schoolEntity); - const result: LegacySchoolDo | null = await repo.findBySchoolNumber(schoolEntity.officialSchoolNumber as string); + const result: SchoolDO | null = await repo.findBySchoolNumber(schoolEntity.officialSchoolNumber as string); expect(result?.officialSchoolNumber).toEqual(schoolEntity.officialSchoolNumber); }); it('should return null when no school is found', async () => { - const result: LegacySchoolDo | null = await repo.findBySchoolNumber('fail'); + const result: SchoolDO | null = await repo.findBySchoolNumber('fail'); expect(result).toBeNull(); }); @@ -188,7 +185,7 @@ describe('LegacySchoolRepo', () => { const userLoginMigration: UserLoginMigration = userLoginMigrationFactory.build({ school: schoolEntity }); schoolEntity.userLoginMigration = userLoginMigration; - const schoolDO: LegacySchoolDo = repo.mapEntityToDO(schoolEntity); + const schoolDO: SchoolDO = repo.mapEntityToDO(schoolEntity); expect(schoolDO).toEqual( expect.objectContaining({ @@ -226,7 +223,7 @@ describe('LegacySchoolRepo', () => { await em.persistAndFlush([userLoginMigration, system1, system2]); - const entityDO: LegacySchoolDo = legacySchoolDoFactory.build({ + const entityDO: SchoolDO = schoolDOFactory.build({ systems: [system1.id, system2.id], userLoginMigrationId: userLoginMigration.id, }); @@ -265,7 +262,7 @@ describe('LegacySchoolRepo', () => { describe('when there are no systems', () => { it('should not call the entity manager to get the system object', () => { - const entityDO: LegacySchoolDo = legacySchoolDoFactory.build({ systems: undefined }); + const entityDO: SchoolDO = schoolDOFactory.build({ systems: undefined }); const emGetReferenceSpy = jest.spyOn(em, 'getReference'); repo.mapDOToEntityProperties(entityDO); @@ -276,7 +273,7 @@ describe('LegacySchoolRepo', () => { describe('when there is no userLoginMigration', () => { it('should not call the entity manager to get the user login migration reference', () => { - const entityDO: LegacySchoolDo = legacySchoolDoFactory.build({ userLoginMigrationId: undefined }); + const entityDO: SchoolDO = schoolDOFactory.build({ userLoginMigrationId: undefined }); const emGetReferenceSpy = jest.spyOn(em, 'getReference'); repo.mapDOToEntityProperties(entityDO); diff --git a/apps/server/src/shared/repo/school/legacy-school.repo.ts b/apps/server/src/shared/repo/school/school.repo.ts similarity index 77% rename from apps/server/src/shared/repo/school/legacy-school.repo.ts rename to apps/server/src/shared/repo/school/school.repo.ts index f305d145125..485c8551dd6 100644 --- a/apps/server/src/shared/repo/school/legacy-school.repo.ts +++ b/apps/server/src/shared/repo/school/school.repo.ts @@ -1,15 +1,13 @@ import { EntityName } from '@mikro-orm/core'; import { EntityManager } from '@mikro-orm/mongodb'; import { Injectable, InternalServerErrorException } from '@nestjs/common'; -import { EntityId, ISchoolProperties, LegacySchoolDo, School, System, UserLoginMigration } from '@shared/domain'; +import { EntityId, ISchoolProperties, School, System, UserLoginMigration } from '@shared/domain'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; import { LegacyLogger } from '@src/core/logger'; import { BaseDORepo } from '../base.do.repo'; -/** - * @deprecated because it uses the deprecated LegacySchoolDo. - */ @Injectable() -export class LegacySchoolRepo extends BaseDORepo { +export class SchoolRepo extends BaseDORepo { constructor(protected readonly _em: EntityManager, protected readonly logger: LegacyLogger) { super(_em, logger); } @@ -18,20 +16,20 @@ export class LegacySchoolRepo extends BaseDORepo { + async findByExternalId(externalId: string, systemId: string): Promise { const school: School | null = await this._em.findOne(School, { externalId, systems: systemId }); - const schoolDo: LegacySchoolDo | null = school ? this.mapEntityToDO(school) : null; + const schoolDo: SchoolDO | null = school ? this.mapEntityToDO(school) : null; return schoolDo; } - async findBySchoolNumber(officialSchoolNumber: string): Promise { + async findBySchoolNumber(officialSchoolNumber: string): Promise { const [schools, count] = await this._em.findAndCount(School, { officialSchoolNumber }); if (count > 1) { throw new InternalServerErrorException(`Multiple schools found for officialSchoolNumber ${officialSchoolNumber}`); } - const schoolDo: LegacySchoolDo | null = schools[0] ? this.mapEntityToDO(schools[0]) : null; + const schoolDo: SchoolDO | null = schools[0] ? this.mapEntityToDO(schools[0]) : null; return schoolDo; } @@ -39,8 +37,8 @@ export class LegacySchoolRepo extends BaseDORepo = { + data: T; + status: number; + statusText: string; + headers: AxiosHeadersKeyValue; + config: InternalAxiosRequestConfig; +}; + +class AxiosResponseImp implements AxiosResponse { + data: T; + + status: number; + + statusText: string; + + headers: AxiosHeaders; + + config: InternalAxiosRequestConfig; + + constructor(props: AxiosResponseProps) { + this.data = props.data; + this.status = props.status; + this.statusText = props.statusText; + this.headers = new AxiosHeaders(props.headers); + this.config = props.config; + } +} + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export const axiosResponseFactory = BaseFactory.define, AxiosResponseProps>( + AxiosResponseImp, + () => { + return { + data: '', + status: 200, + statusText: '', + headers: new AxiosHeaders(), + config: { headers: new AxiosHeaders() }, + }; + } +); diff --git a/apps/server/src/shared/testing/factory/domainobject/domain-object.factory.spec.ts b/apps/server/src/shared/testing/factory/domainobject/domain-object.factory.spec.ts new file mode 100644 index 00000000000..189e552070c --- /dev/null +++ b/apps/server/src/shared/testing/factory/domainobject/domain-object.factory.spec.ts @@ -0,0 +1,24 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { MethodNotAllowedException } from '@nestjs/common'; +import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; +import { DomainObjectFactory } from './domain-object.factory'; + +class TestClass extends DomainObject {} + +const testFactory = DomainObjectFactory.define(TestClass, () => { + return { + id: new ObjectId().toHexString(), + }; +}); + +describe('DomainObjectFactory', () => { + describe('buildWithId', () => { + it('should throw not allowed', () => { + const id = 'id'; + + const func = () => testFactory.buildWithId(undefined, id); + + expect(func).toThrow(MethodNotAllowedException); + }); + }); +}); diff --git a/apps/server/src/shared/testing/factory/domainobject/domain-object.factory.ts b/apps/server/src/shared/testing/factory/domainobject/domain-object.factory.ts new file mode 100644 index 00000000000..8d6d69f9b23 --- /dev/null +++ b/apps/server/src/shared/testing/factory/domainobject/domain-object.factory.ts @@ -0,0 +1,19 @@ +import { MethodNotAllowedException } from '@nestjs/common'; +import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; +import { BuildOptions, DeepPartial } from 'fishery'; +import { BaseFactory } from '../base.factory'; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export class DomainObjectFactory< + T extends DomainObject, + U extends AuthorizableObject = T extends DomainObject ? X : never, + I = any, + C = U +> extends BaseFactory { + // eslint-disable-next-line @typescript-eslint/no-unused-vars + override buildWithId(params?: DeepPartial, id?: string, options: BuildOptions = {}): T { + throw new MethodNotAllowedException( + 'Domain Objects are always generated with an id. Use .build({ id: ... }) to set an id.' + ); + } +} diff --git a/apps/server/src/shared/testing/factory/domainobject/groups/group.factory.ts b/apps/server/src/shared/testing/factory/domainobject/groups/group.factory.ts new file mode 100644 index 00000000000..a65d5141b61 --- /dev/null +++ b/apps/server/src/shared/testing/factory/domainobject/groups/group.factory.ts @@ -0,0 +1,25 @@ +import { ExternalSource } from '@shared/domain'; +import { Group, GroupProps, GroupTypes } from '@src/modules/group/domain'; +import { ObjectId } from 'bson'; +import { DomainObjectFactory } from '../domain-object.factory'; + +export const groupFactory = DomainObjectFactory.define(Group, ({ sequence }) => { + return { + id: new ObjectId().toHexString(), + name: `Group ${sequence}`, + type: GroupTypes.CLASS, + users: [ + { + userId: new ObjectId().toHexString(), + roleId: new ObjectId().toHexString(), + }, + ], + validFrom: new Date(2023, 1), + validUntil: new Date(2023, 6), + organizationId: new ObjectId().toHexString(), + externalSource: new ExternalSource({ + externalId: `externalId-${sequence}`, + systemId: new ObjectId().toHexString(), + }), + }; +}); diff --git a/apps/server/src/shared/testing/factory/domainobject/groups/index.ts b/apps/server/src/shared/testing/factory/domainobject/groups/index.ts new file mode 100644 index 00000000000..3384f6c5ed1 --- /dev/null +++ b/apps/server/src/shared/testing/factory/domainobject/groups/index.ts @@ -0,0 +1 @@ +export * from './group.factory'; diff --git a/apps/server/src/shared/testing/factory/domainobject/index.ts b/apps/server/src/shared/testing/factory/domainobject/index.ts index c537c9f48ed..c1d814b9ca7 100644 --- a/apps/server/src/shared/testing/factory/domainobject/index.ts +++ b/apps/server/src/shared/testing/factory/domainobject/index.ts @@ -1,7 +1,9 @@ export * from './board'; export * from './tool'; +export * from './groups'; export * from './do-base.factory'; -export * from './legacy-school.factory'; +export * from './domain-object.factory'; +export * from './school.factory'; export * from './user-login-migration-do.factory'; export * from './lti-tool.factory'; export * from './pseudonym.factory'; diff --git a/apps/server/src/shared/testing/factory/domainobject/pseudonym.factory.ts b/apps/server/src/shared/testing/factory/domainobject/pseudonym.factory.ts index c3cc2c803a2..08635e26e3c 100644 --- a/apps/server/src/shared/testing/factory/domainobject/pseudonym.factory.ts +++ b/apps/server/src/shared/testing/factory/domainobject/pseudonym.factory.ts @@ -1,8 +1,8 @@ -import { Pseudonym, PseudonymProps } from '@shared/domain'; import { ObjectId } from '@mikro-orm/mongodb'; -import { DoBaseFactory } from './do-base.factory'; +import { Pseudonym, PseudonymProps } from '@shared/domain'; +import { DomainObjectFactory } from './domain-object.factory'; -export const pseudonymFactory = DoBaseFactory.define(Pseudonym, ({ sequence }) => { +export const pseudonymFactory = DomainObjectFactory.define(Pseudonym, ({ sequence }) => { return { id: new ObjectId().toHexString(), pseudonym: `pseudonym${sequence}`, diff --git a/apps/server/src/shared/testing/factory/domainobject/legacy-school.factory.ts b/apps/server/src/shared/testing/factory/domainobject/school.factory.ts similarity index 73% rename from apps/server/src/shared/testing/factory/domainobject/legacy-school.factory.ts rename to apps/server/src/shared/testing/factory/domainobject/school.factory.ts index 9859721c9a5..3b81247f585 100644 --- a/apps/server/src/shared/testing/factory/domainobject/legacy-school.factory.ts +++ b/apps/server/src/shared/testing/factory/domainobject/school.factory.ts @@ -1,11 +1,11 @@ -import { LegacySchoolDo } from '@shared/domain'; +import { SchoolDO } from '@shared/domain/domainobject/school.do'; import { federalStateFactory } from '../federal-state.factory'; import { schoolYearFactory } from '../schoolyear.factory'; import { DoBaseFactory } from './do-base.factory'; -class LegacySchoolFactory extends DoBaseFactory {} +class SchoolFactory extends DoBaseFactory {} -export const legacySchoolDoFactory = LegacySchoolFactory.define(LegacySchoolDo, ({ sequence }) => { +export const schoolDOFactory = SchoolFactory.define(SchoolDO, ({ sequence }) => { return { name: `schoolName-${sequence}`, externalId: '123', diff --git a/apps/server/src/shared/testing/factory/external-group-dto.factory.ts b/apps/server/src/shared/testing/factory/external-group-dto.factory.ts new file mode 100644 index 00000000000..241b1fb45bd --- /dev/null +++ b/apps/server/src/shared/testing/factory/external-group-dto.factory.ts @@ -0,0 +1,29 @@ +import { RoleName } from '@shared/domain'; +import { ObjectId } from 'bson'; +import { ExternalGroupDto } from '@src/modules/provisioning/dto'; +import { GroupTypes } from '@src/modules/group'; +import { BaseFactory } from './base.factory'; + +export const externalGroupDtoFactory = BaseFactory.define( + ExternalGroupDto, + ({ sequence }) => { + return { + externalId: new ObjectId().toHexString(), + name: `Group ${sequence}`, + type: GroupTypes.CLASS, + users: [ + { + externalUserId: new ObjectId().toHexString(), + roleName: RoleName.TEACHER, + }, + { + externalUserId: new ObjectId().toHexString(), + roleName: RoleName.STUDENT, + }, + ], + from: new Date(2023, 1), + until: new Date(2023, 6), + externalOrganizationId: new ObjectId().toHexString(), + }; + } +); diff --git a/apps/server/src/shared/testing/factory/group-entity.factory.ts b/apps/server/src/shared/testing/factory/group-entity.factory.ts new file mode 100644 index 00000000000..591b7c37d41 --- /dev/null +++ b/apps/server/src/shared/testing/factory/group-entity.factory.ts @@ -0,0 +1,33 @@ +import { ExternalSourceEntity, RoleName } from '@shared/domain'; +import { GroupEntity, GroupEntityProps, GroupEntityTypes, GroupValidPeriodEntity } from '@src/modules/group/entity'; +import { BaseFactory } from './base.factory'; +import { roleFactory } from './role.factory'; +import { schoolFactory } from './school.factory'; +import { systemFactory } from './system.factory'; +import { userFactory } from './user.factory'; + +export const groupEntityFactory = BaseFactory.define(GroupEntity, ({ sequence }) => { + return { + name: `Group ${sequence}`, + type: GroupEntityTypes.CLASS, + users: [ + { + user: userFactory.buildWithId(), + role: roleFactory.buildWithId({ name: RoleName.STUDENT }), + }, + { + user: userFactory.buildWithId(), + role: roleFactory.buildWithId({ name: RoleName.TEACHER }), + }, + ], + validPeriod: new GroupValidPeriodEntity({ + from: new Date(2023, 1), + until: new Date(2023, 6), + }), + organization: schoolFactory.buildWithId(), + externalSource: new ExternalSourceEntity({ + externalId: `externalId-${sequence}`, + system: systemFactory.buildWithId(), + }), + }; +}); diff --git a/apps/server/src/shared/testing/factory/index.ts b/apps/server/src/shared/testing/factory/index.ts index f95825bdd41..70c87fd46fb 100644 --- a/apps/server/src/shared/testing/factory/index.ts +++ b/apps/server/src/shared/testing/factory/index.ts @@ -1,18 +1,26 @@ export * from './account-dto.factory'; export * from './account.factory'; +export * from './axios-response.factory'; +export * from './base.factory'; export * from './board.factory'; export * from './boardelement.factory'; export * from './boardnode'; export * from './context-external-tool-entity.factory'; export * from './course.factory'; export * from './coursegroup.factory'; +export * from './domainobject'; +export * from './external-group-dto.factory'; export * from './external-tool-entity.factory'; +export * from './external-tool-pseudonym.factory'; +export * from './federal-state.factory'; export * from './file.factory'; export * from './filerecord.factory'; +export * from './group-entity.factory'; export * from './import-user.factory'; export * from './lesson.factory'; export * from './material.factory'; export * from './news.factory'; +export * from './role-dto.factory'; export * from './role.factory'; export * from './school-external-tool-entity.factory'; export * from './school.factory'; @@ -22,13 +30,9 @@ export * from './storageprovider.factory'; export * from './submission.factory'; export * from './system.factory'; export * from './task.factory'; +export * from './team.factory'; +export * from './teamuser.factory'; export * from './user-and-account.test.factory'; +export * from './user-login-migration.factory'; export * from './user.do.factory'; export * from './user.factory'; -export * from './domainobject'; -export * from './federal-state.factory'; -export * from './user-login-migration.factory'; -export * from './base.factory'; -export * from './external-tool-pseudonym.factory'; -export * from './team.factory'; -export * from './teamuser.factory'; diff --git a/apps/server/src/shared/testing/factory/pseudonym.factory.ts b/apps/server/src/shared/testing/factory/pseudonym.factory.ts index 1d8154995bb..6f2f60e371c 100644 --- a/apps/server/src/shared/testing/factory/pseudonym.factory.ts +++ b/apps/server/src/shared/testing/factory/pseudonym.factory.ts @@ -1,8 +1,8 @@ -import { BaseFactory } from '@shared/testing/factory/base.factory'; import { ObjectId } from '@mikro-orm/mongodb'; -import { IPseudonymEntityProps, PseudonymEntity } from '@src/modules/pseudonym/entity'; +import { BaseFactory } from '@shared/testing/factory/base.factory'; +import { PseudonymEntity, PseudonymEntityProps } from '@src/modules/pseudonym/entity'; -export const pseudonymEntityFactory = BaseFactory.define( +export const pseudonymEntityFactory = BaseFactory.define( PseudonymEntity, ({ sequence }) => { return { diff --git a/apps/server/src/shared/testing/factory/role-dto.factory.ts b/apps/server/src/shared/testing/factory/role-dto.factory.ts new file mode 100644 index 00000000000..03d14965d41 --- /dev/null +++ b/apps/server/src/shared/testing/factory/role-dto.factory.ts @@ -0,0 +1,13 @@ +import { RoleName } from '@shared/domain'; +import { ObjectId } from 'bson'; +import { RoleDto } from '@src/modules/role/service/dto/role.dto'; +import { BaseFactory } from './base.factory'; +import { userPermissions } from '../user-role-permissions'; + +export const roleDtoFactory = BaseFactory.define(RoleDto, () => { + return { + id: new ObjectId().toHexString(), + name: RoleName.USER, + permissions: userPermissions, + }; +}); diff --git a/backup/setup/migrations.json b/backup/setup/migrations.json index e8224045cae..96c0f276522 100644 --- a/backup/setup/migrations.json +++ b/backup/setup/migrations.json @@ -273,5 +273,38 @@ "$date": "2023-08-03T09:46:49.653Z" }, "__v": 0 + }, + { + "_id": { + "$oid": "64f1e7ce5f01dac16032e59d" + }, + "state": "up", + "name": "add-join-meeting-permission-to-admin", + "createdAt": { + "$date": "2023-09-01T13:28:44.835Z" + }, + "__v": 0 + }, + { + "_id": { + "$oid": "64f5c9d76edbcaa5cc4431d2" + }, + "state": "up", + "name": "add-join-meeting-permission-to-teacher", + "createdAt": { + "$date": "2023-09-01T12:31:41.993Z" + }, + "__v": 0 + }, + { + "_id": { + "$oid": "64f5c9d76edbcaa5cc4431d3" + }, + "state": "up", + "name": "add-start-meeting-permission-to-admin", + "createdAt": { + "$date": "2023-09-01T13:14:13.453Z" + }, + "__v": 0 } ] diff --git a/backup/setup/roles.json b/backup/setup/roles.json index b605b90c55f..3d7909a84da 100644 --- a/backup/setup/roles.json +++ b/backup/setup/roles.json @@ -58,7 +58,6 @@ "TOOL_VIEW", "TOPIC_VIEW", "NEXTCLOUD_USER", - "JOIN_MEETING", "CONTEXT_TOOL_USER" ], "__v": 0 @@ -69,7 +68,7 @@ }, "name": "administrator", "updatedAt": { - "$date": "2023-05-16T08:17:53.317Z" + "$date": "2023-09-04T12:13:44.069Z" }, "createdAt": { "$date": "2017-01-01T00:06:37.148Z" @@ -131,7 +130,9 @@ "SYSTEM_CREATE", "SYSTEM_VIEW", "SCHOOL_TOOL_ADMIN", - "USER_LOGIN_MIGRATION_ADMIN" + "USER_LOGIN_MIGRATION_ADMIN", + "START_MEETING", + "JOIN_MEETING" ], "__v": 2 }, @@ -197,7 +198,7 @@ }, "name": "teacher", "updatedAt": { - "$date": "2023-04-28T13:00:08.424Z" + "$date": "2023-09-04T12:54:47.237Z" }, "createdAt": { "$date": "2017-01-01T00:06:37.148Z" @@ -243,7 +244,8 @@ "START_MEETING", "HOMEWORK_CREATE", "HOMEWORK_EDIT", - "CONTEXT_TOOL_ADMIN" + "CONTEXT_TOOL_ADMIN", + "JOIN_MEETING" ], "__v": 2 }, @@ -361,8 +363,7 @@ "CREATE_TOPICS_AND_TASKS", "EDIT_ALL_FILES", "NEWS_CREATE", - "START_MEETING", - "JOIN_MEETING" + "START_MEETING" ], "__v": 0 }, diff --git a/config/default.schema.json b/config/default.schema.json index ec98a913f9b..278caf57a74 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -605,6 +605,11 @@ "default": "604800000", "description": "Grace period before school admin cannot restart oauth migration anymore. Value is given in ms and default is 7 days" }, + "FEATURE_SANIS_GROUP_PROVISIONING_ENABLED": { + "type": "boolean", + "default": false, + "description": "Groups of type class also gets provisioned, when provisioning a user via oauth login." + }, "SC_TITLE": { "type": "string", "default": "dBildungscloud", @@ -645,7 +650,17 @@ "FEATURE_SCHOOL_POLICY_ENABLED": { "type": "boolean", "default": false, - "description": "Custom School Policy can be uploaded by the school admin " + "description": "Enables uploading of school specific Privacy Policy on old admin page " + }, + "FEATURE_SCHOOL_POLICY_ENABLED_NEW": { + "type": "boolean", + "default": false, + "description": "Enables uploading of school specific Privacy Policy on new admin page." + }, + "FEATURE_SCHOOL_TERMS_OF_USE_ENABLED": { + "type": "boolean", + "default": false, + "description": "Enables uploading of school specific Terms of Use on new admin page." }, "ROCKETCHAT_SERVICE_ENABLED": { "type": "boolean", diff --git a/migrations/1693571501993-add-join-meeting-permission-to-teacher.js b/migrations/1693571501993-add-join-meeting-permission-to-teacher.js new file mode 100644 index 00000000000..74118543ce8 --- /dev/null +++ b/migrations/1693571501993-add-join-meeting-permission-to-teacher.js @@ -0,0 +1,75 @@ +const mongoose = require('mongoose'); +// eslint-disable-next-line no-unused-vars +const { alert, error, info } = require('../src/logger'); + +const { connect, close } = require('../src/utils/database'); + +// use your own name for your model, otherwise other migrations may fail. +// The third parameter is the actually relevent one for what collection to write to. +const Roles = mongoose.model( + 'roles0109231450', + new mongoose.Schema( + { + name: { type: String, required: true }, + permissions: [{ type: String }], + }, + { + timestamps: true, + } + ), + 'roles' +); + +// How to use more than one schema per collection on mongodb +// https://stackoverflow.com/questions/14453864/use-more-than-one-schema-per-collection-on-mongodb + +// TODO npm run migration-persist and remove this line +// TODO update seed data and remove this line + +module.exports = { + up: async function up() { + // eslint-disable-next-line no-process-env + if (process.env.SC_THEME !== 'n21') { + info('Migration does not add the JOIN_MEETING permission for this instance.'); + return; + } + + await connect(); + + await Roles.updateOne( + { name: 'teacher' }, + { + $addToSet: { + permissions: { + $each: ['JOIN_MEETING'], + }, + }, + } + ).exec(); + alert(`Permission JOIN_MEETING added to role teacher`); + await close(); + }, + + down: async function down() { + // eslint-disable-next-line no-process-env + if (process.env.SC_THEME !== 'n21') { + info('Migration does not remove the JOIN_MEETING permission for this instance.'); + return; + } + + await connect(); + + await Roles.updateOne( + { name: 'teacher' }, + { + $pull: { + permissions: { + $in: ['JOIN_MEETING'], + }, + }, + } + ).exec(); + alert(`Permission JOIN_MEETING removed from role teacher`); + await close(); + }, +}; diff --git a/migrations/1693574053453-add-start-meeting-permission-to-admin.js b/migrations/1693574053453-add-start-meeting-permission-to-admin.js new file mode 100644 index 00000000000..4d45fe1cc0a --- /dev/null +++ b/migrations/1693574053453-add-start-meeting-permission-to-admin.js @@ -0,0 +1,75 @@ +const mongoose = require('mongoose'); +// eslint-disable-next-line no-unused-vars +const { alert, error, info } = require('../src/logger'); + +const { connect, close } = require('../src/utils/database'); + +// use your own name for your model, otherwise other migrations may fail. +// The third parameter is the actually relevent one for what collection to write to. +const Roles = mongoose.model( + 'roles0109231514', + new mongoose.Schema( + { + name: { type: String, required: true }, + permissions: [{ type: String }], + }, + { + timestamps: true, + } + ), + 'roles' +); + +// How to use more than one schema per collection on mongodb +// https://stackoverflow.com/questions/14453864/use-more-than-one-schema-per-collection-on-mongodb + +// TODO npm run migration-persist and remove this line +// TODO update seed data and remove this line + +module.exports = { + up: async function up() { + // eslint-disable-next-line no-process-env + if (process.env.SC_THEME !== 'n21') { + info('Migration does not add the START_MEETING permission for this instance.'); + return; + } + + await connect(); + + await Roles.updateOne( + { name: 'administrator' }, + { + $addToSet: { + permissions: { + $each: ['START_MEETING'], + }, + }, + } + ).exec(); + alert(`Permission START_MEETING added to role administrator`); + await close(); + }, + + down: async function down() { + // eslint-disable-next-line no-process-env + if (process.env.SC_THEME !== 'n21') { + info('Migration does not remove the START_MEETING permission for this instance.'); + return; + } + + await connect(); + + await Roles.updateOne( + { name: 'administrator' }, + { + $pull: { + permissions: { + $in: ['START_MEETING'], + }, + }, + } + ).exec(); + alert(`Permission START_MEETING removed from role administrator`); + await close(); + }, +}; diff --git a/migrations/1693574924835-add-join-meeting-permission-to-admin.js b/migrations/1693574924835-add-join-meeting-permission-to-admin.js new file mode 100644 index 00000000000..ea291b14405 --- /dev/null +++ b/migrations/1693574924835-add-join-meeting-permission-to-admin.js @@ -0,0 +1,75 @@ +const mongoose = require('mongoose'); +// eslint-disable-next-line no-unused-vars +const { alert, error, info } = require('../src/logger'); + +const { connect, close } = require('../src/utils/database'); + +// use your own name for your model, otherwise other migrations may fail. +// The third parameter is the actually relevent one for what collection to write to. +const Roles = mongoose.model( + 'roles0109231529', + new mongoose.Schema( + { + name: { type: String, required: true }, + permissions: [{ type: String }], + }, + { + timestamps: true, + } + ), + 'roles' +); + +// How to use more than one schema per collection on mongodb +// https://stackoverflow.com/questions/14453864/use-more-than-one-schema-per-collection-on-mongodb + +// TODO npm run migration-persist and remove this line +// TODO update seed data and remove this line + +module.exports = { + up: async function up() { + // eslint-disable-next-line no-process-env + if (process.env.SC_THEME !== 'n21') { + info('Migration does not add the JOIN_MEETING permission for this instance.'); + return; + } + + await connect(); + + await Roles.updateOne( + { name: 'administrator' }, + { + $addToSet: { + permissions: { + $each: ['JOIN_MEETING'], + }, + }, + } + ).exec(); + alert(`Permission JOIN_MEETING added to role administrator`); + await close(); + }, + + down: async function down() { + // eslint-disable-next-line no-process-env + if (process.env.SC_THEME !== 'n21') { + info('Migration does not remove the JOIN_MEETING permission for this instance.'); + return; + } + + await connect(); + + await Roles.updateOne( + { name: 'administrator' }, + { + $pull: { + permissions: { + $in: ['JOIN_MEETING'], + }, + }, + } + ).exec(); + alert(`Permission JOIN_MEETING removed from role administrator`); + await close(); + }, +}; diff --git a/package-lock.json b/package-lock.json index d28e85519d6..0cca8e3ea36 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,23 +15,24 @@ "@feathersjs/errors": "^4.5.11", "@feathersjs/express": "^4.5.11", "@feathersjs/feathers": "^4.5.11", - "@golevelup/nestjs-rabbitmq": "^3.7.0", + "@golevelup/nestjs-rabbitmq": "^4.0.0", "@hendt/xml2json": "^1.0.3", "@hpi-schul-cloud/commons": "^1.3.4", "@keycloak/keycloak-admin-client": "^21.1.2", "@lumieducation/h5p-server": "^9.2.0", "@mikro-orm/core": "^5.4.2", "@mikro-orm/mongodb": "^5.4.2", - "@mikro-orm/nestjs": "^5.1.5", - "@nestjs/axios": "^0.1.0", - "@nestjs/common": "^9.2.1", - "@nestjs/config": "^2.2.0", - "@nestjs/core": "^9.2.1", - "@nestjs/jwt": "^10.0.2", - "@nestjs/microservices": "^9.2.1", - "@nestjs/passport": "^9.0.0", - "@nestjs/platform-express": "^9.2.1", - "@nestjs/swagger": "^6.2.1", + "@mikro-orm/nestjs": "^5.2.1", + "@nestjs/axios": "^3.0.0", + "@nestjs/cache-manager": "^2.1.0", + "@nestjs/common": "^10.2.4", + "@nestjs/config": "^3.0.1", + "@nestjs/core": "^10.2.4", + "@nestjs/jwt": "^10.1.1", + "@nestjs/microservices": "^10.2.4", + "@nestjs/passport": "^10.0.1", + "@nestjs/platform-express": "^10.2.4", + "@nestjs/swagger": "^7.1.10", "@types/cache-manager-redis-store": "^2.0.1", "@types/connect-redis": "^0.0.19", "@types/gm": "^1.25.1", @@ -47,7 +48,7 @@ "async": "^3.2.2", "async-mutex": "^0.4.0", "aws-sdk": "^2.1375.0", - "axios": "^0.27.2", + "axios": "^1.5.0", "axios-mock-adapter": "^1.21.2", "bbb-promise": "^1.2.0", "bcryptjs": "*", @@ -94,8 +95,8 @@ "mongoose-shortid-nodeps": "git://github.com/leeroybrun/mongoose-shortid-nodeps.git", "moodle-client": "^0.5.2", "nanoid": "^3.3.4", - "nest-winston": "^1.8.0", - "nestjs-console": "^8.0.0", + "nest-winston": "^1.9.4", + "nestjs-console": "^9.0.0", "oauth-1.0a": "^2.2.6", "p-limit": "^3.1.0", "papaparse": "^5.1.1", @@ -133,9 +134,9 @@ "@aws-sdk/client-s3": "^3.352.0", "@golevelup/ts-jest": "^0.3.4", "@jest-mock/express": "^1.4.5", - "@nestjs/cli": "^9.3.0", - "@nestjs/schematics": "^9.0.3", - "@nestjs/testing": "^9.2.1", + "@nestjs/cli": "^10.1.17", + "@nestjs/schematics": "^10.0.2", + "@nestjs/testing": "^10.2.4", "@types/adm-zip": "^0.5.0", "@types/amqplib": "^0.8.2", "@types/bcryptjs": "^2.4.2", @@ -218,42 +219,20 @@ "node": ">=6.0.0" } }, - "node_modules/@angular-devkit/schematics-cli": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-15.2.4.tgz", - "integrity": "sha512-QTTKEH5HOkxvQtCxb2Lna2wubehkaIzA6DKUBISijPQliLomw74tzc7lXCywmMqRTbQPVRLG3kBK97hR4x67nA==", - "dev": true, - "dependencies": { - "@angular-devkit/core": "15.2.4", - "@angular-devkit/schematics": "15.2.4", - "ansi-colors": "4.1.3", - "inquirer": "8.2.4", - "symbol-observable": "4.0.0", - "yargs-parser": "21.1.1" - }, - "bin": { - "schematics": "bin/schematics.js" - }, - "engines": { - "node": "^14.20.0 || ^16.13.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@angular-devkit/schematics-cli/node_modules/@angular-devkit/core": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.4.tgz", - "integrity": "sha512-yl+0j1bMwJLKShsyCXw77tbJG8Sd21+itisPLL2MgEpLNAO252kr9zG4TLlFRJyKVftm2l1h78KjqvM5nbOXNg==", + "node_modules/@angular-devkit/core": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.0.tgz", + "integrity": "sha512-l1k6Rqm3YM16BEn3CWyQKrk9xfu+2ux7Bw3oS+h1TO4/RoxO2PgHj8LLRh/WNrYVarhaqO7QZ5ePBkXNMkzJ1g==", "dev": true, "dependencies": { "ajv": "8.12.0", "ajv-formats": "2.1.1", "jsonc-parser": "3.2.0", - "rxjs": "6.6.7", + "rxjs": "7.8.1", "source-map": "0.7.4" }, "engines": { - "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "node": "^16.14.0 || >=18.10.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, @@ -266,46 +245,53 @@ } } }, - "node_modules/@angular-devkit/schematics-cli/node_modules/@angular-devkit/core/node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "node_modules/@angular-devkit/core/node_modules/source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true, - "dependencies": { - "tslib": "^1.9.0" - }, "engines": { - "npm": ">=2.0.0" + "node": ">= 8" } }, - "node_modules/@angular-devkit/schematics-cli/node_modules/@angular-devkit/schematics": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.2.4.tgz", - "integrity": "sha512-/W7/vvn59PAVLzhcvD4/N/E8RDhub8ny1A7I96LTRjC5o+yvVV16YJ4YJzolrRrIEN01KmLVQJ9A58VCaweMgw==", + "node_modules/@angular-devkit/schematics": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.0.tgz", + "integrity": "sha512-QMDJXPE0+YQJ9Ap3MMzb0v7rx6ZbBEokmHgpdIjN3eILYmbAdsSGE8HTV8NjS9nKmcyE9OGzFCMb7PFrDTlTAw==", "dev": true, "dependencies": { - "@angular-devkit/core": "15.2.4", + "@angular-devkit/core": "16.2.0", "jsonc-parser": "3.2.0", - "magic-string": "0.29.0", + "magic-string": "0.30.1", "ora": "5.4.1", - "rxjs": "6.6.7" + "rxjs": "7.8.1" }, "engines": { - "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "node": "^16.14.0 || >=18.10.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, - "node_modules/@angular-devkit/schematics-cli/node_modules/@angular-devkit/schematics/node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "node_modules/@angular-devkit/schematics-cli": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-16.2.0.tgz", + "integrity": "sha512-f3HjrDvSrRMvESogLsqsZXsEg//trIBySCHRXCglPrWLVdBbIRctGOhXqZoclRxXimIKUx14zLsOWzDwZG8+HQ==", "dev": true, "dependencies": { - "tslib": "^1.9.0" + "@angular-devkit/core": "16.2.0", + "@angular-devkit/schematics": "16.2.0", + "ansi-colors": "4.1.3", + "inquirer": "8.2.4", + "symbol-observable": "4.0.0", + "yargs-parser": "21.1.1" + }, + "bin": { + "schematics": "bin/schematics.js" }, "engines": { - "npm": ">=2.0.0" + "node": "^16.14.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" } }, "node_modules/@angular-devkit/schematics-cli/node_modules/ansi-colors": { @@ -392,33 +378,6 @@ "node": ">=12.0.0" } }, - "node_modules/@angular-devkit/schematics-cli/node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true - }, - "node_modules/@angular-devkit/schematics-cli/node_modules/magic-string": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz", - "integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@angular-devkit/schematics-cli/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/@angular-devkit/schematics-cli/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -431,12 +390,6 @@ "node": ">=8" } }, - "node_modules/@angular-devkit/schematics-cli/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/@angular-devkit/schematics-cli/node_modules/yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", @@ -3108,57 +3061,57 @@ } }, "node_modules/@golevelup/nestjs-common": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@golevelup/nestjs-common/-/nestjs-common-1.4.5.tgz", - "integrity": "sha512-WqxGAP4KZjvUea/lYCEfFXB8fS3NwxEWgrQpz2H8jusbz11OSrLECv5TNU9RIRKFnUPbGevy45dvKHF2JTQfJw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-common/-/nestjs-common-2.0.0.tgz", + "integrity": "sha512-D9RLXgkqn9SDLnZ2VoMER9l/+g5CM9Z7sZXa+10+0rZs6yevMepoiWmMVsFoUXLzYG2GwfixHLExwUr3XBCHFw==", "dependencies": { "lodash": "^4.17.21", - "nanoid": "^3.2.0" + "nanoid": "^3.3.6" }, "peerDependencies": { - "@nestjs/common": "^9.x" + "@nestjs/common": "^10.x" } }, "node_modules/@golevelup/nestjs-discovery": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@golevelup/nestjs-discovery/-/nestjs-discovery-3.0.1.tgz", - "integrity": "sha512-kK/GBYVxb8XGlwXgtCWAkPOwDVh7dXyLRaoZuk2bBYntV3DZkYGAIbLKOFTGz+MGz71vEeQ9bGLP7cHKtCee4g==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-discovery/-/nestjs-discovery-4.0.0.tgz", + "integrity": "sha512-iyZLYip9rhVMR0C93vo860xmboRrD5g5F5iEOfpeblGvYSz8ymQrL9RAST7x/Fp3n+TAXSeOLzDIASt+rak68g==", "dependencies": { - "lodash": "^4.17.15" + "lodash": "^4.17.21" }, "peerDependencies": { - "@nestjs/common": "^9.x", - "@nestjs/core": "^9.x" + "@nestjs/common": "^10.x", + "@nestjs/core": "^10.x" } }, - "node_modules/@golevelup/nestjs-rabbitmq": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@golevelup/nestjs-rabbitmq/-/nestjs-rabbitmq-3.7.0.tgz", - "integrity": "sha512-wV6SA8oCTu8v66U5i3vu/k6DEvlHKM+LTb9JY7Iz8C0nUUuf6pufgvAalusAi4iJJ7/7p3Q44LF38SB4buszWw==", + "node_modules/@golevelup/nestjs-modules": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-modules/-/nestjs-modules-0.7.0.tgz", + "integrity": "sha512-4WxGKubYx0IJF2rxL3S4SChKdl4ZDZPwCdSj6HxmmElXRyua/LlcwLH6NYquh4RRIkQGspDd5WpcMTBw3SxR5g==", "dependencies": { - "@golevelup/nestjs-common": "^1.4.5", - "@golevelup/nestjs-discovery": "^3.0.1", - "@golevelup/nestjs-modules": "^0.6.1", - "amqp-connection-manager": "^3.0.0", - "amqplib": "^0.8.0", "lodash": "^4.17.21" }, "peerDependencies": { - "@nestjs/common": "^9.x", - "@nestjs/core": "^9.x", - "reflect-metadata": "^0.1.0", + "@nestjs/common": "^10.x", "rxjs": "^7.x" } }, - "node_modules/@golevelup/nestjs-rabbitmq/node_modules/@golevelup/nestjs-modules": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@golevelup/nestjs-modules/-/nestjs-modules-0.6.1.tgz", - "integrity": "sha512-E0STg8In8fhIivnGDJAA70+XLPHzK5bMTkCnif9FbZ8waTYDQ3T/QQL0h73k+CUFeznn1hmuEW14sNaE+8cd7w==", + "node_modules/@golevelup/nestjs-rabbitmq": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-rabbitmq/-/nestjs-rabbitmq-4.0.0.tgz", + "integrity": "sha512-CQHRq/jyK3GlM7Lv4nVaqd+BJ53tZXsrOtO/8/OZh19i0YOcQxyRM7iDdtULeG8omJB5/aGMZNsbioLuupxoog==", "dependencies": { + "@golevelup/nestjs-common": "^2.0.0", + "@golevelup/nestjs-discovery": "^4.0.0", + "@golevelup/nestjs-modules": "^0.7.0", + "amqp-connection-manager": "^3.0.0", + "amqplib": "^0.8.0", "lodash": "^4.17.21" }, "peerDependencies": { - "@nestjs/common": "^9.x", + "@nestjs/common": "^10.x", + "@nestjs/core": "^10.x", + "reflect-metadata": "^0.1.0", "rxjs": "^7.x" } }, @@ -4207,9 +4160,9 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "dev": true, "dependencies": { "@jridgewell/set-array": "^1.0.1", @@ -4239,9 +4192,9 @@ } }, "node_modules/@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", "dev": true, "dependencies": { "@jridgewell/gen-mapping": "^0.3.0", @@ -4291,6 +4244,14 @@ "node": "^12.20.0 || ^14.13.1 || >=16.0.0" } }, + "node_modules/@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==", + "engines": { + "node": ">=8" + } + }, "node_modules/@lumieducation/h5p-server": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/@lumieducation/h5p-server/-/h5p-server-9.2.0.tgz", @@ -4322,6 +4283,15 @@ "yazl": "^2.5.1" } }, + "node_modules/@lumieducation/h5p-server/node_modules/axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "dependencies": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, "node_modules/@lumieducation/h5p-server/node_modules/cache-manager": { "version": "3.6.3", "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-3.6.3.tgz", @@ -4459,135 +4429,89 @@ } }, "node_modules/@mikro-orm/nestjs": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/@mikro-orm/nestjs/-/nestjs-5.1.5.tgz", - "integrity": "sha512-8Bkr2cWBRp3MBXKNA/mPSph1pVbL6+4ry0cJl2gxgj9n/kaSn9ak/OsULh5MfadDf8OWi0+TVEqmKjauMrJ02g==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@mikro-orm/nestjs/-/nestjs-5.2.1.tgz", + "integrity": "sha512-TrCdPsM7DApxrK3avBbijT6/6Er4TZhtiQ+qlMqtqva13vMCG4HiF2vIWGrKJbFukkLRuhOfZlES+KZ9Y1Lx2A==", "engines": { "node": ">= 14.0.0" }, "peerDependencies": { "@mikro-orm/core": "^5.0.0 || ^6.0.0-dev.0", - "@nestjs/common": "^8.0.0 || ^9.0.0", - "@nestjs/core": "^8.0.0 || ^9.0.0" + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "@nestjs/core": "^8.0.0 || ^9.0.0 || ^10.0.0" } }, "node_modules/@nestjs/axios": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-0.1.0.tgz", - "integrity": "sha512-b2TT2X6BFbnNoeteiaxCIiHaFcSbVW+S5yygYqiIq5i6H77yIU3IVuLdpQkHq8/EqOWFwMopLN8jdkUT71Am9w==", - "dependencies": { - "axios": "0.27.2" - }, + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.0.tgz", + "integrity": "sha512-ULdH03jDWkS5dy9X69XbUVbhC+0pVnrRcj7bIK/ytTZ76w7CgvTZDJqsIyisg3kNOiljRW/4NIjSf3j6YGvl+g==", "peerDependencies": { - "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0", + "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", + "axios": "^1.3.1", "reflect-metadata": "^0.1.12", "rxjs": "^6.0.0 || ^7.0.0" } }, + "node_modules/@nestjs/cache-manager": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-2.1.0.tgz", + "integrity": "sha512-9kep3a8Mq5cMuXN/anGhSYc0P48CRBXk5wyJJRBFxhNkCH8AIzZF4CASGVDIEMmm3OjVcEUHojjyJwCODS17Qw==", + "peerDependencies": { + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", + "cache-manager": "<=5", + "reflect-metadata": "^0.1.12", + "rxjs": "^7.0.0" + } + }, "node_modules/@nestjs/cli": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.3.0.tgz", - "integrity": "sha512-v/E8Y3zFk30+FljETvPgpoGIUiOfWuOe6WUFw3ExGfDeWrF/A8ceupDHPWNknBAqvNtz2kVrWu5mwsZUEKGIgg==", + "version": "10.1.17", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.1.17.tgz", + "integrity": "sha512-jUEnR2DgC15Op+IhcRWb6cyJrhec9CUQO+GtxCF2Dv9MwLcr4sTDq1UOkfs09HAhpuI8otgF2LoWGTlW3qRuqg==", "dev": true, "dependencies": { - "@angular-devkit/core": "15.2.4", - "@angular-devkit/schematics": "15.2.4", - "@angular-devkit/schematics-cli": "15.2.4", - "@nestjs/schematics": "^9.0.4", + "@angular-devkit/core": "16.2.0", + "@angular-devkit/schematics": "16.2.0", + "@angular-devkit/schematics-cli": "16.2.0", + "@nestjs/schematics": "^10.0.1", "chalk": "4.1.2", "chokidar": "3.5.3", "cli-table3": "0.6.3", "commander": "4.1.1", "fork-ts-checker-webpack-plugin": "8.0.0", - "inquirer": "8.2.5", + "inquirer": "8.2.6", "node-emoji": "1.11.0", "ora": "5.4.1", "os-name": "4.0.1", - "rimraf": "4.4.0", + "rimraf": "4.4.1", "shelljs": "0.8.5", "source-map-support": "0.5.21", "tree-kill": "1.2.2", - "tsconfig-paths": "4.1.2", - "tsconfig-paths-webpack-plugin": "4.0.1", - "typescript": "4.9.5", - "webpack": "5.76.2", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.1.0", + "typescript": "5.1.6", + "webpack": "5.88.2", "webpack-node-externals": "3.0.0" }, "bin": { "nest": "bin/nest.js" }, "engines": { - "node": ">= 12.9.0" - } - }, - "node_modules/@nestjs/cli/node_modules/@angular-devkit/core": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.4.tgz", - "integrity": "sha512-yl+0j1bMwJLKShsyCXw77tbJG8Sd21+itisPLL2MgEpLNAO252kr9zG4TLlFRJyKVftm2l1h78KjqvM5nbOXNg==", - "dev": true, - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "rxjs": "6.6.7", - "source-map": "0.7.4" - }, - "engines": { - "node": "^14.20.0 || ^16.13.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" + "node": ">= 16" }, "peerDependencies": { - "chokidar": "^3.5.2" + "@swc/cli": "^0.1.62", + "@swc/core": "^1.3.62" }, "peerDependenciesMeta": { - "chokidar": { + "@swc/cli": { + "optional": true + }, + "@swc/core": { "optional": true } } }, - "node_modules/@nestjs/cli/node_modules/@angular-devkit/core/node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, - "node_modules/@nestjs/cli/node_modules/@angular-devkit/schematics": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.2.4.tgz", - "integrity": "sha512-/W7/vvn59PAVLzhcvD4/N/E8RDhub8ny1A7I96LTRjC5o+yvVV16YJ4YJzolrRrIEN01KmLVQJ9A58VCaweMgw==", - "dev": true, - "dependencies": { - "@angular-devkit/core": "15.2.4", - "jsonc-parser": "3.2.0", - "magic-string": "0.29.0", - "ora": "5.4.1", - "rxjs": "6.6.7" - }, - "engines": { - "node": "^14.20.0 || ^16.13.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "node_modules/@nestjs/cli/node_modules/@angular-devkit/schematics/node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, "node_modules/@nestjs/cli/node_modules/ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -4674,9 +4598,9 @@ } }, "node_modules/@nestjs/cli/node_modules/inquirer": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", - "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", "dev": true, "dependencies": { "ansi-escapes": "^4.2.1", @@ -4693,30 +4617,12 @@ "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", - "wrap-ansi": "^7.0.0" + "wrap-ansi": "^6.0.1" }, "engines": { "node": ">=12.0.0" } }, - "node_modules/@nestjs/cli/node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true - }, - "node_modules/@nestjs/cli/node_modules/magic-string": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz", - "integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" - }, - "engines": { - "node": ">=12" - } - }, "node_modules/@nestjs/cli/node_modules/minimatch": { "version": "8.0.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.4.tgz", @@ -4733,18 +4639,18 @@ } }, "node_modules/@nestjs/cli/node_modules/minipass": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.7.tgz", - "integrity": "sha512-ScVIgqHcXRMyfflqHmEW0bm8z8rb5McHyOY3ewX9JBgZaR77G7nxq9L/mtV96/QbAAwtbCAHVVLzD1kkyfFQEw==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", "dev": true, "engines": { "node": ">=8" } }, "node_modules/@nestjs/cli/node_modules/rimraf": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.4.0.tgz", - "integrity": "sha512-X36S+qpCUR0HjXlkDe4NAOhS//aHH0Z+h8Ckf2auGJk3PTnx5rLmrHkwNdbVQuCSUhOyFrlRvFEllZOYE+yZGQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.4.1.tgz", + "integrity": "sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og==", "dev": true, "dependencies": { "glob": "^9.2.0" @@ -4759,15 +4665,6 @@ "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/@nestjs/cli/node_modules/source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true, - "engines": { - "node": ">= 8" - } - }, "node_modules/@nestjs/cli/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4780,36 +4677,53 @@ "node": ">=8" } }, - "node_modules/@nestjs/cli/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true + "node_modules/@nestjs/cli/node_modules/typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@nestjs/cli/node_modules/wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=8" + } }, "node_modules/@nestjs/common": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-9.2.1.tgz", - "integrity": "sha512-nZuo3oDsSSlC5mti/M2aCWTEIfHPGDXmBwWgPeCpRbrNz3IWd109rkajll+yxgidVjznAdBS9y00JkAVJblNYw==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.2.4.tgz", + "integrity": "sha512-3Lg4PUaSDucf14V8rPCH212NqrK09AJbY0NKqFsb4j5OIE+TuOzVZR/yjaJ8JNxH2hjskJNCZie0D/9tA2lzlA==", "dependencies": { "iterare": "1.2.1", - "tslib": "2.4.1", - "uuid": "9.0.0" + "tslib": "2.6.2", + "uid": "2.0.2" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/nest" }, "peerDependencies": { - "cache-manager": "<=5", "class-transformer": "*", "class-validator": "*", "reflect-metadata": "^0.1.12", "rxjs": "^7.1.0" }, "peerDependenciesMeta": { - "cache-manager": { - "optional": true - }, "class-transformer": { "optional": true }, @@ -4818,66 +4732,62 @@ } } }, - "node_modules/@nestjs/common/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" - }, - "node_modules/@nestjs/common/node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@nestjs/config": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-2.2.0.tgz", - "integrity": "sha512-78Eg6oMbCy3D/YvqeiGBTOWei1Jwi3f2pSIZcZ1QxY67kYsJzTRTkwRT8Iv30DbK0sGKc1mcloDLD5UXgZAZtg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.0.1.tgz", + "integrity": "sha512-a98MMkDlgUlXTv9qtDbimYfXsuafn/YZOh/S35afutr0Qc5T6KzjyWP5VjxRkv26yI2JM0RhFruByFTM6ezwHA==", "dependencies": { - "dotenv": "16.0.1", - "dotenv-expand": "8.0.3", + "dotenv": "16.3.1", + "dotenv-expand": "10.0.0", "lodash": "4.17.21", - "uuid": "8.3.2" + "uuid": "9.0.0" }, "peerDependencies": { - "@nestjs/common": "^7.0.0 || ^8.0.0 || ^9.0.0", - "reflect-metadata": "^0.1.13", - "rxjs": "^6.0.0 || ^7.2.0" + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "reflect-metadata": "^0.1.13" } }, "node_modules/@nestjs/config/node_modules/dotenv": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz", - "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==", + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", "engines": { "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, + "node_modules/@nestjs/config/node_modules/uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", + "bin": { + "uuid": "dist/bin/uuid" } }, "node_modules/@nestjs/core": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-9.2.1.tgz", - "integrity": "sha512-a9GkXuu8uXgNgCVW+17iI8kLCltO+HwHpU2IhR+32JKnN2WEQ1YEWU4t3GJ2MNq44YkjIw9zrKvFkjJBlYrNbQ==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.2.4.tgz", + "integrity": "sha512-aWeii2l+3pNCc9kIRdLbXQMvrgSZD0jZgXOZv7bZwVf9mClMMi7TussLI4On12VbqVE7LE3gsNgRTwgQJlVC8g==", "hasInstallScript": true, "dependencies": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", - "object-hash": "3.0.0", "path-to-regexp": "3.2.0", - "tslib": "2.4.1", - "uuid": "9.0.0" + "tslib": "2.6.2", + "uid": "2.0.2" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/nest" }, "peerDependencies": { - "@nestjs/common": "^9.0.0", - "@nestjs/microservices": "^9.0.0", - "@nestjs/platform-express": "^9.0.0", - "@nestjs/websockets": "^9.0.0", + "@nestjs/common": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0", + "@nestjs/websockets": "^10.0.0", "reflect-metadata": "^0.1.12", "rxjs": "^7.1.0" }, @@ -4893,39 +4803,26 @@ } } }, - "node_modules/@nestjs/core/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" - }, - "node_modules/@nestjs/core/node_modules/uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==", - "bin": { - "uuid": "dist/bin/uuid" - } - }, "node_modules/@nestjs/jwt": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.0.2.tgz", - "integrity": "sha512-MLxjCSbO7C9fN2hst5kpIhnJAgglJmrKppXAXqElB8A9ip3ZuCowMDjjmNWWJyfOzE98NV0E0iEQGE2StMUC+Q==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.1.1.tgz", + "integrity": "sha512-sISYylg8y1Mb7saxPx5Zh11i7v9JOh70CEC/rN6g43MrbFlJ57c1eYFrffxip1YAx3DmV4K67yXob3syKZMOew==", "dependencies": { - "@types/jsonwebtoken": "9.0.1", + "@types/jsonwebtoken": "9.0.2", "jsonwebtoken": "9.0.0" }, "peerDependencies": { - "@nestjs/common": "^8.0.0 || ^9.0.0" + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0" } }, "node_modules/@nestjs/mapped-types": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.2.2.tgz", - "integrity": "sha512-3dHxLXs3M0GPiriAcCFFJQHoDFUuzTD5w6JDhE7TyfT89YKpe6tcCCIqOZWdXmt9AZjjK30RkHRSFF+QEnWFQg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.2.tgz", + "integrity": "sha512-V0izw6tWs6fTp9+KiiPUbGHWALy563Frn8X6Bm87ANLRuE46iuBMD5acKBDP5lKL/75QFvrzSJT7HkCbB0jTpg==", "peerDependencies": { - "@nestjs/common": "^7.0.8 || ^8.0.0 || ^9.0.0", - "class-transformer": "^0.2.0 || ^0.3.0 || ^0.4.0 || ^0.5.0", - "class-validator": "^0.11.1 || ^0.12.0 || ^0.13.0 || ^0.14.0", + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", + "class-transformer": "^0.4.0 || ^0.5.0", + "class-validator": "^0.13.0 || ^0.14.0", "reflect-metadata": "^0.1.12" }, "peerDependenciesMeta": { @@ -4938,12 +4835,12 @@ } }, "node_modules/@nestjs/microservices": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-9.2.1.tgz", - "integrity": "sha512-ve4dqgoIaUG9VtinHHGCvf6SvtPjd3I44Mj8z8mSkcB8BEPfJnf4FNOShFBgsedtxLaiZyjpv8NCLcNVv5KKGg==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-10.2.4.tgz", + "integrity": "sha512-GytBFj4onLveWDUm+aj7Ft4518yiRx3dfHwqBzYfekPFWIfzVHNGWQCZUSNpS/jMbTfbM2PAknkuhWFjV1811A==", "dependencies": { "iterare": "1.2.1", - "tslib": "2.4.1" + "tslib": "2.6.2" }, "funding": { "type": "opencollective", @@ -4951,9 +4848,9 @@ }, "peerDependencies": { "@grpc/grpc-js": "*", - "@nestjs/common": "^9.0.0", - "@nestjs/core": "^9.0.0", - "@nestjs/websockets": "^9.0.0", + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/websockets": "^10.0.0", "amqp-connection-manager": "*", "amqplib": "*", "cache-manager": "*", @@ -4994,152 +4891,65 @@ } } }, - "node_modules/@nestjs/microservices/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" - }, "node_modules/@nestjs/passport": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-9.0.0.tgz", - "integrity": "sha512-Gnh8n1wzFPOLSS/94X1sUP4IRAoXTgG4odl7/AO5h+uwscEGXxJFercrZfqdAwkWhqkKWbsntM3j5mRy/6ZQDA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.1.tgz", + "integrity": "sha512-hS22LeNj0LByS9toBPkpKyZhyKAXoHACLS1EQrjbAJJEQjhocOskVGwcMwvMlz+ohN+VU804/nMF1Zlya4+TiQ==", "peerDependencies": { - "@nestjs/common": "^8.0.0 || ^9.0.0", + "@nestjs/common": "^8.0.0 || ^9.0.0 || ^10.0.0", "passport": "^0.4.0 || ^0.5.0 || ^0.6.0" } }, "node_modules/@nestjs/platform-express": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-9.2.1.tgz", - "integrity": "sha512-7PecaXt8lrdS1p6Vb1X/am3GGv+EO1VahyDzaEGOK6C0zwhc0VPfLtwihkjjfhS6BjpRIXXgviwEjONUvxVZnA==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.2.4.tgz", + "integrity": "sha512-E9F6WYo6bNwvTT0saJpkr8t4BJLbZRwrX5EKbtBRQqyRcw6NAvlKdacKzoo+Sompdre0IbF8AvNRFk4uLZTWqA==", "dependencies": { - "body-parser": "1.20.1", + "body-parser": "1.20.2", "cors": "2.8.5", "express": "4.18.2", "multer": "1.4.4-lts.1", - "tslib": "2.4.1" + "tslib": "2.6.2" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/nest" }, "peerDependencies": { - "@nestjs/common": "^9.0.0", - "@nestjs/core": "^9.0.0" - } - }, - "node_modules/@nestjs/platform-express/node_modules/body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/@nestjs/platform-express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dependencies": { - "ms": "2.0.0" + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0" } }, - "node_modules/@nestjs/platform-express/node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@nestjs/platform-express/node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@nestjs/platform-express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "node_modules/@nestjs/platform-express/node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@nestjs/platform-express/node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/@nestjs/platform-express/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" - }, "node_modules/@nestjs/schematics": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-9.1.0.tgz", - "integrity": "sha512-/7CyMTnPJSK9/xD9CkCqwuHPOlHVlLC2RDnbdCJ7mIO07SdbBbY14msTqtYW9VRQtsjZPLh1GTChf7ryJUImwA==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.0.2.tgz", + "integrity": "sha512-DaZZjymYoIfRqC5W62lnYXIIods1PDY6CGc8+IpRwyinzffjKxZ3DF3exu+mdyvllzkXo9DTXkoX4zOPSJHCkw==", "dev": true, "dependencies": { - "@angular-devkit/core": "15.2.4", - "@angular-devkit/schematics": "15.2.4", + "@angular-devkit/core": "16.1.8", + "@angular-devkit/schematics": "16.1.8", + "comment-json": "4.2.3", "jsonc-parser": "3.2.0", "pluralize": "8.0.0" }, "peerDependencies": { - "typescript": ">=4.3.5" + "typescript": ">=4.8.2" } }, "node_modules/@nestjs/schematics/node_modules/@angular-devkit/core": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.4.tgz", - "integrity": "sha512-yl+0j1bMwJLKShsyCXw77tbJG8Sd21+itisPLL2MgEpLNAO252kr9zG4TLlFRJyKVftm2l1h78KjqvM5nbOXNg==", + "version": "16.1.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.1.8.tgz", + "integrity": "sha512-dSRD/+bGanArIXkj+kaU1kDFleZeQMzmBiOXX+pK0Ah9/0Yn1VmY3RZh1zcX9vgIQXV+t7UPrTpOjaERMUtVGw==", "dev": true, "dependencies": { "ajv": "8.12.0", "ajv-formats": "2.1.1", "jsonc-parser": "3.2.0", - "rxjs": "6.6.7", + "rxjs": "7.8.1", "source-map": "0.7.4" }, "engines": { - "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "node": "^16.14.0 || >=18.10.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, @@ -5153,33 +4963,27 @@ } }, "node_modules/@nestjs/schematics/node_modules/@angular-devkit/schematics": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.2.4.tgz", - "integrity": "sha512-/W7/vvn59PAVLzhcvD4/N/E8RDhub8ny1A7I96LTRjC5o+yvVV16YJ4YJzolrRrIEN01KmLVQJ9A58VCaweMgw==", + "version": "16.1.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.1.8.tgz", + "integrity": "sha512-6LyzMdFJs337RTxxkI2U1Ndw0CW5mMX/aXWl8d7cW2odiSrAg8IdlMqpc+AM8+CPfsB0FtS1aWkEZqJLT0jHOg==", "dev": true, "dependencies": { - "@angular-devkit/core": "15.2.4", + "@angular-devkit/core": "16.1.8", "jsonc-parser": "3.2.0", - "magic-string": "0.29.0", + "magic-string": "0.30.0", "ora": "5.4.1", - "rxjs": "6.6.7" + "rxjs": "7.8.1" }, "engines": { - "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "node": "^16.14.0 || >=18.10.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, - "node_modules/@nestjs/schematics/node_modules/jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true - }, "node_modules/@nestjs/schematics/node_modules/magic-string": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz", - "integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==", + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", + "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", "dev": true, "dependencies": { "@jridgewell/sourcemap-codec": "^1.4.13" @@ -5188,18 +4992,6 @@ "node": ">=12" } }, - "node_modules/@nestjs/schematics/node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "dependencies": { - "tslib": "^1.9.0" - }, - "engines": { - "npm": ">=2.0.0" - } - }, "node_modules/@nestjs/schematics/node_modules/source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", @@ -5209,27 +5001,21 @@ "node": ">= 8" } }, - "node_modules/@nestjs/schematics/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/@nestjs/swagger": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-6.2.1.tgz", - "integrity": "sha512-9M2vkfJHIzLqDZwvM5TEZO0MxRCvIb0xVy0LsmWwxH1lrb0z/4MhU+r2CWDhBtTccVJrKxVPiU2s3T3b9uUJbg==", + "version": "7.1.10", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.1.10.tgz", + "integrity": "sha512-qreCcxgHFyFX1mOfK36pxiziy4xoa/XcxC0h4Zr9yH54WuqMqO9aaNFhFyuQ1iyd/3YBVQB21Un4gQnh9iGm0w==", "dependencies": { - "@nestjs/mapped-types": "1.2.2", + "@nestjs/mapped-types": "2.0.2", "js-yaml": "4.1.0", "lodash": "4.17.21", "path-to-regexp": "3.2.0", - "swagger-ui-dist": "4.15.5" + "swagger-ui-dist": "5.4.2" }, "peerDependencies": { "@fastify/static": "^6.0.0", - "@nestjs/common": "^9.0.0", - "@nestjs/core": "^9.0.0", + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0", "class-transformer": "*", "class-validator": "*", "reflect-metadata": "^0.1.12" @@ -5263,27 +5049,27 @@ } }, "node_modules/@nestjs/swagger/node_modules/swagger-ui-dist": { - "version": "4.15.5", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.15.5.tgz", - "integrity": "sha512-V3eIa28lwB6gg7/wfNvAbjwJYmDXy1Jo1POjyTzlB6wPcHiGlRxq39TSjYGVjQrUSAzpv+a7nzp7mDxgNy57xA==" + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.4.2.tgz", + "integrity": "sha512-vT5QxP/NOr9m4gLZl+SpavWI3M9Fdh30+Sdw9rEtZbkqNmNNEPhjXas2xTD9rsJYYdLzAiMfwXvtooWH3xbLJA==" }, "node_modules/@nestjs/testing": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-9.2.1.tgz", - "integrity": "sha512-lemXZdRSuqoZ87l0orCrS/c7gqwxeduIFOd21g9g2RUeQ4qlWPegbQDKASzbfC28klPyrgJLW4MNq7uv2JwV8w==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.2.4.tgz", + "integrity": "sha512-2qqymiuPbC41yCXXhtt4cL8AOcVNu13gBCT13A8roUUdcs4lmtg+H3oXKF/Gc/vlLv2RkSTNO+JuzxP1hydLPg==", "dev": true, "dependencies": { - "tslib": "2.4.1" + "tslib": "2.6.2" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/nest" }, "peerDependencies": { - "@nestjs/common": "^9.0.0", - "@nestjs/core": "^9.0.0", - "@nestjs/microservices": "^9.0.0", - "@nestjs/platform-express": "^9.0.0" + "@nestjs/common": "^10.0.0", + "@nestjs/core": "^10.0.0", + "@nestjs/microservices": "^10.0.0", + "@nestjs/platform-express": "^10.0.0" }, "peerDependenciesMeta": { "@nestjs/microservices": { @@ -5294,12 +5080,6 @@ } } }, - "node_modules/@nestjs/testing/node_modules/tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", - "dev": true - }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -5726,9 +5506,9 @@ "dev": true }, "node_modules/@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", "dev": true }, "node_modules/@types/express": { @@ -5858,9 +5638,9 @@ "dev": true }, "node_modules/@types/jsonwebtoken": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz", - "integrity": "sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==", "dependencies": { "@types/node": "*" } @@ -6526,148 +6306,148 @@ "dev": true }, "node_modules/@webassemblyjs/ast": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", - "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", "dev": true, "dependencies": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" } }, "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", "dev": true }, "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", "dev": true }, "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", "dev": true }, "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", - "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", "dev": true, "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", "dev": true }, "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", - "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" } }, "node_modules/@webassemblyjs/ieee754": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", - "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", "dev": true, "dependencies": { "@xtuc/ieee754": "^1.2.0" } }, "node_modules/@webassemblyjs/leb128": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", - "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", "dev": true, "dependencies": { "@xtuc/long": "4.2.2" } }, "node_modules/@webassemblyjs/utf8": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", "dev": true }, "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", - "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" } }, "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", - "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" } }, "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", - "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" } }, "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", - "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" } }, "node_modules/@webassemblyjs/wast-printer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", - "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", "dev": true, "dependencies": { - "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/ast": "1.11.6", "@xtuc/long": "4.2.2" } }, @@ -7080,6 +6860,12 @@ "resolved": "https://registry.npmjs.org/array-series/-/array-series-0.1.5.tgz", "integrity": "sha512-L0XlBwfx9QetHOsbLDrE/vh2t018w9462HM3iaFfxRiK83aJjAt/Ja3NMkOW7FICwWTlQBa3ZbL5FKhuQWkDrg==" }, + "node_modules/array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true + }, "node_modules/array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -7296,12 +7082,13 @@ } }, "node_modules/axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz", + "integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==", "dependencies": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "node_modules/axios-mock-adapter": { @@ -7532,20 +7319,20 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, "node_modules/body-parser": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", - "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "dependencies": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.10.3", - "raw-body": "2.5.1", + "qs": "6.11.0", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -7601,18 +7388,18 @@ "node": ">= 0.8" } }, - "node_modules/body-parser/node_modules/qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "node_modules/body-parser/node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "dependencies": { - "side-channel": "^1.0.4" + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" }, "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" + "node": ">= 0.8" } }, "node_modules/body-parser/node_modules/statuses": { @@ -8184,6 +7971,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, "dependencies": { "restore-cursor": "^3.1.0" }, @@ -8195,6 +7983,7 @@ "version": "2.6.1", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "dev": true, "engines": { "node": ">=6" }, @@ -8553,6 +8342,22 @@ "node": ">= 12" } }, + "node_modules/comment-json": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.3.tgz", + "integrity": "sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw==", + "dev": true, + "dependencies": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1", + "has-own-prop": "^2.0.0", + "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/commist": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz", @@ -8855,9 +8660,9 @@ } }, "node_modules/content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "engines": { "node": ">= 0.6" } @@ -9178,6 +8983,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, "dependencies": { "clone": "^1.0.2" } @@ -9186,6 +8992,7 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true, "engines": { "node": ">=0.8" } @@ -9396,9 +9203,9 @@ } }, "node_modules/dotenv-expand": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-8.0.3.tgz", - "integrity": "sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==", "engines": { "node": ">=12" } @@ -9574,9 +9381,9 @@ } }, "node_modules/enhanced-resolve": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", - "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", "dev": true, "dependencies": { "graceful-fs": "^4.2.4", @@ -9653,9 +9460,9 @@ } }, "node_modules/es-module-lexer": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", + "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==", "dev": true }, "node_modules/es-to-primitive": { @@ -12445,6 +12252,15 @@ "node": ">=8" } }, + "node_modules/has-own-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", + "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/has-property-descriptors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", @@ -13093,6 +12909,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true, "engines": { "node": ">=8" } @@ -13268,6 +13085,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, "engines": { "node": ">=10" }, @@ -15584,6 +15402,12 @@ "node": ">=6" } }, + "node_modules/jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, "node_modules/jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -16066,6 +15890,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, "dependencies": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" @@ -16081,6 +15906,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -16095,6 +15921,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -16110,6 +15937,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -16120,12 +15948,14 @@ "node_modules/log-symbols/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/log-symbols/node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -16304,6 +16134,24 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/magic-string": { + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.1.tgz", + "integrity": "sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/magic-string/node_modules/@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + }, "node_modules/make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -16771,6 +16619,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, "engines": { "node": ">=6" } @@ -17755,9 +17604,15 @@ "optional": true }, "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -17801,28 +17656,38 @@ "dev": true }, "node_modules/nest-winston": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/nest-winston/-/nest-winston-1.8.0.tgz", - "integrity": "sha512-DyoNrly6DXjNG2ZPXSD2+w+pgEE8u0y0iwsaLZsufFLIKB/YNgP7zIXV5JxIqns0e5QzLlxdHhO2hx+1W8dvCw==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/nest-winston/-/nest-winston-1.9.4.tgz", + "integrity": "sha512-ilEmHuuYSAI6aMNR120fLBl42EdY13QI9WRggHdEizt9M7qZlmXJwpbemVWKW/tqRmULjSx/otKNQ3GMQbfoUQ==", "dependencies": { "fast-safe-stringify": "^2.1.1" }, "peerDependencies": { - "@nestjs/common": "^5.0.0 || ^6.6.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "@nestjs/common": "^5.0.0 || ^6.6.0 || ^7.0.0 || ^8.0.0 || ^9.0.0 || ^10.0.0", "winston": "^3.0.0" } }, "node_modules/nestjs-console": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/nestjs-console/-/nestjs-console-8.0.0.tgz", - "integrity": "sha512-ColRYRI9aH8/wlbrcFK/AeTAvsLlk0vgNIqL9FivUjo+q63S6AB+vUf7Jx7mQ1oIT2bD4awEOt0qEnkHmxsi4Q==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nestjs-console/-/nestjs-console-9.0.0.tgz", + "integrity": "sha512-5t9r0E9WHei2aCxPTPrtSq0h1rQBsO8TWok9HEwuRDpT3OEZJfA5FF0LaJDTD0DkusDq+GufSK1pZpgs21EeKQ==", "dependencies": { - "commander": "^8.1.0", - "ora": "5.4.1" + "commander": "^11.0.0" + }, + "engines": { + "node": ">=16.0.0" }, "peerDependencies": { - "@nestjs/common": "^9", - "@nestjs/core": "^9" + "@nestjs/common": "^9.0.0 || ^10.0.0", + "@nestjs/core": "^9.0.0 || ^10.0.0" + } + }, + "node_modules/nestjs-console/node_modules/commander": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", + "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==", + "engines": { + "node": ">=16" } }, "node_modules/new-find-package-json": { @@ -18442,14 +18307,6 @@ "node": ">=0.10.0" } }, - "node_modules/object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", - "engines": { - "node": ">= 6" - } - }, "node_modules/object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", @@ -18542,6 +18399,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, "dependencies": { "mimic-fn": "^2.1.0" }, @@ -18598,6 +18456,7 @@ "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, "dependencies": { "bl": "^4.1.0", "chalk": "^4.1.0", @@ -18620,6 +18479,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -18634,6 +18494,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -18644,6 +18505,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, "funding": [ { "type": "github", @@ -18667,6 +18529,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -18682,6 +18545,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -18692,12 +18556,14 @@ "node_modules/ora/node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "node_modules/ora/node_modules/readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -18711,6 +18577,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, "dependencies": { "safe-buffer": "~5.2.0" } @@ -18719,6 +18586,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -18979,13 +18847,13 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "node_modules/path-scurry": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.6.4.tgz", - "integrity": "sha512-Qp/9IHkdNiXJ3/Kon++At2nVpnhRiPq/aSvQN+H3U1WZbvNRK0RIQK/o4HMqPoXjpuGJUEWpHSs6Mnjxqh3TQg==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", "dev": true, "dependencies": { - "lru-cache": "^9.0.0", - "minipass": "^5.0.0" + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "engines": { "node": ">=16 || 14 >=14.17" @@ -18995,21 +18863,21 @@ } }, "node_modules/path-scurry/node_modules/lru-cache": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.0.1.tgz", - "integrity": "sha512-C8QsKIN1UIXeOs3iWmiZ1lQY+EnKDojWd37fXy1aSbJvH4iSma1uy2OWuoB3m4SYRli5+CUjDv3Dij5DVoetmg==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", + "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", "dev": true, "engines": { "node": "14 || >=16.14" } }, "node_modules/path-scurry/node_modules/minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", + "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==", "dev": true, "engines": { - "node": ">=8" + "node": ">=16 || 14 >=14.17" } }, "node_modules/path-to-regexp": { @@ -19904,6 +19772,11 @@ "node": ">= 0.10" } }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "node_modules/proxyquire": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz", @@ -20399,6 +20272,15 @@ "node": ">=4" } }, + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true, + "engines": { + "node": ">=0.10" + } + }, "node_modules/request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -20656,6 +20538,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, "dependencies": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" @@ -21244,9 +21127,9 @@ "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=" }, "node_modules/rxjs": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz", - "integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "dependencies": { "tslib": "^2.1.0" } @@ -21341,9 +21224,9 @@ "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" }, "node_modules/schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "dependencies": { "@types/json-schema": "^7.0.8", @@ -21657,7 +21540,8 @@ "node_modules/signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true }, "node_modules/simple-oauth2": { "version": "4.3.0", @@ -22684,13 +22568,13 @@ } }, "node_modules/terser": { - "version": "5.15.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", - "integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==", + "version": "5.19.4", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.4.tgz", + "integrity": "sha512-6p1DjHeuluwxDXcuT9VR8p64klWJKo1ILiy19s6C9+0Bh2+NWTX6nD9EPppiER4ICkHDVB1RkVpin/YW2nQn/g==", "dev": true, "dependencies": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, @@ -22702,16 +22586,16 @@ } }, "node_modules/terser-webpack-plugin": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz", - "integrity": "sha512-GvlZdT6wPQKbDNW/GDQzZFg/j4vKU96yl2q6mcUkzKOgW4gwf1Z8cZToUCrz31XHlPWH8MVb1r2tFtdDtTGJ7g==", + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", "dev": true, "dependencies": { + "@jridgewell/trace-mapping": "^0.3.17", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "source-map": "^0.6.1", - "terser": "^5.7.2" + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" }, "engines": { "node": ">= 10.13.0" @@ -22735,10 +22619,19 @@ } } }, + "node_modules/terser-webpack-plugin/node_modules/serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, "node_modules/terser/node_modules/acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -23240,9 +23133,9 @@ } }, "node_modules/tsconfig-paths": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.1.2.tgz", - "integrity": "sha512-uhxiMgnXQp1IR622dUXI+9Ehnws7i/y6xvpZB9IbUVOPy0muvdvgXeZOn88UcGPiT98Vp3rJPTa8bFoalZ3Qhw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", "dev": true, "dependencies": { "json5": "^2.2.2", @@ -23254,9 +23147,9 @@ } }, "node_modules/tsconfig-paths-webpack-plugin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.0.1.tgz", - "integrity": "sha512-m5//KzLoKmqu2MVix+dgLKq70MnFi8YL8sdzQZ6DblmCdfuq/y3OqvJd5vMndg2KEVCOeNz8Es4WVZhYInteLw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.1.0.tgz", + "integrity": "sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA==", "dev": true, "dependencies": { "chalk": "^4.1.0", @@ -23338,9 +23231,9 @@ } }, "node_modules/tslib": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz", - "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsup": { "version": "5.12.9", @@ -23608,6 +23501,17 @@ "node": "*" } }, + "node_modules/uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "dependencies": { + "@lukeed/csprng": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/uid-safe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", @@ -23993,6 +23897,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dev": true, "dependencies": { "defaults": "^1.0.3" } @@ -24006,22 +23911,22 @@ } }, "node_modules/webpack": { - "version": "5.76.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.2.tgz", - "integrity": "sha512-Th05ggRm23rVzEOlX8y67NkYCHa9nTNcwHPBhdg+lKG+mtiW7XgggjAeeLnADAe7mLjJ6LUNfgHAuRRh+Z6J7w==", + "version": "5.88.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", + "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", + "acorn-import-assertions": "^1.9.0", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.10.0", - "es-module-lexer": "^0.9.0", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", @@ -24030,9 +23935,9 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", + "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", + "terser-webpack-plugin": "^5.3.7", "watchpack": "^2.4.0", "webpack-sources": "^3.2.3" }, @@ -24071,9 +23976,9 @@ } }, "node_modules/webpack/node_modules/acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true, "bin": { "acorn": "bin/acorn" @@ -24083,9 +23988,9 @@ } }, "node_modules/webpack/node_modules/acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", "dev": true, "peerDependencies": { "acorn": "^8" @@ -24704,68 +24609,54 @@ "@jridgewell/trace-mapping": "^0.3.0" } }, + "@angular-devkit/core": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.2.0.tgz", + "integrity": "sha512-l1k6Rqm3YM16BEn3CWyQKrk9xfu+2ux7Bw3oS+h1TO4/RoxO2PgHj8LLRh/WNrYVarhaqO7QZ5ePBkXNMkzJ1g==", + "dev": true, + "requires": { + "ajv": "8.12.0", + "ajv-formats": "2.1.1", + "jsonc-parser": "3.2.0", + "rxjs": "7.8.1", + "source-map": "0.7.4" + }, + "dependencies": { + "source-map": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", + "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", + "dev": true + } + } + }, + "@angular-devkit/schematics": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.0.tgz", + "integrity": "sha512-QMDJXPE0+YQJ9Ap3MMzb0v7rx6ZbBEokmHgpdIjN3eILYmbAdsSGE8HTV8NjS9nKmcyE9OGzFCMb7PFrDTlTAw==", + "dev": true, + "requires": { + "@angular-devkit/core": "16.2.0", + "jsonc-parser": "3.2.0", + "magic-string": "0.30.1", + "ora": "5.4.1", + "rxjs": "7.8.1" + } + }, "@angular-devkit/schematics-cli": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-15.2.4.tgz", - "integrity": "sha512-QTTKEH5HOkxvQtCxb2Lna2wubehkaIzA6DKUBISijPQliLomw74tzc7lXCywmMqRTbQPVRLG3kBK97hR4x67nA==", + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics-cli/-/schematics-cli-16.2.0.tgz", + "integrity": "sha512-f3HjrDvSrRMvESogLsqsZXsEg//trIBySCHRXCglPrWLVdBbIRctGOhXqZoclRxXimIKUx14zLsOWzDwZG8+HQ==", "dev": true, "requires": { - "@angular-devkit/core": "15.2.4", - "@angular-devkit/schematics": "15.2.4", + "@angular-devkit/core": "16.2.0", + "@angular-devkit/schematics": "16.2.0", "ansi-colors": "4.1.3", "inquirer": "8.2.4", "symbol-observable": "4.0.0", "yargs-parser": "21.1.1" }, "dependencies": { - "@angular-devkit/core": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.4.tgz", - "integrity": "sha512-yl+0j1bMwJLKShsyCXw77tbJG8Sd21+itisPLL2MgEpLNAO252kr9zG4TLlFRJyKVftm2l1h78KjqvM5nbOXNg==", - "dev": true, - "requires": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "rxjs": "6.6.7", - "source-map": "0.7.4" - }, - "dependencies": { - "rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - } - } - }, - "@angular-devkit/schematics": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.2.4.tgz", - "integrity": "sha512-/W7/vvn59PAVLzhcvD4/N/E8RDhub8ny1A7I96LTRjC5o+yvVV16YJ4YJzolrRrIEN01KmLVQJ9A58VCaweMgw==", - "dev": true, - "requires": { - "@angular-devkit/core": "15.2.4", - "jsonc-parser": "3.2.0", - "magic-string": "0.29.0", - "ora": "5.4.1", - "rxjs": "6.6.7" - }, - "dependencies": { - "rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - } - } - }, "ansi-colors": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", @@ -24829,27 +24720,6 @@ "wrap-ansi": "^7.0.0" } }, - "jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true - }, - "magic-string": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz", - "integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==", - "dev": true, - "requires": { - "@jridgewell/sourcemap-codec": "^1.4.13" - } - }, - "source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -24859,12 +24729,6 @@ "has-flag": "^4.0.0" } }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "yargs-parser": { "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", @@ -26889,43 +26753,41 @@ } }, "@golevelup/nestjs-common": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/@golevelup/nestjs-common/-/nestjs-common-1.4.5.tgz", - "integrity": "sha512-WqxGAP4KZjvUea/lYCEfFXB8fS3NwxEWgrQpz2H8jusbz11OSrLECv5TNU9RIRKFnUPbGevy45dvKHF2JTQfJw==", + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-common/-/nestjs-common-2.0.0.tgz", + "integrity": "sha512-D9RLXgkqn9SDLnZ2VoMER9l/+g5CM9Z7sZXa+10+0rZs6yevMepoiWmMVsFoUXLzYG2GwfixHLExwUr3XBCHFw==", "requires": { "lodash": "^4.17.21", - "nanoid": "^3.2.0" + "nanoid": "^3.3.6" } }, "@golevelup/nestjs-discovery": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@golevelup/nestjs-discovery/-/nestjs-discovery-3.0.1.tgz", - "integrity": "sha512-kK/GBYVxb8XGlwXgtCWAkPOwDVh7dXyLRaoZuk2bBYntV3DZkYGAIbLKOFTGz+MGz71vEeQ9bGLP7cHKtCee4g==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-discovery/-/nestjs-discovery-4.0.0.tgz", + "integrity": "sha512-iyZLYip9rhVMR0C93vo860xmboRrD5g5F5iEOfpeblGvYSz8ymQrL9RAST7x/Fp3n+TAXSeOLzDIASt+rak68g==", "requires": { - "lodash": "^4.17.15" + "lodash": "^4.17.21" + } + }, + "@golevelup/nestjs-modules": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-modules/-/nestjs-modules-0.7.0.tgz", + "integrity": "sha512-4WxGKubYx0IJF2rxL3S4SChKdl4ZDZPwCdSj6HxmmElXRyua/LlcwLH6NYquh4RRIkQGspDd5WpcMTBw3SxR5g==", + "requires": { + "lodash": "^4.17.21" } }, "@golevelup/nestjs-rabbitmq": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/@golevelup/nestjs-rabbitmq/-/nestjs-rabbitmq-3.7.0.tgz", - "integrity": "sha512-wV6SA8oCTu8v66U5i3vu/k6DEvlHKM+LTb9JY7Iz8C0nUUuf6pufgvAalusAi4iJJ7/7p3Q44LF38SB4buszWw==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@golevelup/nestjs-rabbitmq/-/nestjs-rabbitmq-4.0.0.tgz", + "integrity": "sha512-CQHRq/jyK3GlM7Lv4nVaqd+BJ53tZXsrOtO/8/OZh19i0YOcQxyRM7iDdtULeG8omJB5/aGMZNsbioLuupxoog==", "requires": { - "@golevelup/nestjs-common": "^1.4.5", - "@golevelup/nestjs-discovery": "^3.0.1", - "@golevelup/nestjs-modules": "^0.6.1", + "@golevelup/nestjs-common": "^2.0.0", + "@golevelup/nestjs-discovery": "^4.0.0", + "@golevelup/nestjs-modules": "^0.7.0", "amqp-connection-manager": "^3.0.0", "amqplib": "^0.8.0", "lodash": "^4.17.21" - }, - "dependencies": { - "@golevelup/nestjs-modules": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/@golevelup/nestjs-modules/-/nestjs-modules-0.6.1.tgz", - "integrity": "sha512-E0STg8In8fhIivnGDJAA70+XLPHzK5bMTkCnif9FbZ8waTYDQ3T/QQL0h73k+CUFeznn1hmuEW14sNaE+8cd7w==", - "requires": { - "lodash": "^4.17.21" - } - } } }, "@golevelup/ts-jest": { @@ -27767,9 +27629,9 @@ } }, "@jridgewell/gen-mapping": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.2.tgz", - "integrity": "sha512-mh65xKQAzI6iBcFzwv28KVWSmCkdRBWoOh+bYQGW3+6OZvbbN3TqMGo5hqYxQniRcH9F2VZIoJCm4pa3BPDK/A==", + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.3.tgz", + "integrity": "sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==", "dev": true, "requires": { "@jridgewell/set-array": "^1.0.1", @@ -27790,9 +27652,9 @@ "dev": true }, "@jridgewell/source-map": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.2.tgz", - "integrity": "sha512-m7O9o2uR8k2ObDysZYzdfhb08VuEml5oWGiosa1VdaPZ/A6QyPkAJuwN0Q1lhULOf6B7MtQmHENS743hWtCrgw==", + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.5.tgz", + "integrity": "sha512-UTYAUj/wviwdsMfzoSJspJxbkH5o1snzwX0//0ENX1u/55kkZZkcTZP6u9bwKGkv+dkk9at4m1Cpt0uY80kcpQ==", "dev": true, "requires": { "@jridgewell/gen-mapping": "^0.3.0", @@ -27838,6 +27700,11 @@ } } }, + "@lukeed/csprng": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@lukeed/csprng/-/csprng-1.1.0.tgz", + "integrity": "sha512-Z7C/xXCiGWsg0KuKsHTKJxbWhpI3Vs5GwLfOean7MGyVFGqdRgBbAjOCh6u4bbjPc/8MJ2pZmK/0DLdCbivLDA==" + }, "@lumieducation/h5p-server": { "version": "9.2.0", "resolved": "https://registry.npmjs.org/@lumieducation/h5p-server/-/h5p-server-9.2.0.tgz", @@ -27869,6 +27736,15 @@ "yazl": "^2.5.1" }, "dependencies": { + "axios": { + "version": "0.27.2", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", + "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "requires": { + "follow-redirects": "^1.14.9", + "form-data": "^4.0.0" + } + }, "cache-manager": { "version": "3.6.3", "resolved": "https://registry.npmjs.org/cache-manager/-/cache-manager-3.6.3.tgz", @@ -27925,97 +27801,53 @@ } }, "@mikro-orm/nestjs": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/@mikro-orm/nestjs/-/nestjs-5.1.5.tgz", - "integrity": "sha512-8Bkr2cWBRp3MBXKNA/mPSph1pVbL6+4ry0cJl2gxgj9n/kaSn9ak/OsULh5MfadDf8OWi0+TVEqmKjauMrJ02g==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/@mikro-orm/nestjs/-/nestjs-5.2.1.tgz", + "integrity": "sha512-TrCdPsM7DApxrK3avBbijT6/6Er4TZhtiQ+qlMqtqva13vMCG4HiF2vIWGrKJbFukkLRuhOfZlES+KZ9Y1Lx2A==", "requires": {} }, "@nestjs/axios": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-0.1.0.tgz", - "integrity": "sha512-b2TT2X6BFbnNoeteiaxCIiHaFcSbVW+S5yygYqiIq5i6H77yIU3IVuLdpQkHq8/EqOWFwMopLN8jdkUT71Am9w==", - "requires": { - "axios": "0.27.2" - } + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@nestjs/axios/-/axios-3.0.0.tgz", + "integrity": "sha512-ULdH03jDWkS5dy9X69XbUVbhC+0pVnrRcj7bIK/ytTZ76w7CgvTZDJqsIyisg3kNOiljRW/4NIjSf3j6YGvl+g==", + "requires": {} + }, + "@nestjs/cache-manager": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nestjs/cache-manager/-/cache-manager-2.1.0.tgz", + "integrity": "sha512-9kep3a8Mq5cMuXN/anGhSYc0P48CRBXk5wyJJRBFxhNkCH8AIzZF4CASGVDIEMmm3OjVcEUHojjyJwCODS17Qw==", + "requires": {} }, "@nestjs/cli": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-9.3.0.tgz", - "integrity": "sha512-v/E8Y3zFk30+FljETvPgpoGIUiOfWuOe6WUFw3ExGfDeWrF/A8ceupDHPWNknBAqvNtz2kVrWu5mwsZUEKGIgg==", + "version": "10.1.17", + "resolved": "https://registry.npmjs.org/@nestjs/cli/-/cli-10.1.17.tgz", + "integrity": "sha512-jUEnR2DgC15Op+IhcRWb6cyJrhec9CUQO+GtxCF2Dv9MwLcr4sTDq1UOkfs09HAhpuI8otgF2LoWGTlW3qRuqg==", "dev": true, "requires": { - "@angular-devkit/core": "15.2.4", - "@angular-devkit/schematics": "15.2.4", - "@angular-devkit/schematics-cli": "15.2.4", - "@nestjs/schematics": "^9.0.4", + "@angular-devkit/core": "16.2.0", + "@angular-devkit/schematics": "16.2.0", + "@angular-devkit/schematics-cli": "16.2.0", + "@nestjs/schematics": "^10.0.1", "chalk": "4.1.2", "chokidar": "3.5.3", "cli-table3": "0.6.3", "commander": "4.1.1", "fork-ts-checker-webpack-plugin": "8.0.0", - "inquirer": "8.2.5", + "inquirer": "8.2.6", "node-emoji": "1.11.0", "ora": "5.4.1", "os-name": "4.0.1", - "rimraf": "4.4.0", + "rimraf": "4.4.1", "shelljs": "0.8.5", "source-map-support": "0.5.21", "tree-kill": "1.2.2", - "tsconfig-paths": "4.1.2", - "tsconfig-paths-webpack-plugin": "4.0.1", - "typescript": "4.9.5", - "webpack": "5.76.2", + "tsconfig-paths": "4.2.0", + "tsconfig-paths-webpack-plugin": "4.1.0", + "typescript": "5.1.6", + "webpack": "5.88.2", "webpack-node-externals": "3.0.0" }, "dependencies": { - "@angular-devkit/core": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.4.tgz", - "integrity": "sha512-yl+0j1bMwJLKShsyCXw77tbJG8Sd21+itisPLL2MgEpLNAO252kr9zG4TLlFRJyKVftm2l1h78KjqvM5nbOXNg==", - "dev": true, - "requires": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "rxjs": "6.6.7", - "source-map": "0.7.4" - }, - "dependencies": { - "rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - } - } - }, - "@angular-devkit/schematics": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.2.4.tgz", - "integrity": "sha512-/W7/vvn59PAVLzhcvD4/N/E8RDhub8ny1A7I96LTRjC5o+yvVV16YJ4YJzolrRrIEN01KmLVQJ9A58VCaweMgw==", - "dev": true, - "requires": { - "@angular-devkit/core": "15.2.4", - "jsonc-parser": "3.2.0", - "magic-string": "0.29.0", - "ora": "5.4.1", - "rxjs": "6.6.7" - }, - "dependencies": { - "rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - } - } - }, "ansi-styles": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", @@ -28078,9 +27910,9 @@ } }, "inquirer": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.5.tgz", - "integrity": "sha512-QAgPDQMEgrDssk1XiwwHoOGYF9BAbUcc1+j+FhEvaOt8/cKRqyLn0U5qA6F74fGhTMGxf92pOvPBeh29jQJDTQ==", + "version": "8.2.6", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-8.2.6.tgz", + "integrity": "sha512-M1WuAmb7pn9zdFRtQYk26ZBoY043Sse0wVDdk4Bppr+JOXyQYybdtvK+l9wUibhtjdjvtoiNy8tk+EgsYIUqKg==", "dev": true, "requires": { "ansi-escapes": "^4.2.1", @@ -28097,22 +27929,7 @@ "string-width": "^4.1.0", "strip-ansi": "^6.0.0", "through": "^2.3.6", - "wrap-ansi": "^7.0.0" - } - }, - "jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true - }, - "magic-string": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz", - "integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==", - "dev": true, - "requires": { - "@jridgewell/sourcemap-codec": "^1.4.13" + "wrap-ansi": "^6.0.1" } }, "minimatch": { @@ -28125,26 +27942,20 @@ } }, "minipass": { - "version": "4.2.7", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.7.tgz", - "integrity": "sha512-ScVIgqHcXRMyfflqHmEW0bm8z8rb5McHyOY3ewX9JBgZaR77G7nxq9L/mtV96/QbAAwtbCAHVVLzD1kkyfFQEw==", + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", "dev": true }, "rimraf": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.4.0.tgz", - "integrity": "sha512-X36S+qpCUR0HjXlkDe4NAOhS//aHH0Z+h8Ckf2auGJk3PTnx5rLmrHkwNdbVQuCSUhOyFrlRvFEllZOYE+yZGQ==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-4.4.1.tgz", + "integrity": "sha512-Gk8NlF062+T9CqNGn6h4tls3k6T1+/nXdOcSZVikNVtlRdYpA7wRJJMoXmuvOnLW844rPjdQ7JgXCYM6PPC/og==", "dev": true, "requires": { "glob": "^9.2.0" } }, - "source-map": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", - "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", - "dev": true - }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -28154,284 +27965,179 @@ "has-flag": "^4.0.0" } }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "typescript": { + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.1.6.tgz", + "integrity": "sha512-zaWCozRZ6DLEWAWFrVDz1H6FVXzUSfTy5FUMWsQlU8Ym5JP9eO4xkTIROFCQvhQf61z6O/G6ugw3SgAnvvm+HA==", "dev": true + }, + "wrap-ansi": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", + "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", + "dev": true, + "requires": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + } } } }, "@nestjs/common": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-9.2.1.tgz", - "integrity": "sha512-nZuo3oDsSSlC5mti/M2aCWTEIfHPGDXmBwWgPeCpRbrNz3IWd109rkajll+yxgidVjznAdBS9y00JkAVJblNYw==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/@nestjs/common/-/common-10.2.4.tgz", + "integrity": "sha512-3Lg4PUaSDucf14V8rPCH212NqrK09AJbY0NKqFsb4j5OIE+TuOzVZR/yjaJ8JNxH2hjskJNCZie0D/9tA2lzlA==", "requires": { "iterare": "1.2.1", - "tslib": "2.4.1", - "uuid": "9.0.0" - }, - "dependencies": { - "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" - }, - "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" - } + "tslib": "2.6.2", + "uid": "2.0.2" } }, "@nestjs/config": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-2.2.0.tgz", - "integrity": "sha512-78Eg6oMbCy3D/YvqeiGBTOWei1Jwi3f2pSIZcZ1QxY67kYsJzTRTkwRT8Iv30DbK0sGKc1mcloDLD5UXgZAZtg==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/config/-/config-3.0.1.tgz", + "integrity": "sha512-a98MMkDlgUlXTv9qtDbimYfXsuafn/YZOh/S35afutr0Qc5T6KzjyWP5VjxRkv26yI2JM0RhFruByFTM6ezwHA==", "requires": { - "dotenv": "16.0.1", - "dotenv-expand": "8.0.3", + "dotenv": "16.3.1", + "dotenv-expand": "10.0.0", "lodash": "4.17.21", - "uuid": "8.3.2" + "uuid": "9.0.0" }, "dependencies": { "dotenv": { - "version": "16.0.1", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.0.1.tgz", - "integrity": "sha512-1K6hR6wtk2FviQ4kEiSjFiH5rpzEVi8WW0x96aztHVMhEspNpc4DVOUTEHtEva5VThQ8IaBX1Pe4gSzpVVUsKQ==" + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==" + }, + "uuid": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", + "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" } } }, "@nestjs/core": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-9.2.1.tgz", - "integrity": "sha512-a9GkXuu8uXgNgCVW+17iI8kLCltO+HwHpU2IhR+32JKnN2WEQ1YEWU4t3GJ2MNq44YkjIw9zrKvFkjJBlYrNbQ==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/@nestjs/core/-/core-10.2.4.tgz", + "integrity": "sha512-aWeii2l+3pNCc9kIRdLbXQMvrgSZD0jZgXOZv7bZwVf9mClMMi7TussLI4On12VbqVE7LE3gsNgRTwgQJlVC8g==", "requires": { "@nuxtjs/opencollective": "0.3.2", "fast-safe-stringify": "2.1.1", "iterare": "1.2.1", - "object-hash": "3.0.0", "path-to-regexp": "3.2.0", - "tslib": "2.4.1", - "uuid": "9.0.0" - }, - "dependencies": { - "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" - }, - "uuid": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.0.tgz", - "integrity": "sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==" - } + "tslib": "2.6.2", + "uid": "2.0.2" } }, "@nestjs/jwt": { - "version": "10.0.2", - "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.0.2.tgz", - "integrity": "sha512-MLxjCSbO7C9fN2hst5kpIhnJAgglJmrKppXAXqElB8A9ip3ZuCowMDjjmNWWJyfOzE98NV0E0iEQGE2StMUC+Q==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/@nestjs/jwt/-/jwt-10.1.1.tgz", + "integrity": "sha512-sISYylg8y1Mb7saxPx5Zh11i7v9JOh70CEC/rN6g43MrbFlJ57c1eYFrffxip1YAx3DmV4K67yXob3syKZMOew==", "requires": { - "@types/jsonwebtoken": "9.0.1", + "@types/jsonwebtoken": "9.0.2", "jsonwebtoken": "9.0.0" } }, "@nestjs/mapped-types": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-1.2.2.tgz", - "integrity": "sha512-3dHxLXs3M0GPiriAcCFFJQHoDFUuzTD5w6JDhE7TyfT89YKpe6tcCCIqOZWdXmt9AZjjK30RkHRSFF+QEnWFQg==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/mapped-types/-/mapped-types-2.0.2.tgz", + "integrity": "sha512-V0izw6tWs6fTp9+KiiPUbGHWALy563Frn8X6Bm87ANLRuE46iuBMD5acKBDP5lKL/75QFvrzSJT7HkCbB0jTpg==", "requires": {} }, "@nestjs/microservices": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-9.2.1.tgz", - "integrity": "sha512-ve4dqgoIaUG9VtinHHGCvf6SvtPjd3I44Mj8z8mSkcB8BEPfJnf4FNOShFBgsedtxLaiZyjpv8NCLcNVv5KKGg==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/@nestjs/microservices/-/microservices-10.2.4.tgz", + "integrity": "sha512-GytBFj4onLveWDUm+aj7Ft4518yiRx3dfHwqBzYfekPFWIfzVHNGWQCZUSNpS/jMbTfbM2PAknkuhWFjV1811A==", "requires": { "iterare": "1.2.1", - "tslib": "2.4.1" - }, - "dependencies": { - "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" - } + "tslib": "2.6.2" } }, "@nestjs/passport": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-9.0.0.tgz", - "integrity": "sha512-Gnh8n1wzFPOLSS/94X1sUP4IRAoXTgG4odl7/AO5h+uwscEGXxJFercrZfqdAwkWhqkKWbsntM3j5mRy/6ZQDA==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@nestjs/passport/-/passport-10.0.1.tgz", + "integrity": "sha512-hS22LeNj0LByS9toBPkpKyZhyKAXoHACLS1EQrjbAJJEQjhocOskVGwcMwvMlz+ohN+VU804/nMF1Zlya4+TiQ==", "requires": {} }, "@nestjs/platform-express": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-9.2.1.tgz", - "integrity": "sha512-7PecaXt8lrdS1p6Vb1X/am3GGv+EO1VahyDzaEGOK6C0zwhc0VPfLtwihkjjfhS6BjpRIXXgviwEjONUvxVZnA==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/@nestjs/platform-express/-/platform-express-10.2.4.tgz", + "integrity": "sha512-E9F6WYo6bNwvTT0saJpkr8t4BJLbZRwrX5EKbtBRQqyRcw6NAvlKdacKzoo+Sompdre0IbF8AvNRFk4uLZTWqA==", "requires": { - "body-parser": "1.20.1", + "body-parser": "1.20.2", "cors": "2.8.5", "express": "4.18.2", "multer": "1.4.4-lts.1", - "tslib": "2.4.1" - }, - "dependencies": { - "body-parser": { - "version": "1.20.1", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", - "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", - "requires": { - "bytes": "3.1.2", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.11.0", - "raw-body": "2.5.1", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==" - }, - "http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "requires": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" - }, - "on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "requires": { - "ee-first": "1.1.1" - } - }, - "statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==" - }, - "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==" - } + "tslib": "2.6.2" } }, "@nestjs/schematics": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-9.1.0.tgz", - "integrity": "sha512-/7CyMTnPJSK9/xD9CkCqwuHPOlHVlLC2RDnbdCJ7mIO07SdbBbY14msTqtYW9VRQtsjZPLh1GTChf7ryJUImwA==", + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@nestjs/schematics/-/schematics-10.0.2.tgz", + "integrity": "sha512-DaZZjymYoIfRqC5W62lnYXIIods1PDY6CGc8+IpRwyinzffjKxZ3DF3exu+mdyvllzkXo9DTXkoX4zOPSJHCkw==", "dev": true, "requires": { - "@angular-devkit/core": "15.2.4", - "@angular-devkit/schematics": "15.2.4", + "@angular-devkit/core": "16.1.8", + "@angular-devkit/schematics": "16.1.8", + "comment-json": "4.2.3", "jsonc-parser": "3.2.0", "pluralize": "8.0.0" }, "dependencies": { "@angular-devkit/core": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-15.2.4.tgz", - "integrity": "sha512-yl+0j1bMwJLKShsyCXw77tbJG8Sd21+itisPLL2MgEpLNAO252kr9zG4TLlFRJyKVftm2l1h78KjqvM5nbOXNg==", + "version": "16.1.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.1.8.tgz", + "integrity": "sha512-dSRD/+bGanArIXkj+kaU1kDFleZeQMzmBiOXX+pK0Ah9/0Yn1VmY3RZh1zcX9vgIQXV+t7UPrTpOjaERMUtVGw==", "dev": true, "requires": { "ajv": "8.12.0", "ajv-formats": "2.1.1", "jsonc-parser": "3.2.0", - "rxjs": "6.6.7", + "rxjs": "7.8.1", "source-map": "0.7.4" } }, "@angular-devkit/schematics": { - "version": "15.2.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-15.2.4.tgz", - "integrity": "sha512-/W7/vvn59PAVLzhcvD4/N/E8RDhub8ny1A7I96LTRjC5o+yvVV16YJ4YJzolrRrIEN01KmLVQJ9A58VCaweMgw==", + "version": "16.1.8", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.1.8.tgz", + "integrity": "sha512-6LyzMdFJs337RTxxkI2U1Ndw0CW5mMX/aXWl8d7cW2odiSrAg8IdlMqpc+AM8+CPfsB0FtS1aWkEZqJLT0jHOg==", "dev": true, "requires": { - "@angular-devkit/core": "15.2.4", + "@angular-devkit/core": "16.1.8", "jsonc-parser": "3.2.0", - "magic-string": "0.29.0", + "magic-string": "0.30.0", "ora": "5.4.1", - "rxjs": "6.6.7" + "rxjs": "7.8.1" } }, - "jsonc-parser": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", - "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", - "dev": true - }, "magic-string": { - "version": "0.29.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.29.0.tgz", - "integrity": "sha512-WcfidHrDjMY+eLjlU+8OvwREqHwpgCeKVBUpQ3OhYYuvfaYCUgcbuBzappNzZvg/v8onU3oQj+BYpkOJe9Iw4Q==", + "version": "0.30.0", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", + "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", "dev": true, "requires": { "@jridgewell/sourcemap-codec": "^1.4.13" } }, - "rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } - }, "source-map": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.4.tgz", "integrity": "sha512-l3BikUxvPOcn5E74dZiq5BGsTb5yEwhaTSzccU6t4sDOH8NWJCstKO5QT2CvtFoK6F0saL7p9xHAqHOlCPJygA==", "dev": true - }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true } } }, "@nestjs/swagger": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-6.2.1.tgz", - "integrity": "sha512-9M2vkfJHIzLqDZwvM5TEZO0MxRCvIb0xVy0LsmWwxH1lrb0z/4MhU+r2CWDhBtTccVJrKxVPiU2s3T3b9uUJbg==", + "version": "7.1.10", + "resolved": "https://registry.npmjs.org/@nestjs/swagger/-/swagger-7.1.10.tgz", + "integrity": "sha512-qreCcxgHFyFX1mOfK36pxiziy4xoa/XcxC0h4Zr9yH54WuqMqO9aaNFhFyuQ1iyd/3YBVQB21Un4gQnh9iGm0w==", "requires": { - "@nestjs/mapped-types": "1.2.2", + "@nestjs/mapped-types": "2.0.2", "js-yaml": "4.1.0", "lodash": "4.17.21", "path-to-regexp": "3.2.0", - "swagger-ui-dist": "4.15.5" + "swagger-ui-dist": "5.4.2" }, "dependencies": { "argparse": { @@ -28448,27 +28154,19 @@ } }, "swagger-ui-dist": { - "version": "4.15.5", - "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.15.5.tgz", - "integrity": "sha512-V3eIa28lwB6gg7/wfNvAbjwJYmDXy1Jo1POjyTzlB6wPcHiGlRxq39TSjYGVjQrUSAzpv+a7nzp7mDxgNy57xA==" + "version": "5.4.2", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-5.4.2.tgz", + "integrity": "sha512-vT5QxP/NOr9m4gLZl+SpavWI3M9Fdh30+Sdw9rEtZbkqNmNNEPhjXas2xTD9rsJYYdLzAiMfwXvtooWH3xbLJA==" } } }, "@nestjs/testing": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-9.2.1.tgz", - "integrity": "sha512-lemXZdRSuqoZ87l0orCrS/c7gqwxeduIFOd21g9g2RUeQ4qlWPegbQDKASzbfC28klPyrgJLW4MNq7uv2JwV8w==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/@nestjs/testing/-/testing-10.2.4.tgz", + "integrity": "sha512-2qqymiuPbC41yCXXhtt4cL8AOcVNu13gBCT13A8roUUdcs4lmtg+H3oXKF/Gc/vlLv2RkSTNO+JuzxP1hydLPg==", "dev": true, "requires": { - "tslib": "2.4.1" - }, - "dependencies": { - "tslib": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.4.1.tgz", - "integrity": "sha512-tGyy4dAjRIEwI7BzsB0lynWgOpfqjUdq91XXAlIWD2OwKBH7oCl/GZG/HT4BOHrTlPMOASlMQ7veyTqpmRcrNA==", - "dev": true - } + "tslib": "2.6.2" } }, "@nodelib/fs.scandir": { @@ -28850,9 +28548,9 @@ "dev": true }, "@types/estree": { - "version": "0.0.51", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-0.0.51.tgz", - "integrity": "sha512-CuPgU6f3eT/XgKKPqKd/gLZV1Xmvf1a2R5POBOGQa6uv82xpls89HU5zKeVoyR8XzHd1RGNOlQlvUe3CFkjWNQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.1.tgz", + "integrity": "sha512-LG4opVs2ANWZ1TJoKc937iMmNstM/d0ae1vNbnBvBhqCSezgVUOzcLCqbI5elV8Vy6WKwKjaqR+zO9VKirBBCA==", "dev": true }, "@types/express": { @@ -28982,9 +28680,9 @@ "dev": true }, "@types/jsonwebtoken": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.1.tgz", - "integrity": "sha512-c5ltxazpWabia/4UzhIoaDcIza4KViOQhdbjRlfcIGVnsE3c3brkz9Z+F/EeJIECOQP7W7US2hNE930cWWkPiw==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz", + "integrity": "sha512-drE6uz7QBKq1fYqqoFKTDRdFCPHd5TCub75BM+D+cMx7NU9hUz7SESLfC2fSCXVFMO5Yj8sOWHuGqPgjc+fz0Q==", "requires": { "@types/node": "*" } @@ -29487,148 +29185,148 @@ "dev": true }, "@webassemblyjs/ast": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.1.tgz", - "integrity": "sha512-ukBh14qFLjxTQNTXocdyksN5QdM28S1CxHt2rdskFyL+xFV7VremuBLVbmCePj+URalXBENx/9Lm7lnhihtCSw==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.11.6.tgz", + "integrity": "sha512-IN1xI7PwOvLPgjcf180gC1bqn3q/QaOCwYUahIOhbYUu8KA/3tw2RT/T0Gidi1l7Hhj5D/INhJxiICObqpMu4Q==", "dev": true, "requires": { - "@webassemblyjs/helper-numbers": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1" + "@webassemblyjs/helper-numbers": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6" } }, "@webassemblyjs/floating-point-hex-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.1.tgz", - "integrity": "sha512-iGRfyc5Bq+NnNuX8b5hwBrRjzf0ocrJPI6GWFodBFzmFnyvrQ83SHKhmilCU/8Jv67i4GJZBMhEzltxzcNagtQ==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.11.6.tgz", + "integrity": "sha512-ejAj9hfRJ2XMsNHk/v6Fu2dGS+i4UaXBXGemOfQ/JfQ6mdQg/WXtwleQRLLS4OvfDhv8rYnVwH27YJLMyYsxhw==", "dev": true }, "@webassemblyjs/helper-api-error": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.1.tgz", - "integrity": "sha512-RlhS8CBCXfRUR/cwo2ho9bkheSXG0+NwooXcc3PAILALf2QLdFyj7KGsKRbVc95hZnhnERon4kW/D3SZpp6Tcg==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.11.6.tgz", + "integrity": "sha512-o0YkoP4pVu4rN8aTJgAyj9hC2Sv5UlkzCHhxqWj8butaLvnpdc2jOwh4ewE6CX0txSfLn/UYaV/pheS2Txg//Q==", "dev": true }, "@webassemblyjs/helper-buffer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.1.tgz", - "integrity": "sha512-gwikF65aDNeeXa8JxXa2BAk+REjSyhrNC9ZwdT0f8jc4dQQeDQ7G4m0f2QCLPJiMTTO6wfDmRmj/pW0PsUvIcA==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.11.6.tgz", + "integrity": "sha512-z3nFzdcp1mb8nEOFFk8DrYLpHvhKC3grJD2ardfKOzmbmJvEf/tPIqCY+sNcwZIY8ZD7IkB2l7/pqhUhqm7hLA==", "dev": true }, "@webassemblyjs/helper-numbers": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.1.tgz", - "integrity": "sha512-vDkbxiB8zfnPdNK9Rajcey5C0w+QJugEglN0of+kmO8l7lDb77AnlKYQF7aarZuCrv+l0UvqL+68gSDr3k9LPQ==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.11.6.tgz", + "integrity": "sha512-vUIhZ8LZoIWHBohiEObxVm6hwP034jwmc9kuq5GdHZH0wiLVLIPcMCdpJzG4C11cHoQ25TFIQj9kaVADVX7N3g==", "dev": true, "requires": { - "@webassemblyjs/floating-point-hex-parser": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", + "@webassemblyjs/floating-point-hex-parser": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", "@xtuc/long": "4.2.2" } }, "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.1.tgz", - "integrity": "sha512-PvpoOGiJwXeTrSf/qfudJhwlvDQxFgelbMqtq52WWiXC6Xgg1IREdngmPN3bs4RoO83PnL/nFrxucXj1+BX62Q==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.11.6.tgz", + "integrity": "sha512-sFFHKwcmBprO9e7Icf0+gddyWYDViL8bpPjJJl0WHxCdETktXdmtWLGVzoHbqUcY4Be1LkNfwTmXOJUFZYSJdA==", "dev": true }, "@webassemblyjs/helper-wasm-section": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.1.tgz", - "integrity": "sha512-10P9No29rYX1j7F3EVPX3JvGPQPae+AomuSTPiF9eBQeChHI6iqjMIwR9JmOJXwpnn/oVGDk7I5IlskuMwU/pg==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.11.6.tgz", + "integrity": "sha512-LPpZbSOwTpEC2cgn4hTydySy1Ke+XEu+ETXuoyvuyezHO3Kjdu90KK95Sh9xTbmjrCsUwvWwCOQQNta37VrS9g==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6" } }, "@webassemblyjs/ieee754": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.1.tgz", - "integrity": "sha512-hJ87QIPtAMKbFq6CGTkZYJivEwZDbQUgYd3qKSadTNOhVY7p+gfP6Sr0lLRVTaG1JjFj+r3YchoqRYxNH3M0GQ==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.11.6.tgz", + "integrity": "sha512-LM4p2csPNvbij6U1f19v6WR56QZ8JcHg3QIJTlSwzFcmx6WSORicYj6I63f9yU1kEUtrpG+kjkiIAkevHpDXrg==", "dev": true, "requires": { "@xtuc/ieee754": "^1.2.0" } }, "@webassemblyjs/leb128": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.1.tgz", - "integrity": "sha512-BJ2P0hNZ0u+Th1YZXJpzW6miwqQUGcIHT1G/sf72gLVD9DZ5AdYTqPNbHZh6K1M5VmKvFXwGSWZADz+qBWxeRw==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.11.6.tgz", + "integrity": "sha512-m7a0FhE67DQXgouf1tbN5XQcdWoNgaAuoULHIfGFIEVKA6tu/edls6XnIlkmS6FrXAquJRPni3ZZKjw6FSPjPQ==", "dev": true, "requires": { "@xtuc/long": "4.2.2" } }, "@webassemblyjs/utf8": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.1.tgz", - "integrity": "sha512-9kqcxAEdMhiwQkHpkNiorZzqpGrodQQ2IGrHHxCy+Ozng0ofyMA0lTqiLkVs1uzTRejX+/O0EOT7KxqVPuXosQ==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.11.6.tgz", + "integrity": "sha512-vtXf2wTQ3+up9Zsg8sa2yWiQpzSsMyXj0qViVP6xKGCUT8p8YJ6HqI7l5eCnWx1T/FYdsv07HQs2wTFbbof/RA==", "dev": true }, "@webassemblyjs/wasm-edit": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.1.tgz", - "integrity": "sha512-g+RsupUC1aTHfR8CDgnsVRVZFJqdkFHpsHMfJuWQzWU3tvnLC07UqHICfP+4XyL2tnr1amvl1Sdp06TnYCmVkA==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.11.6.tgz", + "integrity": "sha512-Ybn2I6fnfIGuCR+Faaz7YcvtBKxvoLV3Lebn1tM4o/IAJzmi9AWYIPWpyBfU8cC+JxAO57bk4+zdsTjJR+VTOw==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/helper-wasm-section": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-opt": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", - "@webassemblyjs/wast-printer": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/helper-wasm-section": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-opt": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6", + "@webassemblyjs/wast-printer": "1.11.6" } }, "@webassemblyjs/wasm-gen": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.1.tgz", - "integrity": "sha512-F7QqKXwwNlMmsulj6+O7r4mmtAlCWfO/0HdgOxSklZfQcDu0TpLiD1mRt/zF25Bk59FIjEuGAIyn5ei4yMfLhA==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.11.6.tgz", + "integrity": "sha512-3XOqkZP/y6B4F0PBAXvI1/bky7GryoogUtfwExeP/v7Nzwo1QLcq5oQmpKlftZLbT+ERUOAZVQjuNVak6UXjPA==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" } }, "@webassemblyjs/wasm-opt": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.1.tgz", - "integrity": "sha512-VqnkNqnZlU5EB64pp1l7hdm3hmQw7Vgqa0KF/KCNO9sIpI6Fk6brDEiX+iCOYrvMuBWDws0NkTOxYEb85XQHHw==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.11.6.tgz", + "integrity": "sha512-cOrKuLRE7PCe6AsOVl7WasYf3wbSo4CeOk6PkrjS7g57MFfVUF9u6ysQBBODX0LdgSvQqRiGz3CXvIDKcPNy4g==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-buffer": "1.11.1", - "@webassemblyjs/wasm-gen": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-buffer": "1.11.6", + "@webassemblyjs/wasm-gen": "1.11.6", + "@webassemblyjs/wasm-parser": "1.11.6" } }, "@webassemblyjs/wasm-parser": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.1.tgz", - "integrity": "sha512-rrBujw+dJu32gYB7/Lup6UhdkPx9S9SnobZzRVL7VcBH9Bt9bCBLEuX/YXOOtBsOZ4NQrRykKhffRWHvigQvOA==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.11.6.tgz", + "integrity": "sha512-6ZwPeGzMJM3Dqp3hCsLgESxBGtT/OeCvCZ4TA1JUPYgmhAx38tTPR9JaKy0S5H3evQpO/h2uWs2j6Yc/fjkpTQ==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/helper-api-error": "1.11.1", - "@webassemblyjs/helper-wasm-bytecode": "1.11.1", - "@webassemblyjs/ieee754": "1.11.1", - "@webassemblyjs/leb128": "1.11.1", - "@webassemblyjs/utf8": "1.11.1" + "@webassemblyjs/ast": "1.11.6", + "@webassemblyjs/helper-api-error": "1.11.6", + "@webassemblyjs/helper-wasm-bytecode": "1.11.6", + "@webassemblyjs/ieee754": "1.11.6", + "@webassemblyjs/leb128": "1.11.6", + "@webassemblyjs/utf8": "1.11.6" } }, "@webassemblyjs/wast-printer": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.1.tgz", - "integrity": "sha512-IQboUWM4eKzWW+N/jij2sRatKMh99QEelo3Eb2q0qXkvPRISAj8Qxtmw5itwqK+TTkBuUIE45AxYPToqPtL5gg==", + "version": "1.11.6", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.11.6.tgz", + "integrity": "sha512-JM7AhRcE+yW2GWYaKeHL5vt4xqee5N2WcezptmgyhNS+ScggqcT1OtXykhAb13Sn5Yas0j2uv9tHgrjwvzAP4A==", "dev": true, "requires": { - "@webassemblyjs/ast": "1.11.1", + "@webassemblyjs/ast": "1.11.6", "@xtuc/long": "4.2.2" } }, @@ -29940,6 +29638,12 @@ "resolved": "https://registry.npmjs.org/array-series/-/array-series-0.1.5.tgz", "integrity": "sha512-L0XlBwfx9QetHOsbLDrE/vh2t018w9462HM3iaFfxRiK83aJjAt/Ja3NMkOW7FICwWTlQBa3ZbL5FKhuQWkDrg==" }, + "array-timsort": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-timsort/-/array-timsort-1.0.3.tgz", + "integrity": "sha512-/+3GRL7dDAGEfM6TseQk/U+mi18TU2Ms9I3UlLdUMhz2hbvGNTKdj9xniwXfUqgYhHxRx0+8UnKkvlNwVU+cWQ==", + "dev": true + }, "array-union": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", @@ -30113,12 +29817,13 @@ "dev": true }, "axios": { - "version": "0.27.2", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz", - "integrity": "sha512-t+yRIyySRTp/wua5xEr+z1q60QmLq8ABsS5O9Me1AsE5dfKqgnCFzwiCZZ/cGNd1lq4/7akDWMxdhVlucjmnOQ==", + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.5.0.tgz", + "integrity": "sha512-D4DdjDo5CY50Qms0qGQTTw6Q44jl7zRwY7bthds06pUGfChBCTcQs+N743eFWGEd6pRTMd6A+I87aWyFV5wiZQ==", "requires": { - "follow-redirects": "^1.14.9", - "form-data": "^4.0.0" + "follow-redirects": "^1.15.0", + "form-data": "^4.0.0", + "proxy-from-env": "^1.1.0" } }, "axios-mock-adapter": { @@ -30315,20 +30020,20 @@ "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, "body-parser": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.0.tgz", - "integrity": "sha512-DfJ+q6EPcGKZD1QWUjSpqp+Q7bDQTsQIF4zfUAtZ6qk+H/3/QRhg9CEp39ss+/T2vw0+HaidC0ecJj/DRLIaKg==", + "version": "1.20.2", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.2.tgz", + "integrity": "sha512-ml9pReCu3M61kGlqoTm2umSXTlRTuGTx0bfYj+uIUKKYycG5NtSbeetV3faSU6R7ajOPw0g/J1PvK4qNy7s5bA==", "requires": { "bytes": "3.1.2", - "content-type": "~1.0.4", + "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", - "qs": "6.10.3", - "raw-body": "2.5.1", + "qs": "6.11.0", + "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" }, @@ -30371,12 +30076,15 @@ "ee-first": "1.1.1" } }, - "qs": { - "version": "6.10.3", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.10.3.tgz", - "integrity": "sha512-wr7M2E0OFRfIfJZjKGieI8lBKb7fRCH4Fv5KNPEs7gJ8jadvotdsS08PzOKR7opXhZ/Xkjtt3WF9g38drmyRqQ==", + "raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", "requires": { - "side-channel": "^1.0.4" + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" } }, "statuses": { @@ -30819,6 +30527,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz", "integrity": "sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw==", + "dev": true, "requires": { "restore-cursor": "^3.1.0" } @@ -30826,7 +30535,8 @@ "cli-spinners": { "version": "2.6.1", "resolved": "https://registry.npmjs.org/cli-spinners/-/cli-spinners-2.6.1.tgz", - "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==" + "integrity": "sha512-x/5fWmGMnbKQAaNwN+UZlV79qBLM9JFnJuJ03gIi5whrob0xV0ofNVHy9DhwGdsMJQc2OKv0oGmLzvaqvAVv+g==", + "dev": true }, "cli-table3": { "version": "0.6.3", @@ -31123,6 +30833,19 @@ "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==" }, + "comment-json": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/comment-json/-/comment-json-4.2.3.tgz", + "integrity": "sha512-SsxdiOf064DWoZLH799Ata6u7iV658A11PlWtZATDlXPpKGJnbJZ5Z24ybixAi+LUUqJ/GKowAejtC5GFUG7Tw==", + "dev": true, + "requires": { + "array-timsort": "^1.0.3", + "core-util-is": "^1.0.3", + "esprima": "^4.0.1", + "has-own-prop": "^2.0.0", + "repeat-string": "^1.6.1" + } + }, "commist": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/commist/-/commist-1.1.0.tgz", @@ -31373,9 +31096,9 @@ } }, "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==" }, "convert-source-map": { "version": "1.8.0", @@ -31618,6 +31341,7 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/defaults/-/defaults-1.0.3.tgz", "integrity": "sha1-xlYFHpgX2f8I7YgUd/P+QBnz730=", + "dev": true, "requires": { "clone": "^1.0.2" }, @@ -31625,7 +31349,8 @@ "clone": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/clone/-/clone-1.0.4.tgz", - "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=" + "integrity": "sha1-2jCcwmPfFZlMaIypAheco8fNfH4=", + "dev": true } } }, @@ -31776,9 +31501,9 @@ "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==" }, "dotenv-expand": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-8.0.3.tgz", - "integrity": "sha512-SErOMvge0ZUyWd5B0NXMQlDkN+8r+HhVUsxgOO7IoPDOdDRD2JjExpN6y3KnFR66jsJMwSn1pqIivhU5rcJiNg==" + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-10.0.0.tgz", + "integrity": "sha512-GopVGCpVS1UKH75VKHGuQFqS1Gusej0z4FyQkPdwjil2gNIv+LNsqBlboOzpJFZKVT95GkCyWJbBSdFEFUWI2A==" }, "dtrace-provider": { "version": "0.8.8", @@ -31942,9 +31667,9 @@ } }, "enhanced-resolve": { - "version": "5.12.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.12.0.tgz", - "integrity": "sha512-QHTXI/sZQmko1cbDoNAa3mJ5qhWUUNAq3vR0/YiD379fWQrcfuoX1+HW2S0MTt7XmoPLapdaDKUtelUSPic7hQ==", + "version": "5.15.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.15.0.tgz", + "integrity": "sha512-LXYT42KJ7lpIKECr2mAXIaMldcNCh/7E0KBKOu4KSfkHmP+mZmSs+8V5gBAqisWBy0OO4W5Oyys0GO1Y8KtdKg==", "dev": true, "requires": { "graceful-fs": "^4.2.4", @@ -32006,9 +31731,9 @@ } }, "es-module-lexer": { - "version": "0.9.3", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-0.9.3.tgz", - "integrity": "sha512-1HQ2M2sPtxwnvOvT1ZClHyQDiggdNjURWpY2we6aMKCQiUVxTmVs2UYPLIrD84sS+kMdUwfBSylbJPwNnBrnHQ==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.3.0.tgz", + "integrity": "sha512-vZK7T0N2CBmBOixhmjdqx2gWVbFZ4DXZ/NyRMZVlJXPa7CyFS+/a4QQsDGDQy9ZfEzxFuNEsMLeQJnKP2p5/JA==", "dev": true }, "es-to-primitive": { @@ -34001,6 +33726,12 @@ "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==" }, + "has-own-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-own-prop/-/has-own-prop-2.0.0.tgz", + "integrity": "sha512-Pq0h+hvsVm6dDEa8x82GnLSYHOzNDt7f0ddFa3FqcQlgzEiptPqL+XrOJNavjOzSYiYWIrgeVYYgGlLmnxwilQ==", + "dev": true + }, "has-property-descriptors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.0.tgz", @@ -34455,7 +34186,8 @@ "is-interactive": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-1.0.0.tgz", - "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==" + "integrity": "sha512-2HvIEKRoqS62guEC+qBjpvRubdX910WCMuJTZ+I9yvqKU2/12eSL549HMwtabb4oupdj2sMP50k+XJfB/8JE6w==", + "dev": true }, "is-iojs": { "version": "1.1.0", @@ -34567,7 +34299,8 @@ "is-unicode-supported": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==" + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true }, "is-utf8": { "version": "0.2.1", @@ -36350,6 +36083,12 @@ "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==" }, + "jsonc-parser": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.2.0.tgz", + "integrity": "sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==", + "dev": true + }, "jsonfile": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", @@ -36756,6 +36495,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, "requires": { "chalk": "^4.1.0", "is-unicode-supported": "^0.1.0" @@ -36765,6 +36505,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -36773,6 +36514,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -36782,6 +36524,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -36789,12 +36532,14 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -36939,6 +36684,23 @@ "integrity": "sha512-EIgv+QZ9r+814gjJj0Bt5vSLJLzswGmSUbUpbi9AIr/fsN2IWFBl2NucV9PAiek+U1STK468tEkxmVYUtuAN3g==", "dev": true }, + "magic-string": { + "version": "0.30.1", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.1.tgz", + "integrity": "sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.4.15" + }, + "dependencies": { + "@jridgewell/sourcemap-codec": { + "version": "1.4.15", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", + "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "dev": true + } + } + }, "make-dir": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", @@ -37306,7 +37068,8 @@ "mimic-fn": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true }, "minimatch": { "version": "3.1.2", @@ -38077,9 +37840,9 @@ "optional": true }, "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==" + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.6.tgz", + "integrity": "sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==" }, "natural-compare": { "version": "1.4.0", @@ -38111,20 +37874,26 @@ "dev": true }, "nest-winston": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/nest-winston/-/nest-winston-1.8.0.tgz", - "integrity": "sha512-DyoNrly6DXjNG2ZPXSD2+w+pgEE8u0y0iwsaLZsufFLIKB/YNgP7zIXV5JxIqns0e5QzLlxdHhO2hx+1W8dvCw==", + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/nest-winston/-/nest-winston-1.9.4.tgz", + "integrity": "sha512-ilEmHuuYSAI6aMNR120fLBl42EdY13QI9WRggHdEizt9M7qZlmXJwpbemVWKW/tqRmULjSx/otKNQ3GMQbfoUQ==", "requires": { "fast-safe-stringify": "^2.1.1" } }, "nestjs-console": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/nestjs-console/-/nestjs-console-8.0.0.tgz", - "integrity": "sha512-ColRYRI9aH8/wlbrcFK/AeTAvsLlk0vgNIqL9FivUjo+q63S6AB+vUf7Jx7mQ1oIT2bD4awEOt0qEnkHmxsi4Q==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/nestjs-console/-/nestjs-console-9.0.0.tgz", + "integrity": "sha512-5t9r0E9WHei2aCxPTPrtSq0h1rQBsO8TWok9HEwuRDpT3OEZJfA5FF0LaJDTD0DkusDq+GufSK1pZpgs21EeKQ==", "requires": { - "commander": "^8.1.0", - "ora": "5.4.1" + "commander": "^11.0.0" + }, + "dependencies": { + "commander": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.0.0.tgz", + "integrity": "sha512-9HMlXtt/BNoYr8ooyjjNRdIilOTkVJXB+GhxMTtOKwk0R4j4lS4NpjuqmRxroBfnfTSHQIHQB7wryHhXarNjmQ==" + } } }, "new-find-package-json": { @@ -38633,11 +38402,6 @@ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" }, - "object-hash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", - "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==" - }, "object-inspect": { "version": "1.12.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.2.tgz", @@ -38706,6 +38470,7 @@ "version": "5.1.2", "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, "requires": { "mimic-fn": "^2.1.0" } @@ -38747,6 +38512,7 @@ "version": "5.4.1", "resolved": "https://registry.npmjs.org/ora/-/ora-5.4.1.tgz", "integrity": "sha512-5b6Y85tPxZZ7QytO+BQzysW31HJku27cRIlkbAXaNx+BdcVi+LlRFmVXzeF6a7JCwJpyw5c4b+YSVImQIrBpuQ==", + "dev": true, "requires": { "bl": "^4.1.0", "chalk": "^4.1.0", @@ -38763,6 +38529,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "requires": { "color-convert": "^2.0.1" } @@ -38771,6 +38538,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, "requires": { "buffer": "^5.5.0", "inherits": "^2.0.4", @@ -38781,6 +38549,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, "requires": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" @@ -38790,6 +38559,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "requires": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -38799,6 +38569,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "requires": { "color-name": "~1.1.4" } @@ -38806,12 +38577,14 @@ "color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true }, "readable-stream": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", + "dev": true, "requires": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", @@ -38822,6 +38595,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, "requires": { "safe-buffer": "~5.2.0" } @@ -38830,6 +38604,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "requires": { "has-flag": "^4.0.0" } @@ -39018,25 +38793,25 @@ "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" }, "path-scurry": { - "version": "1.6.4", - "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.6.4.tgz", - "integrity": "sha512-Qp/9IHkdNiXJ3/Kon++At2nVpnhRiPq/aSvQN+H3U1WZbvNRK0RIQK/o4HMqPoXjpuGJUEWpHSs6Mnjxqh3TQg==", + "version": "1.10.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.10.1.tgz", + "integrity": "sha512-MkhCqzzBEpPvxxQ71Md0b1Kk51W01lrYvlMzSUaIzNsODdd7mqhiimSZlr+VegAz5Z6Vzt9Xg2ttE//XBhH3EQ==", "dev": true, "requires": { - "lru-cache": "^9.0.0", - "minipass": "^5.0.0" + "lru-cache": "^9.1.1 || ^10.0.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" }, "dependencies": { "lru-cache": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-9.0.1.tgz", - "integrity": "sha512-C8QsKIN1UIXeOs3iWmiZ1lQY+EnKDojWd37fXy1aSbJvH4iSma1uy2OWuoB3m4SYRli5+CUjDv3Dij5DVoetmg==", + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.0.1.tgz", + "integrity": "sha512-IJ4uwUTi2qCccrioU6g9g/5rvvVl13bsdczUUcqbciD9iLr095yj8DQKdObriEvuNSx325N1rV1O0sJFszx75g==", "dev": true }, "minipass": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", - "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.0.3.tgz", + "integrity": "sha512-LhbbwCfz3vsb12j/WkWQPZfKTsgqIe1Nf/ti1pKjYESGLHIVjWU96G9/ljLH4F9mWNVhlQOm0VySdAWzf05dpg==", "dev": true } } @@ -39670,6 +39445,11 @@ "ipaddr.js": "1.9.1" } }, + "proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" + }, "proxyquire": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/proxyquire/-/proxyquire-2.1.3.tgz", @@ -40060,6 +39840,12 @@ "es6-error": "^4.0.1" } }, + "repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", + "dev": true + }, "request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -40252,6 +40038,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-3.1.0.tgz", "integrity": "sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA==", + "dev": true, "requires": { "onetime": "^5.1.0", "signal-exit": "^3.0.2" @@ -40692,9 +40479,9 @@ "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=" }, "rxjs": { - "version": "7.5.5", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.5.5.tgz", - "integrity": "sha512-sy+H0pQofO95VDmFLzyaw9xNJU4KTRSwQIGM6+iG3SypAtCiLDzpeG8sJrNCWn2Up9km+KhkvTdbkrdy+yzZdw==", + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.1.tgz", + "integrity": "sha512-AA3TVj+0A2iuIoQkWEK/tqFjBq2j+6PO6Y0zJcvzLAFhEFIO3HL0vls9hWLncZbAAbK0mar7oZ4V079I/qPMxg==", "requires": { "tslib": "^2.1.0" } @@ -40765,9 +40552,9 @@ "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" }, "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", "dev": true, "requires": { "@types/json-schema": "^7.0.8", @@ -41022,7 +40809,8 @@ "signal-exit": { "version": "3.0.7", "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==" + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true }, "simple-oauth2": { "version": "4.3.0", @@ -41843,21 +41631,21 @@ } }, "terser": { - "version": "5.15.1", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.15.1.tgz", - "integrity": "sha512-K1faMUvpm/FBxjBXud0LWVAGxmvoPbZbfTCYbSgaaYQaIXI3/TdI7a7ZGA73Zrou6Q8Zmz3oeUTsp/dj+ag2Xw==", + "version": "5.19.4", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.19.4.tgz", + "integrity": "sha512-6p1DjHeuluwxDXcuT9VR8p64klWJKo1ILiy19s6C9+0Bh2+NWTX6nD9EPppiER4ICkHDVB1RkVpin/YW2nQn/g==", "dev": true, "requires": { - "@jridgewell/source-map": "^0.3.2", - "acorn": "^8.5.0", + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", "commander": "^2.20.0", "source-map-support": "~0.5.20" }, "dependencies": { "acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true }, "commander": { @@ -41869,16 +41657,27 @@ } }, "terser-webpack-plugin": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.1.tgz", - "integrity": "sha512-GvlZdT6wPQKbDNW/GDQzZFg/j4vKU96yl2q6mcUkzKOgW4gwf1Z8cZToUCrz31XHlPWH8MVb1r2tFtdDtTGJ7g==", + "version": "5.3.9", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.9.tgz", + "integrity": "sha512-ZuXsqE07EcggTWQjXUj+Aot/OMcD0bMKGgF63f7UxYcu5/AJF53aIpK1YoP5xR9l6s/Hy2b+t1AM0bLNPRuhwA==", "dev": true, "requires": { + "@jridgewell/trace-mapping": "^0.3.17", "jest-worker": "^27.4.5", "schema-utils": "^3.1.1", - "serialize-javascript": "^6.0.0", - "source-map": "^0.6.1", - "terser": "^5.7.2" + "serialize-javascript": "^6.0.1", + "terser": "^5.16.8" + }, + "dependencies": { + "serialize-javascript": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.1.tgz", + "integrity": "sha512-owoXEFjWRllis8/M1Q+Cw5k8ZH40e3zhp/ovX+Xr/vi1qj6QesbyXXViFbpNvWvPNAD62SutwEXavefrLJWj7w==", + "dev": true, + "requires": { + "randombytes": "^2.1.0" + } + } } }, "test-exclude": { @@ -42239,9 +42038,9 @@ } }, "tsconfig-paths": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.1.2.tgz", - "integrity": "sha512-uhxiMgnXQp1IR622dUXI+9Ehnws7i/y6xvpZB9IbUVOPy0muvdvgXeZOn88UcGPiT98Vp3rJPTa8bFoalZ3Qhw==", + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-4.2.0.tgz", + "integrity": "sha512-NoZ4roiN7LnbKn9QqE1amc9DJfzvZXxF4xDavcOWt1BPkdx+m+0gJuPM+S0vCe7zTJMYUP0R8pO2XMr+Y8oLIg==", "dev": true, "requires": { "json5": "^2.2.2", @@ -42258,9 +42057,9 @@ } }, "tsconfig-paths-webpack-plugin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.0.1.tgz", - "integrity": "sha512-m5//KzLoKmqu2MVix+dgLKq70MnFi8YL8sdzQZ6DblmCdfuq/y3OqvJd5vMndg2KEVCOeNz8Es4WVZhYInteLw==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths-webpack-plugin/-/tsconfig-paths-webpack-plugin-4.1.0.tgz", + "integrity": "sha512-xWFISjviPydmtmgeUAuXp4N1fky+VCtfhOkDUFIv5ea7p4wuTomI4QTrXvFBX2S4jZsmyTSrStQl+E+4w+RzxA==", "dev": true, "requires": { "chalk": "^4.1.0", @@ -42314,9 +42113,9 @@ } }, "tslib": { - "version": "2.5.3", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.5.3.tgz", - "integrity": "sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==" + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "tsup": { "version": "5.12.9", @@ -42511,6 +42310,14 @@ "resolved": "https://registry.npmjs.org/uberproto/-/uberproto-2.0.6.tgz", "integrity": "sha512-68H97HffZoFaa3HFtpstahWorN9dSp5uTU6jo3GjIQ6JkJBR3hC2Nx/e/HFOoYHdUyT/Z1MRWfxN1EiQJZUyCQ==" }, + "uid": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/uid/-/uid-2.0.2.tgz", + "integrity": "sha512-u3xV3X7uzvi5b1MncmZo3i2Aw222Zk1keqLA1YkHldREkAhAqi65wuPfe7lHx8H/Wzy+8CE7S7uS3jekIM5s8g==", + "requires": { + "@lukeed/csprng": "^1.0.0" + } + }, "uid-safe": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/uid-safe/-/uid-safe-2.1.5.tgz", @@ -42836,6 +42643,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/wcwidth/-/wcwidth-1.0.1.tgz", "integrity": "sha1-8LDc+RW8X/FSivrbLA4XtTLaL+g=", + "dev": true, "requires": { "defaults": "^1.0.3" } @@ -42846,22 +42654,22 @@ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==" }, "webpack": { - "version": "5.76.2", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.76.2.tgz", - "integrity": "sha512-Th05ggRm23rVzEOlX8y67NkYCHa9nTNcwHPBhdg+lKG+mtiW7XgggjAeeLnADAe7mLjJ6LUNfgHAuRRh+Z6J7w==", + "version": "5.88.2", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.88.2.tgz", + "integrity": "sha512-JmcgNZ1iKj+aiR0OvTYtWQqJwq37Pf683dY9bVORwVbUrDhLhdn/PlO2sHsFHPkj7sHNQF3JwaAkp49V+Sq1tQ==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.3", - "@types/estree": "^0.0.51", - "@webassemblyjs/ast": "1.11.1", - "@webassemblyjs/wasm-edit": "1.11.1", - "@webassemblyjs/wasm-parser": "1.11.1", + "@types/estree": "^1.0.0", + "@webassemblyjs/ast": "^1.11.5", + "@webassemblyjs/wasm-edit": "^1.11.5", + "@webassemblyjs/wasm-parser": "^1.11.5", "acorn": "^8.7.1", - "acorn-import-assertions": "^1.7.6", + "acorn-import-assertions": "^1.9.0", "browserslist": "^4.14.5", "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.10.0", - "es-module-lexer": "^0.9.0", + "enhanced-resolve": "^5.15.0", + "es-module-lexer": "^1.2.1", "eslint-scope": "5.1.1", "events": "^3.2.0", "glob-to-regexp": "^0.4.1", @@ -42870,23 +42678,23 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.1.0", + "schema-utils": "^3.2.0", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.1.3", + "terser-webpack-plugin": "^5.3.7", "watchpack": "^2.4.0", "webpack-sources": "^3.2.3" }, "dependencies": { "acorn": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.8.1.tgz", - "integrity": "sha512-7zFpHzhnqYKrkYdUjF1HI1bzd0VygEGX8lFk4k5zVMqHEoES+P+7TKI+EvLO9WVMJ8eekdO0aDEK044xTXwPPA==", + "version": "8.10.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.10.0.tgz", + "integrity": "sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==", "dev": true }, "acorn-import-assertions": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.8.0.tgz", - "integrity": "sha512-m7VZ3jwz4eK6A4Vtt8Ew1/mNbP24u0FhdyfA7fSvnJR6LMdfOYnmuIrrJAgrYfYJ10F/otaHTtrtrtmHdMNzEw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/acorn-import-assertions/-/acorn-import-assertions-1.9.0.tgz", + "integrity": "sha512-cmMwop9x+8KFhxvKrKfPYmN6/pKTYYHBqLa0DfvVZcKMJWNyWLnaqND7dx/qn66R7ewM1UX5XMaDVP5wlVTaVA==", "dev": true, "requires": {} } diff --git a/package.json b/package.json index c7725509e6f..7336062fdc0 100644 --- a/package.json +++ b/package.json @@ -97,23 +97,24 @@ "@feathersjs/errors": "^4.5.11", "@feathersjs/express": "^4.5.11", "@feathersjs/feathers": "^4.5.11", - "@golevelup/nestjs-rabbitmq": "^3.7.0", + "@golevelup/nestjs-rabbitmq": "^4.0.0", "@hendt/xml2json": "^1.0.3", "@hpi-schul-cloud/commons": "^1.3.4", "@keycloak/keycloak-admin-client": "^21.1.2", "@lumieducation/h5p-server": "^9.2.0", "@mikro-orm/core": "^5.4.2", "@mikro-orm/mongodb": "^5.4.2", - "@mikro-orm/nestjs": "^5.1.5", - "@nestjs/axios": "^0.1.0", - "@nestjs/common": "^9.2.1", - "@nestjs/config": "^2.2.0", - "@nestjs/core": "^9.2.1", - "@nestjs/jwt": "^10.0.2", - "@nestjs/microservices": "^9.2.1", - "@nestjs/passport": "^9.0.0", - "@nestjs/platform-express": "^9.2.1", - "@nestjs/swagger": "^6.2.1", + "@mikro-orm/nestjs": "^5.2.1", + "@nestjs/axios": "^3.0.0", + "@nestjs/cache-manager": "^2.1.0", + "@nestjs/common": "^10.2.4", + "@nestjs/config": "^3.0.1", + "@nestjs/core": "^10.2.4", + "@nestjs/jwt": "^10.1.1", + "@nestjs/microservices": "^10.2.4", + "@nestjs/passport": "^10.0.1", + "@nestjs/platform-express": "^10.2.4", + "@nestjs/swagger": "^7.1.10", "@types/cache-manager-redis-store": "^2.0.1", "@types/connect-redis": "^0.0.19", "@types/gm": "^1.25.1", @@ -129,7 +130,7 @@ "async": "^3.2.2", "async-mutex": "^0.4.0", "aws-sdk": "^2.1375.0", - "axios": "^0.27.2", + "axios": "^1.5.0", "axios-mock-adapter": "^1.21.2", "bbb-promise": "^1.2.0", "bcryptjs": "*", @@ -176,8 +177,8 @@ "mongoose-shortid-nodeps": "git://github.com/leeroybrun/mongoose-shortid-nodeps.git", "moodle-client": "^0.5.2", "nanoid": "^3.3.4", - "nest-winston": "^1.8.0", - "nestjs-console": "^8.0.0", + "nest-winston": "^1.9.4", + "nestjs-console": "^9.0.0", "oauth-1.0a": "^2.2.6", "p-limit": "^3.1.0", "papaparse": "^5.1.1", @@ -215,9 +216,9 @@ "@aws-sdk/client-s3": "^3.352.0", "@golevelup/ts-jest": "^0.3.4", "@jest-mock/express": "^1.4.5", - "@nestjs/cli": "^9.3.0", - "@nestjs/schematics": "^9.0.3", - "@nestjs/testing": "^9.2.1", + "@nestjs/cli": "^10.1.17", + "@nestjs/schematics": "^10.0.2", + "@nestjs/testing": "^10.2.4", "@types/adm-zip": "^0.5.0", "@types/amqplib": "^0.8.2", "@types/bcryptjs": "^2.4.2", diff --git a/src/services/config/publicAppConfigService.js b/src/services/config/publicAppConfigService.js index a9385919bcf..afaf46ca911 100644 --- a/src/services/config/publicAppConfigService.js +++ b/src/services/config/publicAppConfigService.js @@ -22,7 +22,8 @@ const exposedVars = [ 'TEACHER_STUDENT_VISIBILITY__IS_CONFIGURABLE', 'TEACHER_STUDENT_VISIBILITY__IS_ENABLED_BY_DEFAULT', 'TEACHER_STUDENT_VISIBILITY__IS_VISIBLE', - 'FEATURE_SCHOOL_POLICY_ENABLED', + 'FEATURE_SCHOOL_POLICY_ENABLED_NEW', + 'FEATURE_SCHOOL_TERMS_OF_USE_ENABLED', 'FEATURE_VIDEOCONFERENCE_ENABLED', 'ROCKETCHAT_SERVICE_ENABLED', 'LERNSTORE_MODE', diff --git a/src/services/consent/services/consentVersionService.js b/src/services/consent/services/consentVersionService.js index 66889b900fe..10ac7231d66 100644 --- a/src/services/consent/services/consentVersionService.js +++ b/src/services/consent/services/consentVersionService.js @@ -57,13 +57,14 @@ class ConsentVersionService { } createBase64File(consentDocumentData) { - const { schoolId, consentData } = consentDocumentData; + const { schoolId, consentData, consentTypes } = consentDocumentData; if (consentData) { + const fileName = consentTypes[0].includes('terms') ? 'Terms of Use' : 'Privacy Policy'; return this.app.service('base64Files').create({ schoolId, data: consentDocumentData.consentData, filetype: 'pdf', - filename: 'Privacy Policy', + filename: fileName, }); } return Promise.resolve({}); diff --git a/src/services/sync/repo/school.repo.js b/src/services/sync/repo/school.repo.js index 55281f536c3..22847262741 100644 --- a/src/services/sync/repo/school.repo.js +++ b/src/services/sync/repo/school.repo.js @@ -29,6 +29,7 @@ const findSchoolByPreviousExternalIdAndSystem = async (previousExternalId, syste previousExternalId, systems: { $in: systems }, }) + .populate('userLoginMigration') .lean({ virtuals: true }) .exec(); diff --git a/src/services/sync/repo/user.repo.js b/src/services/sync/repo/user.repo.js index 2884b6c881c..38351857120 100644 --- a/src/services/sync/repo/user.repo.js +++ b/src/services/sync/repo/user.repo.js @@ -139,8 +139,16 @@ const findByPreviousExternalIdAndSchool = async (previousExternalId, schoolId) = const findByLdapDnsAndSchool = async (ldapDns, schoolId) => userModel .find({ - schoolId, - ldapDn: { $in: ldapDns }, + $or: [ + { + schoolId, + ldapDn: { $in: ldapDns }, + }, + { + schoolId, + previousExternalId: { $in: ldapDns }, + }, + ], }) .populate('roles') .lean() diff --git a/src/services/sync/strategies/TSP/SchoolChange.js b/src/services/sync/strategies/TSP/SchoolChange.js index 58e1fc4929d..15fa4c2dc69 100644 --- a/src/services/sync/strategies/TSP/SchoolChange.js +++ b/src/services/sync/strategies/TSP/SchoolChange.js @@ -25,11 +25,11 @@ const invalidateUser = async (app, user) => { const deleteUser = (app, user) => { const userService = app.service('usersModel'); const accountService = app.service('nest-account-service'); - const teamsService = app.service('/teams'); + const teamService = app.service('nest-team-service'); return Promise.all([ userService.remove({ _id: user._id }), accountService.deleteByUserId(user._id.toString()), - teamsService.updateMany({ 'userIds.userId': { $in: [user._id] } }, { $pull: { userIds: { userId: user._id } } }) + teamService.deleteUserDataFromTeams(user._id.toString()), ]); }; @@ -84,7 +84,7 @@ const switchSchool = async (app, currentUser, createUserMethod) => { await deleteUser(app, currentUser); return newUser; } catch (err) { - logError(`Something went wrong during switching school for user: ${err}`); + logError(`Something went wrong during switching school for user (${currentUser.sourceOptions.tspUid})`, err); return null; } }; diff --git a/src/services/sync/strategies/consumerActions/ClassAction.js b/src/services/sync/strategies/consumerActions/ClassAction.js index ee9713154f3..3bf1b1329f6 100644 --- a/src/services/sync/strategies/consumerActions/ClassAction.js +++ b/src/services/sync/strategies/consumerActions/ClassAction.js @@ -1,19 +1,20 @@ const BaseConsumerAction = require('./BaseConsumerAction'); -// TODO: place from where it is importat must be fixed later +// TODO: place from where it is important must be fixed later const { LDAP_SYNC_ACTIONS } = require('../SyncMessageBuilder'); const { SchoolRepo, ClassRepo, UserRepo } = require('../../repo'); const { NotFound } = require('../../../../errors'); +const { SCHOOL_FEATURES } = require('../../../school/model'); const defaultOptions = { allowedLogKeys: ['class', 'ldapDN', 'systemId', 'schoolDn', 'year'], }; // TODO: in all actions it looks not nice that filterActive is not passed as option -// the additional this it not really nessasry because options pass all to this. +// the additional this it not really necessary because options pass all to this. // but we must also keep in mind that we do not hide options behind BaseConsumerStrategie class. // But Enable / Disable filter should come from top this is fine. -// If we pass filterActive to options, we must destructure options -> set defaults (that we do not must set repos if we pass this) -> put the combind keys as optiosn to super -// -> i do not like it because we put knowlege and logic handling to the constructor and every they want to add a new action must do it in the same way and implement the same. +// If we pass filterActive to options, we must destructure options -> set defaults (that we do not must set repos if we pass this) -> put the combined keys as option to super +// -> i do not like it because we put knowledge and logic handling to the constructor and every they want to add a new action must do it in the same way and implement the same. // --> sound weird... class ClassAction extends BaseConsumerAction { constructor(filterActive = true, options = defaultOptions) { @@ -24,24 +25,50 @@ class ClassAction extends BaseConsumerAction { async action(data = {}) { const { class: classData = {} } = data; - const school = await SchoolRepo.findSchoolByLdapIdAndSystem(classData.schoolDn, classData.systemId); + let school = await SchoolRepo.findSchoolByLdapIdAndSystem(classData.schoolDn, classData.systemId); if (!school) { - throw new NotFound( - `School for schoolDn ${classData.schoolDn} and system ${classData.systemId} couldn't be found.`, - { - schoolDn: classData.schoolDn, - systemId: classData.systemId, - } + const migratedSchool = await SchoolRepo.findSchoolByPreviousExternalIdAndSystem( + classData.schoolDn, + classData.systemId ); + + if (migratedSchool) { + if ( + migratedSchool.userLoginMigration && + !migratedSchool.userLoginMigration.closedAt && + migratedSchool.features?.includes(SCHOOL_FEATURES.ENABLE_LDAP_SYNC_DURING_MIGRATION) + ) { + school = migratedSchool; + } else { + throw new NotFound( + `Migrated School for schoolDn ${classData.schoolDn} and system ${classData.systemId} couldn't be synced, since the feature is deactivated.`, + { + schoolDn: classData.schoolDn, + systemId: classData.systemId, + } + ); + } + } else { + throw new NotFound( + `School for schoolDn ${classData.schoolDn} and system ${classData.systemId} couldn't be found.`, + { + schoolDn: classData.schoolDn, + systemId: classData.systemId, + } + ); + } } + if (school.inUserMigration === true) { await UserRepo.addClassToImportUsers(school._id, classData.name, classData.uniqueMembers); return; } + if (school.inMaintenance) { return; } + // default: update classes let classId; const existingClass = await ClassRepo.findClassByYearAndLdapDn(school.currentYear, classData.ldapDN); @@ -54,6 +81,7 @@ class ClassAction extends BaseConsumerAction { const createdClass = await ClassRepo.createClass(classData, school); classId = createdClass._id; } + await this.addUsersToClass(school._id, classId, classData.uniqueMembers); } @@ -61,13 +89,16 @@ class ClassAction extends BaseConsumerAction { const students = []; const teachers = []; const ldapDns = !Array.isArray(uniqueMembers) ? [uniqueMembers] : uniqueMembers; + const users = await UserRepo.findByLdapDnsAndSchool(ldapDns, schoolId); + users.forEach((user) => { user.roles.forEach((role) => { if (role.name === 'student') students.push(user._id); if (role.name === 'teacher') teachers.push(user._id); }); }); + await ClassRepo.updateClassStudents(classId, students); await ClassRepo.updateClassTeachers(classId, teachers); } diff --git a/src/services/sync/strategies/consumerActions/UserAction.js b/src/services/sync/strategies/consumerActions/UserAction.js index 8d3f28d9874..ae2c4628c39 100644 --- a/src/services/sync/strategies/consumerActions/UserAction.js +++ b/src/services/sync/strategies/consumerActions/UserAction.js @@ -1,12 +1,13 @@ const _ = require('lodash'); +const { Configuration } = require('@hpi-schul-cloud/commons'); const BaseConsumerAction = require('./BaseConsumerAction'); // TODO: place from where it is importat must be fixed later const { LDAP_SYNC_ACTIONS } = require('../SyncMessageBuilder'); const { SchoolRepo, UserRepo } = require('../../repo'); const { NotFound } = require('../../../../errors'); const { isNotEmptyString } = require('../../../../helper/stringHelper'); -const { Configuration } = require('@hpi-schul-cloud/commons'); +const { SCHOOL_FEATURES } = require('../../../school/model'); const defaultOptions = { allowedLogKeys: ['ldapId', 'systemId', 'roles', 'activated', 'schoolDn'], @@ -22,13 +23,34 @@ class UserAction extends BaseConsumerAction { async action(data = {}) { const { user = {}, account = {} } = data; - const school = await SchoolRepo.findSchoolByLdapIdAndSystem(user.schoolDn, user.systemId); + let school = await SchoolRepo.findSchoolByLdapIdAndSystem(user.schoolDn, user.systemId); if (!school) { - throw new NotFound(`School for schoolDn ${user.schoolDn} and systemId ${user.systemId} couldn't be found.`, { - schoolDn: user.schoolDn, - systemId: user.systemId, - }); + const migratedSchool = await SchoolRepo.findSchoolByPreviousExternalIdAndSystem(user.schoolDn, user.systemId); + + if (!migratedSchool) { + throw new NotFound(`School for schoolDn ${user.schoolDn} and systemId ${user.systemId} couldn't be found.`, { + schoolDn: user.schoolDn, + systemId: user.systemId, + }); + } + + if ( + migratedSchool?.userLoginMigration && + !migratedSchool.userLoginMigration.closedAt && + migratedSchool?.features?.includes(SCHOOL_FEATURES.ENABLE_LDAP_SYNC_DURING_MIGRATION) + ) { + school = migratedSchool; + } else { + throw new NotFound( + `Migrated School with previous schoolDn ${user.schoolDn} and systemId ${user.systemId} has been found. + The Ldap-Sync for this school has been skipped, because the conditions for an extended Sync for migrated schools have not been met.`, + { + schoolDn: user.schoolDn, + systemId: user.systemId, + } + ); + } } const foundUser = await UserRepo.findByLdapIdAndSchool(user.ldapId, school._id); @@ -36,7 +58,7 @@ class UserAction extends BaseConsumerAction { if (!foundUser) { const oauthMigratedUser = await UserRepo.findByPreviousExternalIdAndSchool(user.ldapId, school._id); if (oauthMigratedUser) { - // skip creating or updating users when user has migrated + // skip creating or updating users when user or school has migrated return; } } diff --git a/src/services/user/services/registrationConsent.js b/src/services/user/services/registrationConsent.js index a9400779ad7..37f24a1d5c8 100644 --- a/src/services/user/services/registrationConsent.js +++ b/src/services/user/services/registrationConsent.js @@ -22,7 +22,12 @@ const registrationConsentServiceHooks = { }; class RegistrationConsentService { - async get(importHash) { + async get(importHash, params) { + const consentType = params.query.consentType || 'privacy'; + if (consentType !== 'privacy' && consentType !== 'termsOfUse') { + throw new BadRequest('Invalid Consent Type!'); + } + const user = await this.importUserLinkService.get(importHash); if (!user.userId) { throw new BadRequest('Invalid Import Hash!'); @@ -30,7 +35,7 @@ class RegistrationConsentService { const baseQuery = { $limit: 1, - consentTypes: 'privacy', + consentTypes: [consentType], $sort: { updatedAt: -1, }, diff --git a/test/services/sync/repo/user.repo.integration.test.js b/test/services/sync/repo/user.repo.integration.test.js index faf8f5df68d..eecb30e9e3c 100644 --- a/test/services/sync/repo/user.repo.integration.test.js +++ b/test/services/sync/repo/user.repo.integration.test.js @@ -206,6 +206,29 @@ describe('user repo', () => { expect(user1).not.to.be.undefined; expect(user2).not.to.be.undefined; }); + + describe('when the user has migrated', () => { + const setup = async () => { + const ldapDn = 'TEST_LDAP_DN'; + const school = await testObjects.createTestSchool(); + const user = await testObjects.createTestUser({ previousExternalId: ldapDn, schoolId: school._id }); + + return { + ldapDn, + user, + school, + }; + }; + + it('should find the user by its old ldap dn and school', async () => { + const { ldapDn, school, user } = await setup(); + + const res = await UserRepo.findByLdapDnsAndSchool([ldapDn], school._id); + + expect(res.length).to.equal(1); + expect(res[0]._id.toString()).to.equal(user._id.toString()); + }); + }); }); describe('createOrUpdateImportUser', () => { diff --git a/test/services/sync/strategies/TSP/SchoolChange.integration.test.js b/test/services/sync/strategies/TSP/SchoolChange.integration.test.js new file mode 100644 index 00000000000..d587b4a3119 --- /dev/null +++ b/test/services/sync/strategies/TSP/SchoolChange.integration.test.js @@ -0,0 +1,58 @@ +const { expect } = require('chai'); + +const appPromise = require('../../../../../src/app'); +const { setupNestServices, closeNestServices } = require('../../../../utils/setup.nest.services'); + +const testObjects = require('../../../helpers/testObjects')(appPromise()); + +const { deleteUser } = require('../../../../../src/services/sync/strategies/TSP/SchoolChange'); + +const userRepo = require('../../../../../src/components/user/repo/user.repo'); +const { NotFound } = require('../../../../../src/errors'); + +describe('SchooolChange API integration tests', () => { + let app; + let server; + let nestServices; + + let nestAccountService; + let teamService; + + before(async () => { + app = await appPromise(); + server = app.listen(0); + nestServices = await setupNestServices(app); + nestAccountService = app.service('nest-account-service'); + teamService = app.service('teams'); + }); + + after(async () => { + await server.close(); + await closeNestServices(nestServices); + }); + + afterEach(async () => { + await testObjects.cleanup(); + }); + + describe('deleteUser', () => { + it('should delete user, users account and delete user from teams', async () => { + const team = await testObjects.createTestTeamWithOwner({ roles: 'teacher' }); + const credentials = { username: team.user.email, password: `${Date.now()}` }; + await testObjects.createTestAccount(credentials, 'local', team.user); + + await deleteUser(app, team.user); + + await expect(userRepo.getUser(team.user._id)).to.be.rejectedWith(NotFound); + const deletedAccount = await nestAccountService.findByUserId(team.user._id); + expect(deletedAccount).to.be.equal(null); + const deletedUserTeams = await teamService.find({ + query: { + $limit: false, + userIds: { $elemMatch: { userId: team.user._id } }, + }, + }); + expect(deletedUserTeams.total).to.be.equal(0); + }); + }); +}); diff --git a/test/services/sync/strategies/consumerActions/ClassAction.test.js b/test/services/sync/strategies/consumerActions/ClassAction.test.js index d6b5ab3a8eb..4b450df5622 100644 --- a/test/services/sync/strategies/consumerActions/ClassAction.test.js +++ b/test/services/sync/strategies/consumerActions/ClassAction.test.js @@ -77,10 +77,115 @@ describe('Class Actions', () => { it('should throw an error if school could not be found', async () => { const findSchoolByLdapIdAndSystemStub = sinon.stub(SchoolRepo, 'findSchoolByLdapIdAndSystem'); + const findSchoolByPreviousExternalIdAndSystemStub = sinon.stub( + SchoolRepo, + 'findSchoolByPreviousExternalIdAndSystem' + ); findSchoolByLdapIdAndSystemStub.returns(null); + findSchoolByPreviousExternalIdAndSystemStub.returns(null); + await expect(classAction.action({ class: { schoolDn: 'SCHOOL_DN', systemId: '' } })).to.be.rejectedWith(NotFound); }); + describe('when migrated school could be found, but does not have the sync feature', () => { + const setup = () => { + const findSchoolByLdapIdAndSystemStub = sinon.stub(SchoolRepo, 'findSchoolByLdapIdAndSystem'); + const findSchoolByPreviousExternalIdAndSystemStub = sinon.stub( + SchoolRepo, + 'findSchoolByPreviousExternalIdAndSystem' + ); + + findSchoolByLdapIdAndSystemStub.returns(null); + findSchoolByPreviousExternalIdAndSystemStub.returns({ name: testSchoolName }); + }; + + it('should throw an error', async () => { + setup(); + + await expect(classAction.action({ class: { schoolDn: 'SCHOOL_DN', systemId: '' } })).to.be.rejectedWith( + NotFound + ); + }); + }); + + describe('when migrated school could be found, but migration is closed', () => { + const setup = () => { + const findSchoolByLdapIdAndSystemStub = sinon.stub(SchoolRepo, 'findSchoolByLdapIdAndSystem'); + const findSchoolByPreviousExternalIdAndSystemStub = sinon.stub( + SchoolRepo, + 'findSchoolByPreviousExternalIdAndSystem' + ); + + findSchoolByLdapIdAndSystemStub.returns(null); + findSchoolByPreviousExternalIdAndSystemStub.returns({ + name: testSchoolName, + features: ['enableLdapSyncDuringMigration'], + userLoginMigration: { + startedAt: new Date(), + closedAt: new Date(), + }, + }); + }; + + it('should throw an error', async () => { + setup(); + + await expect(classAction.action({ class: { schoolDn: 'SCHOOL_DN', systemId: '' } })).to.be.rejectedWith( + NotFound + ); + }); + }); + + describe('when migrated school could be found and has the sync feature', () => { + const setup = () => { + const schoolId = new ObjectId(); + const className = 'Class Name'; + const ldapDn = 'some ldap'; + const currentYear = new ObjectId(); + + const findSchoolByLdapIdAndSystemStub = sinon.stub(SchoolRepo, 'findSchoolByLdapIdAndSystem'); + const findSchoolByPreviousExternalIdAndSystemStub = sinon.stub( + SchoolRepo, + 'findSchoolByPreviousExternalIdAndSystem' + ); + const findClassByYearAndLdapDnStub = sinon.stub(ClassRepo, 'findClassByYearAndLdapDn'); + const createClassStub = sinon.stub(ClassRepo, 'createClass'); + + findSchoolByLdapIdAndSystemStub.returns(null); + findSchoolByPreviousExternalIdAndSystemStub.returns({ + name: testSchoolName, + _id: schoolId, + currentYear, + features: ['enableLdapSyncDuringMigration'], + userLoginMigration: { + startedAt: new Date(), + }, + }); + findClassByYearAndLdapDnStub.returns(null); + createClassStub.returns({ _id: new ObjectId() }); + + return { + className, + ldapDn, + currentYear, + createClassStub, + }; + }; + + it('should sync the classes', async () => { + const { className, ldapDn, currentYear, createClassStub } = setup(); + + await classAction.action({ class: { name: className, ldapDN: ldapDn } }); + + expect(createClassStub.calledOnce).to.be.true; + const { firstArg, lastArg } = createClassStub.firstCall; + expect(firstArg.name).to.be.equal(className); + expect(firstArg.ldapDN).to.be.equal(ldapDn); + expect(lastArg.name).to.be.equal(testSchoolName); + expect(lastArg.currentYear._id.toString()).to.be.equal(currentYear.toString()); + }); + }); + describe('When school is in maintenance mode', () => { it('should not create class', async () => { const findSchoolByLdapIdAndSystemStub = sinon.stub(SchoolRepo, 'findSchoolByLdapIdAndSystem'); diff --git a/test/services/sync/strategies/consumerActions/UserAction.test.js b/test/services/sync/strategies/consumerActions/UserAction.test.js index e3fab6be93a..e7ffa023de5 100644 --- a/test/services/sync/strategies/consumerActions/UserAction.test.js +++ b/test/services/sync/strategies/consumerActions/UserAction.test.js @@ -12,6 +12,7 @@ const { SchoolRepo, UserRepo } = require('../../../../../src/services/sync/repo' const appPromise = require('../../../../../src/app'); const { setupNestServices, closeNestServices } = require('../../../../utils/setup.nest.services'); +const { SCHOOL_FEATURES } = require('../../../../../src/services/school/model'); const { expect } = chai; chai.use(chaiAsPromised); @@ -227,6 +228,196 @@ describe('User Actions', () => { }); }); + describe('When school has migrated its login system', () => { + it('should throw, when school feature does not exist in school', async () => { + const findSchoolByLdapIdAndSystemStub = sinon.stub(SchoolRepo, 'findSchoolByLdapIdAndSystem'); + findSchoolByLdapIdAndSystemStub.returns(null); + + const findSchoolByPreviousExternalIdAndSystemStub = sinon.stub( + SchoolRepo, + 'findSchoolByPreviousExternalIdAndSystem' + ); + findSchoolByPreviousExternalIdAndSystemStub.returns({ + _id: new ObjectId(), + name: 'Test Schhool', + features: [], + userLoginMigration: { startedAt: new Date() }, + }); + + sinon.stub(UserRepo, 'findByLdapIdAndSchool').returns(null); + sinon.stub(UserRepo, 'findByPreviousExternalIdAndSchool').returns(null); + + const testUserInput = { + _id: 1, + firstName: 'New fname', + lastName: 'New lname', + email: 'New email', + ldapDn: 'new ldapdn', + roles: [new ObjectId()], + }; + + const testAccountInput = { _id: 2 }; + + await expect(userAction.action({ user: testUserInput, account: testAccountInput })).to.be.rejectedWith( + NotFound + ); + }); + + it('should throw, when migration is closed', async () => { + const findSchoolByLdapIdAndSystemStub = sinon.stub(SchoolRepo, 'findSchoolByLdapIdAndSystem'); + findSchoolByLdapIdAndSystemStub.returns(null); + + const findSchoolByPreviousExternalIdAndSystemStub = sinon.stub( + SchoolRepo, + 'findSchoolByPreviousExternalIdAndSystem' + ); + findSchoolByPreviousExternalIdAndSystemStub.returns({ + _id: new ObjectId(), + name: 'Test Schhool', + features: [SCHOOL_FEATURES.ENABLE_LDAP_SYNC_DURING_MIGRATION], + userLoginMigration: { closedAt: new Date() }, + }); + + sinon.stub(UserRepo, 'findByLdapIdAndSchool').returns(null); + sinon.stub(UserRepo, 'findByPreviousExternalIdAndSchool').returns(null); + + const testUserInput = { + _id: 1, + firstName: 'New fname', + lastName: 'New lname', + email: 'New email', + ldapDn: 'new ldapdn', + roles: [new ObjectId()], + }; + + const testAccountInput = { _id: 2 }; + + await expect(userAction.action({ user: testUserInput, account: testAccountInput })).to.be.rejectedWith( + NotFound + ); + }); + + it('should throw, when no user login migration exists', async () => { + const findSchoolByLdapIdAndSystemStub = sinon.stub(SchoolRepo, 'findSchoolByLdapIdAndSystem'); + findSchoolByLdapIdAndSystemStub.returns(null); + + const findSchoolByPreviousExternalIdAndSystemStub = sinon.stub( + SchoolRepo, + 'findSchoolByPreviousExternalIdAndSystem' + ); + findSchoolByPreviousExternalIdAndSystemStub.returns({ + _id: new ObjectId(), + name: 'Test Schhool', + features: [SCHOOL_FEATURES.ENABLE_LDAP_SYNC_DURING_MIGRATION], + }); + + sinon.stub(UserRepo, 'findByLdapIdAndSchool').returns(null); + sinon.stub(UserRepo, 'findByPreviousExternalIdAndSchool').returns(null); + + const testUserInput = { + _id: 1, + firstName: 'New fname', + lastName: 'New lname', + email: 'New email', + ldapDn: 'new ldapdn', + roles: [new ObjectId()], + }; + + const testAccountInput = { _id: 2 }; + + await expect(userAction.action({ user: testUserInput, account: testAccountInput })).to.be.rejectedWith( + NotFound + ); + }); + + it('should create new user and account, when school feature exists in school', async () => { + const findSchoolByLdapIdAndSystemStub = sinon.stub(SchoolRepo, 'findSchoolByLdapIdAndSystem'); + findSchoolByLdapIdAndSystemStub.returns(null); + + const testSchoolId = new ObjectId(); + const findSchoolByPreviousExternalIdAndSystemStub = sinon.stub( + SchoolRepo, + 'findSchoolByPreviousExternalIdAndSystem' + ); + findSchoolByPreviousExternalIdAndSystemStub.returns({ + _id: testSchoolId, + name: 'Test Schhool', + features: [SCHOOL_FEATURES.ENABLE_LDAP_SYNC_DURING_MIGRATION], + userLoginMigration: { startedAt: new Date() }, + }); + + sinon.stub(UserRepo, 'findByLdapIdAndSchool').returns(null); + sinon.stub(UserRepo, 'findByPreviousExternalIdAndSchool').returns(null); + + const createUserStub = sinon.stub(userAccountService, 'createUserAndAccount'); + const updateUserStub = sinon.stub(userAccountService, 'updateUserAndAccount'); + + const testUserInput = { + _id: new ObjectId(), + firstName: 'New fname', + lastName: 'New lname', + email: 'New email', + ldapDn: 'new ldapdn', + roles: ['role1'], + }; + const testAccountInput = { _id: 2 }; + + await userAction.action({ user: testUserInput, account: testAccountInput }); + + expect(createUserStub.calledOnce).to.be.true; + expect(updateUserStub.notCalled).to.be.true; + }); + + it('should update found user and account, when school feature exists in school', async () => { + const findSchoolByLdapIdAndSystemStub = sinon.stub(SchoolRepo, 'findSchoolByLdapIdAndSystem'); + findSchoolByLdapIdAndSystemStub.returns(null); + + const testSchoolId = new ObjectId(); + const findSchoolByPreviousExternalIdAndSystemStub = sinon.stub( + SchoolRepo, + 'findSchoolByPreviousExternalIdAndSystem' + ); + findSchoolByPreviousExternalIdAndSystemStub.returns({ + _id: testSchoolId, + name: 'Test Schhool', + features: [SCHOOL_FEATURES.ENABLE_LDAP_SYNC_DURING_MIGRATION], + userLoginMigration: { startedAt: new Date() }, + }); + + const userId = new ObjectId(); + + const existingUser = { + _id: userId, + firstName: 'Old fname', + lastName: 'Old lname', + email: 'Old email', + ldapDn: 'Old ldapdn', + }; + + sinon.stub(UserRepo, 'findByLdapIdAndSchool').returns(existingUser); + sinon.stub(UserRepo, 'findByPreviousExternalIdAndSchool').returns(null); + + const createUserStub = sinon.stub(userAccountService, 'createUserAndAccount'); + const updateUserStub = sinon.stub(userAccountService, 'updateUserAndAccount'); + + const testUserInput = { + _id: userId, + firstName: 'New fname', + lastName: 'New lname', + email: 'New email', + ldapDn: 'new ldapdn', + roles: [new ObjectId()], + }; + + const testAccountInput = { _id: new ObjectId() }; + + await userAction.action({ user: testUserInput, account: testAccountInput }); + + expect(createUserStub.notCalled).to.be.true; + expect(updateUserStub.calledOnce).to.be.true; + }); + }); + describe('When school is in maintenance mode', () => { it('should not create user and account', async () => { const findSchoolByLdapIdAndSystemStub = sinon.stub(SchoolRepo, 'findSchoolByLdapIdAndSystem'); diff --git a/test/utils/setup.nest.services.js b/test/utils/setup.nest.services.js index 715ee3ad1ea..4de3bce181f 100644 --- a/test/utils/setup.nest.services.js +++ b/test/utils/setup.nest.services.js @@ -13,6 +13,8 @@ const { } = require('../../dist/apps/server/modules/account/services/account.validation.service'); const { DB_PASSWORD, DB_URL, DB_USERNAME } = require('../../dist/apps/server/config/database.config'); const { ALL_ENTITIES } = require('../../dist/apps/server/shared/domain/entity/all-entities'); +const { TeamService } = require('../../dist/apps/server/modules/teams/service/team.service'); +const { TeamsApiModule } = require('../../dist/apps/server/modules/teams/teams-api.module'); const setupNestServices = async (app) => { const module = await Test.createTestingModule({ @@ -28,6 +30,7 @@ const setupNestServices = async (app) => { }), ConfigModule.forRoot({ ignoreEnvFile: true, ignoreEnvVars: true, isGlobal: true }), AccountApiModule, + TeamsApiModule, ], }).compile(); const nestApp = await module.createNestApplication().init(); @@ -35,10 +38,12 @@ const setupNestServices = async (app) => { const accountUc = nestApp.get(AccountUc); const accountService = nestApp.get(AccountService); const accountValidationService = nestApp.get(AccountValidationService); + const teamService = nestApp.get(TeamService); app.services['nest-account-uc'] = accountUc; app.services['nest-account-service'] = accountService; app.services['nest-account-validation-service'] = accountValidationService; + app.services['nest-team-service'] = teamService; app.services['nest-orm'] = orm; return { nestApp, orm, accountUc, accountService };