diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 626be1ccf55..964ab20c8f8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -83,8 +83,12 @@ jobs: run: | sudo apt-get install -y lcov find coverage -name *.info -exec echo -a {} \; | xargs lcov -o merged-lcov.info + - uses: actions/setup-java@v3 + with: + distribution: 'temurin' + java-version: '17' - name: SonarCloud upload coverage - uses: SonarSource/sonarcloud-github-action@v1.9 + uses: SonarSource/sonarcloud-github-action@v2.0.2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} SONAR_TOKEN: ${{ secrets.SONARCLOUD_TOKEN }} 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 6b17e522753..a3e5459077f 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 @@ -25,6 +25,9 @@ data: else echo "gg, hacky mongo replicaset" fi + {% if KEDA_NAMESPACE_ACTIVATOR_ENABLED is defined %} + curl -XPUT -H 'Content-Type: application/json' -Lv 'https://activate.cd.dbildungscloud.dev/namespace' -d '{"name" : "{{ NAMESPACE }}"}' + {% endif %} curl --retry 360 --retry-connrefused --retry-delay 10 -X POST 'http://mgmt-svc:3333/api/management/database/seed?with-indexes=true' # Below is a series of a MongoDB-data initializations, meant for the development and testing @@ -177,7 +180,6 @@ data: "redirectUri": "https://{{ NAMESPACE }}.cd.dbildungscloud.dev/api/v3/sso/oauth", "authEndpoint": "https://auth.stage.niedersachsen-login.schule/realms/SANIS/protocol/openid-connect/auth", "provider": "sanis", - "logoutEndpoint": "https://auth.stage.niedersachsen-login.schule/realms/SANIS/protocol/openid-connect/logout", "jwksEndpoint": "https://auth.stage.niedersachsen-login.schule/realms/SANIS/protocol/openid-connect/certs", "issuer": "https://auth.stage.niedersachsen-login.schule/realms/SANIS" } diff --git a/apps/server/src/apps/files-storage-consumer.app.ts b/apps/server/src/apps/files-storage-consumer.app.ts index 777176e1176..a18b5f4604b 100644 --- a/apps/server/src/apps/files-storage-consumer.app.ts +++ b/apps/server/src/apps/files-storage-consumer.app.ts @@ -3,7 +3,7 @@ import { NestFactory } from '@nestjs/core'; // register source-map-support for debugging -import { FilesStorageAMQPModule } from '@src/modules/files-storage'; +import { FilesStorageAMQPModule } from '@modules/files-storage'; import { install as sourceMapInstall } from 'source-map-support'; async function bootstrap() { diff --git a/apps/server/src/apps/files-storage.app.ts b/apps/server/src/apps/files-storage.app.ts index e10a2b3a162..2d2f9343ac2 100644 --- a/apps/server/src/apps/files-storage.app.ts +++ b/apps/server/src/apps/files-storage.app.ts @@ -10,7 +10,7 @@ import { install as sourceMapInstall } from 'source-map-support'; // application imports import { SwaggerDocumentOptions } from '@nestjs/swagger'; import { LegacyLogger } from '@src/core/logger'; -import { API_VERSION_PATH, FilesStorageApiModule } from '@src/modules/files-storage'; +import { API_VERSION_PATH, FilesStorageApiModule } from '@modules/files-storage'; import { enableOpenApiDocs } from '@src/shared/controller/swagger'; async function bootstrap() { diff --git a/apps/server/src/apps/fwu-learning-contents.app.ts b/apps/server/src/apps/fwu-learning-contents.app.ts index b7f2e9f1e25..15279392f12 100644 --- a/apps/server/src/apps/fwu-learning-contents.app.ts +++ b/apps/server/src/apps/fwu-learning-contents.app.ts @@ -9,7 +9,7 @@ import { install as sourceMapInstall } from 'source-map-support'; // application imports import { LegacyLogger } from '@src/core/logger'; -import { FwuLearningContentsModule } from '@src/modules/fwu-learning-contents'; +import { FwuLearningContentsModule } from '@modules/fwu-learning-contents'; import { enableOpenApiDocs } from '@src/shared/controller/swagger'; async function bootstrap() { diff --git a/apps/server/src/apps/h5p-editor.app.ts b/apps/server/src/apps/h5p-editor.app.ts index fcd550c34fa..518eb5c45bc 100644 --- a/apps/server/src/apps/h5p-editor.app.ts +++ b/apps/server/src/apps/h5p-editor.app.ts @@ -9,7 +9,7 @@ import { install as sourceMapInstall } from 'source-map-support'; // application imports import { LegacyLogger } from '@src/core/logger'; -import { H5PEditorModule } from '@src/modules/h5p-editor'; +import { H5PEditorModule } from '@modules/h5p-editor'; import { enableOpenApiDocs } from '@src/shared/controller/swagger'; async function bootstrap() { diff --git a/apps/server/src/apps/management.app.ts b/apps/server/src/apps/management.app.ts index 8be7cfdd866..dd66443300a 100644 --- a/apps/server/src/apps/management.app.ts +++ b/apps/server/src/apps/management.app.ts @@ -9,7 +9,7 @@ import { install as sourceMapInstall } from 'source-map-support'; // application imports import { LegacyLogger } from '@src/core/logger'; -import { ManagementServerModule } from '@src/modules/management'; +import { ManagementServerModule } from '@modules/management'; import { enableOpenApiDocs } from '@src/shared/controller/swagger'; async function bootstrap() { diff --git a/apps/server/src/apps/server.app.ts b/apps/server/src/apps/server.app.ts index 6452ca1cd47..bd235d5261f 100644 --- a/apps/server/src/apps/server.app.ts +++ b/apps/server/src/apps/server.app.ts @@ -7,19 +7,20 @@ import { ExpressAdapter } from '@nestjs/platform-express'; 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'; -import { RocketChatService } from '@src/modules/rocketchat'; -import { ServerModule } from '@src/modules/server'; +import { AccountService } from '@modules/account/services/account.service'; +import { TeamService } from '@modules/teams/service/team.service'; +import { AccountValidationService } from '@modules/account/services/account.validation.service'; +import { AccountUc } from '@modules/account/uc/account.uc'; +import { CollaborativeStorageUc } from '@modules/collaborative-storage/uc/collaborative-storage.uc'; +import { GroupService } from '@modules/group'; +import { RocketChatService } from '@modules/rocketchat'; +import { ServerModule } from '@modules/server'; import express from 'express'; import { join } from 'path'; // register source-map-support for debugging import { install as sourceMapInstall } from 'source-map-support'; -import { FeathersRosterService } from '@src/modules/pseudonym'; +import { FeathersRosterService } from '@modules/pseudonym'; import legacyAppPromise = require('../../../../src/app'); import { AppStartLoggable } from './helpers/app-start-loggable'; @@ -82,6 +83,8 @@ async function bootstrap() { 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-feathers-roster-service'] = nestApp.get(FeathersRosterService); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access + feathersExpress.services['nest-group-service'] = nestApp.get(GroupService); // 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/test-bootstrap.console.ts b/apps/server/src/console/api-test/test-bootstrap.console.ts index 282d448b05d..edb196b6a54 100644 --- a/apps/server/src/console/api-test/test-bootstrap.console.ts +++ b/apps/server/src/console/api-test/test-bootstrap.console.ts @@ -1,7 +1,7 @@ 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 { DatabaseManagementUc } from '@modules/management/uc/database-management.uc'; import { AbstractBootstrapConsole, BootstrapConsole } from 'nestjs-console'; export class TestBootstrapConsole extends AbstractBootstrapConsole { diff --git a/apps/server/src/console/console.module.ts b/apps/server/src/console/console.module.ts index 2aed716e719..2cad08943eb 100644 --- a/apps/server/src/console/console.module.ts +++ b/apps/server/src/console/console.module.ts @@ -7,11 +7,11 @@ import { ALL_ENTITIES } from '@shared/domain'; import { ConsoleWriterModule } from '@shared/infra/console/console-writer/console-writer.module'; import { KeycloakModule } from '@shared/infra/identity-management/keycloak/keycloak.module'; import { DB_PASSWORD, DB_URL, DB_USERNAME, createConfigModuleOptions } from '@src/config'; -import { FilesModule } from '@src/modules/files'; -import { FileEntity } from '@src/modules/files/entity'; -import { FileRecord } from '@src/modules/files-storage/entity'; -import { ManagementModule } from '@src/modules/management/management.module'; -import { serverConfig } from '@src/modules/server'; +import { FilesModule } from '@modules/files'; +import { FileEntity } from '@modules/files/entity'; +import { FileRecord } from '@modules/files-storage/entity'; +import { ManagementModule } from '@modules/management/management.module'; +import { serverConfig } from '@modules/server'; import { ConsoleModule } from 'nestjs-console'; import { ServerConsole } from './server.console'; diff --git a/apps/server/src/modules/account/index.ts b/apps/server/src/modules/account/index.ts index 3893bd18f88..2fa0bcc2334 100644 --- a/apps/server/src/modules/account/index.ts +++ b/apps/server/src/modules/account/index.ts @@ -1,2 +1,3 @@ export * from './account.module'; export * from './account-config'; +export { AccountService } from './services'; diff --git a/apps/server/src/modules/account/mapper/account-response.mapper.spec.ts b/apps/server/src/modules/account/mapper/account-response.mapper.spec.ts index 05c345f166b..64858623c67 100644 --- a/apps/server/src/modules/account/mapper/account-response.mapper.spec.ts +++ b/apps/server/src/modules/account/mapper/account-response.mapper.spec.ts @@ -1,5 +1,5 @@ import { Account } from '@shared/domain'; -import { AccountDto } from '@src/modules/account/services/dto/account.dto'; +import { AccountDto } from '@modules/account/services/dto/account.dto'; import { accountDtoFactory, accountFactory } from '@shared/testing'; import { AccountResponseMapper } from '.'; diff --git a/apps/server/src/modules/account/mapper/account-response.mapper.ts b/apps/server/src/modules/account/mapper/account-response.mapper.ts index 94437737df9..84e20519bfe 100644 --- a/apps/server/src/modules/account/mapper/account-response.mapper.ts +++ b/apps/server/src/modules/account/mapper/account-response.mapper.ts @@ -1,5 +1,5 @@ import { Account } from '@shared/domain'; -import { AccountDto } from '@src/modules/account/services/dto/account.dto'; +import { AccountDto } from '@modules/account/services/dto/account.dto'; import { AccountResponse } from '../controller/dto'; export class AccountResponseMapper { diff --git a/apps/server/src/modules/account/services/account-db.service.spec.ts b/apps/server/src/modules/account/services/account-db.service.spec.ts index 8cc6a33cbb6..7fa4c4e44a8 100644 --- a/apps/server/src/modules/account/services/account-db.service.spec.ts +++ b/apps/server/src/modules/account/services/account-db.service.spec.ts @@ -6,9 +6,9 @@ import { EntityNotFoundError } from '@shared/common'; import { Account, EntityId } from '@shared/domain'; import { IdentityManagementService } from '@shared/infra/identity-management/identity-management.service'; import { accountFactory, setupEntities, userFactory } from '@shared/testing'; -import { AccountEntityToDtoMapper } from '@src/modules/account/mapper'; -import { AccountDto } from '@src/modules/account/services/dto'; -import { IServerConfig } from '@src/modules/server'; +import { AccountEntityToDtoMapper } from '@modules/account/mapper'; +import { AccountDto } from '@modules/account/services/dto'; +import { IServerConfig } from '@modules/server'; import bcrypt from 'bcryptjs'; import { LegacyLogger } from '../../../core/logger'; import { AccountRepo } from '../repo/account.repo'; diff --git a/apps/server/src/modules/account/services/account-idm.service.integration.spec.ts b/apps/server/src/modules/account/services/account-idm.service.integration.spec.ts index f50e8fcc07e..2249a485f98 100644 --- a/apps/server/src/modules/account/services/account-idm.service.integration.spec.ts +++ b/apps/server/src/modules/account/services/account-idm.service.integration.spec.ts @@ -5,7 +5,7 @@ import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { IdmAccount } from '@shared/domain'; import { KeycloakAdministrationService } from '@shared/infra/identity-management/keycloak-administration/service/keycloak-administration.service'; -import { AccountSaveDto } from '@src/modules/account/services/dto'; +import { AccountSaveDto } from '@modules/account/services/dto'; import { LoggerModule } from '@src/core/logger'; import { IdentityManagementModule } from '@shared/infra/identity-management'; import { IdentityManagementService } from '../../../shared/infra/identity-management/identity-management.service'; diff --git a/apps/server/src/modules/account/services/account-lookup.service.ts b/apps/server/src/modules/account/services/account-lookup.service.ts index a0e18870177..b1549590c5b 100644 --- a/apps/server/src/modules/account/services/account-lookup.service.ts +++ b/apps/server/src/modules/account/services/account-lookup.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { EntityId } from '@shared/domain'; import { IdentityManagementService } from '@shared/infra/identity-management'; -import { IServerConfig } from '@src/modules/server/server.config'; +import { IServerConfig } from '@modules/server/server.config'; import { ObjectId } from 'bson'; /** diff --git a/apps/server/src/modules/account/services/account.service.spec.ts b/apps/server/src/modules/account/services/account.service.spec.ts index 834bc5b0f89..67851d0a6b0 100644 --- a/apps/server/src/modules/account/services/account.service.spec.ts +++ b/apps/server/src/modules/account/services/account.service.spec.ts @@ -1,7 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { IServerConfig } from '@src/modules/server'; +import { IServerConfig } from '@modules/server'; import { LegacyLogger } from '../../../core/logger'; import { AccountServiceDb } from './account-db.service'; import { AccountServiceIdm } from './account-idm.service'; diff --git a/apps/server/src/modules/account/services/index.ts b/apps/server/src/modules/account/services/index.ts new file mode 100644 index 00000000000..72778be1f1e --- /dev/null +++ b/apps/server/src/modules/account/services/index.ts @@ -0,0 +1 @@ +export * from './account.service'; diff --git a/apps/server/src/modules/account/uc/account.uc.spec.ts b/apps/server/src/modules/account/uc/account.uc.spec.ts index e210a5c9ab5..526720c43f7 100644 --- a/apps/server/src/modules/account/uc/account.uc.spec.ts +++ b/apps/server/src/modules/account/uc/account.uc.spec.ts @@ -16,10 +16,10 @@ import { import { UserRepo } from '@shared/repo'; import { accountFactory, schoolFactory, setupEntities, systemFactory, userFactory } from '@shared/testing'; import { BruteForcePrevention } from '@src/imports-from-feathers'; -import { AccountService } from '@src/modules/account/services/account.service'; -import { AccountSaveDto } from '@src/modules/account/services/dto'; -import { AccountDto } from '@src/modules/account/services/dto/account.dto'; -import { ICurrentUser } from '@src/modules/authentication'; +import { AccountService } from '@modules/account/services/account.service'; +import { AccountSaveDto } from '@modules/account/services/dto'; +import { AccountDto } from '@modules/account/services/dto/account.dto'; +import { ICurrentUser } from '@modules/authentication'; import { ObjectId } from 'bson'; import { AccountByIdBodyParams, diff --git a/apps/server/src/modules/account/uc/account.uc.ts b/apps/server/src/modules/account/uc/account.uc.ts index 2db4ed8843f..f9e21b28c63 100644 --- a/apps/server/src/modules/account/uc/account.uc.ts +++ b/apps/server/src/modules/account/uc/account.uc.ts @@ -9,11 +9,11 @@ import { import { Account, EntityId, Permission, PermissionService, Role, RoleName, SchoolEntity, User } from '@shared/domain'; import { UserRepo } from '@shared/repo'; // TODO: module internals should be imported with relative paths -import { AccountService } from '@src/modules/account/services/account.service'; -import { AccountDto } from '@src/modules/account/services/dto/account.dto'; +import { AccountService } from '@modules/account/services/account.service'; +import { AccountDto } from '@modules/account/services/dto/account.dto'; import { BruteForcePrevention } from '@src/imports-from-feathers'; -import { ICurrentUser } from '@src/modules/authentication'; +import { ICurrentUser } from '@modules/authentication'; import { ObjectId } from 'bson'; import { IAccountConfig } from '../account-config'; import { diff --git a/apps/server/src/modules/authentication/authentication.module.ts b/apps/server/src/modules/authentication/authentication.module.ts index 1cfdca45fde..26d20a4dfc8 100644 --- a/apps/server/src/modules/authentication/authentication.module.ts +++ b/apps/server/src/modules/authentication/authentication.module.ts @@ -5,10 +5,10 @@ import { CacheWrapperModule } from '@shared/infra/cache'; import { IdentityManagementModule } from '@shared/infra/identity-management'; import { LegacySchoolRepo, 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'; -import { RoleModule } from '@src/modules/role'; -import { SystemModule } from '@src/modules/system'; +import { AccountModule } from '@modules/account'; +import { OauthModule } from '@modules/oauth/oauth.module'; +import { RoleModule } from '@modules/role'; +import { SystemModule } from '@modules/system'; import { Algorithm, SignOptions } from 'jsonwebtoken'; import { jwtConstants } from './constants'; import { AuthenticationService } from './services/authentication.service'; diff --git a/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts b/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts index 80fc43734bc..253d692055d 100644 --- a/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts +++ b/apps/server/src/modules/authentication/controllers/api-test/login.api.spec.ts @@ -3,15 +3,15 @@ import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Account, RoleName, SchoolEntity, SystemEntity, User } from '@shared/domain'; import { accountFactory, roleFactory, schoolFactory, systemFactory, userFactory } from '@shared/testing'; -import { SSOErrorCode } from '@src/modules/oauth/error/sso-error-code.enum'; -import { OauthTokenResponse } from '@src/modules/oauth/service/dto'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { SSOErrorCode } from '@modules/oauth/loggable'; +import { OauthTokenResponse } from '@modules/oauth/service/dto'; +import { ServerTestModule } from '@modules/server/server.module'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import crypto, { KeyPairKeyObjectResult } from 'crypto'; import jwt from 'jsonwebtoken'; import request, { Response } from 'supertest'; -import { LdapAuthorizationBodyParams, LocalAuthorizationBodyParams } from '../dto'; +import { LdapAuthorizationBodyParams, LocalAuthorizationBodyParams, OauthLoginResponse } from '../dto'; const ldapAccountUserName = 'ldapAccountUserName'; const mockUserLdapDN = 'mockUserLdapDN'; @@ -129,6 +129,7 @@ describe('Login Controller (api)', () => { expect(decodedToken).toHaveProperty('accountId'); expect(decodedToken).toHaveProperty('schoolId'); expect(decodedToken).toHaveProperty('roles'); + expect(decodedToken).not.toHaveProperty('externalIdToken'); }); }); @@ -193,6 +194,7 @@ describe('Login Controller (api)', () => { expect(decodedToken).toHaveProperty('accountId'); expect(decodedToken).toHaveProperty('schoolId'); expect(decodedToken).toHaveProperty('roles'); + expect(decodedToken).not.toHaveProperty('externalIdToken'); }); }); @@ -253,10 +255,30 @@ describe('Login Controller (api)', () => { return { system, + idToken, }; }; - it('should return jwt', async () => { + it('should return oauth login response', async () => { + const { system, idToken } = await setup(); + + const response: Response = await request(app.getHttpServer()) + .post(`${basePath}/oauth2`) + .send({ + redirectUri: 'redirectUri', + code: 'code', + systemId: system.id, + }) + .expect(HttpStatus.OK); + + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + expect(response.body).toEqual({ + accessToken: expect.any(String), + externalIdToken: idToken, + }); + }); + + it('should return a valid jwt as access token', async () => { const { system } = await setup(); const response: Response = await request(app.getHttpServer()) @@ -268,8 +290,15 @@ describe('Login Controller (api)', () => { }) .expect(HttpStatus.OK); + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access,@typescript-eslint/no-unsafe-argument + const decodedToken = jwt.decode(response.body.accessToken); // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access expect(response.body.accessToken).toBeDefined(); + expect(decodedToken).toHaveProperty('userId'); + expect(decodedToken).toHaveProperty('accountId'); + expect(decodedToken).toHaveProperty('schoolId'); + expect(decodedToken).toHaveProperty('roles'); + expect(decodedToken).not.toHaveProperty('externalIdToken'); }); }); diff --git a/apps/server/src/modules/authentication/controllers/dto/index.ts b/apps/server/src/modules/authentication/controllers/dto/index.ts index 513041f604a..69c69055f74 100644 --- a/apps/server/src/modules/authentication/controllers/dto/index.ts +++ b/apps/server/src/modules/authentication/controllers/dto/index.ts @@ -2,3 +2,4 @@ export * from './oauth2-authorization.body.params'; export * from './login.response'; export * from './ldap-authorization.body.params'; export * from './local-authorization.body.params'; +export * from './oauth-login.response'; diff --git a/apps/server/src/modules/authentication/controllers/dto/oauth-login.response.ts b/apps/server/src/modules/authentication/controllers/dto/oauth-login.response.ts new file mode 100644 index 00000000000..53bb3cc6b38 --- /dev/null +++ b/apps/server/src/modules/authentication/controllers/dto/oauth-login.response.ts @@ -0,0 +1,15 @@ +import { ApiPropertyOptional } from '@nestjs/swagger'; +import { LoginResponse } from './login.response'; + +export class OauthLoginResponse extends LoginResponse { + @ApiPropertyOptional({ + description: + 'The external id token which is from the external oauth system and set when scope openid is available.', + }) + externalIdToken?: string; + + constructor(props: OauthLoginResponse) { + super(props); + this.externalIdToken = props.externalIdToken; + } +} diff --git a/apps/server/src/modules/authentication/controllers/login.controller.ts b/apps/server/src/modules/authentication/controllers/login.controller.ts index d6d1d1f0b40..68af396233e 100644 --- a/apps/server/src/modules/authentication/controllers/login.controller.ts +++ b/apps/server/src/modules/authentication/controllers/login.controller.ts @@ -2,8 +2,8 @@ import { Body, Controller, HttpCode, HttpStatus, Post, UseGuards } from '@nestjs import { AuthGuard } from '@nestjs/passport'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ForbiddenOperationError, ValidationError } from '@shared/common'; -import { CurrentUser } from '../decorator/auth.decorator'; -import type { ICurrentUser } from '../interface'; +import { CurrentUser } from '../decorator'; +import type { ICurrentUser, OauthCurrentUser } from '../interface'; import { LoginDto } from '../uc/dto'; import { LoginUc } from '../uc/login.uc'; import { @@ -11,6 +11,7 @@ import { LocalAuthorizationBodyParams, LoginResponse, Oauth2AuthorizationBodyParams, + OauthLoginResponse, } from './dto'; import { LoginResponseMapper } from './mapper/login-response.mapper'; @@ -30,7 +31,7 @@ export class LoginController { async loginLdap(@CurrentUser() user: ICurrentUser, @Body() _: LdapAuthorizationBodyParams): Promise { const loginDto: LoginDto = await this.loginUc.getLoginData(user); - const mapped: LoginResponse = LoginResponseMapper.mapLoginDtoToResponse(loginDto); + const mapped: LoginResponse = LoginResponseMapper.mapToLoginResponse(loginDto); return mapped; } @@ -46,7 +47,7 @@ export class LoginController { async loginLocal(@CurrentUser() user: ICurrentUser, @Body() _: LocalAuthorizationBodyParams): Promise { const loginDto: LoginDto = await this.loginUc.getLoginData(user); - const mapped: LoginResponse = LoginResponseMapper.mapLoginDtoToResponse(loginDto); + const mapped: LoginResponse = LoginResponseMapper.mapToLoginResponse(loginDto); return mapped; } @@ -59,13 +60,13 @@ export class LoginController { @ApiResponse({ status: 400, type: ValidationError, description: 'Request data has invalid format.' }) @ApiResponse({ status: 403, type: ForbiddenOperationError, description: 'Invalid user credentials.' }) async loginOauth2( - @CurrentUser() user: ICurrentUser, + @CurrentUser() user: OauthCurrentUser, // eslint-disable-next-line @typescript-eslint/no-unused-vars @Body() _: Oauth2AuthorizationBodyParams - ): Promise { + ): Promise { const loginDto: LoginDto = await this.loginUc.getLoginData(user); - const mapped: LoginResponse = LoginResponseMapper.mapLoginDtoToResponse(loginDto); + const mapped: OauthLoginResponse = LoginResponseMapper.mapToOauthLoginResponse(loginDto, user.externalIdToken); return mapped; } diff --git a/apps/server/src/modules/authentication/controllers/mapper/login-response.mapper.ts b/apps/server/src/modules/authentication/controllers/mapper/login-response.mapper.ts index aeffbfe8960..9ff67a39095 100644 --- a/apps/server/src/modules/authentication/controllers/mapper/login-response.mapper.ts +++ b/apps/server/src/modules/authentication/controllers/mapper/login-response.mapper.ts @@ -1,9 +1,20 @@ -import { LoginResponse } from '../dto'; import { LoginDto } from '../../uc/dto'; +import { LoginResponse, OauthLoginResponse } from '../dto'; export class LoginResponseMapper { - static mapLoginDtoToResponse(loginDto: LoginDto): LoginResponse { - const response: LoginResponse = new LoginResponse({ accessToken: loginDto.accessToken }); + static mapToLoginResponse(loginDto: LoginDto): LoginResponse { + const response: LoginResponse = new LoginResponse({ + accessToken: loginDto.accessToken, + }); + + return response; + } + + static mapToOauthLoginResponse(loginDto: LoginDto, externalIdToken?: string): OauthLoginResponse { + const response: OauthLoginResponse = new OauthLoginResponse({ + accessToken: loginDto.accessToken, + externalIdToken, + }); return response; } diff --git a/apps/server/src/modules/authentication/decorator/auth.decorator.spec.ts b/apps/server/src/modules/authentication/decorator/auth.decorator.spec.ts index bc5a9361b78..f193bd67516 100644 --- a/apps/server/src/modules/authentication/decorator/auth.decorator.spec.ts +++ b/apps/server/src/modules/authentication/decorator/auth.decorator.spec.ts @@ -2,8 +2,8 @@ import { Controller, ExecutionContext, ForbiddenException, Get, INestApplication } from '@nestjs/common'; import request from 'supertest'; import { Test, TestingModule } from '@nestjs/testing'; -import { ICurrentUser } from '@src/modules/authentication'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { ICurrentUser } from '@modules/authentication'; +import { ServerTestModule } from '@modules/server/server.module'; import { JwtAuthGuard } from '../guard/jwt-auth.guard'; import { Authenticate, CurrentUser, JWT } from './auth.decorator'; diff --git a/apps/server/src/modules/authentication/decorator/index.ts b/apps/server/src/modules/authentication/decorator/index.ts new file mode 100644 index 00000000000..9795c6d38ee --- /dev/null +++ b/apps/server/src/modules/authentication/decorator/index.ts @@ -0,0 +1 @@ +export * from './auth.decorator'; diff --git a/apps/server/src/modules/authentication/errors/index.ts b/apps/server/src/modules/authentication/errors/index.ts new file mode 100644 index 00000000000..d87d53df8bf --- /dev/null +++ b/apps/server/src/modules/authentication/errors/index.ts @@ -0,0 +1,4 @@ +export * from './brute-force.error'; +export * from './ldap-connection.error'; +export * from './school-in-migration.error'; +export * from './unauthorized.loggable-exception'; diff --git a/apps/server/src/modules/authentication/index.ts b/apps/server/src/modules/authentication/index.ts index eaaa9dbb61a..904c64ff97b 100644 --- a/apps/server/src/modules/authentication/index.ts +++ b/apps/server/src/modules/authentication/index.ts @@ -1,2 +1,2 @@ -export * from './interface'; -export * from './guard/jwt-auth.guard'; +export { ICurrentUser } from './interface'; +export { JWT, CurrentUser, Authenticate } from './decorator'; diff --git a/apps/server/src/modules/authentication/interface/jwt-payload.ts b/apps/server/src/modules/authentication/interface/jwt-payload.ts index ed69630ccde..aad11700e60 100644 --- a/apps/server/src/modules/authentication/interface/jwt-payload.ts +++ b/apps/server/src/modules/authentication/interface/jwt-payload.ts @@ -5,6 +5,7 @@ export interface CreateJwtPayload { roles: string[]; systemId?: string; // without this the user needs to change his PW during first login support?: boolean; + // support UserId is missed see featherJS } export interface JwtPayload extends CreateJwtPayload { diff --git a/apps/server/src/modules/authentication/interface/user.ts b/apps/server/src/modules/authentication/interface/user.ts index 1baa64d192b..a070367a43b 100644 --- a/apps/server/src/modules/authentication/interface/user.ts +++ b/apps/server/src/modules/authentication/interface/user.ts @@ -1,29 +1,5 @@ import { EntityId } from '@shared/domain'; -export interface IRole { - name: string; - - id: string; -} - -export interface IResolvedUser { - firstName: string; - - lastName: string; - - id: string; - - createdAt: Date; - - updatedAt: Date; - - roles: IRole[]; - - permissions: string[]; - - schoolId: string; -} - export interface ICurrentUser { /** authenticated users id */ userId: EntityId; @@ -40,3 +16,8 @@ export interface ICurrentUser { /** True if a support member impersonates the user */ impersonated?: boolean; } + +export interface OauthCurrentUser extends ICurrentUser { + /** Contains the idToken of the external idp. Will be set during oAuth2 login and used for rp initiated logout */ + externalIdToken?: string; +} diff --git a/apps/server/src/modules/authentication/mapper/current-user.mapper.spec.ts b/apps/server/src/modules/authentication/mapper/current-user.mapper.spec.ts index c89d1e3a037..09a76c1ebfb 100644 --- a/apps/server/src/modules/authentication/mapper/current-user.mapper.spec.ts +++ b/apps/server/src/modules/authentication/mapper/current-user.mapper.spec.ts @@ -2,8 +2,8 @@ import { ValidationError } from '@shared/common'; import { Permission, RoleName } from '@shared/domain'; import { UserDO } from '@shared/domain/domainobject/user.do'; import { roleFactory, schoolFactory, setupEntities, userDoFactory, userFactory } from '@shared/testing'; -import { ICurrentUser } from '../interface'; -import { JwtPayload } from '../interface/jwt-payload'; +import { ICurrentUser, OauthCurrentUser } from '../interface'; +import { CreateJwtPayload, JwtPayload } from '../interface/jwt-payload'; import { CurrentUserMapper } from './current-user.mapper'; describe('CurrentUserMapper', () => { @@ -56,40 +56,89 @@ describe('CurrentUserMapper', () => { }); }); - describe('userDoToICurrentUser', () => { - const userId = 'mockUserId'; + describe('OauthCurrentUser', () => { + const userIdMock = 'mockUserId'; describe('when userDO has no ID', () => { it('should throw error', () => { const user: UserDO = userDoFactory.build({ createdAt: new Date(), updatedAt: new Date() }); - expect(() => CurrentUserMapper.userDoToICurrentUser(accountId, user)).toThrow(ValidationError); + expect(() => CurrentUserMapper.mapToOauthCurrentUser(accountId, user, undefined, 'idToken')).toThrow( + ValidationError + ); }); }); describe('when userDO is valid', () => { - it('should return valid ICurrentUser instance', () => { - const user: UserDO = userDoFactory.buildWithId({ id: userId, createdAt: new Date(), updatedAt: new Date() }); - const currentUser = CurrentUserMapper.userDoToICurrentUser(accountId, user); - expect(currentUser).toMatchObject({ + const setup = () => { + const user: UserDO = userDoFactory.buildWithId({ + id: userIdMock, + createdAt: new Date(), + updatedAt: new Date(), + }); + const idToken = 'idToken'; + + return { + user, + userId: user.id as string, + idToken, + }; + }; + + it('should return valid oauth current user instance', () => { + const { user, userId, idToken } = setup(); + + const currentUser: OauthCurrentUser = CurrentUserMapper.mapToOauthCurrentUser( + accountId, + user, + undefined, + idToken + ); + + expect(currentUser).toMatchObject({ accountId, systemId: undefined, roles: [], schoolId: user.schoolId, - userId: user.id, + userId, + externalIdToken: idToken, }); }); }); describe('when userDO is valid and a systemId is provided', () => { - it('should return valid ICurrentUser instance with systemId', () => { - const user: UserDO = userDoFactory.buildWithId({ id: userId, createdAt: new Date(), updatedAt: new Date() }); + const setup = () => { + const user: UserDO = userDoFactory.buildWithId({ + id: userIdMock, + createdAt: new Date(), + updatedAt: new Date(), + }); const systemId = 'mockSystemId'; - const currentUser = CurrentUserMapper.userDoToICurrentUser(accountId, user, systemId); - expect(currentUser).toMatchObject({ + const idToken = 'idToken'; + + return { + user, + userId: user.id as string, + idToken, + systemId, + }; + }; + + it('should return valid ICurrentUser instance with systemId', () => { + const { user, userId, systemId, idToken } = setup(); + + const currentUser: OauthCurrentUser = CurrentUserMapper.mapToOauthCurrentUser( + accountId, + user, + systemId, + idToken + ); + + expect(currentUser).toMatchObject({ accountId, systemId, roles: [], schoolId: user.schoolId, - userId: user.id, + userId, + externalIdToken: idToken, }); }); }); @@ -104,7 +153,7 @@ describe('CurrentUserMapper', () => { }, ]) .buildWithId({ - id: userId, + id: userIdMock, createdAt: new Date(), updatedAt: new Date(), }); @@ -117,7 +166,7 @@ describe('CurrentUserMapper', () => { it('should return valid ICurrentUser instance without systemId', () => { const { user } = setup(); - const currentUser = CurrentUserMapper.userDoToICurrentUser(accountId, user); + const currentUser = CurrentUserMapper.mapToOauthCurrentUser(accountId, user, undefined, 'idToken'); expect(currentUser).toMatchObject({ accountId, @@ -158,6 +207,7 @@ describe('CurrentUserMapper', () => { }); }); }); + describe('when JWT is provided without optional claims', () => { it('should return current user', () => { const jwtPayload: JwtPayload = { @@ -182,4 +232,28 @@ describe('CurrentUserMapper', () => { }); }); }); + + describe('mapCurrentUserToCreateJwtPayload', () => { + it('should map current user to create jwt payload', () => { + const currentUser: ICurrentUser = { + accountId: 'dummyAccountId', + systemId: 'dummySystemId', + roles: ['mockRoleId'], + schoolId: 'dummySchoolId', + userId: 'dummyUserId', + impersonated: true, + }; + + const createJwtPayload: CreateJwtPayload = CurrentUserMapper.mapCurrentUserToCreateJwtPayload(currentUser); + + expect(createJwtPayload).toMatchObject({ + accountId: currentUser.accountId, + systemId: currentUser.systemId, + roles: currentUser.roles, + schoolId: currentUser.schoolId, + userId: currentUser.userId, + support: currentUser.impersonated, + }); + }); + }); }); diff --git a/apps/server/src/modules/authentication/mapper/current-user.mapper.ts b/apps/server/src/modules/authentication/mapper/current-user.mapper.ts index d4eb31fbede..80ca91b56b0 100644 --- a/apps/server/src/modules/authentication/mapper/current-user.mapper.ts +++ b/apps/server/src/modules/authentication/mapper/current-user.mapper.ts @@ -2,8 +2,8 @@ import { ValidationError } from '@shared/common'; import { Role, User } from '@shared/domain'; import { RoleReference } from '@shared/domain/domainobject'; import { UserDO } from '@shared/domain/domainobject/user.do'; -import { ICurrentUser } from '../interface'; -import { JwtPayload } from '../interface/jwt-payload'; +import { ICurrentUser, OauthCurrentUser } from '../interface'; +import { CreateJwtPayload, JwtPayload } from '../interface/jwt-payload'; export class CurrentUserMapper { static userToICurrentUser(accountId: string, user: User, systemId?: string): ICurrentUser { @@ -16,7 +16,12 @@ export class CurrentUserMapper { }; } - static userDoToICurrentUser(accountId: string, user: UserDO, systemId?: string): ICurrentUser { + static mapToOauthCurrentUser( + accountId: string, + user: UserDO, + systemId?: string, + externalIdToken?: string + ): OauthCurrentUser { if (!user.id) { throw new ValidationError('user has no ID'); } @@ -27,6 +32,18 @@ export class CurrentUserMapper { roles: user.roles.map((roleRef: RoleReference) => roleRef.id), schoolId: user.schoolId, userId: user.id, + externalIdToken, + }; + } + + static mapCurrentUserToCreateJwtPayload(currentUser: ICurrentUser): CreateJwtPayload { + return { + accountId: currentUser.accountId, + userId: currentUser.userId, + schoolId: currentUser.schoolId, + roles: currentUser.roles, + systemId: currentUser.systemId, + support: currentUser.impersonated, }; } diff --git a/apps/server/src/modules/authentication/services/authentication.service.spec.ts b/apps/server/src/modules/authentication/services/authentication.service.spec.ts index 461fe0e7246..3d5b6d3a1b7 100644 --- a/apps/server/src/modules/authentication/services/authentication.service.spec.ts +++ b/apps/server/src/modules/authentication/services/authentication.service.spec.ts @@ -3,9 +3,9 @@ import { UnauthorizedException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; import { Test, TestingModule } from '@nestjs/testing'; -import { AccountService } from '@src/modules/account/services/account.service'; -import { AccountDto } from '@src/modules/account/services/dto'; -import { ICurrentUser } from '@src/modules/authentication'; +import { AccountService } from '@modules/account/services/account.service'; +import { AccountDto } from '@modules/account/services/dto'; +import { ICurrentUser } from '@modules/authentication'; import jwt from 'jsonwebtoken'; import { BruteForceError } from '../errors/brute-force.error'; import { JwtValidationAdapter } from '../strategy/jwt-validation.adapter'; diff --git a/apps/server/src/modules/authentication/services/authentication.service.ts b/apps/server/src/modules/authentication/services/authentication.service.ts index f4bdf319e6b..41aab6153ea 100644 --- a/apps/server/src/modules/authentication/services/authentication.service.ts +++ b/apps/server/src/modules/authentication/services/authentication.service.ts @@ -1,14 +1,15 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { JwtService } from '@nestjs/jwt'; -import { AccountService } from '@src/modules/account/services/account.service'; -import { AccountDto } from '@src/modules/account/services/dto'; -import { JwtValidationAdapter } from '@src/modules/authentication/strategy/jwt-validation.adapter'; -import type { IServerConfig } from '@src/modules/server'; +import { AccountService } from '@modules/account'; +// invalid import +import { AccountDto } from '@modules/account/services/dto'; +// invalid import, can produce dependency cycles +import type { IServerConfig } from '@modules/server'; import { randomUUID } from 'crypto'; import jwt, { JwtPayload } from 'jsonwebtoken'; -import { BruteForceError } from '../errors/brute-force.error'; -import { UnauthorizedLoggableException } from '../errors/unauthorized.loggable-exception'; +import { JwtValidationAdapter } from '../strategy/jwt-validation.adapter'; +import { BruteForceError, UnauthorizedLoggableException } from '../errors'; import { CreateJwtPayload } from '../interface/jwt-payload'; import { LoginDto } from '../uc/dto'; 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 db4a1a60878..d686dfcac72 100644 --- a/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts +++ b/apps/server/src/modules/authentication/strategy/ldap.strategy.spec.ts @@ -15,7 +15,7 @@ import { userFactory, } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { AccountDto } from '@src/modules/account/services/dto'; +import { AccountDto } from '@modules/account/services/dto'; import { LdapAuthorizationBodyParams } from '../controllers/dto'; import { ICurrentUser } from '../interface'; import { AuthenticationService } from '../services/authentication.service'; diff --git a/apps/server/src/modules/authentication/strategy/ldap.strategy.ts b/apps/server/src/modules/authentication/strategy/ldap.strategy.ts index 5edc650ee9c..1622e434310 100644 --- a/apps/server/src/modules/authentication/strategy/ldap.strategy.ts +++ b/apps/server/src/modules/authentication/strategy/ldap.strategy.ts @@ -4,7 +4,7 @@ import { LegacySchoolDo, SystemEntity, User } from '@shared/domain'; import { LegacySchoolRepo, 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'; +import { AccountDto } from '@modules/account/services/dto'; import { Strategy } from 'passport-custom'; import { LdapAuthorizationBodyParams } from '../controllers/dto'; import { ICurrentUser } from '../interface'; diff --git a/apps/server/src/modules/authentication/strategy/local.strategy.spec.ts b/apps/server/src/modules/authentication/strategy/local.strategy.spec.ts index cac3d204a4b..d1330270fb7 100644 --- a/apps/server/src/modules/authentication/strategy/local.strategy.spec.ts +++ b/apps/server/src/modules/authentication/strategy/local.strategy.spec.ts @@ -5,9 +5,9 @@ import { RoleName, User } from '@shared/domain'; import { IdentityManagementOauthService } from '@shared/infra/identity-management'; import { UserRepo } from '@shared/repo'; import { accountFactory, setupEntities, userFactory } from '@shared/testing'; -import { AccountEntityToDtoMapper } from '@src/modules/account/mapper'; -import { AccountDto } from '@src/modules/account/services/dto'; -import { IServerConfig } from '@src/modules/server'; +import { AccountEntityToDtoMapper } from '@modules/account/mapper'; +import { AccountDto } from '@modules/account/services/dto'; +import { IServerConfig } from '@modules/server'; import bcrypt from 'bcryptjs'; import { AuthenticationService } from '../services/authentication.service'; import { LocalStrategy } from './local.strategy'; diff --git a/apps/server/src/modules/authentication/strategy/local.strategy.ts b/apps/server/src/modules/authentication/strategy/local.strategy.ts index ab34fe05b64..7963a5166e7 100644 --- a/apps/server/src/modules/authentication/strategy/local.strategy.ts +++ b/apps/server/src/modules/authentication/strategy/local.strategy.ts @@ -4,7 +4,7 @@ import { PassportStrategy } from '@nestjs/passport'; import { Injectable, UnauthorizedException } from '@nestjs/common'; import bcrypt from 'bcryptjs'; import { UserRepo } from '@shared/repo'; -import { AccountDto } from '@src/modules/account/services/dto'; +import { AccountDto } from '@modules/account/services/dto'; import { GuardAgainst } from '@shared/common/utils/guard-against'; import { IdentityManagementOauthService, IIdentityManagementConfig } from '@shared/infra/identity-management'; import { CurrentUserMapper } from '../mapper'; diff --git a/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts b/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts index 01feb93b044..8fd8f096dbe 100644 --- a/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts +++ b/apps/server/src/modules/authentication/strategy/oauth2.strategy.spec.ts @@ -4,12 +4,12 @@ import { Test, TestingModule } from '@nestjs/testing'; import { EntityId, RoleName } from '@shared/domain'; import { UserDO } from '@shared/domain/domainobject/user.do'; import { userDoFactory } from '@shared/testing'; -import { AccountService } from '@src/modules/account/services/account.service'; -import { AccountDto } from '@src/modules/account/services/dto'; -import { OAuthTokenDto } from '@src/modules/oauth'; -import { OAuthService } from '@src/modules/oauth/service/oauth.service'; +import { AccountService } from '@modules/account/services/account.service'; +import { AccountDto } from '@modules/account/services/dto'; +import { OAuthTokenDto } from '@modules/oauth'; +import { OAuthService } from '@modules/oauth/service/oauth.service'; import { SchoolInMigrationError } from '../errors/school-in-migration.error'; -import { ICurrentUser } from '../interface'; +import { ICurrentUser, OauthCurrentUser } from '../interface'; import { Oauth2Strategy } from './oauth2.strategy'; describe('Oauth2Strategy', () => { @@ -60,9 +60,10 @@ describe('Oauth2Strategy', () => { username: 'username', }); + const idToken = 'idToken'; oauthService.authenticateUser.mockResolvedValue( new OAuthTokenDto({ - idToken: 'idToken', + idToken, accessToken: 'accessToken', refreshToken: 'refreshToken', }) @@ -70,22 +71,23 @@ describe('Oauth2Strategy', () => { oauthService.provisionUser.mockResolvedValue({ user, redirect: '' }); accountService.findByUserId.mockResolvedValue(account); - return { systemId, user, account }; + return { systemId, user, account, idToken }; }; it('should return the ICurrentUser', async () => { - const { systemId, user, account } = setup(); + const { systemId, user, account, idToken } = setup(); const result: ICurrentUser = await strategy.validate({ body: { code: 'code', redirectUri: 'redirectUri', systemId }, }); - expect(result).toEqual({ + expect(result).toEqual({ systemId, userId: user.id as EntityId, roles: [user.roles[0].id], schoolId: user.schoolId, accountId: account.id, + externalIdToken: idToken, }); }); }); diff --git a/apps/server/src/modules/authentication/strategy/oauth2.strategy.ts b/apps/server/src/modules/authentication/strategy/oauth2.strategy.ts index 2d774594a3d..599744cc1a7 100644 --- a/apps/server/src/modules/authentication/strategy/oauth2.strategy.ts +++ b/apps/server/src/modules/authentication/strategy/oauth2.strategy.ts @@ -1,14 +1,14 @@ import { Injectable, UnauthorizedException } from '@nestjs/common'; import { PassportStrategy } from '@nestjs/passport'; import { UserDO } from '@shared/domain/domainobject/user.do'; -import { AccountService } from '@src/modules/account/services/account.service'; -import { AccountDto } from '@src/modules/account/services/dto'; -import { OAuthTokenDto } from '@src/modules/oauth'; -import { OAuthService } from '@src/modules/oauth/service/oauth.service'; +import { AccountService } from '@modules/account/services/account.service'; +import { AccountDto } from '@modules/account/services/dto'; +import { OAuthTokenDto } from '@modules/oauth'; +import { OAuthService } from '@modules/oauth/service/oauth.service'; import { Strategy } from 'passport-custom'; import { Oauth2AuthorizationBodyParams } from '../controllers/dto'; import { SchoolInMigrationError } from '../errors/school-in-migration.error'; -import { ICurrentUser } from '../interface'; +import { ICurrentUser, OauthCurrentUser } from '../interface'; import { CurrentUserMapper } from '../mapper'; @Injectable() @@ -37,7 +37,12 @@ export class Oauth2Strategy extends PassportStrategy(Strategy, 'oauth2') { throw new UnauthorizedException('no account found'); } - const currentUser: ICurrentUser = CurrentUserMapper.userDoToICurrentUser(account.id, user, systemId); + const currentUser: OauthCurrentUser = CurrentUserMapper.mapToOauthCurrentUser( + account.id, + user, + systemId, + tokenDto.idToken + ); return currentUser; } diff --git a/apps/server/src/modules/authentication/uc/login.uc.spec.ts b/apps/server/src/modules/authentication/uc/login.uc.spec.ts index 2a6b3ab12b9..a14b741ae88 100644 --- a/apps/server/src/modules/authentication/uc/login.uc.spec.ts +++ b/apps/server/src/modules/authentication/uc/login.uc.spec.ts @@ -1,6 +1,5 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { CreateJwtPayload } from '../interface/jwt-payload'; import { AuthenticationService } from '../services/authentication.service'; import { LoginDto } from './dto'; import { LoginUc } from './login.uc'; @@ -29,7 +28,15 @@ describe('LoginUc', () => { describe('getLoginData', () => { describe('when userInfo is given', () => { const setup = () => { - const userInfo: CreateJwtPayload = { accountId: '', roles: [], schoolId: '', userId: '' }; + const userInfo = { + accountId: '', + roles: [], + schoolId: '', + userId: '', + systemId: '', + impersonated: false, + someProperty: 'shouldNotBeMapped', + }; const loginDto: LoginDto = new LoginDto({ accessToken: 'accessToken' }); authenticationService.generateJwt.mockResolvedValue(loginDto); @@ -44,7 +51,14 @@ describe('LoginUc', () => { await loginUc.getLoginData(userInfo); - expect(authenticationService.generateJwt).toHaveBeenCalledWith(userInfo); + expect(authenticationService.generateJwt).toHaveBeenCalledWith({ + accountId: userInfo.accountId, + userId: userInfo.userId, + schoolId: userInfo.schoolId, + roles: userInfo.roles, + systemId: userInfo.systemId, + support: userInfo.impersonated, + }); }); it('should return a loginDto', async () => { diff --git a/apps/server/src/modules/authentication/uc/login.uc.ts b/apps/server/src/modules/authentication/uc/login.uc.ts index f6ce6fbdd16..2a6404b0a87 100644 --- a/apps/server/src/modules/authentication/uc/login.uc.ts +++ b/apps/server/src/modules/authentication/uc/login.uc.ts @@ -1,14 +1,18 @@ import { Injectable } from '@nestjs/common'; -import { AuthenticationService } from '../services/authentication.service'; +import { ICurrentUser } from '../interface'; import { CreateJwtPayload } from '../interface/jwt-payload'; +import { CurrentUserMapper } from '../mapper'; +import { AuthenticationService } from '../services/authentication.service'; import { LoginDto } from './dto'; @Injectable() export class LoginUc { constructor(private readonly authService: AuthenticationService) {} - async getLoginData(userInfo: CreateJwtPayload): Promise { - const accessTokenDto: LoginDto = await this.authService.generateJwt(userInfo); + async getLoginData(userInfo: ICurrentUser): Promise { + const createJwtPayload: CreateJwtPayload = CurrentUserMapper.mapCurrentUserToCreateJwtPayload(userInfo); + + const accessTokenDto: LoginDto = await this.authService.generateJwt(createJwtPayload); const loginDto: LoginDto = new LoginDto({ accessToken: accessTokenDto.accessToken, diff --git a/apps/server/src/modules/authorization/README.md b/apps/server/src/modules/authorization/README.md index 645feed2e64..7d2b69d209b 100644 --- a/apps/server/src/modules/authorization/README.md +++ b/apps/server/src/modules/authorization/README.md @@ -132,17 +132,7 @@ When calling other internal micro service for already authorized operations plea // next orchestration steps ``` -### Example 2 - Execute a Single Operation with Loading Resources - -```javascript -// If you don't have an entity but an entity type and id, you can check permission by reference -await this.authorizationService.checkPermissionByReferences(userId, AllowedEntity.course, courseId, AuthorizationContextBuilder.read([])); -// or -await this.authorizationService.hasPermissionByReferences(userId, AllowedEntity.course, courseId, AuthorizationContextBuilder.read([])); -// next orchestration steps -``` - -### Example 3 - Set Permission(s) of User as Required +### Example 2 - Set Permission(s) of User as Required ```javascript // Multiple permissions can be added. For a successful authorization, the user need all of them. @@ -173,14 +163,13 @@ this.authorizationService.hasPermission(userId, course, PermissionContexts.creat ```ts async createSchoolBySuperhero(userId: EntityId, params: { name: string }) { - const user = this.authorizationService.getUserWithPermissions(userId); - this.authorizationService.hasAllPermissions(user, [Permission.SCHOOL_CREATE]); - - const school = new School(params); + const user = this.authorizationService.getUserWithPermissions(userId); + this.authorizationService.hasAllPermissions(user, [Permission.SCHOOL_CREATE]); - await this.schoolService.save(school); + const school = new School(params); + await this.schoolService.save(school); - return true; + return true; } ``` @@ -191,15 +180,15 @@ async createSchoolBySuperhero(userId: EntityId, params: { name: string }) { async createUserByAdmin(userId: EntityId, params: { email: string, firstName: string, lastName: string, schoolId: EntityId }) { - const user = this.authorizationService.getUserWithPermissions(userId); - - await this.authorizationService.checkPermissionByReferences(userId, AllowedEntity.school, schoolId, AuthorizationContextBuilder.write([Permission.INSTANCE, Permission.CREATE_USER])); - - const newUser = new User(params) + const user = this.authorizationService.getUserWithPermissions(userId); + + const context = AuthorizationContextBuilder.write([Permission.INSTANCE, Permission.CREATE_USER]) + await this.authorizationService.checkPermission(user, school, context); - await this.userService.save(newUser); + const newUser = new User(params) + await this.userService.save(newUser); - return true; + return true; } ``` @@ -210,18 +199,17 @@ async createUserByAdmin(userId: EntityId, params: { email: string, firstName: st // admin async editCourseByAdmin(userId: EntityId, params: { courseId: EntityId, description: string }) { - const course = this.courseService.getCourse(params.courseId); - const user = this.authorizationService.getUserWithPermissions(userId); - - const school = course.school - - this.authorizationService.hasPermissions(user, school, [Permission.INSTANCE, Permission.COURSE_EDIT]); + const course = this.courseService.getCourse(params.courseId); + const user = this.authorizationService.getUserWithPermissions(userId); + const school = course.school; - course.description = params.description; + const context = AuthorizationContextBuilder.write([Permission.INSTANCE, Permission.CREATE_USER]); + this.authorizationService.checkPermissions(user, school, context); - await this.courseService.save(course); + course.description = params.description; + await this.courseService.save(course); - return true; + return true; } ``` @@ -234,18 +222,17 @@ async createCourse(userId: EntityId, params: { schoolId: EntityId }) { const user = this.authorizationService.getUserWithPermissions(userId); const school = this.schoolService.getSchool(params.schoolId); - this.authorizationService.checkPermission(user, school - { - action: Actions.write, - requiredPermissions: [Permission.COURSE_CREATE], - } - ); + this.authorizationService.checkPermission(user, school + { + action: Actions.write, + requiredPermissions: [Permission.COURSE_CREATE], + } + ); - const course = new Course({ school }); + const course = new Course({ school }); + await this.courseService.saveCourse(course); - await this.courseService.saveCourse(course); - - return course; + return course; } ``` @@ -255,21 +242,20 @@ async createCourse(userId: EntityId, params: { schoolId: EntityId }) { ```ts // User can create a lesson to course, so you have a courseId async createLesson(userId: EntityId, params: { courseId: EntityId }) { - const course = this.courseService.getCourse(params.courseId); - const user = this.authorizationService.getUserWithPermissions(userId); + const course = this.courseService.getCourse(params.courseId); + const user = this.authorizationService.getUserWithPermissions(userId); // check authorization for user and course - this.authorizationService.checkPermission(user, course - { - action: Actions.write, - requiredPermissions: [Permission.COURSE_EDIT], - } - ); - - const lesson = new Lesson({course}); + this.authorizationService.checkPermission(user, course + { + action: Actions.write, + requiredPermissions: [Permission.COURSE_EDIT], + } + ); - await this.lessonService.saveLesson(lesson); + const lesson = new Lesson({course}); + await this.lessonService.saveLesson(lesson); - return true; + return true; } ``` @@ -345,8 +331,9 @@ The authorization module is the core of authorization. It collects all needed in ### Reference.loader -For situations where only the id and the domain object (string) type is known, it is possible to use the \*ByReferences methods. -They load the reference directly. +It should be use only inside of the authorization module. +It is use to load registrated ressouces by the id and name of the ressource. +This is needed to solve the API requests from external services. (API implementation is missing for now) > Please keep in mind that it can have an impact on the performance if you use it wrongly. > We keep it as a seperate method to avoid the usage in areas where the domain object should exist, because we see the risk that a developer could be tempted by the ease of only passing the id. diff --git a/apps/server/src/modules/authorization/authorization-reference.module.ts b/apps/server/src/modules/authorization/authorization-reference.module.ts new file mode 100644 index 00000000000..e253587af7b --- /dev/null +++ b/apps/server/src/modules/authorization/authorization-reference.module.ts @@ -0,0 +1,43 @@ +import { forwardRef, Module } from '@nestjs/common'; +import { + CourseGroupRepo, + CourseRepo, + LessonRepo, + SchoolExternalToolRepo, + LegacySchoolRepo, + SubmissionRepo, + TaskRepo, + TeamsRepo, + UserRepo, +} from '@shared/repo'; +import { ToolModule } from '@modules/tool'; +import { LoggerModule } from '@src/core/logger'; +import { BoardModule } from '@modules/board'; +import { ReferenceLoader, AuthorizationReferenceService, AuthorizationHelper } from './domain'; +import { AuthorizationModule } from './authorization.module'; + +/** + * This module is part of an intermediate state. In the future it should be replaced by an AuthorizationApiModule. + * For now it is used where the authorization itself needs to load data from the database. + * Avoid using this module and load the needed data in your use cases and then use the normal AuthorizationModule! + */ +@Module({ + // TODO: remove forwardRef to TooModule N21-1055 + imports: [AuthorizationModule, forwardRef(() => ToolModule), forwardRef(() => BoardModule), LoggerModule], + providers: [ + AuthorizationHelper, + ReferenceLoader, + UserRepo, + CourseRepo, + CourseGroupRepo, + TaskRepo, + LegacySchoolRepo, + LessonRepo, + TeamsRepo, + SubmissionRepo, + SchoolExternalToolRepo, + AuthorizationReferenceService, + ], + exports: [AuthorizationReferenceService], +}) +export class AuthorizationReferenceModule {} diff --git a/apps/server/src/modules/authorization/authorization.module.ts b/apps/server/src/modules/authorization/authorization.module.ts index c983ee187fd..c555f13dc7b 100644 --- a/apps/server/src/modules/authorization/authorization.module.ts +++ b/apps/server/src/modules/authorization/authorization.module.ts @@ -1,53 +1,48 @@ -import { forwardRef, Module } from '@nestjs/common'; -import { ALL_RULES } from '@shared/domain/rules'; +import { Module } from '@nestjs/common'; +import { UserRepo } from '@shared/repo'; +import { LoggerModule } from '@src/core/logger'; import { FeathersModule } from '@shared/infra/feathers'; import { - CourseGroupRepo, - CourseRepo, - LessonRepo, - SchoolExternalToolRepo, - LegacySchoolRepo, - SubmissionRepo, - TaskRepo, - TeamsRepo, - UserRepo, -} from '@shared/repo'; -import { LoggerModule } from '@src/core/logger'; -import { LegacySchoolModule } from '@src/modules/legacy-school'; -import { ToolModule } from '@src/modules/tool'; -import { BoardModule } from '../board'; -import { AuthorizationHelper } from './authorization.helper'; -import { AuthorizationService } from './authorization.service'; + BoardDoRule, + ContextExternalToolRule, + CourseGroupRule, + CourseRule, + LessonRule, + SchoolExternalToolRule, + SubmissionRule, + TaskRule, + TeamRule, + UserRule, + UserLoginMigrationRule, + LegacySchoolRule, + GroupRule, +} from './domain/rules'; +import { AuthorizationHelper, AuthorizationService, RuleManager } from './domain'; import { FeathersAuthorizationService, FeathersAuthProvider } from './feathers'; -import { ReferenceLoader } from './reference.loader'; -import { RuleManager } from './rule-manager'; @Module({ - // TODO: remove forwardRef to TooModule N21-1055 - imports: [ - FeathersModule, - LoggerModule, - LegacySchoolModule, - forwardRef(() => ToolModule), - forwardRef(() => BoardModule), - ], + imports: [FeathersModule, LoggerModule], providers: [ FeathersAuthorizationService, FeathersAuthProvider, AuthorizationService, - ...ALL_RULES, - ReferenceLoader, UserRepo, - CourseRepo, - CourseGroupRepo, - TaskRepo, - LegacySchoolRepo, - LessonRepo, - TeamsRepo, - SubmissionRepo, - SchoolExternalToolRepo, RuleManager, AuthorizationHelper, + // rules + BoardDoRule, + ContextExternalToolRule, + CourseGroupRule, + CourseRule, + GroupRule, + LessonRule, + SchoolExternalToolRule, + SubmissionRule, + TaskRule, + TeamRule, + UserRule, + UserLoginMigrationRule, + LegacySchoolRule, ], exports: [FeathersAuthorizationService, AuthorizationService], }) diff --git a/apps/server/src/modules/authorization/authorization.service.ts b/apps/server/src/modules/authorization/authorization.service.ts deleted file mode 100644 index b89561f1c30..00000000000 --- a/apps/server/src/modules/authorization/authorization.service.ts +++ /dev/null @@ -1,100 +0,0 @@ -import { ForbiddenException, Injectable, UnauthorizedException } from '@nestjs/common'; -import { BaseDO, EntityId, User } from '@shared/domain'; -import { AuthorizableObject } from '@shared/domain/domain-object'; -import { ErrorUtils } from '@src/core/error/utils'; -import { AuthorizationHelper } from './authorization.helper'; -import { ForbiddenLoggableException } from './errors/forbidden.loggable-exception'; -import { ReferenceLoader } from './reference.loader'; -import { RuleManager } from './rule-manager'; -import { AuthorizableReferenceType, AuthorizationContext } from './types'; - -@Injectable() -export class AuthorizationService { - constructor( - private readonly ruleManager: RuleManager, - private readonly loader: ReferenceLoader, - private readonly authorizationHelper: AuthorizationHelper - ) {} - - public checkPermission(user: User, object: AuthorizableObject | BaseDO, context: AuthorizationContext): void { - if (!this.hasPermission(user, object, context)) { - throw new ForbiddenLoggableException(user.id, object.constructor.name, context); - } - } - - public hasPermission(user: User, object: AuthorizableObject | BaseDO, context: AuthorizationContext): boolean { - const rule = this.ruleManager.selectRule(user, object, context); - const hasPermission = rule.hasPermission(user, object, context); - - return hasPermission; - } - - /** - * @deprecated - */ - public async checkPermissionByReferences( - userId: EntityId, - entityName: AuthorizableReferenceType, - entityId: EntityId, - context: AuthorizationContext - ): Promise { - if (!(await this.hasPermissionByReferences(userId, entityName, entityId, context))) { - throw new ForbiddenLoggableException(userId, entityName, context); - } - } - - /** - * @deprecated - */ - public async hasPermissionByReferences( - userId: EntityId, - entityName: AuthorizableReferenceType, - entityId: EntityId, - context: AuthorizationContext - ): Promise { - // TODO: This try-catch-block should be removed. See ticket: https://ticketsystem.dbildungscloud.de/browse/BC-4023 - try { - const [user, object] = await Promise.all([ - this.getUserWithPermissions(userId), - this.loader.loadAuthorizableObject(entityName, entityId), - ]); - const rule = this.ruleManager.selectRule(user, object, context); - const hasPermission = rule.hasPermission(user, object, context); - - return hasPermission; - } catch (error) { - throw new ForbiddenException( - null, - ErrorUtils.createHttpExceptionOptions(error, 'AuthorizationService:hasPermissionByReferences') - ); - } - } - - public checkAllPermissions(user: User, requiredPermissions: string[]): void { - if (!this.authorizationHelper.hasAllPermissions(user, requiredPermissions)) { - // TODO: Should be ForbiddenException - throw new UnauthorizedException(); - } - } - - public hasAllPermissions(user: User, requiredPermissions: string[]): boolean { - return this.authorizationHelper.hasAllPermissions(user, requiredPermissions); - } - - public checkOneOfPermissions(user: User, requiredPermissions: string[]): void { - if (!this.authorizationHelper.hasOneOfPermissions(user, requiredPermissions)) { - // TODO: Should be ForbiddenException - throw new UnauthorizedException(); - } - } - - public hasOneOfPermissions(user: User, requiredPermissions: string[]): boolean { - return this.authorizationHelper.hasOneOfPermissions(user, requiredPermissions); - } - - public async getUserWithPermissions(userId: EntityId): Promise { - const userWithPermissions = await this.loader.getUserWithPermissions(userId); - - return userWithPermissions; - } -} diff --git a/apps/server/src/modules/authorization/errors/forbidden.loggable-exception.ts b/apps/server/src/modules/authorization/domain/error/forbidden.loggable-exception.ts similarity index 94% rename from apps/server/src/modules/authorization/errors/forbidden.loggable-exception.ts rename to apps/server/src/modules/authorization/domain/error/forbidden.loggable-exception.ts index f775fb903df..9557ed14ede 100644 --- a/apps/server/src/modules/authorization/errors/forbidden.loggable-exception.ts +++ b/apps/server/src/modules/authorization/domain/error/forbidden.loggable-exception.ts @@ -2,7 +2,7 @@ import { ForbiddenException } from '@nestjs/common'; import { EntityId } from '@shared/domain'; import { Loggable } from '@src/core/logger/interfaces'; import { ErrorLogMessage } from '@src/core/logger/types'; -import { AuthorizationContext } from '../types'; +import { AuthorizationContext } from '../type'; export class ForbiddenLoggableException extends ForbiddenException implements Loggable { constructor( diff --git a/apps/server/src/modules/authorization/domain/error/index.ts b/apps/server/src/modules/authorization/domain/error/index.ts new file mode 100644 index 00000000000..f2c782cbe56 --- /dev/null +++ b/apps/server/src/modules/authorization/domain/error/index.ts @@ -0,0 +1 @@ +export * from './forbidden.loggable-exception'; diff --git a/apps/server/src/modules/authorization/domain/index.ts b/apps/server/src/modules/authorization/domain/index.ts new file mode 100644 index 00000000000..0f5cfe67874 --- /dev/null +++ b/apps/server/src/modules/authorization/domain/index.ts @@ -0,0 +1,4 @@ +export * from './service'; +export * from './mapper'; +export * from './error'; +export * from './type'; diff --git a/apps/server/src/modules/authorization/authorization-context.builder.spec.ts b/apps/server/src/modules/authorization/domain/mapper/authorization-context.builder.spec.ts similarity index 96% rename from apps/server/src/modules/authorization/authorization-context.builder.spec.ts rename to apps/server/src/modules/authorization/domain/mapper/authorization-context.builder.spec.ts index 856ec92d4a6..5944d7f22e0 100644 --- a/apps/server/src/modules/authorization/authorization-context.builder.spec.ts +++ b/apps/server/src/modules/authorization/domain/mapper/authorization-context.builder.spec.ts @@ -1,6 +1,6 @@ import { Permission } from '@shared/domain'; import { AuthorizationContextBuilder } from './authorization-context.builder'; -import { Action } from './types'; +import { Action } from '../type'; describe('AuthorizationContextBuilder', () => { it('Should allow to set required permissions.', () => { diff --git a/apps/server/src/modules/authorization/authorization-context.builder.ts b/apps/server/src/modules/authorization/domain/mapper/authorization-context.builder.ts similarity index 91% rename from apps/server/src/modules/authorization/authorization-context.builder.ts rename to apps/server/src/modules/authorization/domain/mapper/authorization-context.builder.ts index 58259aa1b6f..86b16685b58 100644 --- a/apps/server/src/modules/authorization/authorization-context.builder.ts +++ b/apps/server/src/modules/authorization/domain/mapper/authorization-context.builder.ts @@ -1,5 +1,5 @@ import { Permission } from '@shared/domain'; -import { AuthorizationContext, Action } from './types'; +import { AuthorizationContext, Action } from '../type'; export class AuthorizationContextBuilder { private static build(requiredPermissions: Permission[], action: Action): AuthorizationContext { diff --git a/apps/server/src/modules/authorization/domain/mapper/index.ts b/apps/server/src/modules/authorization/domain/mapper/index.ts new file mode 100644 index 00000000000..6f21d79acad --- /dev/null +++ b/apps/server/src/modules/authorization/domain/mapper/index.ts @@ -0,0 +1 @@ +export * from './authorization-context.builder'; diff --git a/apps/server/src/shared/domain/rules/board-do.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/board-do.rule.spec.ts similarity index 95% rename from apps/server/src/shared/domain/rules/board-do.rule.spec.ts rename to apps/server/src/modules/authorization/domain/rules/board-do.rule.spec.ts index 3574250b67c..bda3680b460 100644 --- a/apps/server/src/shared/domain/rules/board-do.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/board-do.rule.spec.ts @@ -1,10 +1,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { roleFactory, setupEntities, userFactory } from '@shared/testing'; -import { Action } from '@src/modules'; -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; import { ObjectId } from 'bson'; -import { BoardDoAuthorizable, BoardRoles, UserRoleEnum } from '../domainobject'; -import { Permission } from '../interface'; +import { BoardDoAuthorizable, BoardRoles, UserRoleEnum } from '@shared/domain/domainobject'; +import { Permission } from '@shared/domain/interface'; +import { Action } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; import { BoardDoRule } from './board-do.rule'; describe(BoardDoRule.name, () => { diff --git a/apps/server/src/shared/domain/rules/board-do.rule.ts b/apps/server/src/modules/authorization/domain/rules/board-do.rule.ts similarity index 81% rename from apps/server/src/shared/domain/rules/board-do.rule.ts rename to apps/server/src/modules/authorization/domain/rules/board-do.rule.ts index 575c9f0db5a..2042365e071 100644 --- a/apps/server/src/shared/domain/rules/board-do.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/board-do.rule.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; -import { Action, AuthorizationContext, Rule } from '@src/modules/authorization/types'; -import { BoardDoAuthorizable, BoardRoles } from '../domainobject'; -import { User } from '../entity'; +import { BoardDoAuthorizable, BoardRoles } from '@shared/domain/domainobject'; +import { User } from '@shared/domain/entity'; +import { Action, AuthorizationContext, Rule } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; @Injectable() export class BoardDoRule implements Rule { diff --git a/apps/server/src/shared/domain/rules/context-external-tool.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/context-external-tool.rule.spec.ts similarity index 83% rename from apps/server/src/shared/domain/rules/context-external-tool.rule.spec.ts rename to apps/server/src/modules/authorization/domain/rules/context-external-tool.rule.spec.ts index 90a25a1ca82..c47f5c0ef1e 100644 --- a/apps/server/src/shared/domain/rules/context-external-tool.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/context-external-tool.rule.spec.ts @@ -7,16 +7,15 @@ import { setupEntities, userFactory, } from '@shared/testing'; - -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; -import { Action } from '@src/modules/authorization/types'; -import { ContextExternalTool } from '@src/modules/tool/context-external-tool/domain'; -import { ContextExternalToolEntity } from '@src/modules/tool/context-external-tool/entity'; -import { SchoolExternalTool } from '@src/modules/tool/school-external-tool/domain'; -import { SchoolExternalToolEntity } from '@src/modules/tool/school-external-tool/entity'; -import { Role, User } from '../entity'; -import { Permission } from '../interface'; +import { ContextExternalTool } from '@modules/tool/context-external-tool/domain'; +import { ContextExternalToolEntity } from '@modules/tool/context-external-tool/entity'; +import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; +import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; +import { Role, User } from '@shared/domain/entity'; +import { Permission } from '@shared/domain/interface'; import { ContextExternalToolRule } from './context-external-tool.rule'; +import { Action } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; describe('ContextExternalToolRule', () => { let service: ContextExternalToolRule; diff --git a/apps/server/src/shared/domain/rules/context-external-tool.rule.ts b/apps/server/src/modules/authorization/domain/rules/context-external-tool.rule.ts similarity index 72% rename from apps/server/src/shared/domain/rules/context-external-tool.rule.ts rename to apps/server/src/modules/authorization/domain/rules/context-external-tool.rule.ts index 35be641e550..0aee8334aac 100644 --- a/apps/server/src/shared/domain/rules/context-external-tool.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/context-external-tool.rule.ts @@ -1,9 +1,9 @@ import { Injectable } from '@nestjs/common'; -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; -import { AuthorizationContext, Rule } from '@src/modules/authorization/types'; -import { ContextExternalTool } from '@src/modules/tool/context-external-tool/domain'; -import { ContextExternalToolEntity } from '@src/modules/tool/context-external-tool/entity'; -import { User } from '../entity'; +import { ContextExternalTool } from '@modules/tool/context-external-tool/domain'; +import { ContextExternalToolEntity } from '@modules/tool/context-external-tool/entity'; +import { User } from '@shared/domain/entity'; +import { AuthorizationContext, Rule } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; @Injectable() export class ContextExternalToolRule implements Rule { diff --git a/apps/server/src/shared/domain/rules/course-group.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/course-group.rule.spec.ts similarity index 97% rename from apps/server/src/shared/domain/rules/course-group.rule.spec.ts rename to apps/server/src/modules/authorization/domain/rules/course-group.rule.spec.ts index 8296f75f917..62c14baa138 100644 --- a/apps/server/src/shared/domain/rules/course-group.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/course-group.rule.spec.ts @@ -2,10 +2,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { CourseGroup, User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { courseFactory, courseGroupFactory, roleFactory, setupEntities, userFactory } from '@shared/testing'; -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; -import { Action } from '@src/modules/authorization/types'; import { CourseGroupRule } from './course-group.rule'; import { CourseRule } from './course.rule'; +import { Action } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; describe('CourseGroupRule', () => { let service: CourseGroupRule; diff --git a/apps/server/src/shared/domain/rules/course-group.rule.ts b/apps/server/src/modules/authorization/domain/rules/course-group.rule.ts similarity index 84% rename from apps/server/src/shared/domain/rules/course-group.rule.ts rename to apps/server/src/modules/authorization/domain/rules/course-group.rule.ts index 14638862ba2..863d7072ec8 100644 --- a/apps/server/src/shared/domain/rules/course-group.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/course-group.rule.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { CourseGroup, User } from '@shared/domain/entity'; -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; -import { Action, AuthorizationContext, Rule } from '@src/modules/authorization/types'; import { CourseRule } from './course.rule'; +import { Action, AuthorizationContext, Rule } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; @Injectable() export class CourseGroupRule implements Rule { diff --git a/apps/server/src/shared/domain/rules/course.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/course.rule.spec.ts similarity index 95% rename from apps/server/src/shared/domain/rules/course.rule.spec.ts rename to apps/server/src/modules/authorization/domain/rules/course.rule.spec.ts index eff0bd41fee..1c4dcc7d670 100644 --- a/apps/server/src/shared/domain/rules/course.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/course.rule.spec.ts @@ -2,9 +2,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Course, User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { courseFactory, roleFactory, setupEntities, userFactory } from '@shared/testing'; -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; -import { Action } from '@src/modules/authorization/types'; import { CourseRule } from './course.rule'; +import { Action } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; describe('CourseRule', () => { let service: CourseRule; diff --git a/apps/server/src/shared/domain/rules/course.rule.ts b/apps/server/src/modules/authorization/domain/rules/course.rule.ts similarity index 82% rename from apps/server/src/shared/domain/rules/course.rule.ts rename to apps/server/src/modules/authorization/domain/rules/course.rule.ts index 90183dbfd0f..e923e1ab967 100644 --- a/apps/server/src/shared/domain/rules/course.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/course.rule.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { Course, User } from '@shared/domain/entity'; -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; -import { Action, AuthorizationContext, Rule } from '@src/modules/authorization/types'; +import { Action, AuthorizationContext, Rule } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; @Injectable() export class CourseRule implements Rule { diff --git a/apps/server/src/modules/authorization/domain/rules/group.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/group.rule.spec.ts new file mode 100644 index 00000000000..bb2bc2e48b3 --- /dev/null +++ b/apps/server/src/modules/authorization/domain/rules/group.rule.spec.ts @@ -0,0 +1,210 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Permission, Role, SchoolEntity, User } from '@shared/domain'; +import { groupFactory, roleFactory, schoolFactory, setupEntities, userFactory } from '@shared/testing'; +import { Action, AuthorizationContext, AuthorizationHelper } from '@src/modules/authorization'; +import { Group } from '@src/modules/group'; +import { ObjectId } from 'bson'; +import { GroupRule } from './group.rule'; + +describe('GroupRule', () => { + let module: TestingModule; + let rule: GroupRule; + + let authorizationHelper: DeepMocked; + + beforeAll(async () => { + await setupEntities(); + + module = await Test.createTestingModule({ + providers: [ + GroupRule, + { + provide: AuthorizationHelper, + useValue: createMock(), + }, + ], + }).compile(); + + rule = module.get(GroupRule); + authorizationHelper = module.get(AuthorizationHelper); + }); + + afterAll(async () => { + await module.close(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('isApplicable', () => { + describe('when the entity is applicable', () => { + const setup = () => { + const role: Role = roleFactory.buildWithId(); + const user: User = userFactory.buildWithId({ roles: [role] }); + const group: Group = groupFactory.build({ + users: [ + { + userId: user.id, + roleId: user.roles[0].id, + }, + ], + }); + + return { + user, + group, + }; + }; + + it('should return true', () => { + const { user, group } = setup(); + + const result = rule.isApplicable(user, group); + + expect(result).toEqual(true); + }); + }); + + describe('when the entity is not applicable', () => { + const setup = () => { + const role: Role = roleFactory.buildWithId(); + const userNotInGroup: User = userFactory.buildWithId({ roles: [role] }); + + return { + userNotInGroup, + }; + }; + + it('should return false', () => { + const { userNotInGroup } = setup(); + + const result = rule.isApplicable(userNotInGroup, {} as unknown as Group); + + expect(result).toEqual(false); + }); + }); + }); + + describe('hasPermission', () => { + describe('when the user has all required permissions and is at the same school then the group', () => { + const setup = () => { + const role: Role = roleFactory.buildWithId(); + const school: SchoolEntity = schoolFactory.buildWithId(); + const user: User = userFactory.buildWithId({ school, roles: [role] }); + const group: Group = groupFactory.build({ + users: [ + { + userId: user.id, + roleId: user.roles[0].id, + }, + ], + organizationId: user.school.id, + }); + const context: AuthorizationContext = { + action: Action.write, + requiredPermissions: [Permission.GROUP_VIEW], + }; + + authorizationHelper.hasAllPermissions.mockReturnValue(true); + + return { + user, + group, + context, + }; + }; + + it('should check all permissions', () => { + const { user, group, context } = setup(); + + rule.hasPermission(user, group, context); + + expect(authorizationHelper.hasAllPermissions).toHaveBeenCalledWith(user, context.requiredPermissions); + }); + + it('should return true', () => { + const { user, group, context } = setup(); + + const result = rule.hasPermission(user, group, context); + + expect(result).toEqual(true); + }); + }); + + describe('when the user has not the required permission', () => { + const setup = () => { + const role: Role = roleFactory.buildWithId({ permissions: [] }); + const school: SchoolEntity = schoolFactory.buildWithId(); + const user: User = userFactory.buildWithId({ school, roles: [role] }); + const group: Group = groupFactory.build({ + users: [ + { + userId: user.id, + roleId: user.roles[0].id, + }, + ], + organizationId: user.school.id, + }); + const context: AuthorizationContext = { + action: Action.write, + requiredPermissions: [Permission.GROUP_VIEW], + }; + + authorizationHelper.hasAllPermissions.mockReturnValue(false); + + return { + user, + group, + context, + }; + }; + + it('should return false', () => { + const { user, group, context } = setup(); + + const result = rule.hasPermission(user, group, context); + + expect(result).toEqual(false); + }); + }); + + describe('when the user is at another school then the group', () => { + const setup = () => { + const role: Role = roleFactory.buildWithId({ permissions: [] }); + const school: SchoolEntity = schoolFactory.buildWithId(); + const user: User = userFactory.buildWithId({ school, roles: [role] }); + const group: Group = groupFactory.build({ + users: [ + { + userId: user.id, + roleId: user.roles[0].id, + }, + ], + organizationId: new ObjectId().toHexString(), + }); + const context: AuthorizationContext = { + action: Action.write, + requiredPermissions: [Permission.GROUP_VIEW], + }; + + authorizationHelper.hasAllPermissions.mockReturnValue(true); + + return { + user, + group, + context, + }; + }; + + it('should return false', () => { + const { user, group, context } = setup(); + + const result = rule.hasPermission(user, group, context); + + expect(result).toEqual(false); + }); + }); + }); +}); diff --git a/apps/server/src/modules/authorization/domain/rules/group.rule.ts b/apps/server/src/modules/authorization/domain/rules/group.rule.ts new file mode 100644 index 00000000000..e25e90230c8 --- /dev/null +++ b/apps/server/src/modules/authorization/domain/rules/group.rule.ts @@ -0,0 +1,24 @@ +import { Injectable } from '@nestjs/common'; +import { User } from '@shared/domain'; +import { Group } from '@src/modules/group'; +import { AuthorizationContext, Rule } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; + +@Injectable() +export class GroupRule implements Rule { + constructor(private readonly authorizationHelper: AuthorizationHelper) {} + + public isApplicable(user: User, domainObject: Group): boolean { + const isMatched: boolean = domainObject instanceof Group; + + return isMatched; + } + + public hasPermission(user: User, domainObject: Group, context: AuthorizationContext): boolean { + const hasPermission: boolean = + this.authorizationHelper.hasAllPermissions(user, context.requiredPermissions) && + (domainObject.organizationId ? user.school.id === domainObject.organizationId : true); + + return hasPermission; + } +} diff --git a/apps/server/src/modules/authorization/domain/rules/index.ts b/apps/server/src/modules/authorization/domain/rules/index.ts new file mode 100644 index 00000000000..b78f43051d0 --- /dev/null +++ b/apps/server/src/modules/authorization/domain/rules/index.ts @@ -0,0 +1,17 @@ +/** + * Rules are currently placed in authorization module to avoid dependency cycles. + * In future they must be moved to the feature modules and register it in registration service. + */ +export * from './board-do.rule'; +export * from './context-external-tool.rule'; +export * from './course-group.rule'; +export * from './course.rule'; +export * from './legacy-school.rule'; +export * from './lesson.rule'; +export * from './school-external-tool.rule'; +export * from './submission.rule'; +export * from './task.rule'; +export * from './team.rule'; +export * from './user-login-migration.rule'; +export * from './user.rule'; +export * from './group.rule'; diff --git a/apps/server/src/shared/domain/rules/legacy-school.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/legacy-school.rule.spec.ts similarity index 93% rename from apps/server/src/shared/domain/rules/legacy-school.rule.spec.ts rename to apps/server/src/modules/authorization/domain/rules/legacy-school.rule.spec.ts index c547f772de5..489def39318 100644 --- a/apps/server/src/shared/domain/rules/legacy-school.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/legacy-school.rule.spec.ts @@ -1,9 +1,9 @@ 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 { Action } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; import { LegacySchoolRule } from './legacy-school.rule'; describe('LegacySchoolRule', () => { diff --git a/apps/server/src/shared/domain/rules/legacy-school.rule.ts b/apps/server/src/modules/authorization/domain/rules/legacy-school.rule.ts similarity index 78% rename from apps/server/src/shared/domain/rules/legacy-school.rule.ts rename to apps/server/src/modules/authorization/domain/rules/legacy-school.rule.ts index 5068d327c35..e115727091a 100644 --- a/apps/server/src/shared/domain/rules/legacy-school.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/legacy-school.rule.ts @@ -1,9 +1,9 @@ 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'; +import { AuthorizableObject } from '@shared/domain/domain-object'; +import { AuthorizationContext, Rule } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; /** * @deprecated because it uses the deprecated LegacySchoolDo. diff --git a/apps/server/src/shared/domain/rules/lesson.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/lesson.rule.spec.ts similarity index 50% rename from apps/server/src/shared/domain/rules/lesson.rule.spec.ts rename to apps/server/src/modules/authorization/domain/rules/lesson.rule.spec.ts index 13c605f77e4..a8e4bdd8038 100644 --- a/apps/server/src/shared/domain/rules/lesson.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/lesson.rule.spec.ts @@ -10,17 +10,20 @@ import { setupEntities, userFactory, } from '@shared/testing'; -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; -import { Action } from '@src/modules/authorization/types'; -import { CourseGroupRule, CourseRule } from '.'; +import { NotImplementedException } from '@nestjs/common'; +import { Action, AuthorizationContext } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; +import { CourseGroupRule } from './course-group.rule'; +import { CourseRule } from './course.rule'; import { LessonRule } from './lesson.rule'; +import { AuthorizationContextBuilder } from '../mapper'; describe('LessonRule', () => { - let service: LessonRule; + let rule: LessonRule; let authorizationHelper: AuthorizationHelper; let courseRule: DeepPartial; let courseGroupRule: DeepPartial; - let user: User; + let globalUser: User; let entity: LessonEntity; const permissionA = 'a' as Permission; const permissionB = 'b' as Permission; @@ -33,7 +36,7 @@ describe('LessonRule', () => { providers: [AuthorizationHelper, LessonRule, CourseRule, CourseGroupRule], }).compile(); - service = await module.get(LessonRule); + rule = await module.get(LessonRule); authorizationHelper = await module.get(AuthorizationHelper); courseRule = await module.get(CourseRule); courseGroupRule = await module.get(CourseGroupRule); @@ -41,58 +44,117 @@ describe('LessonRule', () => { beforeEach(() => { const role = roleFactory.build({ permissions: [permissionA, permissionB] }); - user = userFactory.build({ roles: [role] }); + globalUser = userFactory.build({ roles: [role] }); }); it('should call hasAllPermissions on AuthorizationHelper', () => { entity = lessonFactory.build(); const spy = jest.spyOn(authorizationHelper, 'hasAllPermissions'); - service.hasPermission(user, entity, { action: Action.read, requiredPermissions: [] }); - expect(spy).toBeCalledWith(user, []); + rule.hasPermission(globalUser, entity, { action: Action.read, requiredPermissions: [] }); + expect(spy).toBeCalledWith(globalUser, []); }); it('should call courseRule.hasPermission', () => { - const course = courseFactory.build({ teachers: [user] }); + const course = courseFactory.build({ teachers: [globalUser] }); entity = lessonFactory.build({ course }); const spy = jest.spyOn(courseRule, 'hasPermission'); - service.hasPermission(user, entity, { action: Action.write, requiredPermissions: [permissionA] }); - expect(spy).toBeCalledWith(user, entity.course, { action: Action.write, requiredPermissions: [] }); + rule.hasPermission(globalUser, entity, { action: Action.write, requiredPermissions: [permissionA] }); + expect(spy).toBeCalledWith(globalUser, entity.course, { action: Action.write, requiredPermissions: [] }); }); it('should call courseGroupRule.hasPermission', () => { - const course = courseFactory.build({ teachers: [user] }); + const course = courseFactory.build({ teachers: [globalUser] }); const courseGroup = courseGroupFactory.build({ course }); entity = lessonFactory.build({ course: undefined, courseGroup }); const spy = jest.spyOn(courseGroupRule, 'hasPermission'); - service.hasPermission(user, entity, { action: Action.write, requiredPermissions: [permissionA] }); - expect(spy).toBeCalledWith(user, entity.courseGroup, { action: Action.write, requiredPermissions: [] }); + rule.hasPermission(globalUser, entity, { action: Action.write, requiredPermissions: [permissionA] }); + expect(spy).toBeCalledWith(globalUser, entity.courseGroup, { action: Action.write, requiredPermissions: [] }); + }); + + describe('Given user request not implemented action', () => { + const getContext = (): AuthorizationContext => { + const context: AuthorizationContext = { + requiredPermissions: [], + // @ts-expect-error Testcase + action: 'not_implemented', + }; + + return context; + }; + + describe('when valid data exists', () => { + const setup = () => { + const user = userFactory.build(); + const course = courseFactory.build({ teachers: [user] }); + const lesson = lessonFactory.build({ course }); + const context = getContext(); + + return { + user, + lesson, + context, + }; + }; + + it('should reject with NotImplementedException', () => { + const { user, lesson, context } = setup(); + + expect(() => rule.hasPermission(user, lesson, context)).toThrowError(NotImplementedException); + }); + }); + }); + + describe('Given user request Action.write', () => { + const getWriteContext = () => AuthorizationContextBuilder.write([]); + + describe('when lesson has no course or coursegroup', () => { + const setup = () => { + const user = userFactory.build(); + const lessonEntity = lessonFactory.build({ course: undefined }); + const context = getWriteContext(); + + return { + user, + lessonEntity, + context, + }; + }; + + it('should return false', () => { + const { user, lessonEntity, context } = setup(); + + const result = rule.hasPermission(user, lessonEntity, context); + + expect(result).toBe(false); + }); + }); }); describe('User [TEACHER]', () => { it('should return "true" if user in scope', () => { - const course = courseFactory.build({ teachers: [user] }); + const course = courseFactory.build({ teachers: [globalUser] }); entity = lessonFactory.build({ course }); - const res = service.hasPermission(user, entity, { action: Action.read, requiredPermissions: [permissionA] }); + const res = rule.hasPermission(globalUser, entity, { action: Action.read, requiredPermissions: [permissionA] }); expect(res).toBe(true); }); it('should return "true" if user has access to hidden entity', () => { - const course = courseFactory.build({ teachers: [user] }); + const course = courseFactory.build({ teachers: [globalUser] }); entity = lessonFactory.build({ course, hidden: true }); - const res = service.hasPermission(user, entity, { action: Action.read, requiredPermissions: [permissionA] }); + const res = rule.hasPermission(globalUser, entity, { action: Action.read, requiredPermissions: [permissionA] }); expect(res).toBe(true); }); it('should return "false" if user has not permission', () => { entity = lessonFactory.build(); - const res = service.hasPermission(user, entity, { action: Action.read, requiredPermissions: [permissionC] }); + const res = rule.hasPermission(globalUser, entity, { action: Action.read, requiredPermissions: [permissionC] }); expect(res).toBe(false); }); it('should return "false" if user has not access to entity', () => { entity = lessonFactory.build(); - const res = service.hasPermission(user, entity, { action: Action.read, requiredPermissions: [permissionC] }); + const res = rule.hasPermission(globalUser, entity, { action: Action.read, requiredPermissions: [permissionC] }); expect(res).toBe(false); }); }); @@ -106,14 +168,14 @@ describe('LessonRule', () => { it('should return "false" if user has access to entity', () => { const course = courseFactory.build({ students: [student] }); entity = lessonFactory.build({ course }); - const res = service.hasPermission(student, entity, { action: Action.read, requiredPermissions: [permissionA] }); + const res = rule.hasPermission(student, entity, { action: Action.read, requiredPermissions: [permissionA] }); expect(res).toBe(true); }); it('should return "false" if user has not access to hidden entity', () => { const course = courseFactory.build({ students: [student] }); entity = lessonFactory.build({ course, hidden: true }); - const res = service.hasPermission(student, entity, { action: Action.read, requiredPermissions: [permissionA] }); + const res = rule.hasPermission(student, entity, { action: Action.read, requiredPermissions: [permissionA] }); expect(res).toBe(false); }); }); diff --git a/apps/server/src/shared/domain/rules/lesson.rule.ts b/apps/server/src/modules/authorization/domain/rules/lesson.rule.ts similarity index 88% rename from apps/server/src/shared/domain/rules/lesson.rule.ts rename to apps/server/src/modules/authorization/domain/rules/lesson.rule.ts index ff264af13ae..1f59f98ad49 100644 --- a/apps/server/src/shared/domain/rules/lesson.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/lesson.rule.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotImplementedException } from '@nestjs/common'; import { Course, CourseGroup, LessonEntity, User } from '@shared/domain/entity'; -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; -import { Action, AuthorizationContext, Rule } from '@src/modules/authorization/types'; +import { Action, AuthorizationContext, Rule } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; import { CourseGroupRule } from './course-group.rule'; import { CourseRule } from './course.rule'; @@ -27,6 +27,8 @@ export class LessonRule implements Rule { hasLessonPermission = this.lessonReadPermission(user, entity); } else if (action === Action.write) { hasLessonPermission = this.lessonWritePermission(user, entity); + } else { + throw new NotImplementedException('Action is not supported.'); } const hasUserPermission = this.authorizationHelper.hasAllPermissions(user, requiredPermissions); @@ -55,12 +57,14 @@ export class LessonRule implements Rule { } private parentPermission(user: User, entity: LessonEntity, action: Action): boolean { - let result = false; + let result: boolean; if (entity.courseGroup) { result = this.courseGroupPermission(user, entity.courseGroup, action); } else if (entity.course) { result = this.coursePermission(user, entity.course, action); // ask course for student = read || teacher, sub-teacher = write + } else { + result = false; } return result; diff --git a/apps/server/src/shared/domain/rules/school-external-tool.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/school-external-tool.rule.spec.ts similarity index 87% rename from apps/server/src/shared/domain/rules/school-external-tool.rule.spec.ts rename to apps/server/src/modules/authorization/domain/rules/school-external-tool.rule.spec.ts index b24ed4d0ac8..453f77fc968 100644 --- a/apps/server/src/shared/domain/rules/school-external-tool.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/school-external-tool.rule.spec.ts @@ -7,13 +7,11 @@ import { userFactory, schoolExternalToolFactory, } from '@shared/testing'; - -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; -import { Action } from '@src/modules/authorization/types'; -import { SchoolExternalTool } from '@src/modules/tool/school-external-tool/domain'; -import { SchoolExternalToolEntity } from '@src/modules/tool/school-external-tool/entity'; -import { Role, User } from '../entity'; -import { Permission } from '../interface'; +import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; +import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; +import { Role, User, Permission } from '@shared/domain'; +import { Action } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; import { SchoolExternalToolRule } from './school-external-tool.rule'; describe('SchoolExternalToolRule', () => { diff --git a/apps/server/src/shared/domain/rules/school-external-tool.rule.ts b/apps/server/src/modules/authorization/domain/rules/school-external-tool.rule.ts similarity index 72% rename from apps/server/src/shared/domain/rules/school-external-tool.rule.ts rename to apps/server/src/modules/authorization/domain/rules/school-external-tool.rule.ts index bd28502faa2..46126fe0589 100644 --- a/apps/server/src/shared/domain/rules/school-external-tool.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/school-external-tool.rule.ts @@ -1,9 +1,9 @@ import { Injectable } from '@nestjs/common'; -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; -import { AuthorizationContext, Rule } from '@src/modules/authorization/types'; -import { SchoolExternalTool } from '@src/modules/tool/school-external-tool/domain'; -import { SchoolExternalToolEntity } from '@src/modules/tool/school-external-tool/entity'; -import { User } from '../entity'; +import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; +import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; +import { User } from '@shared/domain/entity'; +import { AuthorizationContext, Rule } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; @Injectable() export class SchoolExternalToolRule implements Rule { diff --git a/apps/server/src/shared/domain/rules/submission.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/submission.rule.spec.ts similarity index 93% rename from apps/server/src/shared/domain/rules/submission.rule.spec.ts rename to apps/server/src/modules/authorization/domain/rules/submission.rule.spec.ts index 098f83547e8..8b054671970 100644 --- a/apps/server/src/shared/domain/rules/submission.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/submission.rule.spec.ts @@ -9,9 +9,14 @@ import { taskFactory, userFactory, } from '@shared/testing'; -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; -import { Action } from '@src/modules/authorization/types'; -import { CourseGroupRule, CourseRule, LessonRule, SubmissionRule, TaskRule } from '.'; +import { NotImplementedException } from '@nestjs/common'; +import { Action, AuthorizationContext } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; +import { SubmissionRule } from './submission.rule'; +import { TaskRule } from './task.rule'; +import { CourseRule } from './course.rule'; +import { LessonRule } from './lesson.rule'; +import { CourseGroupRule } from './course-group.rule'; const buildUserWithPermission = (permission) => { const role = roleFactory.buildWithId({ permissions: [permission] }); @@ -76,6 +81,38 @@ describe('SubmissionRule', () => { }); describe('hasPermission', () => { + describe('Given user request not implemented action', () => { + const getContext = (): AuthorizationContext => { + const context: AuthorizationContext = { + requiredPermissions: [], + // @ts-expect-error Testcase + action: 'not_implemented', + }; + + return context; + }; + + describe('when valid data exists', () => { + const setup = () => { + const user = userFactory.build(); + const submission = submissionFactory.build({ student: user }); + const context = getContext(); + + return { + user, + submission, + context, + }; + }; + + it('should reject with NotImplementedException', () => { + const { user, submission, context } = setup(); + + expect(() => submissionRule.hasPermission(user, submission, context)).toThrowError(NotImplementedException); + }); + }); + }); + describe('when user roles do not contain required permissions', () => { const setup = () => { const permission = 'a' as Permission; diff --git a/apps/server/src/shared/domain/rules/submission.rule.ts b/apps/server/src/modules/authorization/domain/rules/submission.rule.ts similarity index 88% rename from apps/server/src/shared/domain/rules/submission.rule.ts rename to apps/server/src/modules/authorization/domain/rules/submission.rule.ts index 3234f8e8cff..6bff9504f5c 100644 --- a/apps/server/src/shared/domain/rules/submission.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/submission.rule.ts @@ -1,7 +1,7 @@ -import { Injectable } from '@nestjs/common'; +import { Injectable, NotImplementedException } from '@nestjs/common'; import { Submission, User } from '@shared/domain/entity'; -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; -import { Action, AuthorizationContext, Rule } from '@src/modules/authorization/types'; +import { Action, AuthorizationContext, Rule } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; import { TaskRule } from './task.rule'; @Injectable() @@ -31,6 +31,8 @@ export class SubmissionRule implements Rule { hasAccessToSubmission = this.hasWriteAccess(user, submission); } else if (action === Action.read) { hasAccessToSubmission = this.hasReadAccess(user, submission); + } else { + throw new NotImplementedException('Action is not supported.'); } return hasAccessToSubmission; diff --git a/apps/server/src/shared/domain/rules/task.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/task.rule.spec.ts similarity index 96% rename from apps/server/src/shared/domain/rules/task.rule.spec.ts rename to apps/server/src/modules/authorization/domain/rules/task.rule.spec.ts index 0fc886df84f..31d68661ff0 100644 --- a/apps/server/src/shared/domain/rules/task.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/task.rule.spec.ts @@ -2,9 +2,12 @@ import { DeepPartial } from '@mikro-orm/core'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission, RoleName } from '@shared/domain/interface'; import { courseFactory, lessonFactory, roleFactory, setupEntities, taskFactory, userFactory } from '@shared/testing'; -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; -import { CourseGroupRule, CourseRule, LessonRule, TaskRule } from '.'; -import { Action } from '../../../modules/authorization/types/action.enum'; +import { Action } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; +import { CourseGroupRule } from './course-group.rule'; +import { TaskRule } from './task.rule'; +import { CourseRule } from './course.rule'; +import { LessonRule } from './lesson.rule'; describe('TaskRule', () => { let service: TaskRule; diff --git a/apps/server/src/shared/domain/rules/task.rule.ts b/apps/server/src/modules/authorization/domain/rules/task.rule.ts similarity index 90% rename from apps/server/src/shared/domain/rules/task.rule.ts rename to apps/server/src/modules/authorization/domain/rules/task.rule.ts index 4c358593109..3ebc04d9f71 100644 --- a/apps/server/src/shared/domain/rules/task.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/task.rule.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { Task, User } from '@shared/domain/entity'; -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; -import { Action, AuthorizationContext, Rule } from '@src/modules/authorization/types'; +import { Action, AuthorizationContext, Rule } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; import { CourseRule } from './course.rule'; import { LessonRule } from './lesson.rule'; diff --git a/apps/server/src/shared/domain/rules/team.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/team.rule.spec.ts similarity index 91% rename from apps/server/src/shared/domain/rules/team.rule.spec.ts rename to apps/server/src/modules/authorization/domain/rules/team.rule.spec.ts index d29eaaaa6f8..da99354a49b 100644 --- a/apps/server/src/shared/domain/rules/team.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/team.rule.spec.ts @@ -1,10 +1,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain/interface'; -import { roleFactory, setupEntities, userFactory } from '@shared/testing'; -import { teamFactory } from '@shared/testing/factory/team.factory'; -import { TeamRule } from '@shared/domain/rules/team.rule'; -import { AuthorizationContextBuilder } from '@src/modules/authorization/authorization-context.builder'; -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; +import { roleFactory, setupEntities, userFactory, teamFactory } from '@shared/testing'; +import { AuthorizationHelper } from '../service/authorization.helper'; +import { TeamRule } from './team.rule'; +import { AuthorizationContextBuilder } from '../mapper'; describe('TeamRule', () => { let rule: TeamRule; diff --git a/apps/server/src/shared/domain/rules/team.rule.ts b/apps/server/src/modules/authorization/domain/rules/team.rule.ts similarity index 81% rename from apps/server/src/shared/domain/rules/team.rule.ts rename to apps/server/src/modules/authorization/domain/rules/team.rule.ts index 23ad0d55cf7..2d8f5e90edf 100644 --- a/apps/server/src/shared/domain/rules/team.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/team.rule.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { TeamEntity, TeamUserEntity, User } from '@shared/domain/entity'; -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; -import { AuthorizationContext, Rule } from '@src/modules/authorization/types'; +import { AuthorizationContext, Rule } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; @Injectable() export class TeamRule implements Rule { diff --git a/apps/server/src/shared/domain/rules/user-login-migration.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/user-login-migration.rule.spec.ts similarity index 94% rename from apps/server/src/shared/domain/rules/user-login-migration.rule.spec.ts rename to apps/server/src/modules/authorization/domain/rules/user-login-migration.rule.spec.ts index a7c6b1e5f7a..f7fe9d3c53f 100644 --- a/apps/server/src/shared/domain/rules/user-login-migration.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/user-login-migration.rule.spec.ts @@ -2,10 +2,10 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { schoolFactory, setupEntities, userFactory, userLoginMigrationDOFactory } from '@shared/testing'; -import { Action, AuthorizationContext } from '@src/modules/authorization'; -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; -import { UserLoginMigrationDO } from '../domainobject'; -import { Permission } from '../interface'; +import { UserLoginMigrationDO } from '@shared/domain/domainobject'; +import { Permission } from '@shared/domain/interface'; +import { Action, AuthorizationContext } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; import { UserLoginMigrationRule } from './user-login-migration.rule'; describe('UserLoginMigrationRule', () => { diff --git a/apps/server/src/shared/domain/rules/user-login-migration.rule.ts b/apps/server/src/modules/authorization/domain/rules/user-login-migration.rule.ts similarity index 72% rename from apps/server/src/shared/domain/rules/user-login-migration.rule.ts rename to apps/server/src/modules/authorization/domain/rules/user-login-migration.rule.ts index 084e4d26372..3ae82d02505 100644 --- a/apps/server/src/shared/domain/rules/user-login-migration.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/user-login-migration.rule.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; -import { AuthorizationContext, Rule } from '@src/modules/authorization/types'; -import { UserLoginMigrationDO } from '../domainobject'; -import { User } from '../entity'; +import { UserLoginMigrationDO } from '@shared/domain/domainobject'; +import { User } from '@shared/domain/entity'; +import { AuthorizationContext, Rule } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; @Injectable() export class UserLoginMigrationRule implements Rule { diff --git a/apps/server/src/shared/domain/rules/user.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/user.rule.spec.ts similarity index 94% rename from apps/server/src/shared/domain/rules/user.rule.spec.ts rename to apps/server/src/modules/authorization/domain/rules/user.rule.spec.ts index bd771961ed3..85492348f75 100644 --- a/apps/server/src/shared/domain/rules/user.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/user.rule.spec.ts @@ -2,8 +2,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Role, User } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; import { roleFactory, setupEntities, userFactory } from '@shared/testing'; -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; -import { Action } from '@src/modules/authorization/types'; +import { Action } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; import { UserRule } from './user.rule'; describe('UserRule', () => { diff --git a/apps/server/src/shared/domain/rules/user.rule.ts b/apps/server/src/modules/authorization/domain/rules/user.rule.ts similarity index 78% rename from apps/server/src/shared/domain/rules/user.rule.ts rename to apps/server/src/modules/authorization/domain/rules/user.rule.ts index 3dd9bc6d229..2a1365881e1 100644 --- a/apps/server/src/shared/domain/rules/user.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/user.rule.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { User } from '@shared/domain/entity'; -import { AuthorizationHelper } from '@src/modules/authorization/authorization.helper'; -import { AuthorizationContext, Rule } from '@src/modules/authorization/types'; +import { AuthorizationContext, Rule } from '../type'; +import { AuthorizationHelper } from '../service/authorization.helper'; @Injectable() export class UserRule implements Rule { diff --git a/apps/server/src/modules/authorization/domain/service/authorization-reference.service.spec.ts b/apps/server/src/modules/authorization/domain/service/authorization-reference.service.spec.ts new file mode 100644 index 00000000000..8ab1719a72d --- /dev/null +++ b/apps/server/src/modules/authorization/domain/service/authorization-reference.service.spec.ts @@ -0,0 +1,183 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { NotFoundException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { courseFactory, setupEntities, userFactory } from '@shared/testing'; +import { ObjectId } from 'bson'; +import { AuthorizableReferenceType } from '../type'; +import { AuthorizationService } from './authorization.service'; +import { ReferenceLoader } from './reference.loader'; +import { AuthorizationContextBuilder } from '../mapper'; +import { ForbiddenLoggableException } from '../error'; +import { AuthorizationReferenceService } from './authorization-reference.service'; + +describe('AuthorizationReferenceService', () => { + let service: AuthorizationReferenceService; + let authorizationService: DeepMocked; + let loader: DeepMocked; + + beforeAll(async () => { + await setupEntities(); + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + AuthorizationReferenceService, + { + provide: AuthorizationService, + useValue: createMock(), + }, + { + provide: ReferenceLoader, + useValue: createMock(), + }, + ], + }).compile(); + + service = await module.get(AuthorizationReferenceService); + authorizationService = await module.get(AuthorizationService); + loader = await module.get(ReferenceLoader); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('checkPermissionByReferences', () => { + const setupData = () => { + const entityId = new ObjectId().toHexString(); + const userId = new ObjectId().toHexString(); + const context = AuthorizationContextBuilder.read([]); + const entityName = AuthorizableReferenceType.Course; + + return { context, entityId, userId, entityName }; + }; + + describe('when hasPermissionByReferences returns false', () => { + const setup = () => { + const { entityId, userId, context, entityName } = setupData(); + + const spy = jest.spyOn(service, 'hasPermissionByReferences').mockResolvedValueOnce(false); + + return { context, userId, entityId, entityName, spy }; + }; + + it('should reject with ForbiddenLoggableException', async () => { + const { context, userId, entityId, entityName, spy } = setup(); + + await expect(service.checkPermissionByReferences(userId, entityName, entityId, context)).rejects.toThrow( + new ForbiddenLoggableException(userId, entityName, context) + ); + + spy.mockRestore(); + }); + }); + + describe('when hasPermissionByReferences returns true', () => { + const setup = () => { + const { entityId, userId, context, entityName } = setupData(); + + const spy = jest.spyOn(service, 'hasPermissionByReferences').mockResolvedValueOnce(true); + + return { context, userId, entityId, entityName, spy }; + }; + + it('should resolve without error', async () => { + const { context, userId, entityId, entityName, spy } = setup(); + + await expect(service.checkPermissionByReferences(userId, entityName, entityId, context)).resolves.not.toThrow(); + + spy.mockRestore(); + }); + }); + }); + + describe('hasPermissionByReferences', () => { + const setupData = () => { + const entity = courseFactory.buildWithId(); + const user = userFactory.buildWithId(); + const context = AuthorizationContextBuilder.read([]); + const entityName = AuthorizableReferenceType.Course; + + return { context, entity, user, entityName }; + }; + + describe('when loader throws an error', () => { + const setup = () => { + const { entity, user, context, entityName } = setupData(); + + loader.loadAuthorizableObject.mockRejectedValueOnce(new NotFoundException()); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + authorizationService.hasPermission.mockReturnValueOnce(true); + + return { context, userId: user.id, entityId: entity.id, entityName }; + }; + + it('should reject with this error', async () => { + const { context, userId, entityId, entityName } = setup(); + + await expect(service.hasPermissionByReferences(userId, entityName, entityId, context)).rejects.toThrow( + new NotFoundException() + ); + }); + }); + + describe('when authorizationService throws an error', () => { + const setup = () => { + const { entity, user, context, entityName } = setupData(); + + loader.loadAuthorizableObject.mockRejectedValueOnce(entity); + authorizationService.getUserWithPermissions.mockRejectedValueOnce(new NotFoundException()); + authorizationService.hasPermission.mockReturnValueOnce(true); + + return { context, userId: user.id, entityId: entity.id, entityName }; + }; + + it('should reject with this error', async () => { + const { context, userId, entityId, entityName } = setup(); + + await expect(service.hasPermissionByReferences(userId, entityName, entityId, context)).rejects.toThrow( + new NotFoundException() + ); + }); + }); + + describe('when loader can load entites and authorization resolve with true', () => { + const setup = () => { + const { entity, user, context, entityName } = setupData(); + + loader.loadAuthorizableObject.mockResolvedValueOnce(entity); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + authorizationService.hasPermission.mockReturnValueOnce(true); + + return { context, userId: user.id, entityId: entity.id, entityName }; + }; + + it('should resolve to true', async () => { + const { context, userId, entityId, entityName } = setup(); + + const result = await service.hasPermissionByReferences(userId, entityName, entityId, context); + + expect(result).toBe(true); + }); + }); + + describe('when loader can load entities and authorization resolve with false', () => { + const setup = () => { + const { entity, user, context, entityName } = setupData(); + + loader.loadAuthorizableObject.mockResolvedValueOnce(entity); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + authorizationService.hasPermission.mockReturnValueOnce(false); + + return { context, userId: user.id, entityId: entity.id, entityName }; + }; + + it('should resolve to false', async () => { + const { context, userId, entityId, entityName } = setup(); + + const result = await service.hasPermissionByReferences(userId, entityName, entityId, context); + + expect(result).toBe(false); + }); + }); + }); +}); diff --git a/apps/server/src/modules/authorization/domain/service/authorization-reference.service.ts b/apps/server/src/modules/authorization/domain/service/authorization-reference.service.ts new file mode 100644 index 00000000000..814df9378da --- /dev/null +++ b/apps/server/src/modules/authorization/domain/service/authorization-reference.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { ReferenceLoader } from './reference.loader'; +import { AuthorizationContext, AuthorizableReferenceType } from '../type'; +import { ForbiddenLoggableException } from '../error'; +import { AuthorizationService } from './authorization.service'; + +/** + * Should by use only internal in authorization module. See ticket: BC-3990 + */ +@Injectable() +export class AuthorizationReferenceService { + constructor(private readonly loader: ReferenceLoader, private readonly authorizationService: AuthorizationService) {} + + public async checkPermissionByReferences( + userId: EntityId, + entityName: AuthorizableReferenceType, + entityId: EntityId, + context: AuthorizationContext + ): Promise { + if (!(await this.hasPermissionByReferences(userId, entityName, entityId, context))) { + throw new ForbiddenLoggableException(userId, entityName, context); + } + } + + public async hasPermissionByReferences( + userId: EntityId, + entityName: AuthorizableReferenceType, + entityId: EntityId, + context: AuthorizationContext + ): Promise { + const [user, object] = await Promise.all([ + this.authorizationService.getUserWithPermissions(userId), + this.loader.loadAuthorizableObject(entityName, entityId), + ]); + + const hasPermission = this.authorizationService.hasPermission(user, object, context); + + return hasPermission; + } +} diff --git a/apps/server/src/modules/authorization/authorization.helper.spec.ts b/apps/server/src/modules/authorization/domain/service/authorization.helper.spec.ts similarity index 100% rename from apps/server/src/modules/authorization/authorization.helper.spec.ts rename to apps/server/src/modules/authorization/domain/service/authorization.helper.spec.ts diff --git a/apps/server/src/modules/authorization/authorization.helper.ts b/apps/server/src/modules/authorization/domain/service/authorization.helper.ts similarity index 100% rename from apps/server/src/modules/authorization/authorization.helper.ts rename to apps/server/src/modules/authorization/domain/service/authorization.helper.ts diff --git a/apps/server/src/modules/authorization/authorization.service.spec.ts b/apps/server/src/modules/authorization/domain/service/authorization.service.spec.ts similarity index 60% rename from apps/server/src/modules/authorization/authorization.service.spec.ts rename to apps/server/src/modules/authorization/domain/service/authorization.service.spec.ts index 766c2c84d23..f113c64472c 100644 --- a/apps/server/src/modules/authorization/authorization.service.spec.ts +++ b/apps/server/src/modules/authorization/domain/service/authorization.service.spec.ts @@ -1,33 +1,34 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { ForbiddenException, InternalServerErrorException, UnauthorizedException } from '@nestjs/common'; +import { UnauthorizedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain'; import { setupEntities, userFactory } from '@shared/testing'; -import { AuthorizationContextBuilder } from './authorization-context.builder'; +import { UserRepo } from '@shared/repo'; +import { AuthorizationContextBuilder } from '../mapper'; import { AuthorizationHelper } from './authorization.helper'; import { AuthorizationService } from './authorization.service'; -import { ForbiddenLoggableException } from './errors/forbidden.loggable-exception'; +import { ForbiddenLoggableException } from '../error'; import { ReferenceLoader } from './reference.loader'; import { RuleManager } from './rule-manager'; -import { AuthorizableReferenceType, Rule } from './types'; +import { Rule } from '../type'; -describe('AuthorizationService', () => { - class TestRule implements Rule { - constructor(private returnValueOfhasPermission: boolean) {} +class TestRule implements Rule { + constructor(private returnValueOfhasPermission: boolean) {} - isApplicable(): boolean { - return true; - } + isApplicable(): boolean { + return true; + } - hasPermission(): boolean { - return this.returnValueOfhasPermission; - } + hasPermission(): boolean { + return this.returnValueOfhasPermission; } +} +describe('AuthorizationService', () => { let service: AuthorizationService; let ruleManager: DeepMocked; - let loader: DeepMocked; let authorizationHelper: DeepMocked; + let userRepo: DeepMocked; const testPermission = 'CAN_TEST' as Permission; @@ -49,13 +50,17 @@ describe('AuthorizationService', () => { provide: AuthorizationHelper, useValue: createMock(), }, + { + provide: UserRepo, + useValue: createMock(), + }, ], }).compile(); service = await module.get(AuthorizationService); ruleManager = await module.get(RuleManager); - loader = await module.get(ReferenceLoader); authorizationHelper = await module.get(AuthorizationHelper); + userRepo = await module.get(UserRepo); }); afterEach(() => { @@ -66,7 +71,7 @@ describe('AuthorizationService', () => { describe('when hasPermission returns false', () => { const setup = () => { const context = AuthorizationContextBuilder.read([]); - const user = userFactory.build(); + const user = userFactory.buildWithId(); const spy = jest.spyOn(service, 'hasPermission').mockReturnValueOnce(false); @@ -85,7 +90,7 @@ describe('AuthorizationService', () => { describe('when hasPermission returns true', () => { const setup = () => { const context = AuthorizationContextBuilder.read([]); - const user = userFactory.build(); + const user = userFactory.buildWithId(); const spy = jest.spyOn(service, 'hasPermission').mockReturnValueOnce(true); @@ -106,7 +111,7 @@ describe('AuthorizationService', () => { describe('when the selected rule returns false', () => { const setup = () => { const context = AuthorizationContextBuilder.read([]); - const user = userFactory.build(); + const user = userFactory.buildWithId(); const testRule = new TestRule(false); ruleManager.selectRule.mockReturnValueOnce(testRule); @@ -126,7 +131,7 @@ describe('AuthorizationService', () => { describe('when the selected rule returns true', () => { const setup = () => { const context = AuthorizationContextBuilder.read([]); - const user = userFactory.build(); + const user = userFactory.buildWithId(); const testRule = new TestRule(true); ruleManager.selectRule.mockReturnValueOnce(testRule); @@ -144,123 +149,10 @@ describe('AuthorizationService', () => { }); }); - describe('checkPermissionByReferences', () => { - describe('when hasPermissionByReferences returns false', () => { - const setup = () => { - const context = AuthorizationContextBuilder.read([]); - const userId = 'test'; - const entityId = 'test'; - const entityName = AuthorizableReferenceType.Course; - - const spy = jest.spyOn(service, 'hasPermissionByReferences').mockResolvedValueOnce(false); - - return { context, userId, entityId, entityName, spy }; - }; - - it('should reject with ForbiddenLoggableException', async () => { - const { context, userId, entityId, entityName, spy } = setup(); - - await expect(service.checkPermissionByReferences(userId, entityName, entityId, context)).rejects.toThrow( - ForbiddenLoggableException - ); - - spy.mockRestore(); - }); - }); - - describe('when hasPermissionByReferences returns true', () => { - const setup = () => { - const context = AuthorizationContextBuilder.read([]); - const userId = 'test'; - const entityId = 'test'; - const entityName = AuthorizableReferenceType.Course; - - const spy = jest.spyOn(service, 'hasPermissionByReferences').mockResolvedValueOnce(true); - - return { context, userId, entityId, entityName, spy }; - }; - - it('should resolve', async () => { - const { context, userId, entityId, entityName, spy } = setup(); - - await expect(service.checkPermissionByReferences(userId, entityName, entityId, context)).resolves.not.toThrow(); - - spy.mockRestore(); - }); - }); - }); - - describe('hasPermissionByReferences', () => { - describe('when loader throws an error', () => { - const setup = () => { - const context = AuthorizationContextBuilder.read([]); - const userId = 'test'; - const entityId = 'test'; - const entityName = AuthorizableReferenceType.Course; - - loader.loadAuthorizableObject.mockRejectedValueOnce(InternalServerErrorException); - - return { context, userId, entityId, entityName }; - }; - - it('should reject with ForbiddenException', async () => { - const { context, userId, entityId, entityName } = setup(); - - await expect(service.hasPermissionByReferences(userId, entityName, entityId, context)).rejects.toThrow( - ForbiddenException - ); - }); - }); - - describe('when the selected rule returns true', () => { - const setup = () => { - const context = AuthorizationContextBuilder.read([]); - const userId = 'test'; - const entityId = 'test'; - const entityName = AuthorizableReferenceType.Course; - const testRule = new TestRule(true); - - ruleManager.selectRule.mockReturnValueOnce(testRule); - - return { context, userId, entityId, entityName }; - }; - - it('should resolve to true', async () => { - const { context, userId, entityId, entityName } = setup(); - - const result = await service.hasPermissionByReferences(userId, entityName, entityId, context); - - expect(result).toBe(true); - }); - }); - - describe('when the selected rule returns false', () => { - const setup = () => { - const context = AuthorizationContextBuilder.read([]); - const userId = 'test'; - const entityId = 'test'; - const entityName = AuthorizableReferenceType.Course; - const testRule = new TestRule(false); - - ruleManager.selectRule.mockReturnValueOnce(testRule); - - return { context, userId, entityId, entityName }; - }; - - it('should resolve to false', async () => { - const { context, userId, entityId, entityName } = setup(); - - const result = await service.hasPermissionByReferences(userId, entityName, entityId, context); - - expect(result).toBe(false); - }); - }); - }); - describe('checkAllPermissions', () => { describe('when helper method returns false', () => { const setup = () => { - const user = userFactory.build(); + const user = userFactory.buildWithId(); const requiredPermissions = [testPermission]; authorizationHelper.hasAllPermissions.mockReturnValueOnce(false); @@ -277,7 +169,7 @@ describe('AuthorizationService', () => { describe('when helper method returns true', () => { const setup = () => { - const user = userFactory.build(); + const user = userFactory.buildWithId(); const requiredPermissions = [testPermission]; authorizationHelper.hasAllPermissions.mockReturnValueOnce(true); @@ -296,7 +188,7 @@ describe('AuthorizationService', () => { describe('hasAllPermissions', () => { describe('when helper method returns false', () => { const setup = () => { - const user = userFactory.build(); + const user = userFactory.buildWithId(); const requiredPermissions = [testPermission]; authorizationHelper.hasAllPermissions.mockReturnValueOnce(false); @@ -315,7 +207,7 @@ describe('AuthorizationService', () => { describe('when helper method returns true', () => { const setup = () => { - const user = userFactory.build(); + const user = userFactory.buildWithId(); const requiredPermissions = [testPermission]; authorizationHelper.hasAllPermissions.mockReturnValueOnce(true); @@ -336,7 +228,7 @@ describe('AuthorizationService', () => { describe('checkOneOfPermissions', () => { describe('when helper method returns false', () => { const setup = () => { - const user = userFactory.build(); + const user = userFactory.buildWithId(); const requiredPermissions = [testPermission]; authorizationHelper.hasOneOfPermissions.mockReturnValueOnce(false); @@ -353,7 +245,7 @@ describe('AuthorizationService', () => { describe('when helper method returns true', () => { const setup = () => { - const user = userFactory.build(); + const user = userFactory.buildWithId(); const requiredPermissions = [testPermission]; authorizationHelper.hasOneOfPermissions.mockReturnValueOnce(true); @@ -372,7 +264,7 @@ describe('AuthorizationService', () => { describe('hasOneOfPermissions', () => { describe('when helper method returns false', () => { const setup = () => { - const user = userFactory.build(); + const user = userFactory.buildWithId(); const requiredPermissions = [testPermission]; authorizationHelper.hasOneOfPermissions.mockReturnValueOnce(false); @@ -391,7 +283,7 @@ describe('AuthorizationService', () => { describe('when helper method returns true', () => { const setup = () => { - const user = userFactory.build(); + const user = userFactory.buildWithId(); const requiredPermissions = [testPermission]; authorizationHelper.hasOneOfPermissions.mockReturnValueOnce(true); @@ -410,12 +302,18 @@ describe('AuthorizationService', () => { }); describe('getUserWithPermissions', () => { + const setup = () => { + const user = userFactory.buildWithId(); + + userRepo.findById.mockResolvedValueOnce(user); + + return { user }; + }; + it('should return user received from loader', async () => { - const userId = 'test'; - const user = userFactory.build(); - loader.getUserWithPermissions.mockResolvedValueOnce(user); + const { user } = setup(); - const result = await service.getUserWithPermissions(userId); + const result = await service.getUserWithPermissions(user.id); expect(result).toEqual(user); }); diff --git a/apps/server/src/modules/authorization/domain/service/authorization.service.ts b/apps/server/src/modules/authorization/domain/service/authorization.service.ts new file mode 100644 index 00000000000..5218dffda81 --- /dev/null +++ b/apps/server/src/modules/authorization/domain/service/authorization.service.ts @@ -0,0 +1,59 @@ +import { Injectable, UnauthorizedException } from '@nestjs/common'; +import { BaseDO, EntityId, User } from '@shared/domain'; +import { AuthorizableObject } from '@shared/domain/domain-object'; +import { UserRepo } from '@shared/repo'; +import { AuthorizationHelper } from './authorization.helper'; +import { ForbiddenLoggableException } from '../error'; +import { RuleManager } from './rule-manager'; +import { AuthorizationContext } from '../type'; + +@Injectable() +export class AuthorizationService { + constructor( + private readonly ruleManager: RuleManager, + private readonly authorizationHelper: AuthorizationHelper, + private readonly userRepo: UserRepo + ) {} + + public checkPermission(user: User, object: AuthorizableObject | BaseDO, context: AuthorizationContext): void { + if (!this.hasPermission(user, object, context)) { + throw new ForbiddenLoggableException(user.id, object.constructor.name, context); + } + } + + public hasPermission(user: User, object: AuthorizableObject | BaseDO, context: AuthorizationContext): boolean { + const rule = this.ruleManager.selectRule(user, object, context); + const hasPermission = rule.hasPermission(user, object, context); + + return hasPermission; + } + + public checkAllPermissions(user: User, requiredPermissions: string[]): void { + if (!this.authorizationHelper.hasAllPermissions(user, requiredPermissions)) { + // TODO: Should be ForbiddenLoggableException + throw new UnauthorizedException(); + } + } + + public hasAllPermissions(user: User, requiredPermissions: string[]): boolean { + return this.authorizationHelper.hasAllPermissions(user, requiredPermissions); + } + + public checkOneOfPermissions(user: User, requiredPermissions: string[]): void { + if (!this.authorizationHelper.hasOneOfPermissions(user, requiredPermissions)) { + // TODO: Should be ForbiddenLoggableException + throw new UnauthorizedException(); + } + } + + public hasOneOfPermissions(user: User, requiredPermissions: string[]): boolean { + return this.authorizationHelper.hasOneOfPermissions(user, requiredPermissions); + } + + public async getUserWithPermissions(userId: EntityId): Promise { + // replace with service method getUserWithPermissions BC-5069 + const userWithPopulatedRoles = await this.userRepo.findById(userId, true); + + return userWithPopulatedRoles; + } +} diff --git a/apps/server/src/modules/authorization/domain/service/index.ts b/apps/server/src/modules/authorization/domain/service/index.ts new file mode 100644 index 00000000000..4175cc4b7a7 --- /dev/null +++ b/apps/server/src/modules/authorization/domain/service/index.ts @@ -0,0 +1,5 @@ +export * from './authorization.service'; +export * from './authorization.helper'; +export * from './rule-manager'; +export * from './authorization-reference.service'; +export * from './reference.loader'; diff --git a/apps/server/src/modules/authorization/reference.loader.spec.ts b/apps/server/src/modules/authorization/domain/service/reference.loader.spec.ts similarity index 84% rename from apps/server/src/modules/authorization/reference.loader.spec.ts rename to apps/server/src/modules/authorization/domain/service/reference.loader.spec.ts index 9bba78b1880..e2e77212cab 100644 --- a/apps/server/src/modules/authorization/reference.loader.spec.ts +++ b/apps/server/src/modules/authorization/domain/service/reference.loader.spec.ts @@ -14,11 +14,11 @@ import { TeamsRepo, UserRepo, } from '@shared/repo'; -import { roleFactory, setupEntities, userFactory } from '@shared/testing'; -import { BoardDoAuthorizableService } from '@src/modules/board'; -import { ContextExternalToolAuthorizableService } from '@src/modules/tool/context-external-tool/service/context-external-tool-authorizable.service'; +import { setupEntities, userFactory } from '@shared/testing'; +import { BoardDoAuthorizableService } from '@modules/board'; +import { ContextExternalToolAuthorizableService } from '@modules/tool/context-external-tool/service/context-external-tool-authorizable.service'; import { ReferenceLoader } from './reference.loader'; -import { AuthorizableReferenceType } from './types'; +import { AuthorizableReferenceType } from '../type'; describe('reference.loader', () => { let service: ReferenceLoader; @@ -138,7 +138,7 @@ describe('reference.loader', () => { it('should call userRepo.findById', async () => { await service.loadAuthorizableObject(AuthorizableReferenceType.User, entityId); - expect(userRepo.findById).toBeCalledWith(entityId, true); + expect(userRepo.findById).toBeCalledWith(entityId); }); it('should call lessonRepo.findById', async () => { @@ -192,33 +192,4 @@ describe('reference.loader', () => { ).rejects.toThrow(NotImplementedException); }); }); - - describe('getUserWithPermissions', () => { - describe('when user successfully', () => { - const setup = () => { - const roles = [roleFactory.build()]; - const user = userFactory.buildWithId({ roles }); - userRepo.findById.mockResolvedValue(user); - return { - user, - }; - }; - - it('should call userRepo.findById with specific arguments', async () => { - const { user } = setup(); - - await service.getUserWithPermissions(user.id); - - expect(userRepo.findById).toBeCalledWith(user.id, true); - }); - - it('should return user', async () => { - const { user } = setup(); - - const result = await service.getUserWithPermissions(user.id); - - expect(result).toBe(user); - }); - }); - }); }); diff --git a/apps/server/src/modules/authorization/reference.loader.ts b/apps/server/src/modules/authorization/domain/service/reference.loader.ts similarity index 85% rename from apps/server/src/modules/authorization/reference.loader.ts rename to apps/server/src/modules/authorization/domain/service/reference.loader.ts index 9afe013fd24..d584561be9e 100644 --- a/apps/server/src/modules/authorization/reference.loader.ts +++ b/apps/server/src/modules/authorization/domain/service/reference.loader.ts @@ -1,5 +1,5 @@ import { Injectable, NotImplementedException } from '@nestjs/common'; -import { BaseDO, EntityId, User } from '@shared/domain'; +import { BaseDO, EntityId } from '@shared/domain'; import { AuthorizableObject } from '@shared/domain/domain-object'; import { CourseGroupRepo, @@ -12,11 +12,10 @@ import { TeamsRepo, UserRepo, } from '@shared/repo'; -import { BoardDoAuthorizableService } from '@src/modules/board/service'; -import { ContextExternalToolAuthorizableService } from '@src/modules/tool/context-external-tool/service'; -import { AuthorizableReferenceType } from './types'; +import { BoardDoAuthorizableService } from '@modules/board'; +import { ContextExternalToolAuthorizableService } from '@modules/tool/context-external-tool/service'; +import { AuthorizableReferenceType } from '../type'; -// replace later with general "base" do-repo type RepoType = | TaskRepo | CourseRepo @@ -55,7 +54,7 @@ export class ReferenceLoader { this.repos.set(AuthorizableReferenceType.Task, { repo: this.taskRepo }); this.repos.set(AuthorizableReferenceType.Course, { repo: this.courseRepo }); this.repos.set(AuthorizableReferenceType.CourseGroup, { repo: this.courseGroupRepo }); - this.repos.set(AuthorizableReferenceType.User, { repo: this.userRepo, populate: true }); + this.repos.set(AuthorizableReferenceType.User, { repo: this.userRepo }); this.repos.set(AuthorizableReferenceType.School, { repo: this.schoolRepo }); this.repos.set(AuthorizableReferenceType.Lesson, { repo: this.lessonRepo }); this.repos.set(AuthorizableReferenceType.Team, { repo: this.teamsRepo, populate: true }); @@ -90,10 +89,4 @@ export class ReferenceLoader { return object; } - - async getUserWithPermissions(userId: EntityId): Promise { - const user = await this.userRepo.findById(userId, true); - - return user; - } } diff --git a/apps/server/src/modules/authorization/rule-manager.spec.ts b/apps/server/src/modules/authorization/domain/service/rule-manager.spec.ts similarity index 94% rename from apps/server/src/modules/authorization/rule-manager.spec.ts rename to apps/server/src/modules/authorization/domain/service/rule-manager.spec.ts index 0a2b90c7639..5b62f850416 100644 --- a/apps/server/src/modules/authorization/rule-manager.spec.ts +++ b/apps/server/src/modules/authorization/domain/service/rule-manager.spec.ts @@ -1,6 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { InternalServerErrorException, NotImplementedException } from '@nestjs/common'; import { Test } from '@nestjs/testing'; +import { courseFactory, setupEntities, userFactory } from '@shared/testing'; +import { AuthorizationContextBuilder } from '../mapper'; import { BoardDoRule, ContextExternalToolRule, @@ -13,10 +15,9 @@ import { TaskRule, TeamRule, UserRule, -} from '@shared/domain/rules'; -import { UserLoginMigrationRule } from '@shared/domain/rules/user-login-migration.rule'; -import { courseFactory, setupEntities, userFactory } from '@shared/testing'; -import { AuthorizationContextBuilder } from './authorization-context.builder'; + UserLoginMigrationRule, + GroupRule, +} from '../rules'; import { RuleManager } from './rule-manager'; describe('RuleManager', () => { @@ -33,6 +34,7 @@ describe('RuleManager', () => { let boardDoRule: DeepMocked; let contextExternalToolRule: DeepMocked; let userLoginMigrationRule: DeepMocked; + let groupRule: DeepMocked; beforeAll(async () => { await setupEntities(); @@ -42,6 +44,7 @@ describe('RuleManager', () => { RuleManager, { provide: CourseRule, useValue: createMock() }, { provide: CourseGroupRule, useValue: createMock() }, + { provide: GroupRule, useValue: createMock() }, { provide: LessonRule, useValue: createMock() }, { provide: LegacySchoolRule, useValue: createMock() }, { provide: UserRule, useValue: createMock() }, @@ -68,6 +71,7 @@ describe('RuleManager', () => { boardDoRule = await module.get(BoardDoRule); contextExternalToolRule = await module.get(ContextExternalToolRule); userLoginMigrationRule = await module.get(UserLoginMigrationRule); + groupRule = await module.get(GroupRule); }); afterEach(() => { @@ -98,6 +102,7 @@ describe('RuleManager', () => { boardDoRule.isApplicable.mockReturnValueOnce(false); contextExternalToolRule.isApplicable.mockReturnValueOnce(false); userLoginMigrationRule.isApplicable.mockReturnValueOnce(false); + groupRule.isApplicable.mockReturnValueOnce(false); return { user, object, context }; }; @@ -119,6 +124,7 @@ describe('RuleManager', () => { expect(boardDoRule.isApplicable).toBeCalled(); expect(contextExternalToolRule.isApplicable).toBeCalled(); expect(userLoginMigrationRule.isApplicable).toBeCalled(); + expect(groupRule.isApplicable).toBeCalled(); }); it('should return CourseRule', () => { @@ -148,6 +154,7 @@ describe('RuleManager', () => { boardDoRule.isApplicable.mockReturnValueOnce(false); contextExternalToolRule.isApplicable.mockReturnValueOnce(false); userLoginMigrationRule.isApplicable.mockReturnValueOnce(false); + groupRule.isApplicable.mockReturnValueOnce(false); return { user, object, context }; }; @@ -177,6 +184,7 @@ describe('RuleManager', () => { boardDoRule.isApplicable.mockReturnValueOnce(false); contextExternalToolRule.isApplicable.mockReturnValueOnce(false); userLoginMigrationRule.isApplicable.mockReturnValueOnce(false); + groupRule.isApplicable.mockReturnValueOnce(false); return { user, object, context }; }; diff --git a/apps/server/src/modules/authorization/rule-manager.ts b/apps/server/src/modules/authorization/domain/service/rule-manager.ts similarity index 87% rename from apps/server/src/modules/authorization/rule-manager.ts rename to apps/server/src/modules/authorization/domain/service/rule-manager.ts index 3aece68402a..6e6237d125f 100644 --- a/apps/server/src/modules/authorization/rule-manager.ts +++ b/apps/server/src/modules/authorization/domain/service/rule-manager.ts @@ -1,21 +1,22 @@ import { Injectable, InternalServerErrorException, NotImplementedException } from '@nestjs/common'; import { BaseDO, User } from '@shared/domain'; import { AuthorizableObject } from '@shared/domain/domain-object'; // fix import when it is avaible +import type { AuthorizationContext, Rule } from '../type'; import { BoardDoRule, + ContextExternalToolRule, CourseGroupRule, CourseRule, + LegacySchoolRule, LessonRule, SchoolExternalToolRule, - LegacySchoolRule, SubmissionRule, TaskRule, TeamRule, + UserLoginMigrationRule, UserRule, -} from '@shared/domain/rules'; -import { ContextExternalToolRule } from '@shared/domain/rules/context-external-tool.rule'; -import { UserLoginMigrationRule } from '@shared/domain/rules/user-login-migration.rule'; -import { AuthorizationContext, Rule } from './types'; + GroupRule, +} from '../rules'; @Injectable() export class RuleManager { @@ -33,7 +34,8 @@ export class RuleManager { private readonly schoolExternalToolRule: SchoolExternalToolRule, private readonly boardDoRule: BoardDoRule, private readonly contextExternalToolRule: ContextExternalToolRule, - private readonly userLoginMigrationRule: UserLoginMigrationRule + private readonly userLoginMigrationRule: UserLoginMigrationRule, + private readonly groupRule: GroupRule ) { this.rules = [ this.courseRule, @@ -48,6 +50,7 @@ export class RuleManager { this.boardDoRule, this.contextExternalToolRule, this.userLoginMigrationRule, + this.groupRule, ]; } diff --git a/apps/server/src/modules/authorization/types/action.enum.ts b/apps/server/src/modules/authorization/domain/type/action.enum.ts similarity index 100% rename from apps/server/src/modules/authorization/types/action.enum.ts rename to apps/server/src/modules/authorization/domain/type/action.enum.ts diff --git a/apps/server/src/modules/authorization/types/allowed-authorization-object-type.enum.ts b/apps/server/src/modules/authorization/domain/type/allowed-authorization-object-type.enum.ts similarity index 100% rename from apps/server/src/modules/authorization/types/allowed-authorization-object-type.enum.ts rename to apps/server/src/modules/authorization/domain/type/allowed-authorization-object-type.enum.ts diff --git a/apps/server/src/modules/authorization/types/authorization-context.interface.ts b/apps/server/src/modules/authorization/domain/type/authorization-context.interface.ts similarity index 100% rename from apps/server/src/modules/authorization/types/authorization-context.interface.ts rename to apps/server/src/modules/authorization/domain/type/authorization-context.interface.ts diff --git a/apps/server/src/modules/authorization/types/authorization-loader-service.ts b/apps/server/src/modules/authorization/domain/type/authorization-loader-service.ts similarity index 100% rename from apps/server/src/modules/authorization/types/authorization-loader-service.ts rename to apps/server/src/modules/authorization/domain/type/authorization-loader-service.ts diff --git a/apps/server/src/modules/authorization/types/index.ts b/apps/server/src/modules/authorization/domain/type/index.ts similarity index 100% rename from apps/server/src/modules/authorization/types/index.ts rename to apps/server/src/modules/authorization/domain/type/index.ts index 92e7b0c8bf5..b1942491098 100644 --- a/apps/server/src/modules/authorization/types/index.ts +++ b/apps/server/src/modules/authorization/domain/type/index.ts @@ -1,5 +1,5 @@ export * from './action.enum'; export * from './authorization-context.interface'; export * from './rule.interface'; -export * from './allowed-authorization-object-type.enum'; export * from './authorization-loader-service'; +export * from './allowed-authorization-object-type.enum'; diff --git a/apps/server/src/modules/authorization/types/rule.interface.ts b/apps/server/src/modules/authorization/domain/type/rule.interface.ts similarity index 100% rename from apps/server/src/modules/authorization/types/rule.interface.ts rename to apps/server/src/modules/authorization/domain/type/rule.interface.ts diff --git a/apps/server/src/modules/authorization/index.ts b/apps/server/src/modules/authorization/index.ts index bee8b7d4bb1..e129df2cd11 100644 --- a/apps/server/src/modules/authorization/index.ts +++ b/apps/server/src/modules/authorization/index.ts @@ -1,5 +1,15 @@ -export * from './authorization.module'; -export * from './authorization.service'; -export * from './authorization-context.builder'; -export * from './types'; -export * from './feathers'; +export { AuthorizationModule } from './authorization.module'; +export { + AuthorizationService, + AuthorizationHelper, + AuthorizationContextBuilder, + ForbiddenLoggableException, + Rule, + AuthorizationContext, + // Action should not be exported, but hard to solve for now. The AuthorizationContextBuilder is the prefared way + Action, + AuthorizationLoaderService, + AuthorizationLoaderServiceGeneric, +} from './domain'; +// Should not used anymore +export { FeathersAuthorizationService } from './feathers'; diff --git a/apps/server/src/modules/board/board-api.module.ts b/apps/server/src/modules/board/board-api.module.ts index 2ac4581dfd0..10b868e67b3 100644 --- a/apps/server/src/modules/board/board-api.module.ts +++ b/apps/server/src/modules/board/board-api.module.ts @@ -1,6 +1,6 @@ import { forwardRef, Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; -import { AuthorizationModule } from '@src/modules/authorization'; +import { AuthorizationModule } from '@modules/authorization'; import { BoardModule } from './board.module'; import { BoardController, diff --git a/apps/server/src/modules/board/board.module.ts b/apps/server/src/modules/board/board.module.ts index a002766b56d..fb04364b6c3 100644 --- a/apps/server/src/modules/board/board.module.ts +++ b/apps/server/src/modules/board/board.module.ts @@ -14,6 +14,7 @@ import { ColumnBoardService, ColumnService, ContentElementService, + OpenGraphProxyService, SubmissionItemService, } from './service'; import { BoardDoCopyService, SchoolSpecificFileCopyServiceFactory } from './service/board-do-copy-service'; @@ -37,6 +38,7 @@ import { ColumnBoardCopyService } from './service/column-board-copy.service'; BoardDoCopyService, ColumnBoardCopyService, SchoolSpecificFileCopyServiceFactory, + OpenGraphProxyService, ], exports: [ BoardDoAuthorizableService, diff --git a/apps/server/src/modules/board/controller/api-test/board-context.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-context.api.spec.ts index 99bb5403692..0cbdad3b8c2 100644 --- a/apps/server/src/modules/board/controller/api-test/board-context.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-context.api.spec.ts @@ -9,7 +9,7 @@ import { TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; -import { ServerTestModule } from '@src/modules/server'; +import { ServerTestModule } from '@modules/server'; import { BoardContextResponse } from '../dto/board/board-context.reponse'; const baseRouteName = '/boards'; diff --git a/apps/server/src/modules/board/controller/api-test/board-delete.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-delete.api.spec.ts index cd5a1d5c34b..e92bb645872 100644 --- a/apps/server/src/modules/board/controller/api-test/board-delete.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-delete.api.spec.ts @@ -11,9 +11,9 @@ import { mapUserToCurrentUser, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@src/modules/authentication'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server'; import { Request } from 'express'; import request from 'supertest'; import { BoardResponse } from '../dto'; diff --git a/apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts index ebcb9c280a0..8423d68f793 100644 --- a/apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-lookup.api.spec.ts @@ -12,9 +12,9 @@ import { mapUserToCurrentUser, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@src/modules/authentication'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import request from 'supertest'; import { BoardResponse } from '../dto'; diff --git a/apps/server/src/modules/board/controller/api-test/board-update-title.api.spec.ts b/apps/server/src/modules/board/controller/api-test/board-update-title.api.spec.ts index 4dc93d91417..9b77f2f3824 100644 --- a/apps/server/src/modules/board/controller/api-test/board-update-title.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/board-update-title.api.spec.ts @@ -9,7 +9,7 @@ import { TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { ServerTestModule } from '@modules/server/server.module'; const baseRouteName = '/boards'; diff --git a/apps/server/src/modules/board/controller/api-test/card-create.api.spec.ts b/apps/server/src/modules/board/controller/api-test/card-create.api.spec.ts index beb31d4dd5c..4b0f57c361d 100644 --- a/apps/server/src/modules/board/controller/api-test/card-create.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/card-create.api.spec.ts @@ -11,9 +11,9 @@ import { mapUserToCurrentUser, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@src/modules/authentication'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import request from 'supertest'; import { CardResponse } from '../dto'; @@ -87,7 +87,7 @@ describe(`card create (api)`, () => { em.clear(); const createCardBodyParams = { - requiredEmptyElements: [ContentElementType.RICH_TEXT, ContentElementType.FILE], + requiredEmptyElements: [ContentElementType.RICH_TEXT, ContentElementType.FILE, ContentElementType.LINK], }; return { user, columnBoardNode, columnNode, createCardBodyParams }; @@ -111,7 +111,7 @@ describe(`card create (api)`, () => { expect(result.id).toBeDefined(); }); - it('created card should contain empty text and file elements', async () => { + it('created card should contain empty text, file and link elements', async () => { const { user, columnNode, createCardBodyParams } = await setup(); currentUser = mapUserToCurrentUser(user); @@ -129,6 +129,12 @@ describe(`card create (api)`, () => { alternativeText: '', }, }, + { + type: 'link', + content: { + url: '', + }, + }, ]; const { result } = await api.post(columnNode.id, createCardBodyParams); @@ -136,6 +142,7 @@ describe(`card create (api)`, () => { expect(elements[0]).toMatchObject(expectedEmptyElements[0]); expect(elements[1]).toMatchObject(expectedEmptyElements[1]); + expect(elements[2]).toMatchObject(expectedEmptyElements[2]); }); it('should return status 400 as the content element is unknown', async () => { const { user, columnNode } = await setup(); diff --git a/apps/server/src/modules/board/controller/api-test/card-delete.api.spec.ts b/apps/server/src/modules/board/controller/api-test/card-delete.api.spec.ts index 297fc296998..3fff41b3660 100644 --- a/apps/server/src/modules/board/controller/api-test/card-delete.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/card-delete.api.spec.ts @@ -13,9 +13,9 @@ import { richTextElementNodeFactory, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@src/modules/authentication'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/board/controller/api-test/card-lookup.api.spec.ts b/apps/server/src/modules/board/controller/api-test/card-lookup.api.spec.ts index 8f2c7bee9a9..711ccf1ac8f 100644 --- a/apps/server/src/modules/board/controller/api-test/card-lookup.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/card-lookup.api.spec.ts @@ -15,9 +15,9 @@ import { schoolFactory, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@src/modules/authentication'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import request from 'supertest'; import { CardIdsParams, CardListResponse } from '../dto'; diff --git a/apps/server/src/modules/board/controller/api-test/card-move.api.spec.ts b/apps/server/src/modules/board/controller/api-test/card-move.api.spec.ts index 892234c122a..e9e55b27dd2 100644 --- a/apps/server/src/modules/board/controller/api-test/card-move.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/card-move.api.spec.ts @@ -12,9 +12,9 @@ import { mapUserToCurrentUser, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@src/modules/authentication'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/board/controller/api-test/card-update-height.api.spec.ts b/apps/server/src/modules/board/controller/api-test/card-update-height.api.spec.ts index 6e0de6e8739..2d4ab15b365 100644 --- a/apps/server/src/modules/board/controller/api-test/card-update-height.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/card-update-height.api.spec.ts @@ -11,7 +11,7 @@ import { columnNodeFactory, courseFactory, } from '@shared/testing'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { ServerTestModule } from '@modules/server/server.module'; describe(`card update height (api)`, () => { let app: INestApplication; diff --git a/apps/server/src/modules/board/controller/api-test/card-update-title.api.spec.ts b/apps/server/src/modules/board/controller/api-test/card-update-title.api.spec.ts index 5939c9c0cc0..bc598d69051 100644 --- a/apps/server/src/modules/board/controller/api-test/card-update-title.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/card-update-title.api.spec.ts @@ -11,7 +11,7 @@ import { TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { ServerTestModule } from '@modules/server/server.module'; const baseRouteName = '/cards'; diff --git a/apps/server/src/modules/board/controller/api-test/column-create.api.spec.ts b/apps/server/src/modules/board/controller/api-test/column-create.api.spec.ts index 1f6777ab447..52aaa4608ca 100644 --- a/apps/server/src/modules/board/controller/api-test/column-create.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/column-create.api.spec.ts @@ -10,9 +10,9 @@ import { mapUserToCurrentUser, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@src/modules/authentication'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import request from 'supertest'; import { ColumnResponse } from '../dto'; diff --git a/apps/server/src/modules/board/controller/api-test/column-delete.api.spec.ts b/apps/server/src/modules/board/controller/api-test/column-delete.api.spec.ts index 1d03e6579ba..86e3e666c84 100644 --- a/apps/server/src/modules/board/controller/api-test/column-delete.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/column-delete.api.spec.ts @@ -12,9 +12,9 @@ import { mapUserToCurrentUser, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@src/modules/authentication'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/board/controller/api-test/column-move.api.spec.ts b/apps/server/src/modules/board/controller/api-test/column-move.api.spec.ts index 2f65a6f2a0e..6fda90f739a 100644 --- a/apps/server/src/modules/board/controller/api-test/column-move.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/column-move.api.spec.ts @@ -12,9 +12,9 @@ import { mapUserToCurrentUser, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@src/modules/authentication'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/board/controller/api-test/column-update-title.api.spec.ts b/apps/server/src/modules/board/controller/api-test/column-update-title.api.spec.ts index 82fa2264f7a..350406041c7 100644 --- a/apps/server/src/modules/board/controller/api-test/column-update-title.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/column-update-title.api.spec.ts @@ -10,7 +10,7 @@ import { TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { ServerTestModule } from '@modules/server/server.module'; const baseRouteName = '/columns'; diff --git a/apps/server/src/modules/board/controller/api-test/content-element-create.api.spec.ts b/apps/server/src/modules/board/controller/api-test/content-element-create.api.spec.ts index 0f7a3795580..57ef692ace1 100644 --- a/apps/server/src/modules/board/controller/api-test/content-element-create.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/content-element-create.api.spec.ts @@ -11,7 +11,7 @@ import { TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { ServerTestModule } from '@modules/server/server.module'; import { AnyContentElementResponse, SubmissionContainerElementResponse } from '../dto'; const baseRouteName = '/cards'; diff --git a/apps/server/src/modules/board/controller/api-test/content-element-delete.api.spec.ts b/apps/server/src/modules/board/controller/api-test/content-element-delete.api.spec.ts index 107c12a723a..6b99a64e7ab 100644 --- a/apps/server/src/modules/board/controller/api-test/content-element-delete.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/content-element-delete.api.spec.ts @@ -13,9 +13,9 @@ import { richTextElementNodeFactory, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@src/modules/authentication'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/board/controller/api-test/content-element-move.api.spec.ts b/apps/server/src/modules/board/controller/api-test/content-element-move.api.spec.ts index 0793dce8951..dbcd9acbc31 100644 --- a/apps/server/src/modules/board/controller/api-test/content-element-move.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/content-element-move.api.spec.ts @@ -13,9 +13,9 @@ import { richTextElementNodeFactory, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@src/modules/authentication'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/board/controller/api-test/content-element-update-content.spec.ts b/apps/server/src/modules/board/controller/api-test/content-element-update-content.spec.ts index bee1ad63f0f..3be8e7dda15 100644 --- a/apps/server/src/modules/board/controller/api-test/content-element-update-content.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/content-element-update-content.spec.ts @@ -11,6 +11,8 @@ import { SubmissionContainerElementNode, } from '@shared/domain'; import { + TestApiClient, + UserAndAccountTestFactory, cardNodeFactory, cleanupCollections, columnBoardNodeFactory, @@ -19,10 +21,8 @@ import { fileElementNodeFactory, richTextElementNodeFactory, submissionContainerElementNodeFactory, - TestApiClient, - UserAndAccountTestFactory, } from '@shared/testing'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { ServerTestModule } from '@modules/server/server.module'; describe(`content element update content (api)`, () => { let app: INestApplication; @@ -63,7 +63,10 @@ describe(`content element update content (api)`, () => { const parentCard = cardNodeFactory.buildWithId({ parent: column }); const richTextElement = richTextElementNodeFactory.buildWithId({ parent: parentCard }); const fileElement = fileElementNodeFactory.buildWithId({ parent: parentCard }); - const submissionContainerElement = submissionContainerElementNodeFactory.buildWithId({ parent: parentCard }); + const submissionContainerElement = submissionContainerElementNodeFactory.buildWithId({ + parent: parentCard, + dueDate: null, + }); const tomorrow = new Date(Date.now() + 86400000); const submissionContainerElementWithDueDate = submissionContainerElementNodeFactory.buildWithId({ @@ -95,7 +98,7 @@ describe(`content element update content (api)`, () => { }; }; - it('should return status 204', async () => { + it('should return status 201', async () => { const { loggedInClient, richTextElement } = await setup(); const response = await loggedInClient.patch(`${richTextElement.id}/content`, { @@ -105,7 +108,7 @@ describe(`content element update content (api)`, () => { }, }); - expect(response.statusCode).toEqual(204); + expect(response.statusCode).toEqual(201); }); it('should actually change content of the element', async () => { @@ -164,9 +167,8 @@ describe(`content element update content (api)`, () => { expect(result.alternativeText).toEqual('rich text 1 some more text'); }); - it('should return status 204 (nothing changed) without dueDate parameter for submission container element', async () => { + it('should return status 201', async () => { const { loggedInClient, submissionContainerElement } = await setup(); - const response = await loggedInClient.patch(`${submissionContainerElement.id}/content`, { data: { content: {}, @@ -174,10 +176,10 @@ describe(`content element update content (api)`, () => { }, }); - expect(response.statusCode).toEqual(204); + expect(response.statusCode).toEqual(201); }); - it('should not change dueDate value without dueDate parameter for submission container element', async () => { + it('should not change dueDate when not proviced in submission container element without dueDate', async () => { const { loggedInClient, submissionContainerElement } = await setup(); await loggedInClient.patch(`${submissionContainerElement.id}/content`, { @@ -187,11 +189,10 @@ describe(`content element update content (api)`, () => { }, }); const result = await em.findOneOrFail(SubmissionContainerElementNode, submissionContainerElement.id); - - expect(result.dueDate).toBeUndefined(); + expect(result.dueDate).toBeNull(); }); - it('should set dueDate value when dueDate parameter is provided for submission container element', async () => { + it('should set dueDate value when provided for submission container element', async () => { const { loggedInClient, submissionContainerElement } = await setup(); const inThreeDays = new Date(Date.now() + 259200000); @@ -207,18 +208,20 @@ describe(`content element update content (api)`, () => { expect(result.dueDate).toEqual(inThreeDays); }); - it('should unset dueDate value when dueDate parameter is not provided for submission container element', async () => { + it('should unset dueDate value when dueDate parameter is null for submission container element', async () => { const { loggedInClient, submissionContainerElementWithDueDate } = await setup(); await loggedInClient.patch(`${submissionContainerElementWithDueDate.id}/content`, { data: { - content: {}, + content: { + dueDate: null, + }, type: 'submissionContainer', }, }); const result = await em.findOneOrFail(SubmissionContainerElementNode, submissionContainerElementWithDueDate.id); - expect(result.dueDate).toBeUndefined(); + expect(result.dueDate).toBeNull(); }); it('should return status 400 for wrong date format for submission container element', async () => { 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..0fb70869e18 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 @@ -13,7 +13,7 @@ import { submissionContainerElementNodeFactory, userFactory, } from '@shared/testing'; -import { ServerTestModule } from '@src/modules/server'; +import { ServerTestModule } from '@modules/server'; import { SubmissionItemResponse } from '../dto'; const baseRouteName = '/elements'; diff --git a/apps/server/src/modules/board/controller/api-test/submission-item-lookup.api.spec.ts b/apps/server/src/modules/board/controller/api-test/submission-item-lookup.api.spec.ts index 6f31d5a4e5a..c119ace1d3c 100644 --- a/apps/server/src/modules/board/controller/api-test/submission-item-lookup.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/submission-item-lookup.api.spec.ts @@ -14,8 +14,8 @@ import { submissionItemNodeFactory, userFactory, } from '@shared/testing'; -import { ServerTestModule } from '@src/modules/server'; -import { SubmissionsResponse } from '../dto/submission-item/submissions.response'; +import { ServerTestModule } from '@modules/server'; +import { SubmissionsResponse } from '../dto'; const baseRouteName = '/board-submissions'; describe('submission item lookup (api)', () => { diff --git a/apps/server/src/modules/board/controller/api-test/submission-item-update.api.spec.ts b/apps/server/src/modules/board/controller/api-test/submission-item-update.api.spec.ts index dfa8bf017b3..f8058a3ee4b 100644 --- a/apps/server/src/modules/board/controller/api-test/submission-item-update.api.spec.ts +++ b/apps/server/src/modules/board/controller/api-test/submission-item-update.api.spec.ts @@ -13,7 +13,7 @@ import { submissionContainerElementNodeFactory, submissionItemNodeFactory, } from '@shared/testing'; -import { ServerTestModule } from '@src/modules/server'; +import { ServerTestModule } from '@modules/server'; import { SubmissionItemResponse } from '../dto'; const baseRouteName = '/board-submissions'; diff --git a/apps/server/src/modules/board/controller/board-submission.controller.ts b/apps/server/src/modules/board/controller/board-submission.controller.ts index 56c6ae76ce2..cffdcd64467 100644 --- a/apps/server/src/modules/board/controller/board-submission.controller.ts +++ b/apps/server/src/modules/board/controller/board-submission.controller.ts @@ -1,9 +1,8 @@ import { Body, Controller, ForbiddenException, Get, HttpCode, NotFoundException, Param, Patch } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiValidationError } from '@shared/common'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; -import { SubmissionsResponse } from '@src/modules/board/controller/dto/submission-item/submissions.response'; +import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; +import { SubmissionsResponse } from './dto/submission-item/submissions.response'; import { CardUc } from '../uc'; import { ElementUc } from '../uc/element.uc'; import { SubmissionItemUc } from '../uc/submission-item.uc'; diff --git a/apps/server/src/modules/board/controller/board.controller.ts b/apps/server/src/modules/board/controller/board.controller.ts index f52d03142d7..0d77aa80b3d 100644 --- a/apps/server/src/modules/board/controller/board.controller.ts +++ b/apps/server/src/modules/board/controller/board.controller.ts @@ -12,8 +12,7 @@ import { } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiValidationError } from '@shared/common'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; +import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; import { BoardUc } from '../uc'; import { BoardResponse, BoardUrlParams, ColumnResponse, RenameBodyParams } from './dto'; import { BoardContextResponse } from './dto/board/board-context.reponse'; diff --git a/apps/server/src/modules/board/controller/card.controller.ts b/apps/server/src/modules/board/controller/card.controller.ts index e76bdbe088c..62afa262439 100644 --- a/apps/server/src/modules/board/controller/card.controller.ts +++ b/apps/server/src/modules/board/controller/card.controller.ts @@ -14,8 +14,7 @@ import { } from '@nestjs/common'; import { ApiExtraModels, ApiOperation, ApiResponse, ApiTags, getSchemaPath } from '@nestjs/swagger'; import { ApiValidationError } from '@shared/common'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; +import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; import { BoardUc, CardUc } from '../uc'; import { AnyContentElementResponse, @@ -25,6 +24,7 @@ import { CreateContentElementBodyParams, ExternalToolElementResponse, FileElementResponse, + LinkElementResponse, MoveCardBodyParams, RenameBodyParams, RichTextElementResponse, @@ -116,19 +116,21 @@ export class CardController { @ApiOperation({ summary: 'Create a new element on a card.' }) @ApiExtraModels( - RichTextElementResponse, + ExternalToolElementResponse, FileElementResponse, - SubmissionContainerElementResponse, - ExternalToolElementResponse + LinkElementResponse, + RichTextElementResponse, + SubmissionContainerElementResponse ) @ApiResponse({ status: 201, schema: { oneOf: [ - { $ref: getSchemaPath(RichTextElementResponse) }, + { $ref: getSchemaPath(ExternalToolElementResponse) }, { $ref: getSchemaPath(FileElementResponse) }, + { $ref: getSchemaPath(LinkElementResponse) }, + { $ref: getSchemaPath(RichTextElementResponse) }, { $ref: getSchemaPath(SubmissionContainerElementResponse) }, - { $ref: getSchemaPath(ExternalToolElementResponse) }, ], }, }) @@ -137,7 +139,7 @@ export class CardController { @ApiResponse({ status: 404, type: NotFoundException }) @Post(':cardId/elements') async createElement( - @Param() urlParams: CardUrlParams, // TODO add type-property ? + @Param() urlParams: CardUrlParams, @Body() bodyParams: CreateContentElementBodyParams, @CurrentUser() currentUser: ICurrentUser ): Promise { diff --git a/apps/server/src/modules/board/controller/column.controller.ts b/apps/server/src/modules/board/controller/column.controller.ts index 57359f4cc93..9862ef23a74 100644 --- a/apps/server/src/modules/board/controller/column.controller.ts +++ b/apps/server/src/modules/board/controller/column.controller.ts @@ -12,8 +12,7 @@ import { } from '@nestjs/common'; import { ApiBody, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiValidationError } from '@shared/common'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; +import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; import { BoardUc } from '../uc'; import { CardResponse, ColumnUrlParams, MoveColumnBodyParams, RenameBodyParams } from './dto'; import { CardResponseMapper } from './mapper'; diff --git a/apps/server/src/modules/board/controller/dto/card/card.response.ts b/apps/server/src/modules/board/controller/dto/card/card.response.ts index 44ee426fb6b..3577fcbc2a1 100644 --- a/apps/server/src/modules/board/controller/dto/card/card.response.ts +++ b/apps/server/src/modules/board/controller/dto/card/card.response.ts @@ -1,11 +1,23 @@ import { ApiExtraModels, ApiProperty, ApiPropertyOptional, getSchemaPath } from '@nestjs/swagger'; import { DecodeHtmlEntities } from '@shared/controller'; -import { AnyContentElementResponse, FileElementResponse, SubmissionContainerElementResponse } from '../element'; -import { RichTextElementResponse } from '../element/rich-text-element.response'; +import { + AnyContentElementResponse, + ExternalToolElementResponse, + FileElementResponse, + LinkElementResponse, + RichTextElementResponse, + SubmissionContainerElementResponse, +} from '../element'; import { TimestampsResponse } from '../timestamps.response'; import { VisibilitySettingsResponse } from './visibility-settings.response'; -@ApiExtraModels(RichTextElementResponse) +@ApiExtraModels( + ExternalToolElementResponse, + FileElementResponse, + LinkElementResponse, + RichTextElementResponse, + SubmissionContainerElementResponse +) export class CardResponse { constructor({ id, title, height, elements, visibilitySettings, timestamps }: CardResponse) { this.id = id; @@ -32,8 +44,10 @@ export class CardResponse { type: 'array', items: { oneOf: [ - { $ref: getSchemaPath(RichTextElementResponse) }, + { $ref: getSchemaPath(ExternalToolElementResponse) }, { $ref: getSchemaPath(FileElementResponse) }, + { $ref: getSchemaPath(LinkElementResponse) }, + { $ref: getSchemaPath(RichTextElementResponse) }, { $ref: getSchemaPath(SubmissionContainerElementResponse) }, ], }, diff --git a/apps/server/src/modules/board/controller/dto/element/any-content-element.response.ts b/apps/server/src/modules/board/controller/dto/element/any-content-element.response.ts index 1382b75b3f5..18415d172fa 100644 --- a/apps/server/src/modules/board/controller/dto/element/any-content-element.response.ts +++ b/apps/server/src/modules/board/controller/dto/element/any-content-element.response.ts @@ -1,10 +1,12 @@ import { ExternalToolElementResponse } from './external-tool-element.response'; import { FileElementResponse } from './file-element.response'; +import { LinkElementResponse } from './link-element.response'; import { RichTextElementResponse } from './rich-text-element.response'; import { SubmissionContainerElementResponse } from './submission-container-element.response'; export type AnyContentElementResponse = | FileElementResponse + | LinkElementResponse | RichTextElementResponse | SubmissionContainerElementResponse | ExternalToolElementResponse; diff --git a/apps/server/src/modules/board/controller/dto/element/external-tool-element.response.ts b/apps/server/src/modules/board/controller/dto/element/external-tool-element.response.ts index 5f51a1a26ec..fc67b7631b6 100644 --- a/apps/server/src/modules/board/controller/dto/element/external-tool-element.response.ts +++ b/apps/server/src/modules/board/controller/dto/element/external-tool-element.response.ts @@ -1,4 +1,4 @@ -import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ApiProperty } from '@nestjs/swagger'; import { ContentElementType } from '@shared/domain'; import { TimestampsResponse } from '../timestamps.response'; @@ -7,8 +7,8 @@ export class ExternalToolElementContent { this.contextExternalToolId = props.contextExternalToolId; } - @ApiPropertyOptional() - contextExternalToolId?: string; + @ApiProperty({ type: String, required: true, nullable: true }) + contextExternalToolId: string | null; } export class ExternalToolElementResponse { diff --git a/apps/server/src/modules/board/controller/dto/element/index.ts b/apps/server/src/modules/board/controller/dto/element/index.ts index b1ef77f8ec0..6787c007c1b 100644 --- a/apps/server/src/modules/board/controller/dto/element/index.ts +++ b/apps/server/src/modules/board/controller/dto/element/index.ts @@ -1,7 +1,8 @@ export * from './any-content-element.response'; export * from './create-content-element.body.params'; -export * from './update-element-content.body.params'; +export * from './external-tool-element.response'; export * from './file-element.response'; +export * from './link-element.response'; export * from './rich-text-element.response'; export * from './submission-container-element.response'; -export * from './external-tool-element.response'; +export * from './update-element-content.body.params'; diff --git a/apps/server/src/modules/board/controller/dto/element/link-element.response.ts b/apps/server/src/modules/board/controller/dto/element/link-element.response.ts new file mode 100644 index 00000000000..d6c4a7e7080 --- /dev/null +++ b/apps/server/src/modules/board/controller/dto/element/link-element.response.ts @@ -0,0 +1,45 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ContentElementType } from '@shared/domain'; +import { TimestampsResponse } from '../timestamps.response'; + +export class LinkElementContent { + constructor({ url, title, description, imageUrl }: LinkElementContent) { + this.url = url; + this.title = title; + this.description = description; + this.imageUrl = imageUrl; + } + + @ApiProperty() + url: string; + + @ApiProperty() + title: string; + + @ApiPropertyOptional() + description?: string; + + @ApiPropertyOptional() + imageUrl?: string; +} + +export class LinkElementResponse { + constructor({ id, content, timestamps, type }: LinkElementResponse) { + this.id = id; + this.content = content; + this.timestamps = timestamps; + this.type = type; + } + + @ApiProperty({ pattern: '[a-f0-9]{24}' }) + id: string; + + @ApiProperty({ enum: ContentElementType, enumName: 'ContentElementType' }) + type: ContentElementType.LINK; + + @ApiProperty() + content: LinkElementContent; + + @ApiProperty() + timestamps: TimestampsResponse; +} diff --git a/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts b/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts index 05856e9ef5f..d9b709d8d67 100644 --- a/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts +++ b/apps/server/src/modules/board/controller/dto/element/update-element-content.body.params.ts @@ -31,6 +31,20 @@ export class FileElementContentBody extends ElementContentBody { @ApiProperty() content!: FileContentBody; } +export class LinkContentBody { + @IsString() + @ApiProperty({}) + url!: string; +} + +export class LinkElementContentBody extends ElementContentBody { + @ApiProperty({ type: ContentElementType.LINK }) + type!: ContentElementType.LINK; + + @ValidateNested() + @ApiProperty({}) + content!: LinkContentBody; +} export class RichTextContentBody { @IsString() @@ -55,7 +69,6 @@ export class SubmissionContainerContentBody { @IsDate() @IsOptional() @ApiPropertyOptional({ - required: false, description: 'The point in time until when a submission can be handed in.', }) dueDate?: Date; @@ -88,6 +101,7 @@ export class ExternalToolElementContentBody extends ElementContentBody { export type AnyElementContentBody = | FileContentBody + | LinkContentBody | RichTextContentBody | SubmissionContainerContentBody | ExternalToolContentBody; @@ -99,6 +113,7 @@ export class UpdateElementContentBodyParams { property: 'type', subTypes: [ { value: FileElementContentBody, name: ContentElementType.FILE }, + { value: LinkElementContentBody, name: ContentElementType.LINK }, { value: RichTextElementContentBody, name: ContentElementType.RICH_TEXT }, { value: SubmissionContainerElementContentBody, name: ContentElementType.SUBMISSION_CONTAINER }, { value: ExternalToolElementContentBody, name: ContentElementType.EXTERNAL_TOOL }, @@ -109,6 +124,7 @@ export class UpdateElementContentBodyParams { @ApiProperty({ oneOf: [ { $ref: getSchemaPath(FileElementContentBody) }, + { $ref: getSchemaPath(LinkElementContentBody) }, { $ref: getSchemaPath(RichTextElementContentBody) }, { $ref: getSchemaPath(SubmissionContainerElementContentBody) }, { $ref: getSchemaPath(ExternalToolElementContentBody) }, @@ -116,6 +132,7 @@ export class UpdateElementContentBodyParams { }) data!: | FileElementContentBody + | LinkElementContentBody | RichTextElementContentBody | SubmissionContainerElementContentBody | ExternalToolElementContentBody; diff --git a/apps/server/src/modules/board/controller/dto/submission-item/index.ts b/apps/server/src/modules/board/controller/dto/submission-item/index.ts index b009f3e0560..affa6cedc24 100644 --- a/apps/server/src/modules/board/controller/dto/submission-item/index.ts +++ b/apps/server/src/modules/board/controller/dto/submission-item/index.ts @@ -4,5 +4,5 @@ export * from './submission-item.response'; export * from './submission-item.url.params'; // TODO for some reason, api generator messes up the types // import it directly, not via this index seems to fix it -// export * from './submissions.response'; +export * from './submissions.response'; export * from './update-submission-item.body.params'; diff --git a/apps/server/src/modules/board/controller/element.controller.ts b/apps/server/src/modules/board/controller/element.controller.ts index 361e59bf6b6..229d2d6f2e1 100644 --- a/apps/server/src/modules/board/controller/element.controller.ts +++ b/apps/server/src/modules/board/controller/element.controller.ts @@ -10,24 +10,30 @@ import { Post, Put, } from '@nestjs/common'; -import { ApiBody, ApiExtraModels, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; +import { ApiBody, ApiExtraModels, ApiOperation, ApiResponse, ApiTags, getSchemaPath } from '@nestjs/swagger'; import { ApiValidationError } from '@shared/common'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; +import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; import { CardUc } from '../uc'; import { ElementUc } from '../uc/element.uc'; import { + AnyContentElementResponse, ContentElementUrlParams, CreateSubmissionItemBodyParams, ExternalToolElementContentBody, + ExternalToolElementResponse, FileElementContentBody, + FileElementResponse, + LinkElementContentBody, + LinkElementResponse, MoveContentElementBody, RichTextElementContentBody, + RichTextElementResponse, SubmissionContainerElementContentBody, + SubmissionContainerElementResponse, SubmissionItemResponse, UpdateElementContentBodyParams, } from './dto'; -import { SubmissionItemResponseMapper } from './mapper'; +import { ContentElementResponseFactory, SubmissionItemResponseMapper } from './mapper'; @ApiTags('Board Element') @Authenticate('jwt') @@ -60,20 +66,38 @@ export class ElementController { FileElementContentBody, RichTextElementContentBody, SubmissionContainerElementContentBody, - ExternalToolElementContentBody + ExternalToolElementContentBody, + LinkElementContentBody ) - @ApiResponse({ status: 204 }) + @ApiResponse({ + status: 201, + schema: { + oneOf: [ + { $ref: getSchemaPath(ExternalToolElementResponse) }, + { $ref: getSchemaPath(FileElementResponse) }, + { $ref: getSchemaPath(LinkElementResponse) }, + { $ref: getSchemaPath(RichTextElementResponse) }, + { $ref: getSchemaPath(SubmissionContainerElementResponse) }, + ], + }, + }) @ApiResponse({ status: 400, type: ApiValidationError }) @ApiResponse({ status: 403, type: ForbiddenException }) @ApiResponse({ status: 404, type: NotFoundException }) - @HttpCode(204) + @HttpCode(201) @Patch(':contentElementId/content') async updateElement( @Param() urlParams: ContentElementUrlParams, @Body() bodyParams: UpdateElementContentBodyParams, @CurrentUser() currentUser: ICurrentUser - ): Promise { - await this.elementUc.updateElementContent(currentUser.userId, urlParams.contentElementId, bodyParams.data.content); + ): Promise { + const element = await this.elementUc.updateElementContent( + currentUser.userId, + urlParams.contentElementId, + bodyParams.data.content + ); + const response = ContentElementResponseFactory.mapToResponse(element); + return response; } @ApiOperation({ summary: 'Delete a single content element.' }) diff --git a/apps/server/src/modules/board/controller/mapper/content-element-response.factory.spec.ts b/apps/server/src/modules/board/controller/mapper/content-element-response.factory.spec.ts index f813e44ae7a..2b61e273185 100644 --- a/apps/server/src/modules/board/controller/mapper/content-element-response.factory.spec.ts +++ b/apps/server/src/modules/board/controller/mapper/content-element-response.factory.spec.ts @@ -1,27 +1,36 @@ import { NotImplementedException } from '@nestjs/common'; -import { fileElementFactory, richTextElementFactory, submissionContainerElementFactory } from '@shared/testing'; -import { FileElementResponse, RichTextElementResponse, SubmissionContainerElementResponse } from '../dto'; +import { + fileElementFactory, + linkElementFactory, + richTextElementFactory, + submissionContainerElementFactory, +} from '@shared/testing'; +import { + FileElementResponse, + LinkElementResponse, + RichTextElementResponse, + SubmissionContainerElementResponse, +} from '../dto'; import { ContentElementResponseFactory } from './content-element-response.factory'; describe(ContentElementResponseFactory.name, () => { - const setup = () => { - const fileElement = fileElementFactory.build(); - const richTextElement = richTextElementFactory.build(); - const submissionContainerElement = submissionContainerElementFactory.build(); - - return { fileElement, richTextElement, submissionContainerElement }; - }; - it('should return instance of FileElementResponse', () => { - const { fileElement } = setup(); + const fileElement = fileElementFactory.build(); const result = ContentElementResponseFactory.mapToResponse(fileElement); expect(result).toBeInstanceOf(FileElementResponse); }); + it('should return instance of LinkElementResponse', () => { + const linkElement = linkElementFactory.build(); + const result = ContentElementResponseFactory.mapToResponse(linkElement); + + expect(result).toBeInstanceOf(LinkElementResponse); + }); + it('should return instance of RichTextElementResponse', () => { - const { richTextElement } = setup(); + const richTextElement = richTextElementFactory.build(); const result = ContentElementResponseFactory.mapToResponse(richTextElement); @@ -29,7 +38,7 @@ describe(ContentElementResponseFactory.name, () => { }); it('should return instance of SubmissionContainerElementResponse', () => { - const { submissionContainerElement } = setup(); + const submissionContainerElement = submissionContainerElementFactory.build(); const result = ContentElementResponseFactory.mapToResponse(submissionContainerElement); diff --git a/apps/server/src/modules/board/controller/mapper/content-element-response.factory.ts b/apps/server/src/modules/board/controller/mapper/content-element-response.factory.ts index 3ccf11b1bf2..bda46e4b73f 100644 --- a/apps/server/src/modules/board/controller/mapper/content-element-response.factory.ts +++ b/apps/server/src/modules/board/controller/mapper/content-element-response.factory.ts @@ -4,12 +4,14 @@ import { AnyContentElementResponse } from '../dto'; import { BaseResponseMapper } from './base-mapper.interface'; import { ExternalToolElementResponseMapper } from './external-tool-element-response.mapper'; import { FileElementResponseMapper } from './file-element-response.mapper'; +import { LinkElementResponseMapper } from './link-element-response.mapper'; import { RichTextElementResponseMapper } from './rich-text-element-response.mapper'; import { SubmissionContainerElementResponseMapper } from './submission-container-element-response.mapper'; export class ContentElementResponseFactory { private static mappers: BaseResponseMapper[] = [ FileElementResponseMapper.getInstance(), + LinkElementResponseMapper.getInstance(), RichTextElementResponseMapper.getInstance(), SubmissionContainerElementResponseMapper.getInstance(), ExternalToolElementResponseMapper.getInstance(), diff --git a/apps/server/src/modules/board/controller/mapper/external-tool-element-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/external-tool-element-response.mapper.ts index a907f4eb157..a27cab41d63 100644 --- a/apps/server/src/modules/board/controller/mapper/external-tool-element-response.mapper.ts +++ b/apps/server/src/modules/board/controller/mapper/external-tool-element-response.mapper.ts @@ -18,7 +18,7 @@ export class ExternalToolElementResponseMapper implements BaseResponseMapper { id: element.id, timestamps: new TimestampsResponse({ lastUpdatedAt: element.updatedAt, createdAt: element.createdAt }), type: ContentElementType.EXTERNAL_TOOL, - content: new ExternalToolElementContent({ contextExternalToolId: element.contextExternalToolId }), + content: new ExternalToolElementContent({ contextExternalToolId: element.contextExternalToolId ?? null }), }); return result; diff --git a/apps/server/src/modules/board/controller/mapper/index.ts b/apps/server/src/modules/board/controller/mapper/index.ts index 116692df5a4..a24a905ae3f 100644 --- a/apps/server/src/modules/board/controller/mapper/index.ts +++ b/apps/server/src/modules/board/controller/mapper/index.ts @@ -2,7 +2,9 @@ export * from './board-response.mapper'; export * from './card-response.mapper'; export * from './column-response.mapper'; export * from './content-element-response.factory'; +export * from './external-tool-element-response.mapper'; +export * from './file-element-response.mapper'; +export * from './link-element-response.mapper'; export * from './rich-text-element-response.mapper'; export * from './submission-container-element-response.mapper'; export * from './submission-item-response.mapper'; -export * from './external-tool-element-response.mapper'; diff --git a/apps/server/src/modules/board/controller/mapper/link-element-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/link-element-response.mapper.ts new file mode 100644 index 00000000000..e6a31b2c07a --- /dev/null +++ b/apps/server/src/modules/board/controller/mapper/link-element-response.mapper.ts @@ -0,0 +1,35 @@ +import { ContentElementType, LinkElement } from '@shared/domain'; +import { LinkElementContent, LinkElementResponse, TimestampsResponse } from '../dto'; +import { BaseResponseMapper } from './base-mapper.interface'; + +export class LinkElementResponseMapper implements BaseResponseMapper { + private static instance: LinkElementResponseMapper; + + public static getInstance(): LinkElementResponseMapper { + if (!LinkElementResponseMapper.instance) { + LinkElementResponseMapper.instance = new LinkElementResponseMapper(); + } + + return LinkElementResponseMapper.instance; + } + + mapToResponse(element: LinkElement): LinkElementResponse { + const result = new LinkElementResponse({ + id: element.id, + timestamps: new TimestampsResponse({ lastUpdatedAt: element.updatedAt, createdAt: element.createdAt }), + type: ContentElementType.LINK, + content: new LinkElementContent({ + url: element.url, + title: element.title, + description: element.description, + imageUrl: element.imageUrl, + }), + }); + + return result; + } + + canMap(element: LinkElement): boolean { + return element instanceof LinkElement; + } +} diff --git a/apps/server/src/modules/board/controller/mapper/submission-container-element-response.mapper.ts b/apps/server/src/modules/board/controller/mapper/submission-container-element-response.mapper.ts index 8b3dc6ae54f..fc68da31d13 100644 --- a/apps/server/src/modules/board/controller/mapper/submission-container-element-response.mapper.ts +++ b/apps/server/src/modules/board/controller/mapper/submission-container-element-response.mapper.ts @@ -19,7 +19,7 @@ export class SubmissionContainerElementResponseMapper implements BaseResponseMap timestamps: new TimestampsResponse({ lastUpdatedAt: element.updatedAt, createdAt: element.createdAt }), type: ContentElementType.SUBMISSION_CONTAINER, content: new SubmissionContainerElementContent({ - dueDate: element.dueDate || null, + dueDate: element.dueDate, }), }); 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 82d2292ba11..53efb37a482 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,6 +1,5 @@ import { SubmissionItem, UserBoardRoles } from '@shared/domain'; -import { SubmissionsResponse } from '../dto/submission-item/submissions.response'; -import { SubmissionItemResponse, TimestampsResponse, UserDataResponse } from '../dto'; +import { SubmissionItemResponse, SubmissionsResponse, TimestampsResponse, UserDataResponse } from '../dto'; export class SubmissionItemResponseMapper { private static instance: SubmissionItemResponseMapper; diff --git a/apps/server/src/modules/board/repo/board-do.builder-impl.spec.ts b/apps/server/src/modules/board/repo/board-do.builder-impl.spec.ts index d640d3f6330..8bbc859fa17 100644 --- a/apps/server/src/modules/board/repo/board-do.builder-impl.spec.ts +++ b/apps/server/src/modules/board/repo/board-do.builder-impl.spec.ts @@ -1,10 +1,11 @@ -import { BoardNodeType, ExternalToolElement } from '@shared/domain'; +import { BoardNodeType, ExternalToolElement, LinkElement } from '@shared/domain'; import { cardNodeFactory, columnBoardNodeFactory, columnNodeFactory, externalToolElementNodeFactory, fileElementNodeFactory, + linkElementNodeFactory, richTextElementNodeFactory, setupEntities, submissionContainerElementNodeFactory, @@ -195,7 +196,7 @@ describe(BoardDoBuilderImpl.name, () => { expect(domainObject.constructor.name).toBe(ExternalToolElement.name); }); - it('should throw error if submissionContainerElement is not a leaf', () => { + it('should throw error if externalToolElement is not a leaf', () => { const externalToolElementNode = externalToolElementNodeFactory.buildWithId(); const columnNode = columnNodeFactory.buildWithId({ parent: externalToolElementNode }); @@ -205,6 +206,25 @@ describe(BoardDoBuilderImpl.name, () => { }); }); + describe('when building a link element', () => { + it('should work without descendants', () => { + const linkElementNode = linkElementNodeFactory.buildWithId(); + + const domainObject = new BoardDoBuilderImpl().buildLinkElement(linkElementNode); + + expect(domainObject.constructor.name).toBe(LinkElement.name); + }); + + it('should throw error if linkElement is not a leaf', () => { + const linkElementNode = linkElementNodeFactory.buildWithId(); + const columnNode = columnNodeFactory.buildWithId({ parent: linkElementNode }); + + expect(() => { + new BoardDoBuilderImpl([columnNode]).buildLinkElement(linkElementNode); + }).toThrowError(); + }); + }); + describe('ensure board node types', () => { it('should do nothing if type is correct', () => { const card = cardNodeFactory.build(); diff --git a/apps/server/src/modules/board/repo/board-do.builder-impl.ts b/apps/server/src/modules/board/repo/board-do.builder-impl.ts index af58280b33f..18b0583daa1 100644 --- a/apps/server/src/modules/board/repo/board-do.builder-impl.ts +++ b/apps/server/src/modules/board/repo/board-do.builder-impl.ts @@ -7,6 +7,7 @@ import type { ColumnNode, ExternalToolElementNodeEntity, FileElementNode, + LinkElementNode, RichTextElementNode, SubmissionContainerElementNode, SubmissionItemNode, @@ -19,6 +20,7 @@ import { ColumnBoard, ExternalToolElement, FileElement, + LinkElement, RichTextElement, SubmissionContainerElement, SubmissionItem, @@ -73,12 +75,15 @@ export class BoardDoBuilderImpl implements BoardDoBuilder { public buildCard(boardNode: CardNode): Card { this.ensureBoardNodeType(this.getChildren(boardNode), [ BoardNodeType.FILE_ELEMENT, + BoardNodeType.LINK_ELEMENT, BoardNodeType.RICH_TEXT_ELEMENT, BoardNodeType.SUBMISSION_CONTAINER_ELEMENT, BoardNodeType.EXTERNAL_TOOL, ]); - const elements = this.buildChildren(boardNode); + const elements = this.buildChildren< + ExternalToolElement | FileElement | LinkElement | RichTextElement | SubmissionContainerElement + >(boardNode); const card = new Card({ id: boardNode.id, @@ -105,6 +110,21 @@ export class BoardDoBuilderImpl implements BoardDoBuilder { return element; } + public buildLinkElement(boardNode: LinkElementNode): LinkElement { + this.ensureLeafNode(boardNode); + + const element = new LinkElement({ + id: boardNode.id, + url: boardNode.url, + title: boardNode.title, + imageUrl: boardNode.imageUrl, + children: [], + createdAt: boardNode.createdAt, + updatedAt: boardNode.updatedAt, + }); + return element; + } + public buildRichTextElement(boardNode: RichTextElementNode): RichTextElement { this.ensureLeafNode(boardNode); @@ -128,12 +148,9 @@ export class BoardDoBuilderImpl implements BoardDoBuilder { children: elements, createdAt: boardNode.createdAt, updatedAt: boardNode.updatedAt, + dueDate: boardNode.dueDate, }); - if (boardNode.dueDate) { - element.dueDate = boardNode.dueDate; - } - return element; } diff --git a/apps/server/src/modules/board/repo/board-do.repo.spec.ts b/apps/server/src/modules/board/repo/board-do.repo.spec.ts index 446d8b8cfa3..aa1c49224fe 100644 --- a/apps/server/src/modules/board/repo/board-do.repo.spec.ts +++ b/apps/server/src/modules/board/repo/board-do.repo.spec.ts @@ -26,7 +26,7 @@ import { richTextElementFactory, richTextElementNodeFactory, } from '@shared/testing'; -import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; import { BoardDoRepo } from './board-do.repo'; import { BoardNodeRepo } from './board-node.repo'; import { RecursiveDeleteVisitor } from './recursive-delete.vistor'; diff --git a/apps/server/src/modules/board/repo/recursive-delete.visitor.spec.ts b/apps/server/src/modules/board/repo/recursive-delete.visitor.spec.ts index 75f0e9e2e99..9142cb33553 100644 --- a/apps/server/src/modules/board/repo/recursive-delete.visitor.spec.ts +++ b/apps/server/src/modules/board/repo/recursive-delete.visitor.spec.ts @@ -7,11 +7,12 @@ import { columnFactory, externalToolElementFactory, fileElementFactory, + linkElementFactory, setupEntities, submissionContainerElementFactory, submissionItemFactory, } from '@shared/testing'; -import { FileDto, FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; +import { FileDto, FilesStorageClientAdapterService } from '@modules/files-storage-client'; import { RecursiveDeleteVisitor } from './recursive-delete.vistor'; describe(RecursiveDeleteVisitor.name, () => { @@ -145,6 +146,26 @@ describe(RecursiveDeleteVisitor.name, () => { }); }); + describe('visitLinkElementAsync', () => { + const setup = () => { + const childLinkElement = linkElementFactory.build(); + const linkElement = linkElementFactory.build({ + children: [childLinkElement], + }); + + return { linkElement, childLinkElement }; + }; + + it('should call entity remove', async () => { + const { linkElement, childLinkElement } = setup(); + + await service.visitLinkElementAsync(linkElement); + + expect(em.remove).toHaveBeenCalledWith(em.getReference(linkElement.constructor, linkElement.id)); + expect(em.remove).toHaveBeenCalledWith(em.getReference(childLinkElement.constructor, childLinkElement.id)); + }); + }); + describe('visitSubmissionContainerElementAsync', () => { const setup = () => { const childSubmissionContainerElement = submissionContainerElementFactory.build(); diff --git a/apps/server/src/modules/board/repo/recursive-delete.vistor.ts b/apps/server/src/modules/board/repo/recursive-delete.vistor.ts index aaed699c26c..1c407391da4 100644 --- a/apps/server/src/modules/board/repo/recursive-delete.vistor.ts +++ b/apps/server/src/modules/board/repo/recursive-delete.vistor.ts @@ -13,7 +13,8 @@ import { SubmissionContainerElement, SubmissionItem, } from '@shared/domain'; -import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; +import { LinkElement } from '@shared/domain/domainobject/board/link-element.do'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; @Injectable() export class RecursiveDeleteVisitor implements BoardCompositeVisitorAsync { @@ -44,6 +45,12 @@ export class RecursiveDeleteVisitor implements BoardCompositeVisitorAsync { await this.visitChildrenAsync(fileElement); } + async visitLinkElementAsync(linkElement: LinkElement): Promise { + this.deleteNode(linkElement); + + await this.visitChildrenAsync(linkElement); + } + async visitRichTextElementAsync(richTextElement: RichTextElement): Promise { this.deleteNode(richTextElement); await this.visitChildrenAsync(richTextElement); diff --git a/apps/server/src/modules/board/repo/recursive-save.visitor.spec.ts b/apps/server/src/modules/board/repo/recursive-save.visitor.spec.ts index 088b6f7f54c..3fd95c18525 100644 --- a/apps/server/src/modules/board/repo/recursive-save.visitor.spec.ts +++ b/apps/server/src/modules/board/repo/recursive-save.visitor.spec.ts @@ -7,6 +7,7 @@ import { ColumnNode, ExternalToolElementNodeEntity, FileElementNode, + LinkElementNode, RichTextElementNode, SubmissionContainerElementNode, SubmissionItemNode, @@ -19,6 +20,7 @@ import { contextExternalToolEntityFactory, externalToolElementFactory, fileElementFactory, + linkElementFactory, richTextElementFactory, setupEntities, submissionContainerElementFactory, @@ -137,6 +139,22 @@ describe(RecursiveSaveVisitor.name, () => { }); }); + describe('when visiting a link element composite', () => { + it('should create or update the node', () => { + const linkElement = linkElementFactory.build(); + jest.spyOn(visitor, 'createOrUpdateBoardNode'); + + visitor.visitLinkElement(linkElement); + + const expectedNode: Partial = { + id: linkElement.id, + type: BoardNodeType.LINK_ELEMENT, + url: linkElement.url, + }; + expect(visitor.createOrUpdateBoardNode).toHaveBeenCalledWith(expect.objectContaining(expectedNode)); + }); + }); + describe('when visiting a rich text element composite', () => { it('should create or update the node', () => { const richTextElement = richTextElementFactory.build(); diff --git a/apps/server/src/modules/board/repo/recursive-save.visitor.ts b/apps/server/src/modules/board/repo/recursive-save.visitor.ts index 5561e636267..5e8249f1fee 100644 --- a/apps/server/src/modules/board/repo/recursive-save.visitor.ts +++ b/apps/server/src/modules/board/repo/recursive-save.visitor.ts @@ -22,7 +22,9 @@ import { SubmissionItem, SubmissionItemNode, } from '@shared/domain'; -import { ContextExternalToolEntity } from '@src/modules/tool/context-external-tool/entity'; +import { LinkElement } from '@shared/domain/domainobject/board/link-element.do'; +import { LinkElementNode } from '@shared/domain/entity/boardnode/link-element-node.entity'; +import { ContextExternalToolEntity } from '@modules/tool/context-external-tool/entity'; import { BoardNodeRepo } from './board-node.repo'; type ParentData = { @@ -108,6 +110,22 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor { this.visitChildren(fileElement, boardNode); } + visitLinkElement(linkElement: LinkElement): void { + const parentData = this.parentsMap.get(linkElement.id); + + const boardNode = new LinkElementNode({ + id: linkElement.id, + url: linkElement.url, + title: linkElement.title, + imageUrl: linkElement.imageUrl, + parent: parentData?.boardNode, + position: parentData?.position, + }); + + this.createOrUpdateBoardNode(boardNode); + this.visitChildren(linkElement, boardNode); + } + visitRichTextElement(richTextElement: RichTextElement): void { const parentData = this.parentsMap.get(richTextElement.id); @@ -130,12 +148,9 @@ export class RecursiveSaveVisitor implements BoardCompositeVisitor { id: submissionContainerElement.id, parent: parentData?.boardNode, position: parentData?.position, + dueDate: submissionContainerElement.dueDate, }); - if (submissionContainerElement.dueDate) { - boardNode.dueDate = submissionContainerElement.dueDate; - } - this.createOrUpdateBoardNode(boardNode); this.visitChildren(submissionContainerElement, boardNode); } diff --git a/apps/server/src/modules/board/service/board-do-authorizable.service.ts b/apps/server/src/modules/board/service/board-do-authorizable.service.ts index 7b8b653f9cf..46ef7d3d45b 100644 --- a/apps/server/src/modules/board/service/board-do-authorizable.service.ts +++ b/apps/server/src/modules/board/service/board-do-authorizable.service.ts @@ -11,7 +11,7 @@ import { UserRoleEnum, } from '@shared/domain'; import { CourseRepo } from '@shared/repo'; -import { AuthorizationLoaderService } from '@src/modules/authorization'; +import { AuthorizationLoaderService } from '@modules/authorization'; import { BoardDoRepo } from '../repo'; @Injectable() diff --git a/apps/server/src/modules/board/service/board-do-copy-service/board-do-copy.service.spec.ts b/apps/server/src/modules/board/service/board-do-copy-service/board-do-copy.service.spec.ts index ba3643e6051..d7c71352166 100644 --- a/apps/server/src/modules/board/service/board-do-copy-service/board-do-copy.service.spec.ts +++ b/apps/server/src/modules/board/service/board-do-copy-service/board-do-copy.service.spec.ts @@ -11,8 +11,10 @@ import { isColumnBoard, isExternalToolElement, isFileElement, + isLinkElement, isRichTextElement, isSubmissionContainerElement, + LinkElement, RichTextElement, SubmissionContainerElement, } from '@shared/domain'; @@ -23,12 +25,13 @@ import { columnFactory, externalToolElementFactory, fileElementFactory, + linkElementFactory, richTextElementFactory, setupEntities, submissionContainerElementFactory, submissionItemFactory, } from '@shared/testing'; -import { CopyElementType, CopyStatus, CopyStatusEnum } from '@src/modules/copy-helper'; +import { CopyElementType, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; import { ObjectId } from 'bson'; import { BoardDoCopyService } from './board-do-copy.service'; import { SchoolSpecificFileCopyService } from './school-specific-file-copy.interface'; @@ -706,4 +709,53 @@ describe('recursive board copy visitor', () => { expect(result.type).toEqual(CopyElementType.EXTERNAL_TOOL_ELEMENT); }); }); + + describe('when copying a link element', () => { + const setup = () => { + const original = linkElementFactory.build(); + + return { original, ...setupfileCopyService() }; + }; + + const getLinkElementFromStatus = (status: CopyStatus): LinkElement => { + const copy = status.copyEntity; + + expect(isLinkElement(copy)).toEqual(true); + + return copy as LinkElement; + }; + + it('should return a link element as copy', async () => { + const { original, fileCopyService } = setup(); + + const result = await service.copy({ original, fileCopyService }); + + expect(isLinkElement(result.copyEntity)).toEqual(true); + }); + + it('should create new id', async () => { + const { original, fileCopyService } = setup(); + + const result = await service.copy({ original, fileCopyService }); + const copy = getLinkElementFromStatus(result); + + expect(copy.id).not.toEqual(original.id); + }); + + it('should show status successful', async () => { + const { original, fileCopyService } = setup(); + + const result = await service.copy({ original, fileCopyService }); + + expect(result.status).toEqual(CopyStatusEnum.SUCCESS); + }); + + it('should be of type LinkElement', async () => { + const { original, fileCopyService } = setup(); + + const result = await service.copy({ original, fileCopyService }); + + expect(result.type).toEqual(CopyElementType.LINK_ELEMENT); + }); + }); }); diff --git a/apps/server/src/modules/board/service/board-do-copy-service/board-do-copy.service.ts b/apps/server/src/modules/board/service/board-do-copy-service/board-do-copy.service.ts index 0d457436f44..b2458dd6cfb 100644 --- a/apps/server/src/modules/board/service/board-do-copy-service/board-do-copy.service.ts +++ b/apps/server/src/modules/board/service/board-do-copy-service/board-do-copy.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { AnyBoardDo } from '@shared/domain'; -import { CopyStatus } from '@src/modules/copy-helper'; +import { CopyStatus } from '@modules/copy-helper'; import { RecursiveCopyVisitor } from './recursive-copy.visitor'; import { SchoolSpecificFileCopyService } from './school-specific-file-copy.interface'; diff --git a/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.ts b/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.ts index 4d1bf55f5ae..ba76693bb93 100644 --- a/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.ts +++ b/apps/server/src/modules/board/service/board-do-copy-service/recursive-copy.visitor.ts @@ -11,8 +11,9 @@ import { SubmissionContainerElement, SubmissionItem, } from '@shared/domain'; +import { LinkElement } from '@shared/domain/domainobject/board/link-element.do'; import { FileRecordParentType } from '@shared/infra/rabbitmq'; -import { CopyElementType, CopyStatus, CopyStatusEnum } from '@src/modules/copy-helper'; +import { CopyElementType, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; import { ObjectId } from 'bson'; import { SchoolSpecificFileCopyService } from './school-specific-file-copy.interface'; @@ -122,6 +123,26 @@ export class RecursiveCopyVisitor implements BoardCompositeVisitorAsync { this.copyMap.set(original.id, copy); } + async visitLinkElementAsync(original: LinkElement): Promise { + const copy = new LinkElement({ + id: new ObjectId().toHexString(), + url: original.url, + title: original.title, + imageUrl: original.imageUrl, + children: [], + createdAt: new Date(), + updatedAt: new Date(), + }); + this.resultMap.set(original.id, { + copyEntity: copy, + type: CopyElementType.LINK_ELEMENT, + status: CopyStatusEnum.SUCCESS, + }); + this.copyMap.set(original.id, copy); + + return Promise.resolve(); + } + async visitRichTextElementAsync(original: RichTextElement): Promise { const copy = new RichTextElement({ id: new ObjectId().toHexString(), diff --git a/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy-service.factory.spec.ts b/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy-service.factory.spec.ts index 9d6eaf1b24e..c780f9b9c50 100644 --- a/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy-service.factory.spec.ts +++ b/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy-service.factory.spec.ts @@ -1,8 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { setupEntities } from '@shared/testing'; -import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; -import { FileRecordParentType } from '@src/modules/files-storage/entity'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; +import { FileRecordParentType } from '@modules/files-storage/entity'; import { ObjectId } from 'bson'; import { SchoolSpecificFileCopyServiceFactory } from './school-specific-file-copy-service.factory'; import { SchoolSpecificFileCopyServiceImpl } from './school-specific-file-copy.service'; diff --git a/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy-service.factory.ts b/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy-service.factory.ts index dda07c8c1f9..424033fa974 100644 --- a/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy-service.factory.ts +++ b/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy-service.factory.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; import { SchoolSpecificFileCopyService, SchoolSpecificFileCopyServiceProps, diff --git a/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy.interface.ts b/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy.interface.ts index e3b03d0b059..ce7870f6c86 100644 --- a/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy.interface.ts +++ b/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy.interface.ts @@ -1,6 +1,6 @@ import { EntityId } from '@shared/domain'; -import { CopyFileDto } from '@src/modules/files-storage-client/dto'; -import { FileRecordParentType } from '@src/modules/files-storage/entity'; +import { CopyFileDto } from '@modules/files-storage-client/dto'; +import { FileRecordParentType } from '@modules/files-storage/entity'; export type SchoolSpecificFileCopyServiceCopyParams = { sourceParentId: EntityId; diff --git a/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy.service.ts b/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy.service.ts index c162dbafdc7..1f3fa5f5193 100644 --- a/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy.service.ts +++ b/apps/server/src/modules/board/service/board-do-copy-service/school-specific-file-copy.service.ts @@ -1,5 +1,5 @@ -import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; -import { CopyFileDto } from '@src/modules/files-storage-client/dto'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; +import { CopyFileDto } from '@modules/files-storage-client/dto'; import { SchoolSpecificFileCopyService, SchoolSpecificFileCopyServiceCopyParams, diff --git a/apps/server/src/modules/board/service/card.service.spec.ts b/apps/server/src/modules/board/service/card.service.spec.ts index 6a9a8f1d944..eb155793412 100644 --- a/apps/server/src/modules/board/service/card.service.spec.ts +++ b/apps/server/src/modules/board/service/card.service.spec.ts @@ -88,7 +88,8 @@ describe(CardService.name, () => { }; it('should call the card repository', async () => { - const { cardIds } = setup(); + const { cards, cardIds } = setup(); + boardDoRepo.findByIds.mockResolvedValueOnce(cards); await service.findByIds(cardIds); diff --git a/apps/server/src/modules/board/service/card.service.ts b/apps/server/src/modules/board/service/card.service.ts index 3ef34806397..b7fee25c94f 100644 --- a/apps/server/src/modules/board/service/card.service.ts +++ b/apps/server/src/modules/board/service/card.service.ts @@ -14,16 +14,16 @@ export class CardService { ) {} async findById(cardId: EntityId): Promise { - const card = await this.boardDoRepo.findByClassAndId(Card, cardId); - return card; + return this.boardDoRepo.findByClassAndId(Card, cardId); } async findByIds(cardIds: EntityId[]): Promise { const cards = await this.boardDoRepo.findByIds(cardIds); - if (cards.every((card) => card instanceof Card)) { - return cards as Card[]; + if (cards.some((card) => !(card instanceof Card))) { + throw new NotFoundException('some ids do not belong to a card'); } - throw new NotFoundException('some ids do not belong to a card'); + + return cards as Card[]; } async create(parent: Column, requiredEmptyElements?: ContentElementType[]): Promise { diff --git a/apps/server/src/modules/board/service/column-board-copy.service.spec.ts b/apps/server/src/modules/board/service/column-board-copy.service.spec.ts index 6824b82e133..bbfeb27a1f3 100644 --- a/apps/server/src/modules/board/service/column-board-copy.service.spec.ts +++ b/apps/server/src/modules/board/service/column-board-copy.service.spec.ts @@ -3,8 +3,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { BoardExternalReferenceType, ColumnBoard, UserDO } from '@shared/domain'; import { CourseRepo } from '@shared/repo'; import { columnBoardFactory, courseFactory, schoolFactory, setupEntities, userFactory } from '@shared/testing'; -import { CopyElementType, CopyStatus, CopyStatusEnum } from '@src/modules/copy-helper'; -import { UserService } from '@src/modules/user'; +import { CopyElementType, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; +import { UserService } from '@modules/user'; import { BoardDoRepo } from '../repo'; import { BoardDoCopyService, diff --git a/apps/server/src/modules/board/service/column-board-copy.service.ts b/apps/server/src/modules/board/service/column-board-copy.service.ts index c57e01d6c26..79ef29f4752 100644 --- a/apps/server/src/modules/board/service/column-board-copy.service.ts +++ b/apps/server/src/modules/board/service/column-board-copy.service.ts @@ -7,8 +7,8 @@ import { isColumnBoard, } from '@shared/domain'; import { CourseRepo } from '@shared/repo'; -import { CopyStatus } from '@src/modules/copy-helper'; -import { UserService } from '@src/modules/user'; +import { CopyStatus } from '@modules/copy-helper'; +import { UserService } from '@modules/user'; import { BoardDoRepo } from '../repo'; import { BoardDoCopyService, SchoolSpecificFileCopyServiceFactory } from './board-do-copy-service'; diff --git a/apps/server/src/modules/board/service/content-element-update.visitor.spec.ts b/apps/server/src/modules/board/service/content-element-update.visitor.spec.ts index b96a6ca9a41..8a8368fce2b 100644 --- a/apps/server/src/modules/board/service/content-element-update.visitor.spec.ts +++ b/apps/server/src/modules/board/service/content-element-update.visitor.spec.ts @@ -6,12 +6,14 @@ import { columnFactory, externalToolElementFactory, fileElementFactory, + linkElementFactory, richTextElementFactory, submissionContainerElementFactory, submissionItemFactory, } from '@shared/testing'; import { ExternalToolContentBody, FileContentBody, RichTextContentBody } from '../controller/dto'; import { ContentElementUpdateVisitor } from './content-element-update.visitor'; +import { OpenGraphProxyService } from './open-graph-proxy.service'; describe(ContentElementUpdateVisitor.name, () => { describe('when visiting an unsupported component', () => { @@ -23,36 +25,37 @@ describe(ContentElementUpdateVisitor.name, () => { content.text = 'a text'; content.inputFormat = InputFormat.RICH_TEXT_CK5; const submissionItem = submissionItemFactory.build(); - const updater = new ContentElementUpdateVisitor(content); + const openGraphProxyService = new OpenGraphProxyService(); + const updater = new ContentElementUpdateVisitor(content, openGraphProxyService); return { board, column, card, submissionItem, updater }; }; describe('when component is a column board', () => { - it('should throw an error', () => { + it('should throw an error', async () => { const { board, updater } = setup(); - expect(() => updater.visitColumnBoard(board)).toThrow(); + await expect(updater.visitColumnBoardAsync(board)).rejects.toThrow(); }); }); describe('when component is a column', () => { - it('should throw an error', () => { + it('should throw an error', async () => { const { column, updater } = setup(); - expect(() => updater.visitColumn(column)).toThrow(); + await expect(() => updater.visitColumnAsync(column)).rejects.toThrow(); }); }); describe('when component is a card', () => { - it('should throw an error', () => { + it('should throw an error', async () => { const { card, updater } = setup(); - expect(() => updater.visitCard(card)).toThrow(); + await expect(() => updater.visitCardAsync(card)).rejects.toThrow(); }); }); describe('when component is a submission-item', () => { - it('should throw an error', () => { + it('should throw an error', async () => { const { submissionItem, updater } = setup(); - expect(() => updater.visitSubmissionItem(submissionItem)).toThrow(); + await expect(() => updater.visitSubmissionItemAsync(submissionItem)).rejects.toThrow(); }); }); }); @@ -63,15 +66,34 @@ describe(ContentElementUpdateVisitor.name, () => { const content = new RichTextContentBody(); content.text = 'a text'; content.inputFormat = InputFormat.RICH_TEXT_CK5; - const updater = new ContentElementUpdateVisitor(content); + const openGraphProxyService = new OpenGraphProxyService(); + const updater = new ContentElementUpdateVisitor(content, openGraphProxyService); return { fileElement, updater }; }; - it('should throw an error', () => { + it('should throw an error', async () => { const { fileElement, updater } = setup(); - expect(() => updater.visitFileElement(fileElement)).toThrow(); + await expect(() => updater.visitFileElementAsync(fileElement)).rejects.toThrow(); + }); + }); + + describe('when visiting a link element using the wrong content', () => { + const setup = () => { + const linkElement = linkElementFactory.build(); + const content = new FileContentBody(); + content.caption = 'a caption'; + const openGraphProxyService = new OpenGraphProxyService(); + const updater = new ContentElementUpdateVisitor(content, openGraphProxyService); + + return { linkElement, updater }; + }; + + it('should throw an error', async () => { + const { linkElement, updater } = setup(); + + await expect(() => updater.visitLinkElementAsync(linkElement)).rejects.toThrow(); }); }); @@ -80,15 +102,16 @@ describe(ContentElementUpdateVisitor.name, () => { const richTextElement = richTextElementFactory.build(); const content = new FileContentBody(); content.caption = 'a caption'; - const updater = new ContentElementUpdateVisitor(content); + const openGraphProxyService = new OpenGraphProxyService(); + const updater = new ContentElementUpdateVisitor(content, openGraphProxyService); return { richTextElement, updater }; }; - it('should throw an error', () => { + it('should throw an error', async () => { const { richTextElement, updater } = setup(); - expect(() => updater.visitRichTextElement(richTextElement)).toThrow(); + await expect(() => updater.visitRichTextElementAsync(richTextElement)).rejects.toThrow(); }); }); @@ -98,15 +121,16 @@ describe(ContentElementUpdateVisitor.name, () => { const content = new RichTextContentBody(); content.text = 'a text'; content.inputFormat = InputFormat.RICH_TEXT_CK5; - const updater = new ContentElementUpdateVisitor(content); + const openGraphProxyService = new OpenGraphProxyService(); + const updater = new ContentElementUpdateVisitor(content, openGraphProxyService); return { submissionContainerElement, updater }; }; - it('should throw an error', () => { + it('should throw an error', async () => { const { submissionContainerElement, updater } = setup(); - expect(() => updater.visitSubmissionContainerElement(submissionContainerElement)).toThrow(); + await expect(() => updater.visitSubmissionContainerElementAsync(submissionContainerElement)).rejects.toThrow(); }); }); @@ -116,15 +140,16 @@ describe(ContentElementUpdateVisitor.name, () => { const externalToolElement = externalToolElementFactory.build({ contextExternalToolId: undefined }); const content = new ExternalToolContentBody(); content.contextExternalToolId = new ObjectId().toHexString(); - const updater = new ContentElementUpdateVisitor(content); + const openGraphProxyService = new OpenGraphProxyService(); + const updater = new ContentElementUpdateVisitor(content, openGraphProxyService); return { externalToolElement, updater, content }; }; - it('should update the content', () => { + it('should update the content', async () => { const { externalToolElement, updater, content } = setup(); - updater.visitExternalToolElement(externalToolElement); + await updater.visitExternalToolElementAsync(externalToolElement); expect(externalToolElement.contextExternalToolId).toEqual(content.contextExternalToolId); }); @@ -136,15 +161,16 @@ describe(ContentElementUpdateVisitor.name, () => { const content = new RichTextContentBody(); content.text = 'a text'; content.inputFormat = InputFormat.RICH_TEXT_CK5; - const updater = new ContentElementUpdateVisitor(content); + const openGraphProxyService = new OpenGraphProxyService(); + const updater = new ContentElementUpdateVisitor(content, openGraphProxyService); return { externalToolElement, updater }; }; - it('should throw an error', () => { + it('should throw an error', async () => { const { externalToolElement, updater } = setup(); - expect(() => updater.visitExternalToolElement(externalToolElement)).toThrow(); + await expect(() => updater.visitExternalToolElementAsync(externalToolElement)).rejects.toThrow(); }); }); @@ -152,15 +178,16 @@ describe(ContentElementUpdateVisitor.name, () => { const setup = () => { const externalToolElement = externalToolElementFactory.build(); const content = new ExternalToolContentBody(); - const updater = new ContentElementUpdateVisitor(content); + const openGraphProxyService = new OpenGraphProxyService(); + const updater = new ContentElementUpdateVisitor(content, openGraphProxyService); return { externalToolElement, updater }; }; - it('should throw an error', () => { + it('should throw an error', async () => { const { externalToolElement, updater } = setup(); - expect(() => updater.visitExternalToolElement(externalToolElement)).toThrow(); + await expect(() => updater.visitExternalToolElementAsync(externalToolElement)).rejects.toThrow(); }); }); }); diff --git a/apps/server/src/modules/board/service/content-element-update.visitor.ts b/apps/server/src/modules/board/service/content-element-update.visitor.ts index dfd430aa250..0f75bcaee2d 100644 --- a/apps/server/src/modules/board/service/content-element-update.visitor.ts +++ b/apps/server/src/modules/board/service/content-element-update.visitor.ts @@ -1,7 +1,8 @@ +import { Injectable } from '@nestjs/common'; import { sanitizeRichText } from '@shared/controller'; import { AnyBoardDo, - BoardCompositeVisitor, + BoardCompositeVisitorAsync, Card, Column, ColumnBoard, @@ -12,73 +13,94 @@ import { SubmissionContainerElement, SubmissionItem, } from '@shared/domain'; +import { LinkElement } from '@shared/domain/domainobject/board/link-element.do'; import { AnyElementContentBody, ExternalToolContentBody, FileContentBody, + LinkContentBody, RichTextContentBody, SubmissionContainerContentBody, } from '../controller/dto'; +import { OpenGraphProxyService } from './open-graph-proxy.service'; -export class ContentElementUpdateVisitor implements BoardCompositeVisitor { +@Injectable() +export class ContentElementUpdateVisitor implements BoardCompositeVisitorAsync { private readonly content: AnyElementContentBody; - constructor(content: AnyElementContentBody) { + constructor(content: AnyElementContentBody, private readonly openGraphProxyService: OpenGraphProxyService) { this.content = content; } - visitColumnBoard(columnBoard: ColumnBoard): void { - this.throwNotHandled(columnBoard); + async visitColumnBoardAsync(columnBoard: ColumnBoard): Promise { + return this.rejectNotHandled(columnBoard); } - visitColumn(column: Column): void { - this.throwNotHandled(column); + async visitColumnAsync(column: Column): Promise { + return this.rejectNotHandled(column); } - visitCard(card: Card): void { - this.throwNotHandled(card); + async visitCardAsync(card: Card): Promise { + return this.rejectNotHandled(card); } - visitFileElement(fileElement: FileElement): void { + async visitFileElementAsync(fileElement: FileElement): Promise { if (this.content instanceof FileContentBody) { fileElement.caption = sanitizeRichText(this.content.caption, InputFormat.PLAIN_TEXT); fileElement.alternativeText = sanitizeRichText(this.content.alternativeText, InputFormat.PLAIN_TEXT); - } else { - this.throwNotHandled(fileElement); + return Promise.resolve(); } + return this.rejectNotHandled(fileElement); } - visitRichTextElement(richTextElement: RichTextElement): void { + async visitLinkElementAsync(linkElement: LinkElement): Promise { + if (this.content instanceof LinkContentBody) { + const urlWithProtocol = /:\/\//.test(this.content.url) ? this.content.url : `https://${this.content.url}`; + linkElement.url = new URL(urlWithProtocol).toString(); + const openGraphData = await this.openGraphProxyService.fetchOpenGraphData(linkElement.url); + linkElement.title = openGraphData.title; + linkElement.description = openGraphData.description; + if (openGraphData.image) { + linkElement.imageUrl = openGraphData.image.url; + } + return Promise.resolve(); + } + return this.rejectNotHandled(linkElement); + } + + async visitRichTextElementAsync(richTextElement: RichTextElement): Promise { if (this.content instanceof RichTextContentBody) { richTextElement.text = sanitizeRichText(this.content.text, this.content.inputFormat); richTextElement.inputFormat = this.content.inputFormat; - } else { - this.throwNotHandled(richTextElement); + return Promise.resolve(); } + return this.rejectNotHandled(richTextElement); } - visitSubmissionContainerElement(submissionContainerElement: SubmissionContainerElement): void { + async visitSubmissionContainerElementAsync(submissionContainerElement: SubmissionContainerElement): Promise { if (this.content instanceof SubmissionContainerContentBody) { - submissionContainerElement.dueDate = this.content.dueDate ?? undefined; - } else { - this.throwNotHandled(submissionContainerElement); + if (this.content.dueDate !== undefined) { + submissionContainerElement.dueDate = this.content.dueDate; + } + return Promise.resolve(); } + return this.rejectNotHandled(submissionContainerElement); } - visitSubmissionItem(submission: SubmissionItem): void { - this.throwNotHandled(submission); + async visitSubmissionItemAsync(submission: SubmissionItem): Promise { + return this.rejectNotHandled(submission); } - visitExternalToolElement(externalToolElement: ExternalToolElement): void { + async visitExternalToolElementAsync(externalToolElement: ExternalToolElement): Promise { if (this.content instanceof ExternalToolContentBody && this.content.contextExternalToolId !== undefined) { // Updates should not remove an existing reference to a tool, to prevent orphan tool instances externalToolElement.contextExternalToolId = this.content.contextExternalToolId; - } else { - this.throwNotHandled(externalToolElement); + return Promise.resolve(); } + return this.rejectNotHandled(externalToolElement); } - private throwNotHandled(component: AnyBoardDo) { - throw new Error(`Cannot update element of type: '${component.constructor.name}'`); + private rejectNotHandled(component: AnyBoardDo): Promise { + return Promise.reject(new Error(`Cannot update element of type: '${component.constructor.name}'`)); } } diff --git a/apps/server/src/modules/board/service/content-element.service.spec.ts b/apps/server/src/modules/board/service/content-element.service.spec.ts index 1d41925dfab..b1326450089 100644 --- a/apps/server/src/modules/board/service/content-element.service.spec.ts +++ b/apps/server/src/modules/board/service/content-element.service.spec.ts @@ -1,18 +1,32 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { ContentElementFactory, ContentElementType, FileElement, InputFormat, RichTextElement } from '@shared/domain'; +import { + ContentElementFactory, + ContentElementType, + FileElement, + InputFormat, + RichTextElement, + SubmissionContainerElement, +} from '@shared/domain'; import { setupEntities } from '@shared/testing'; import { cardFactory, fileElementFactory, + linkElementFactory, richTextElementFactory, submissionContainerElementFactory, } from '@shared/testing/factory/domainobject'; -import { FileContentBody, RichTextContentBody, SubmissionContainerContentBody } from '../controller/dto'; +import { + FileContentBody, + LinkContentBody, + RichTextContentBody, + SubmissionContainerContentBody, +} from '../controller/dto'; import { BoardDoRepo } from '../repo'; import { BoardDoService } from './board-do.service'; import { ContentElementService } from './content-element.service'; +import { OpenGraphProxyService } from './open-graph-proxy.service'; describe(ContentElementService.name, () => { let module: TestingModule; @@ -20,6 +34,7 @@ describe(ContentElementService.name, () => { let boardDoRepo: DeepMocked; let boardDoService: DeepMocked; let contentElementFactory: DeepMocked; + let openGraphProxyService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -37,6 +52,10 @@ describe(ContentElementService.name, () => { provide: ContentElementFactory, useValue: createMock(), }, + { + provide: OpenGraphProxyService, + useValue: createMock(), + }, ], }).compile(); @@ -44,6 +63,7 @@ describe(ContentElementService.name, () => { boardDoRepo = module.get(BoardDoRepo); boardDoService = module.get(BoardDoService); contentElementFactory = module.get(ContentElementFactory); + openGraphProxyService = module.get(OpenGraphProxyService); await setupEntities(); }); @@ -229,6 +249,44 @@ describe(ContentElementService.name, () => { }); }); + describe('when element is a link element', () => { + const setup = () => { + const linkElement = linkElementFactory.build(); + + const content = new LinkContentBody(); + content.url = 'https://www.medium.com/great-article'; + const card = cardFactory.build(); + boardDoRepo.findParentOfId.mockResolvedValue(card); + + const imageResponse = { + title: 'Webpage-title', + description: '', + url: linkElement.url, + image: { url: 'https://my-open-graph-proxy.scvs.de/image/adefcb12ed3a' }, + }; + + openGraphProxyService.fetchOpenGraphData.mockResolvedValueOnce(imageResponse); + + return { linkElement, content, card, imageResponse }; + }; + + it('should persist the element', async () => { + const { linkElement, content, card } = setup(); + + await service.update(linkElement, content); + + expect(boardDoRepo.save).toHaveBeenCalledWith(linkElement, card); + }); + + it('should call open graph service', async () => { + const { linkElement, content, card } = setup(); + + await service.update(linkElement, content); + + expect(boardDoRepo.save).toHaveBeenCalledWith(linkElement, card); + }); + }); + describe('when element is a submission container element', () => { const setup = () => { const submissionContainerElement = submissionContainerElementFactory.build(); @@ -245,17 +303,17 @@ describe(ContentElementService.name, () => { it('should update the element', async () => { const { submissionContainerElement, content } = setup(); - await service.update(submissionContainerElement, content); + const element = (await service.update(submissionContainerElement, content)) as SubmissionContainerElement; - expect(submissionContainerElement.dueDate).toEqual(content.dueDate); + expect(element.dueDate).toEqual(content.dueDate); }); it('should persist the element', async () => { const { submissionContainerElement, content, card } = setup(); - await service.update(submissionContainerElement, content); + const element = await service.update(submissionContainerElement, content); - expect(boardDoRepo.save).toHaveBeenCalledWith(submissionContainerElement, card); + expect(boardDoRepo.save).toHaveBeenCalledWith(element, card); }); }); }); diff --git a/apps/server/src/modules/board/service/content-element.service.ts b/apps/server/src/modules/board/service/content-element.service.ts index bef5d076fc6..a7c957173f3 100644 --- a/apps/server/src/modules/board/service/content-element.service.ts +++ b/apps/server/src/modules/board/service/content-element.service.ts @@ -11,13 +11,15 @@ import { AnyElementContentBody } from '../controller/dto'; import { BoardDoRepo } from '../repo'; import { BoardDoService } from './board-do.service'; import { ContentElementUpdateVisitor } from './content-element-update.visitor'; +import { OpenGraphProxyService } from './open-graph-proxy.service'; @Injectable() export class ContentElementService { constructor( private readonly boardDoRepo: BoardDoRepo, private readonly boardDoService: BoardDoService, - private readonly contentElementFactory: ContentElementFactory + private readonly contentElementFactory: ContentElementFactory, + private readonly openGraphProxyService: OpenGraphProxyService ) {} async findById(elementId: EntityId): Promise { @@ -45,13 +47,14 @@ export class ContentElementService { await this.boardDoService.move(element, targetCard, targetPosition); } - async update(element: AnyContentElementDo, content: AnyElementContentBody): Promise { - const updater = new ContentElementUpdateVisitor(content); - - element.accept(updater); + async update(element: AnyContentElementDo, content: AnyElementContentBody): Promise { + const updater = new ContentElementUpdateVisitor(content, this.openGraphProxyService); + await element.acceptAsync(updater); const parent = await this.boardDoRepo.findParentOfId(element.id); await this.boardDoRepo.save(element, parent); + + return element; } } diff --git a/apps/server/src/modules/board/service/index.ts b/apps/server/src/modules/board/service/index.ts index 8ff2787f35d..ac9c686d4b4 100644 --- a/apps/server/src/modules/board/service/index.ts +++ b/apps/server/src/modules/board/service/index.ts @@ -4,4 +4,5 @@ export * from './card.service'; export * from './column-board.service'; export * from './column.service'; export * from './content-element.service'; +export * from './open-graph-proxy.service'; export * from './submission-item.service'; diff --git a/apps/server/src/modules/board/service/open-graph-proxy.service.spec.ts b/apps/server/src/modules/board/service/open-graph-proxy.service.spec.ts new file mode 100644 index 00000000000..debe76cdeba --- /dev/null +++ b/apps/server/src/modules/board/service/open-graph-proxy.service.spec.ts @@ -0,0 +1,91 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { setupEntities } from '@shared/testing'; +import { ImageObject } from 'open-graph-scraper/dist/lib/types'; +import { OpenGraphProxyService } from './open-graph-proxy.service'; + +let ogsResponseMock = {}; +jest.mock( + 'open-graph-scraper', + () => () => + Promise.resolve({ + error: false, + html: '', + response: {}, + result: ogsResponseMock, + }) +); + +describe(OpenGraphProxyService.name, () => { + let module: TestingModule; + let service: OpenGraphProxyService; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [OpenGraphProxyService], + }).compile(); + + service = module.get(OpenGraphProxyService); + + await setupEntities(); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('create', () => { + it('should return also the original url', async () => { + const url = 'https://de.wikipedia.org'; + + const result = await service.fetchOpenGraphData(url); + + expect(result).toEqual(expect.objectContaining({ url })); + }); + + it('should thrown an error if url is an empty string', async () => { + const url = ''; + + await expect(service.fetchOpenGraphData(url)).rejects.toThrow(); + }); + + it('should return ogTitle as title', async () => { + const ogTitle = 'My Title'; + const url = 'https://de.wikipedia.org'; + ogsResponseMock = { ogTitle }; + + const result = await service.fetchOpenGraphData(url); + + expect(result).toEqual(expect.objectContaining({ title: ogTitle })); + }); + + it('should return ogImage as title', async () => { + const ogImage: ImageObject[] = [ + { + width: 800, + type: 'jpeg', + url: 'big-image.jpg', + }, + { + width: 500, + type: 'jpeg', + url: 'medium-image.jpg', + }, + { + width: 300, + type: 'jpeg', + url: 'small-image.jpg', + }, + ]; + const url = 'https://de.wikipedia.org'; + ogsResponseMock = { ogImage }; + + const result = await service.fetchOpenGraphData(url); + + expect(result).toEqual(expect.objectContaining({ image: ogImage[1] })); + }); + }); +}); diff --git a/apps/server/src/modules/board/service/open-graph-proxy.service.ts b/apps/server/src/modules/board/service/open-graph-proxy.service.ts new file mode 100644 index 00000000000..2b54d75ee82 --- /dev/null +++ b/apps/server/src/modules/board/service/open-graph-proxy.service.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@nestjs/common'; +import ogs from 'open-graph-scraper'; +import { ImageObject } from 'open-graph-scraper/dist/lib/types'; + +type OpenGraphData = { + title: string; + description: string; + url: string; + image?: ImageObject; +}; + +@Injectable() +export class OpenGraphProxyService { + async fetchOpenGraphData(url: string): Promise { + if (url.length === 0) { + throw new Error(`OpenGraphProxyService requires a valid URL. Given URL: ${url}`); + } + + const data = await ogs({ url }); + // WIP: add nice debug logging for available openGraphData?!? + + const title = data.result.ogTitle ?? ''; + const description = data.result.ogDescription ?? ''; + const image = data.result.ogImage ? this.pickImage(data.result.ogImage) : undefined; + + return { + title, + description, + image, + url, + }; + } + + private pickImage(images: ImageObject[], minWidth = 400): ImageObject | undefined { + const sortedImages = [...images]; + sortedImages.sort((a, b) => (a.width && b.width ? Number(a.width) - Number(b.width) : 0)); + const smallestBigEnoughImage = sortedImages.find((i) => i.width && i.width >= minWidth); + const fallbackImage = images[0] && images[0].width === undefined ? images[0] : undefined; + return smallestBigEnoughImage ?? fallbackImage; + } +} diff --git a/apps/server/src/modules/board/uc/board-management.uc.spec.ts b/apps/server/src/modules/board/uc/board-management.uc.spec.ts index 3d0d1fa835f..7c3464a8a52 100644 --- a/apps/server/src/modules/board/uc/board-management.uc.spec.ts +++ b/apps/server/src/modules/board/uc/board-management.uc.spec.ts @@ -4,7 +4,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ConsoleWriterService } from '@shared/infra/console'; import { MongoMemoryDatabaseModule } from '@shared/infra/database'; import { courseFactory } from '@shared/testing'; -import { BoardManagementUc } from '@src/modules/management/uc/board-management.uc'; +import { BoardManagementUc } from '@modules/management/uc/board-management.uc'; describe(BoardManagementUc.name, () => { let module: TestingModule; diff --git a/apps/server/src/modules/board/uc/board.uc.spec.ts b/apps/server/src/modules/board/uc/board.uc.spec.ts index efd41e49ce6..c644bb120da 100644 --- a/apps/server/src/modules/board/uc/board.uc.spec.ts +++ b/apps/server/src/modules/board/uc/board.uc.spec.ts @@ -5,7 +5,7 @@ import { BoardDoAuthorizable, BoardRoles, ContentElementType, UserRoleEnum } fro import { setupEntities, userFactory } from '@shared/testing'; import { cardFactory, columnBoardFactory, columnFactory } from '@shared/testing/factory/domainobject'; import { LegacyLogger } from '@src/core/logger'; -import { AuthorizationService } from '@src/modules/authorization'; +import { AuthorizationService } from '@modules/authorization'; import { ObjectId } from 'bson'; import { ContentElementService } from '../service'; import { BoardDoAuthorizableService } from '../service/board-do-authorizable.service'; diff --git a/apps/server/src/modules/board/uc/board.uc.ts b/apps/server/src/modules/board/uc/board.uc.ts index 7c3194916ac..36d45dcd5fc 100644 --- a/apps/server/src/modules/board/uc/board.uc.ts +++ b/apps/server/src/modules/board/uc/board.uc.ts @@ -9,8 +9,8 @@ import { EntityId, } from '@shared/domain'; import { LegacyLogger } from '@src/core/logger'; -import { AuthorizationService } from '@src/modules/authorization/authorization.service'; -import { Action } from '@src/modules/authorization/types/action.enum'; +import { AuthorizationService } from '@modules/authorization/domain'; +import { Action } from '@modules/authorization'; import { CardService, ColumnBoardService, ColumnService } from '../service'; import { BoardDoAuthorizableService } from '../service/board-do-authorizable.service'; diff --git a/apps/server/src/modules/board/uc/card.uc.spec.ts b/apps/server/src/modules/board/uc/card.uc.spec.ts index 130a7feac40..fce595085c7 100644 --- a/apps/server/src/modules/board/uc/card.uc.spec.ts +++ b/apps/server/src/modules/board/uc/card.uc.spec.ts @@ -4,7 +4,7 @@ import { BoardDoAuthorizable, BoardRoles, ContentElementType, UserRoleEnum } fro import { setupEntities, userFactory } from '@shared/testing'; import { cardFactory, richTextElementFactory } from '@shared/testing/factory/domainobject'; import { LegacyLogger } from '@src/core/logger'; -import { AuthorizationService } from '@src/modules/authorization'; +import { AuthorizationService } from '@modules/authorization'; import { ObjectId } from 'bson'; import { BoardDoAuthorizableService, ContentElementService } from '../service'; import { CardService } from '../service/card.service'; diff --git a/apps/server/src/modules/board/uc/card.uc.ts b/apps/server/src/modules/board/uc/card.uc.ts index 577f3a8b963..488f93fd4d8 100644 --- a/apps/server/src/modules/board/uc/card.uc.ts +++ b/apps/server/src/modules/board/uc/card.uc.ts @@ -1,8 +1,7 @@ import { Injectable } from '@nestjs/common'; import { AnyBoardDo, AnyContentElementDo, Card, ContentElementType, EntityId } from '@shared/domain'; import { LegacyLogger } from '@src/core/logger'; -import { AuthorizationService } from '@src/modules/authorization/authorization.service'; -import { Action } from '@src/modules/authorization/types/action.enum'; +import { AuthorizationService, Action } from '@modules/authorization'; import { BoardDoAuthorizableService, CardService, ContentElementService } from '../service'; @Injectable() diff --git a/apps/server/src/modules/board/uc/element.uc.spec.ts b/apps/server/src/modules/board/uc/element.uc.spec.ts index 9c4b2706ab6..03124305dfa 100644 --- a/apps/server/src/modules/board/uc/element.uc.spec.ts +++ b/apps/server/src/modules/board/uc/element.uc.spec.ts @@ -10,7 +10,7 @@ import { userFactory, } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { AuthorizationService } from '@src/modules/authorization'; +import { AuthorizationService } from '@modules/authorization'; import { ObjectId } from 'bson'; import { BoardDoAuthorizableService, ContentElementService } from '../service'; import { SubmissionItemService } from '../service/submission-item.service'; diff --git a/apps/server/src/modules/board/uc/element.uc.ts b/apps/server/src/modules/board/uc/element.uc.ts index 0dafd9eb98f..6f71f202e66 100644 --- a/apps/server/src/modules/board/uc/element.uc.ts +++ b/apps/server/src/modules/board/uc/element.uc.ts @@ -8,8 +8,7 @@ import { UserRoleEnum, } from '@shared/domain'; import { Logger } from '@src/core/logger'; -import { AuthorizationService } from '@src/modules/authorization'; -import { Action } from '@src/modules/authorization/types/action.enum'; +import { AuthorizationService, Action } from '@modules/authorization'; import { AnyElementContentBody } from '../controller/dto'; import { BoardDoAuthorizableService, ContentElementService } from '../service'; import { SubmissionItemService } from '../service/submission-item.service'; @@ -28,11 +27,12 @@ export class ElementUc { } async updateElementContent(userId: EntityId, elementId: EntityId, content: AnyElementContentBody) { - const element = await this.elementService.findById(elementId); + let element = await this.elementService.findById(elementId); await this.checkPermission(userId, element, Action.write); - await this.elementService.update(element, content); + element = await this.elementService.update(element, content); + return element; } async createSubmissionItem( diff --git a/apps/server/src/modules/board/uc/submission-item.uc.spec.ts b/apps/server/src/modules/board/uc/submission-item.uc.spec.ts index 33bc8468fc9..8e9b0d052b7 100644 --- a/apps/server/src/modules/board/uc/submission-item.uc.spec.ts +++ b/apps/server/src/modules/board/uc/submission-item.uc.spec.ts @@ -9,8 +9,7 @@ import { userFactory, } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { AuthorizationService } from '@src/modules/authorization'; -import { Action } from '@src/modules/authorization/types/action.enum'; +import { AuthorizationService, Action } from '@modules/authorization'; import { BoardDoAuthorizableService, ContentElementService, SubmissionItemService } from '../service'; import { SubmissionItemUc } from './submission-item.uc'; diff --git a/apps/server/src/modules/board/uc/submission-item.uc.ts b/apps/server/src/modules/board/uc/submission-item.uc.ts index e59afa4b49b..4748b64d84e 100644 --- a/apps/server/src/modules/board/uc/submission-item.uc.ts +++ b/apps/server/src/modules/board/uc/submission-item.uc.ts @@ -9,8 +9,7 @@ import { UserRoleEnum, } from '@shared/domain'; import { Logger } from '@src/core/logger'; -import { AuthorizationService } from '@src/modules/authorization'; -import { Action } from '@src/modules/authorization/types/action.enum'; +import { AuthorizationService, Action } from '@modules/authorization'; import { BoardDoAuthorizableService, ContentElementService, SubmissionItemService } from '../service'; @Injectable() 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 index eb16f608238..7339e74ddb4 100644 --- a/apps/server/src/modules/class/entity/testing/class.entity.spec.ts +++ b/apps/server/src/modules/class/entity/testing/class.entity.spec.ts @@ -1,6 +1,6 @@ /* eslint-disable no-new */ import { setupEntities } from '@shared/testing'; -import { classEntityFactory } from '@src/modules/class/entity/testing/factory/class.entity.factory'; +import { classEntityFactory } from '@modules/class/entity/testing/factory/class.entity.factory'; import { ObjectId } from '@mikro-orm/mongodb'; import { ClassEntity } from '../class.entity'; 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 index 68e7514d3bc..b98d20853fc 100644 --- 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 @@ -1,6 +1,6 @@ import { DeepPartial } from 'fishery'; import { BaseFactory } from '@shared/testing/factory/base.factory'; -import { ClassEntity, ClassSourceOptionsEntity, IClassEntityProps } from '@src/modules/class/entity'; +import { ClassEntity, ClassSourceOptionsEntity, IClassEntityProps } from '@modules/class/entity'; import { ObjectId } from 'bson'; class ClassEntityFactory extends BaseFactory { diff --git a/apps/server/src/modules/class/repo/classes.repo.spec.ts b/apps/server/src/modules/class/repo/classes.repo.spec.ts index 8059cdf8557..302a8f2de8e 100644 --- a/apps/server/src/modules/class/repo/classes.repo.spec.ts +++ b/apps/server/src/modules/class/repo/classes.repo.spec.ts @@ -4,7 +4,7 @@ import { TestingModule } from '@nestjs/testing/testing-module'; import { SchoolEntity } from '@shared/domain'; import { MongoMemoryDatabaseModule } from '@shared/infra/database'; import { cleanupCollections, schoolFactory } from '@shared/testing'; -import { classEntityFactory } from '@src/modules/class/entity/testing/factory/class.entity.factory'; +import { classEntityFactory } from '@modules/class/entity/testing/factory/class.entity.factory'; import { Class } from '../domain'; import { ClassEntity } from '../entity'; import { ClassesRepo } from './classes.repo'; diff --git a/apps/server/src/modules/class/service/class.service.spec.ts b/apps/server/src/modules/class/service/class.service.spec.ts index 3d1e851bd32..850eaf655a6 100644 --- a/apps/server/src/modules/class/service/class.service.spec.ts +++ b/apps/server/src/modules/class/service/class.service.spec.ts @@ -4,7 +4,7 @@ import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityId } from '@shared/domain'; import { setupEntities } from '@shared/testing'; -import { classEntityFactory } from '@src/modules/class/entity/testing/factory/class.entity.factory'; +import { classEntityFactory } from '@modules/class/entity/testing/factory/class.entity.factory'; import { Class } from '../domain'; import { classFactory } from '../domain/testing/factory/class.factory'; import { ClassesRepo } from '../repo'; diff --git a/apps/server/src/modules/collaborative-storage/collaborative-storage.module.ts b/apps/server/src/modules/collaborative-storage/collaborative-storage.module.ts index d8580f9292b..eedf1b5638e 100644 --- a/apps/server/src/modules/collaborative-storage/collaborative-storage.module.ts +++ b/apps/server/src/modules/collaborative-storage/collaborative-storage.module.ts @@ -2,11 +2,11 @@ import { Module } from '@nestjs/common'; import { CollaborativeStorageAdapterModule } from '@shared/infra/collaborative-storage/collaborative-storage-adapter.module'; import { TeamsRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; -import { AuthorizationModule } from '@src/modules/authorization'; -import { TeamPermissionsMapper } from '@src/modules/collaborative-storage/mapper/team-permissions.mapper'; -import { TeamMapper } from '@src/modules/collaborative-storage/mapper/team.mapper'; -import { CollaborativeStorageService } from '@src/modules/collaborative-storage/services/collaborative-storage.service'; -import { RoleModule } from '@src/modules/role/role.module'; +import { AuthorizationModule } from '@modules/authorization'; +import { TeamPermissionsMapper } from '@modules/collaborative-storage/mapper/team-permissions.mapper'; +import { TeamMapper } from '@modules/collaborative-storage/mapper/team.mapper'; +import { CollaborativeStorageService } from '@modules/collaborative-storage/services/collaborative-storage.service'; +import { RoleModule } from '@modules/role/role.module'; import { CollaborativeStorageController } from './controller/collaborative-storage.controller'; import { CollaborativeStorageUc } from './uc/collaborative-storage.uc'; diff --git a/apps/server/src/modules/collaborative-storage/controller/collaborative-storage.controller.spec.ts b/apps/server/src/modules/collaborative-storage/controller/collaborative-storage.controller.spec.ts index b3f940747fa..4622fed4c00 100644 --- a/apps/server/src/modules/collaborative-storage/controller/collaborative-storage.controller.spec.ts +++ b/apps/server/src/modules/collaborative-storage/controller/collaborative-storage.controller.spec.ts @@ -1,8 +1,8 @@ -import { CollaborativeStorageController } from '@src/modules/collaborative-storage/controller/collaborative-storage.controller'; +import { CollaborativeStorageController } from '@modules/collaborative-storage/controller/collaborative-storage.controller'; import { Test, TestingModule } from '@nestjs/testing'; -import { CollaborativeStorageUc } from '@src/modules/collaborative-storage/uc/collaborative-storage.uc'; +import { CollaborativeStorageUc } from '@modules/collaborative-storage/uc/collaborative-storage.uc'; import { createMock } from '@golevelup/ts-jest'; -import { ICurrentUser } from '@src/modules/authentication'; +import { ICurrentUser } from '@modules/authentication'; import { LegacyLogger } from '@src/core/logger'; describe('CollaborativeStorage Controller', () => { diff --git a/apps/server/src/modules/collaborative-storage/controller/collaborative-storage.controller.ts b/apps/server/src/modules/collaborative-storage/controller/collaborative-storage.controller.ts index 8293ea7452f..19fbccbed1c 100644 --- a/apps/server/src/modules/collaborative-storage/controller/collaborative-storage.controller.ts +++ b/apps/server/src/modules/collaborative-storage/controller/collaborative-storage.controller.ts @@ -1,8 +1,7 @@ import { ApiResponse, ApiTags } from '@nestjs/swagger'; import { Body, Controller, Param, Patch } from '@nestjs/common'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { LegacyLogger } from '@src/core/logger'; -import { ICurrentUser } from '../../authentication/interface/user'; import { CollaborativeStorageUc } from '../uc/collaborative-storage.uc'; import { TeamPermissionsBody } from './dto/team-permissions.body.params'; import { TeamRoleDto } from './dto/team-role.params'; diff --git a/apps/server/src/modules/collaborative-storage/mapper/team-permissions.mapper.spec.ts b/apps/server/src/modules/collaborative-storage/mapper/team-permissions.mapper.spec.ts index 195319c0d89..3e3e8c434e5 100644 --- a/apps/server/src/modules/collaborative-storage/mapper/team-permissions.mapper.spec.ts +++ b/apps/server/src/modules/collaborative-storage/mapper/team-permissions.mapper.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { TeamPermissionsBody } from '@src/modules/collaborative-storage/controller/dto/team-permissions.body.params'; -import { TeamPermissionsMapper } from '@src/modules/collaborative-storage/mapper/team-permissions.mapper'; +import { TeamPermissionsBody } from '@modules/collaborative-storage/controller/dto/team-permissions.body.params'; +import { TeamPermissionsMapper } from '@modules/collaborative-storage/mapper/team-permissions.mapper'; describe('TeamMapper', () => { let module: TestingModule; diff --git a/apps/server/src/modules/collaborative-storage/mapper/team.mapper.spec.ts b/apps/server/src/modules/collaborative-storage/mapper/team.mapper.spec.ts index c46577499f0..a2d9808f5a6 100644 --- a/apps/server/src/modules/collaborative-storage/mapper/team.mapper.spec.ts +++ b/apps/server/src/modules/collaborative-storage/mapper/team.mapper.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { setupEntities } from '@shared/testing'; import { teamFactory } from '@shared/testing/factory/team.factory'; -import { TeamMapper } from '@src/modules/collaborative-storage/mapper/team.mapper'; +import { TeamMapper } from '@modules/collaborative-storage/mapper/team.mapper'; describe('TeamMapper', () => { let module: TestingModule; diff --git a/apps/server/src/modules/collaborative-storage/services/collaborative-storage.service.spec.ts b/apps/server/src/modules/collaborative-storage/services/collaborative-storage.service.spec.ts index 1ed1aa3635c..4f95ae44a11 100644 --- a/apps/server/src/modules/collaborative-storage/services/collaborative-storage.service.spec.ts +++ b/apps/server/src/modules/collaborative-storage/services/collaborative-storage.service.spec.ts @@ -8,11 +8,11 @@ import { TeamsRepo } from '@shared/repo'; import { setupEntities } from '@shared/testing'; import { teamFactory } from '@shared/testing/factory/team.factory'; import { LegacyLogger } from '@src/core/logger'; -import { AuthorizationService } from '@src/modules/authorization'; -import { TeamMapper } from '@src/modules/collaborative-storage/mapper/team.mapper'; -import { CollaborativeStorageService } from '@src/modules/collaborative-storage/services/collaborative-storage.service'; -import { RoleDto } from '@src/modules/role/service/dto/role.dto'; -import { RoleService } from '@src/modules/role/service/role.service'; +import { AuthorizationService } from '@modules/authorization'; +import { TeamMapper } from '@modules/collaborative-storage/mapper/team.mapper'; +import { CollaborativeStorageService } from '@modules/collaborative-storage/services/collaborative-storage.service'; +import { RoleDto } from '@modules/role/service/dto/role.dto'; +import { RoleService } from '@modules/role/service/role.service'; import { TeamDto } from './dto/team.dto'; describe('Collaborative Storage Service', () => { diff --git a/apps/server/src/modules/collaborative-storage/services/collaborative-storage.service.ts b/apps/server/src/modules/collaborative-storage/services/collaborative-storage.service.ts index dccb28a3bb3..f9807cf691c 100644 --- a/apps/server/src/modules/collaborative-storage/services/collaborative-storage.service.ts +++ b/apps/server/src/modules/collaborative-storage/services/collaborative-storage.service.ts @@ -3,8 +3,8 @@ import { EntityId, Permission } from '@shared/domain'; import { CollaborativeStorageAdapter } from '@shared/infra/collaborative-storage'; import { TeamsRepo } from '@shared/repo'; import { LegacyLogger } from '@src/core/logger'; -import { AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; -import { RoleService } from '@src/modules/role/service/role.service'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { RoleService } from '@modules/role/service/role.service'; import { TeamMapper } from '../mapper/team.mapper'; import { TeamPermissionsDto } from './dto/team-permissions.dto'; import { TeamDto } from './dto/team.dto'; diff --git a/apps/server/src/modules/collaborative-storage/uc/collaborative-storage.uc.spec.ts b/apps/server/src/modules/collaborative-storage/uc/collaborative-storage.uc.spec.ts index cef601131b9..bfc52a11a59 100644 --- a/apps/server/src/modules/collaborative-storage/uc/collaborative-storage.uc.spec.ts +++ b/apps/server/src/modules/collaborative-storage/uc/collaborative-storage.uc.spec.ts @@ -1,11 +1,11 @@ import { createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { TeamPermissionsBody } from '@src/modules/collaborative-storage/controller/dto/team-permissions.body.params'; -import { TeamRoleDto } from '@src/modules/collaborative-storage/controller/dto/team-role.params'; -import { TeamPermissionsMapper } from '@src/modules/collaborative-storage/mapper/team-permissions.mapper'; -import { CollaborativeStorageService } from '@src/modules/collaborative-storage/services/collaborative-storage.service'; -import { TeamDto } from '@src/modules/collaborative-storage/services/dto/team.dto'; -import { CollaborativeStorageUc } from '@src/modules/collaborative-storage/uc/collaborative-storage.uc'; +import { TeamPermissionsBody } from '@modules/collaborative-storage/controller/dto/team-permissions.body.params'; +import { TeamRoleDto } from '@modules/collaborative-storage/controller/dto/team-role.params'; +import { TeamPermissionsMapper } from '@modules/collaborative-storage/mapper/team-permissions.mapper'; +import { CollaborativeStorageService } from '@modules/collaborative-storage/services/collaborative-storage.service'; +import { TeamDto } from '@modules/collaborative-storage/services/dto/team.dto'; +import { CollaborativeStorageUc } from '@modules/collaborative-storage/uc/collaborative-storage.uc'; describe('TeamStorageUc', () => { let module: TestingModule; diff --git a/apps/server/src/modules/collaborative-storage/uc/collaborative-storage.uc.ts b/apps/server/src/modules/collaborative-storage/uc/collaborative-storage.uc.ts index 046c2f22112..137f89a54c6 100644 --- a/apps/server/src/modules/collaborative-storage/uc/collaborative-storage.uc.ts +++ b/apps/server/src/modules/collaborative-storage/uc/collaborative-storage.uc.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { CollaborativeStorageService } from '@src/modules/collaborative-storage/services/collaborative-storage.service'; -import { TeamPermissionsMapper } from '@src/modules/collaborative-storage/mapper/team-permissions.mapper'; -import { TeamDto } from '@src/modules/collaborative-storage/services/dto/team.dto'; +import { CollaborativeStorageService } from '@modules/collaborative-storage/services/collaborative-storage.service'; +import { TeamPermissionsMapper } from '@modules/collaborative-storage/mapper/team-permissions.mapper'; +import { TeamDto } from '@modules/collaborative-storage/services/dto/team.dto'; import { TeamPermissionsBody } from '../controller/dto/team-permissions.body.params'; import { TeamRoleDto } from '../controller/dto/team-role.params'; diff --git a/apps/server/src/modules/copy-helper/dto/copy.response.ts b/apps/server/src/modules/copy-helper/dto/copy.response.ts index b343042f6e3..549dcac7014 100644 --- a/apps/server/src/modules/copy-helper/dto/copy.response.ts +++ b/apps/server/src/modules/copy-helper/dto/copy.response.ts @@ -1,5 +1,5 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; -import { CopyElementType, CopyStatusEnum } from '@src/modules/copy-helper/types/copy.types'; +import { CopyElementType, CopyStatusEnum } from '@modules/copy-helper/types/copy.types'; /** * DTO for returning a copy status document via api. diff --git a/apps/server/src/modules/copy-helper/mapper/copy.mapper.spec.ts b/apps/server/src/modules/copy-helper/mapper/copy.mapper.spec.ts index b2cd8db4212..bb82db63761 100644 --- a/apps/server/src/modules/copy-helper/mapper/copy.mapper.spec.ts +++ b/apps/server/src/modules/copy-helper/mapper/copy.mapper.spec.ts @@ -1,11 +1,11 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { setupEntities } from '@shared/testing'; -import { CopyElementType, CopyStatusEnum } from '@src/modules/copy-helper'; -import { LessonCopyApiParams } from '@src/modules/learnroom/controller/dto/lesson/lesson-copy.params'; -import { LessonCopyParentParams } from '@src/modules/lesson'; -import { TaskCopyApiParams } from '@src/modules/task/controller/dto/task-copy.params'; -import { TaskCopyParentParams } from '@src/modules/task/types'; +import { CopyElementType, CopyStatusEnum } from '@modules/copy-helper'; +import { LessonCopyApiParams } from '@modules/learnroom/controller/dto/lesson/lesson-copy.params'; +import { LessonCopyParentParams } from '@modules/lesson'; +import { TaskCopyApiParams } from '@modules/task/controller/dto/task-copy.params'; +import { TaskCopyParentParams } from '@modules/task/types'; import { CopyApiResponse } from '../dto/copy.response'; import { CopyMapper } from './copy.mapper'; diff --git a/apps/server/src/modules/copy-helper/mapper/copy.mapper.ts b/apps/server/src/modules/copy-helper/mapper/copy.mapper.ts index dcc266d607a..20aae3e888b 100644 --- a/apps/server/src/modules/copy-helper/mapper/copy.mapper.ts +++ b/apps/server/src/modules/copy-helper/mapper/copy.mapper.ts @@ -1,8 +1,8 @@ import { EntityId, LessonEntity, Task } from '@shared/domain'; -import { LessonCopyApiParams } from '@src/modules/learnroom/controller/dto/lesson/lesson-copy.params'; -import { LessonCopyParentParams } from '@src/modules/lesson/types'; -import { TaskCopyApiParams } from '@src/modules/task/controller/dto/task-copy.params'; -import { TaskCopyParentParams } from '@src/modules/task/types'; +import { LessonCopyApiParams } from '@modules/learnroom/controller/dto/lesson/lesson-copy.params'; +import { LessonCopyParentParams } from '@modules/lesson/types'; +import { TaskCopyApiParams } from '@modules/task/controller/dto/task-copy.params'; +import { TaskCopyParentParams } from '@modules/task/types'; import { CopyApiResponse } from '../dto/copy.response'; import { CopyStatus, CopyStatusEnum } from '../types/copy.types'; diff --git a/apps/server/src/modules/copy-helper/types/copy.types.ts b/apps/server/src/modules/copy-helper/types/copy.types.ts index 77f0f80e4cc..fff1f0da795 100644 --- a/apps/server/src/modules/copy-helper/types/copy.types.ts +++ b/apps/server/src/modules/copy-helper/types/copy.types.ts @@ -34,6 +34,7 @@ export enum CopyElementType { 'LESSON_CONTENT_TEXT' = 'LESSON_CONTENT_TEXT', 'LERNSTORE_MATERIAL' = 'LERNSTORE_MATERIAL', 'LERNSTORE_MATERIAL_GROUP' = 'LERNSTORE_MATERIAL_GROUP', + 'LINK_ELEMENT' = 'LINK_ELEMENT', 'LTITOOL_GROUP' = 'LTITOOL_GROUP', 'METADATA' = 'METADATA', 'RICHTEXT_ELEMENT' = 'RICHTEXT_ELEMENT', diff --git a/apps/server/src/modules/files-storage-client/files-storage-client.module.ts b/apps/server/src/modules/files-storage-client/files-storage-client.module.ts index 6c9036f91f9..11c8eccdd3b 100644 --- a/apps/server/src/modules/files-storage-client/files-storage-client.module.ts +++ b/apps/server/src/modules/files-storage-client/files-storage-client.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; -import { CopyHelperModule } from '@src/modules/copy-helper'; +import { CopyHelperModule } from '@modules/copy-helper'; import { CopyFilesService } from './service/copy-files.service'; import { FilesStorageClientAdapterService } from './service/files-storage-client.service'; import { FilesStorageProducer } from './service/files-storage.producer'; diff --git a/apps/server/src/modules/files-storage-client/service/copy-files.service.spec.ts b/apps/server/src/modules/files-storage-client/service/copy-files.service.spec.ts index 0dadb1a87e1..5bfc98a361a 100644 --- a/apps/server/src/modules/files-storage-client/service/copy-files.service.spec.ts +++ b/apps/server/src/modules/files-storage-client/service/copy-files.service.spec.ts @@ -8,7 +8,7 @@ import { legacyFileEntityMockFactory, setupEntities, } from '@shared/testing'; -import { CopyElementType, CopyHelperService } from '@src/modules/copy-helper'; +import { CopyElementType, CopyHelperService } from '@modules/copy-helper'; import { CopyFilesService } from './copy-files.service'; import { FilesStorageClientAdapterService } from './files-storage-client.service'; diff --git a/apps/server/src/modules/files-storage-client/service/copy-files.service.ts b/apps/server/src/modules/files-storage-client/service/copy-files.service.ts index 60cbf06c27d..1dc5904eb9b 100644 --- a/apps/server/src/modules/files-storage-client/service/copy-files.service.ts +++ b/apps/server/src/modules/files-storage-client/service/copy-files.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain'; -import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@src/modules/copy-helper'; +import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; import { CopyFileDto } from '../dto'; import { EntityWithEmbeddedFiles } from '../interfaces'; import { CopyFilesOfParentParamBuilder, FileParamBuilder } from '../mapper'; diff --git a/apps/server/src/modules/files-storage/README.md b/apps/server/src/modules/files-storage/README.md index 4bb6fb4ccb8..f44be536309 100644 --- a/apps/server/src/modules/files-storage/README.md +++ b/apps/server/src/modules/files-storage/README.md @@ -88,7 +88,7 @@ folder structure in S3 > schoolId/fileRecordId > .trash/schoolId/fileRecordId (see: ## Goals and Ideas > ### Deleting Files) -### Authorisation Module +### Authorization Module The authorisation is solved by parents. Therefore it is required that the parent types must be known to the authentication service. diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-security.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-security.api.spec.ts index 956697a11d4..d6b6f4b7479 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-security.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-security.api.spec.ts @@ -3,7 +3,7 @@ import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { Permission } from '@shared/domain'; -import { ICurrentUser } from '@src/modules/authentication'; +import { ICurrentUser } from '@modules/authentication'; import { cleanupCollections, fileRecordFactory, @@ -12,9 +12,9 @@ import { schoolFactory, userFactory, } from '@shared/testing'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { FilesStorageTestModule } from '@src/modules/files-storage'; -import { FileRecordListResponse, ScanResultParams } from '@src/modules/files-storage/controller/dto'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { FilesStorageTestModule } from '@modules/files-storage'; +import { FileRecordListResponse, ScanResultParams } from '@modules/files-storage/controller/dto'; import { Request } from 'express'; import request from 'supertest'; import { FileRecord, FileRecordParentType } from '../../entity'; 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 a0b0c63a8a6..e1b792dea36 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 @@ -4,7 +4,7 @@ import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { EntityId, Permission } from '@shared/domain'; -import { AntivirusService } from '@shared/infra/antivirus/antivirus.service'; +import { AntivirusService } from '@shared/infra/antivirus'; import { S3ClientAdapter } from '@shared/infra/s3-client'; import { cleanupCollections, @@ -15,15 +15,15 @@ import { schoolFactory, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@src/modules/authentication'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { FILES_STORAGE_S3_CONNECTION, FilesStorageTestModule } from '@src/modules/files-storage'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { FILES_STORAGE_S3_CONNECTION, FilesStorageTestModule } from '@modules/files-storage'; import { CopyFileParams, CopyFilesOfParentParams, FileRecordListResponse, FileRecordResponse, -} from '@src/modules/files-storage/controller/dto'; +} from '@modules/files-storage/controller/dto'; import { Request } from 'express'; import FileType from 'file-type-cjs/file-type-cjs-index'; import request from 'supertest'; 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 de360aa60f9..7a25915b3d5 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 @@ -4,7 +4,7 @@ import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { EntityId, Permission } from '@shared/domain'; -import { AntivirusService } from '@shared/infra/antivirus/antivirus.service'; +import { AntivirusService } from '@shared/infra/antivirus'; import { S3ClientAdapter } from '@shared/infra/s3-client'; import { cleanupCollections, @@ -14,10 +14,10 @@ import { schoolFactory, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@src/modules/authentication'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { FILES_STORAGE_S3_CONNECTION, FilesStorageTestModule } from '@src/modules/files-storage'; -import { FileRecordListResponse, FileRecordResponse } from '@src/modules/files-storage/controller/dto'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { FILES_STORAGE_S3_CONNECTION, FilesStorageTestModule } from '@modules/files-storage'; +import { FileRecordListResponse, FileRecordResponse } from '@modules/files-storage/controller/dto'; import { Request } from 'express'; import FileType from 'file-type-cjs/file-type-cjs-index'; import request from 'supertest'; 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 9ec672dfd36..331282060e2 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 @@ -4,13 +4,13 @@ import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { EntityId, Permission } from '@shared/domain'; -import { AntivirusService } from '@shared/infra/antivirus/antivirus.service'; +import { AntivirusService } from '@shared/infra/antivirus'; 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 { FILES_STORAGE_S3_CONNECTION, FilesStorageTestModule } from '@src/modules/files-storage'; -import { FileRecordResponse } from '@src/modules/files-storage/controller/dto'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { FILES_STORAGE_S3_CONNECTION, FilesStorageTestModule } from '@modules/files-storage'; +import { FileRecordResponse } from '@modules/files-storage/controller/dto'; import { Request } from 'express'; import FileType from 'file-type-cjs/file-type-cjs-index'; import request from 'supertest'; diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-list-files.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-list-files.api.spec.ts index 6cc436fa7f6..b4d974fb24e 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-list-files.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-list-files.api.spec.ts @@ -11,10 +11,10 @@ import { 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 { FileRecordListResponse, FileRecordResponse } from '@src/modules/files-storage/controller/dto'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { FilesStorageTestModule } from '@modules/files-storage'; +import { FileRecordListResponse, FileRecordResponse } from '@modules/files-storage/controller/dto'; import { Request } from 'express'; import request from 'supertest'; import { FileRecordParentType, PreviewStatus } from '../../entity'; 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 d1aedc97f9e..82cc8545a97 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 @@ -4,13 +4,13 @@ import { ExecutionContext, INestApplication, NotFoundException, StreamableFile } import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { EntityId, Permission } from '@shared/domain'; -import { AntivirusService } from '@shared/infra/antivirus/antivirus.service'; +import { AntivirusService } from '@shared/infra/antivirus'; 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 { FILES_STORAGE_S3_CONNECTION, FilesStorageTestModule } from '@src/modules/files-storage'; -import { FileRecordResponse } from '@src/modules/files-storage/controller/dto'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { FILES_STORAGE_S3_CONNECTION, FilesStorageTestModule } from '@modules/files-storage'; +import { FileRecordResponse } from '@modules/files-storage/controller/dto'; import { Request } from 'express'; import FileType from 'file-type-cjs/file-type-cjs-index'; import request from 'supertest'; diff --git a/apps/server/src/modules/files-storage/controller/api-test/files-storage-rename-file.api.spec.ts b/apps/server/src/modules/files-storage/controller/api-test/files-storage-rename-file.api.spec.ts index 2b3cb75f55d..e28f4dc327f 100644 --- a/apps/server/src/modules/files-storage/controller/api-test/files-storage-rename-file.api.spec.ts +++ b/apps/server/src/modules/files-storage/controller/api-test/files-storage-rename-file.api.spec.ts @@ -3,7 +3,7 @@ import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { Permission } from '@shared/domain'; -import { ICurrentUser } from '@src/modules/authentication'; +import { ICurrentUser } from '@modules/authentication'; import { cleanupCollections, fileRecordFactory, @@ -12,9 +12,9 @@ import { schoolFactory, userFactory, } from '@shared/testing'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { FilesStorageTestModule } from '@src/modules/files-storage'; -import { FileRecordResponse, RenameFileParams } from '@src/modules/files-storage/controller/dto'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { FilesStorageTestModule } from '@modules/files-storage'; +import { FileRecordResponse, RenameFileParams } from '@modules/files-storage/controller/dto'; import { Request } from 'express'; import request from 'supertest'; import { FileRecord, FileRecordParentType } from '../../entity'; 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 603221b6651..f6e9a694ad3 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 @@ -4,7 +4,7 @@ import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { EntityId, Permission } from '@shared/domain'; -import { AntivirusService } from '@shared/infra/antivirus/antivirus.service'; +import { AntivirusService } from '@shared/infra/antivirus'; import { S3ClientAdapter } from '@shared/infra/s3-client'; import { cleanupCollections, @@ -14,10 +14,10 @@ import { schoolFactory, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@src/modules/authentication'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { FILES_STORAGE_S3_CONNECTION, FilesStorageTestModule } from '@src/modules/files-storage'; -import { FileRecordListResponse, FileRecordResponse } from '@src/modules/files-storage/controller/dto'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { FILES_STORAGE_S3_CONNECTION, FilesStorageTestModule } from '@modules/files-storage'; +import { FileRecordListResponse, FileRecordResponse } from '@modules/files-storage/controller/dto'; import { Request } from 'express'; import FileType from 'file-type-cjs/file-type-cjs-index'; import request from 'supertest'; diff --git a/apps/server/src/modules/files-storage/controller/dto/file-storage.params.ts b/apps/server/src/modules/files-storage/controller/dto/file-storage.params.ts index d474685caf3..6555b7bd0f9 100644 --- a/apps/server/src/modules/files-storage/controller/dto/file-storage.params.ts +++ b/apps/server/src/modules/files-storage/controller/dto/file-storage.params.ts @@ -1,6 +1,7 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; import { StringToBoolean } from '@shared/controller'; import { EntityId } from '@shared/domain'; +import { ScanResult } from '@shared/infra/antivirus'; import { Allow, IsBoolean, IsEnum, IsMongoId, IsNotEmpty, IsOptional, IsString, ValidateNested } from 'class-validator'; import { FileRecordParentType } from '../../entity'; import { PreviewOutputMimeTypes, PreviewWidth } from '../../interface'; @@ -51,7 +52,7 @@ export class DownloadFileParams { fileName!: string; } -export class ScanResultParams { +export class ScanResultParams implements ScanResult { @ApiProperty() @Allow() virus_detected?: boolean; diff --git a/apps/server/src/modules/files-storage/controller/files-storage.controller.ts b/apps/server/src/modules/files-storage/controller/files-storage.controller.ts index c1c17d685b4..564919670e4 100644 --- a/apps/server/src/modules/files-storage/controller/files-storage.controller.ts +++ b/apps/server/src/modules/files-storage/controller/files-storage.controller.ts @@ -24,8 +24,7 @@ import { import { ApiConsumes, ApiHeader, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiValidationError, RequestLoggingInterceptor } from '@shared/common'; import { PaginationParams } from '@shared/controller'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; +import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; import { Request, Response } from 'express'; import { GetFileResponse } from '../interface'; import { FilesStorageMapper } from '../mapper'; diff --git a/apps/server/src/modules/files-storage/entity/filerecord.entity.ts b/apps/server/src/modules/files-storage/entity/filerecord.entity.ts index 81fe5b34ba6..a87789d30fc 100644 --- a/apps/server/src/modules/files-storage/entity/filerecord.entity.ts +++ b/apps/server/src/modules/files-storage/entity/filerecord.entity.ts @@ -254,6 +254,12 @@ export class FileRecord extends BaseEntityWithTimestamps { return isVerified; } + public isPreviewPossible(): boolean { + const isPreviewPossible = Object.values(PreviewInputMimeTypes).includes(this.mimeType); + + return isPreviewPossible; + } + public getParentInfo(): IParentInfo { const { parentId, parentType, schoolId } = this; @@ -269,7 +275,7 @@ export class FileRecord extends BaseEntityWithTimestamps { return PreviewStatus.PREVIEW_NOT_POSSIBLE_SCAN_STATUS_BLOCKED; } - if (!Object.values(PreviewInputMimeTypes).includes(this.mimeType)) { + if (!this.isPreviewPossible()) { return PreviewStatus.PREVIEW_NOT_POSSIBLE_WRONG_MIME_TYPE; } diff --git a/apps/server/src/modules/files-storage/files-storage-api.module.ts b/apps/server/src/modules/files-storage/files-storage-api.module.ts index 9d5283b47b7..3bf18aa4047 100644 --- a/apps/server/src/modules/files-storage/files-storage-api.module.ts +++ b/apps/server/src/modules/files-storage/files-storage-api.module.ts @@ -1,14 +1,14 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { CoreModule } from '@src/core'; -import { AuthenticationModule } from '@src/modules/authentication/authentication.module'; -import { AuthorizationModule } from '@src/modules/authorization'; +import { AuthenticationModule } from '@modules/authentication/authentication.module'; +import { AuthorizationReferenceModule } from '@modules/authorization/authorization-reference.module'; import { FileSecurityController, FilesStorageController } from './controller'; import { FilesStorageModule } from './files-storage.module'; import { FilesStorageUC } from './uc'; @Module({ - imports: [AuthorizationModule, FilesStorageModule, AuthenticationModule, CoreModule, HttpModule], + imports: [AuthorizationReferenceModule, FilesStorageModule, AuthenticationModule, CoreModule, HttpModule], controllers: [FilesStorageController, FileSecurityController], providers: [FilesStorageUC], }) diff --git a/apps/server/src/modules/files-storage/files-storage-test.module.ts b/apps/server/src/modules/files-storage/files-storage-test.module.ts index ac3777a041f..6f3d865ebb2 100644 --- a/apps/server/src/modules/files-storage/files-storage-test.module.ts +++ b/apps/server/src/modules/files-storage/files-storage-test.module.ts @@ -5,8 +5,8 @@ import { MongoDatabaseModuleOptions } from '@shared/infra/database/mongo-memory- import { RabbitMQWrapperTestModule } from '@shared/infra/rabbitmq/rabbitmq.module'; 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 { AuthenticationModule } from '@modules/authentication/authentication.module'; +import { AuthorizationModule } from '@modules/authorization'; import { FileRecord } from './entity'; import { FilesStorageApiModule } from './files-storage-api.module'; 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 f3532b157e8..7dc01e9f4e1 100644 --- a/apps/server/src/modules/files-storage/files-storage.config.ts +++ b/apps/server/src/modules/files-storage/files-storage.config.ts @@ -6,14 +6,16 @@ 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; + USE_STREAM_TO_ANTIVIRUS: boolean; } const fileStorageConfig: IFileStorageConfig = { INCOMING_REQUEST_TIMEOUT: Configuration.get('FILES_STORAGE__INCOMING_REQUEST_TIMEOUT') as number, INCOMING_REQUEST_TIMEOUT_COPY_API: Configuration.get('INCOMING_REQUEST_TIMEOUT_COPY_API') as number, MAX_FILE_SIZE: Configuration.get('FILES_STORAGE__MAX_FILE_SIZE') as number, - MAX_SECURITY_CHECK_FILE_SIZE: Configuration.get('FILE_SECURITY_CHECK_MAX_FILE_SIZE') as number, + MAX_SECURITY_CHECK_FILE_SIZE: Configuration.get('FILES_STORAGE__MAX_FILE_SIZE') as number, NEST_LOG_LEVEL: Configuration.get('NEST_LOG_LEVEL') as string, + USE_STREAM_TO_ANTIVIRUS: Configuration.get('FILES_STORAGE__USE_STREAM_TO_ANTIVIRUS') as boolean, }; // The configurations lookup 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 29caa14e8c8..248654218ef 100644 --- a/apps/server/src/modules/files-storage/files-storage.module.ts +++ b/apps/server/src/modules/files-storage/files-storage.module.ts @@ -23,6 +23,8 @@ const imports = [ filesServiceBaseUrl: Configuration.get('FILES_STORAGE__SERVICE_BASE_URL') as string, exchange: Configuration.get('ANTIVIRUS_EXCHANGE') as string, routingKey: Configuration.get('ANTIVIRUS_ROUTING_KEY') as string, + hostname: Configuration.get('CLAMAV__SERVICE_HOSTNAME') as string, + port: Configuration.get('CLAMAV__SERVICE_PORT') as number, }), S3ClientModule.register([s3Config]), ]; diff --git a/apps/server/src/modules/files-storage/mapper/files-storage.mapper.spec.ts b/apps/server/src/modules/files-storage/mapper/files-storage.mapper.spec.ts index 3165ec49021..1f681c371d1 100644 --- a/apps/server/src/modules/files-storage/mapper/files-storage.mapper.spec.ts +++ b/apps/server/src/modules/files-storage/mapper/files-storage.mapper.spec.ts @@ -1,6 +1,6 @@ import { NotImplementedException } from '@nestjs/common'; import { fileRecordFactory, setupEntities } from '@shared/testing'; -import { AuthorizableReferenceType } from '@src/modules/authorization'; +import { AuthorizableReferenceType } from '@modules/authorization/domain'; import { DownloadFileParams, FileRecordListResponse, diff --git a/apps/server/src/modules/files-storage/mapper/files-storage.mapper.ts b/apps/server/src/modules/files-storage/mapper/files-storage.mapper.ts index 3d298cd3b2e..5b786aff96e 100644 --- a/apps/server/src/modules/files-storage/mapper/files-storage.mapper.ts +++ b/apps/server/src/modules/files-storage/mapper/files-storage.mapper.ts @@ -1,5 +1,5 @@ import { NotImplementedException, StreamableFile } from '@nestjs/common'; -import { AuthorizableReferenceType } from '@src/modules/authorization'; +import { AuthorizableReferenceType } from '@modules/authorization/domain'; import { plainToClass } from 'class-transformer'; import { DownloadFileParams, 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 3605ae74080..4ba05e540a8 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 @@ -2,7 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus/antivirus.service'; +import { AntivirusService } from '@shared/infra/antivirus'; import { S3ClientAdapter } from '@shared/infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; 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 627f35d91da..cd76b564b31 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 @@ -3,7 +3,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { InternalServerErrorException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus/antivirus.service'; +import { AntivirusService } from '@shared/infra/antivirus'; import { S3ClientAdapter } from '@shared/infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; 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 a578bca51a1..bcee168c2b9 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 @@ -3,7 +3,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { NotAcceptableException, NotFoundException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus/antivirus.service'; +import { AntivirusService } from '@shared/infra/antivirus'; import { GetFile, S3ClientAdapter } from '@shared/infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; 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 3565e5a328a..95f7c2d204c 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 @@ -2,7 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus/antivirus.service'; +import { AntivirusService } from '@shared/infra/antivirus'; import { S3ClientAdapter } from '@shared/infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; 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 1c3460c3926..c82f96074f1 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 @@ -2,7 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus/antivirus.service'; +import { AntivirusService } from '@shared/infra/antivirus'; import { S3ClientAdapter } from '@shared/infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; 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 9da800baf90..8523c7388fd 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 @@ -3,7 +3,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { ConflictException, NotFoundException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus/antivirus.service'; +import { AntivirusService } from '@shared/infra/antivirus'; import { S3ClientAdapter } from '@shared/infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; 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 0ba3f729dae..022e6a4bf0d 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 @@ -3,14 +3,14 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { BadRequestException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus/antivirus.service'; +import { AntivirusService } from '@shared/infra/antivirus'; import { S3ClientAdapter } from '@shared/infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { readableStreamWithFileTypeFactory } from '@shared/testing/factory/readable-stream-with-file-type.factory'; import { LegacyLogger } from '@src/core/logger'; import { MimeType } from 'file-type'; import FileType from 'file-type-cjs/file-type-cjs-index'; -import { Readable } from 'stream'; +import { PassThrough, Readable } from 'stream'; import { FileRecordParams } from '../controller/dto'; import { FileDto } from '../dto'; import { FileRecord, FileRecordParentType } from '../entity'; @@ -122,6 +122,12 @@ describe('FilesStorageService upload methods', () => { const readableStreamWithFileType = readableStreamWithFileTypeFactory.build(); + antivirusService.checkStream.mockResolvedValueOnce({ + virus_detected: undefined, + virus_signature: undefined, + error: undefined, + }); + return { params, file, @@ -170,7 +176,7 @@ describe('FilesStorageService upload methods', () => { await service.uploadFile(userId, params, file); - expect(getMimeTypeSpy).toHaveBeenCalledWith(file.data); + expect(getMimeTypeSpy).toHaveBeenCalledWith(expect.any(PassThrough)); }); it('should call getFileRecordsOfParent with correct params', async () => { @@ -199,14 +205,6 @@ describe('FilesStorageService upload methods', () => { ); }); - it('should call antivirusService.send with fileRecord', async () => { - const { params, file, userId } = setup(); - - const fileRecord = await service.uploadFile(userId, params, file); - - expect(antivirusService.send).toHaveBeenCalledWith(fileRecord.securityCheck.requestToken); - }); - it('should call storageClient.create with correct params', async () => { const { params, file, userId } = setup(); @@ -223,6 +221,29 @@ describe('FilesStorageService upload methods', () => { expect(result).toBeInstanceOf(FileRecord); }); + + describe('Antivirus handling by upload ', () => { + describe('when useStreamToAntivirus is true', () => { + it('should call antivirusService.send with fileRecord', async () => { + const { params, file, userId } = setup(); + configService.get.mockReturnValueOnce(true); + await service.uploadFile(userId, params, file); + + expect(antivirusService.checkStream).toHaveBeenCalledWith(file); + }); + }); + + describe('when useStreamToAntivirus is false', () => { + it('should call antivirusService.send with fileRecord', async () => { + const { params, file, userId } = setup(); + configService.get.mockReturnValueOnce(false); + + const fileRecord = await service.uploadFile(userId, params, file); + + expect(antivirusService.send).toHaveBeenCalledWith(fileRecord.securityCheck.requestToken); + }); + }); + }); }); describe('WHEN file record repo throws error', () => { @@ -294,6 +315,7 @@ describe('FilesStorageService upload methods', () => { } }); + configService.get.mockReturnValueOnce(true); configService.get.mockReturnValueOnce(2); const error = new BadRequestException(ErrorType.FILE_TOO_BIG); @@ -315,6 +337,9 @@ describe('FilesStorageService upload methods', () => { jest.spyOn(service, 'getFileRecordsOfParent').mockResolvedValue([[fileRecord], 1]); + // Mock for useStreamToAntivirus + configService.get.mockReturnValueOnce(false); + // Mock for max file size configService.get.mockReturnValueOnce(10); @@ -364,6 +389,8 @@ describe('FilesStorageService upload methods', () => { jest.spyOn(FileType, 'fileTypeStream').mockResolvedValueOnce(readableStreamWithFileType); + configService.get.mockReturnValueOnce(false); + // The fileRecord._id must be set by fileRecordRepo.save. Otherwise createPath fails. // eslint-disable-next-line @typescript-eslint/require-await fileRecordRepo.save.mockImplementation(async (fr) => { 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 2b6bc4b179a..8c0c85630de 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 @@ -8,11 +8,11 @@ import { } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Counted, EntityId } from '@shared/domain'; -import { AntivirusService } from '@shared/infra/antivirus/antivirus.service'; +import { AntivirusService } from '@shared/infra/antivirus'; import { S3ClientAdapter } from '@shared/infra/s3-client'; import { LegacyLogger } from '@src/core/logger'; import FileType from 'file-type-cjs/file-type-cjs-index'; -import { Readable } from 'stream'; +import { PassThrough, Readable } from 'stream'; import { CopyFileResponse, CopyFilesOfParentParams, @@ -104,7 +104,8 @@ export class FilesStorageService { private async detectMimeType(file: FileDto): Promise<{ mimeType: string; stream: Readable }> { if (this.isStreamMimeTypeDetectionPossible(file.mimeType)) { - const { stream, mime: detectedMimeType } = await this.detectMimeTypeByStream(file.data); + const source = file.data.pipe(new PassThrough()); + const { stream, mime: detectedMimeType } = await this.detectMimeTypeByStream(source); const mimeType = detectedMimeType ?? file.mimeType; @@ -151,18 +152,32 @@ export class FilesStorageService { file: FileDto ): Promise { const filePath = createPath(params.schoolId, fileRecord.id); + const useStreamToAntivirus = this.configService.get('USE_STREAM_TO_ANTIVIRUS'); try { const fileSizePromise = this.countFileSize(file); - await this.storageClient.create(filePath, file); + if (useStreamToAntivirus && fileRecord.isPreviewPossible()) { + const streamToAntivirus = file.data.pipe(new PassThrough()); + + const [, antivirusClientResponse] = await Promise.all([ + this.storageClient.create(filePath, file), + this.antivirusService.checkStream(streamToAntivirus), + ]); + const { status, reason } = FileRecordMapper.mapScanResultParamsToDto(antivirusClientResponse); + fileRecord.updateSecurityCheckStatus(status, reason); + } else { + await this.storageClient.create(filePath, file); + } // The actual file size is set here because it is known only after the whole file is streamed. fileRecord.size = await fileSizePromise; this.throwErrorIfFileIsTooBig(fileRecord.size); await this.fileRecordRepo.save(fileRecord); - await this.sendToAntivirus(fileRecord); + if (!useStreamToAntivirus || !fileRecord.isPreviewPossible()) { + await this.sendToAntivirus(fileRecord); + } } catch (error) { await this.storageClient.delete([filePath]); await this.fileRecordRepo.delete(fileRecord); diff --git a/apps/server/src/modules/files-storage/uc/files-storage-copy.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-copy.uc.spec.ts index 21592585ce4..2b7f1052121 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-copy.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-copy.uc.spec.ts @@ -4,11 +4,12 @@ import { HttpService } from '@nestjs/axios'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityId, Permission } from '@shared/domain'; -import { AntivirusService } from '@shared/infra/antivirus/antivirus.service'; +import { AntivirusService } from '@shared/infra/antivirus'; import { S3ClientAdapter } from '@shared/infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { Action, AuthorizationService } from '@src/modules/authorization'; +import { Action } from '@modules/authorization'; +import { AuthorizationReferenceService } from '@modules/authorization/domain'; import { FileRecordParams } from '../controller/dto'; import { FileRecord, FileRecordParentType } from '../entity'; import { CopyFileResponseBuilder } from '../mapper'; @@ -68,7 +69,7 @@ describe('FilesStorageUC', () => { let module: TestingModule; let filesStorageUC: FilesStorageUC; let filesStorageService: DeepMocked; - let authorizationService: DeepMocked; + let authorizationReferenceService: DeepMocked; beforeEach(() => { jest.resetAllMocks(); @@ -97,8 +98,8 @@ describe('FilesStorageUC', () => { useValue: createMock(), }, { - provide: AuthorizationService, - useValue: createMock(), + provide: AuthorizationReferenceService, + useValue: createMock(), }, { provide: HttpService, @@ -112,7 +113,7 @@ describe('FilesStorageUC', () => { }).compile(); filesStorageUC = module.get(FilesStorageUC); - authorizationService = module.get(AuthorizationService); + authorizationReferenceService = module.get(AuthorizationReferenceService); filesStorageService = module.get(FilesStorageService); }); @@ -134,7 +135,7 @@ describe('FilesStorageUC', () => { const fileResponse = CopyFileResponseBuilder.build(targetFile.id, sourceFile.id, targetFile.name); - authorizationService.checkPermissionByReferences.mockResolvedValueOnce().mockResolvedValueOnce(); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce().mockResolvedValueOnce(); filesStorageService.copyFilesOfParent.mockResolvedValueOnce([[fileResponse], 1]); return { sourceParams, targetParams, userId, fileResponse }; @@ -145,7 +146,7 @@ describe('FilesStorageUC', () => { await filesStorageUC.copyFilesOfParent(userId, sourceParams, targetParams); - expect(authorizationService.checkPermissionByReferences).toHaveBeenNthCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenNthCalledWith( 1, userId, sourceParams.parentType, @@ -159,7 +160,7 @@ describe('FilesStorageUC', () => { await filesStorageUC.copyFilesOfParent(userId, sourceParams, targetParams); - expect(authorizationService.checkPermissionByReferences).toHaveBeenNthCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenNthCalledWith( 2, userId, targetParams.target.parentType, @@ -191,7 +192,7 @@ describe('FilesStorageUC', () => { const targetParams = createTargetParams(); const error = new ForbiddenException(); - authorizationService.checkPermissionByReferences.mockRejectedValueOnce(error).mockResolvedValueOnce(); + authorizationReferenceService.checkPermissionByReferences.mockRejectedValueOnce(error).mockResolvedValueOnce(); return { sourceParams, targetParams, userId, error }; }; @@ -210,7 +211,7 @@ describe('FilesStorageUC', () => { const targetParams = createTargetParams(); const error = new ForbiddenException(); - authorizationService.checkPermissionByReferences.mockResolvedValueOnce().mockRejectedValueOnce(error); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce().mockRejectedValueOnce(error); return { sourceParams, targetParams, userId, error }; }; @@ -229,7 +230,9 @@ describe('FilesStorageUC', () => { const targetParams = createTargetParams(); const error = new ForbiddenException(); - authorizationService.checkPermissionByReferences.mockRejectedValueOnce(error).mockRejectedValueOnce(error); + authorizationReferenceService.checkPermissionByReferences + .mockRejectedValueOnce(error) + .mockRejectedValueOnce(error); return { sourceParams, targetParams, userId, error }; }; @@ -249,7 +252,7 @@ describe('FilesStorageUC', () => { const error = new Error('test'); - authorizationService.checkPermissionByReferences.mockResolvedValueOnce().mockResolvedValueOnce(); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce().mockResolvedValueOnce(); filesStorageService.copyFilesOfParent.mockRejectedValueOnce(error); return { sourceParams, targetParams, userId, error }; @@ -289,7 +292,7 @@ describe('FilesStorageUC', () => { ); filesStorageService.getFileRecord.mockResolvedValue(fileRecord); - authorizationService.checkPermissionByReferences.mockResolvedValueOnce().mockResolvedValueOnce(); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce().mockResolvedValueOnce(); filesStorageService.copy.mockResolvedValueOnce([fileResponse]); return { singleFileParams, copyFileParams, userId, fileResponse, fileRecord }; @@ -308,7 +311,7 @@ describe('FilesStorageUC', () => { await filesStorageUC.copyOneFile(userId, singleFileParams, copyFileParams); - expect(authorizationService.checkPermissionByReferences).toHaveBeenNthCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenNthCalledWith( 1, userId, fileRecord.parentType, @@ -322,7 +325,7 @@ describe('FilesStorageUC', () => { await filesStorageUC.copyOneFile(userId, singleFileParams, copyFileParams); - expect(authorizationService.checkPermissionByReferences).toHaveBeenNthCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenNthCalledWith( 2, userId, copyFileParams.target.parentType, @@ -355,7 +358,7 @@ describe('FilesStorageUC', () => { const error = new ForbiddenException(); filesStorageService.getFileRecord.mockResolvedValueOnce(fileRecord); - authorizationService.checkPermissionByReferences.mockRejectedValueOnce(error).mockResolvedValueOnce(); + authorizationReferenceService.checkPermissionByReferences.mockRejectedValueOnce(error).mockResolvedValueOnce(); return { singleFileParams, copyFileParams, userId, fileRecord, error }; }; @@ -375,7 +378,7 @@ describe('FilesStorageUC', () => { const error = new ForbiddenException(); filesStorageService.getFileRecord.mockResolvedValue(fileRecord); - authorizationService.checkPermissionByReferences.mockResolvedValueOnce().mockRejectedValueOnce(error); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce().mockRejectedValueOnce(error); return { singleFileParams, copyFileParams, userId, fileRecord, error }; }; @@ -395,7 +398,9 @@ describe('FilesStorageUC', () => { const error = new ForbiddenException(); filesStorageService.getFileRecord.mockResolvedValue(fileRecord); - authorizationService.checkPermissionByReferences.mockRejectedValueOnce(error).mockRejectedValueOnce(error); + authorizationReferenceService.checkPermissionByReferences + .mockRejectedValueOnce(error) + .mockRejectedValueOnce(error); return { singleFileParams, copyFileParams, userId, fileRecord, error }; }; @@ -434,7 +439,7 @@ describe('FilesStorageUC', () => { const error = new Error('test'); filesStorageService.getFileRecord.mockResolvedValue(fileRecord); - authorizationService.checkPermissionByReferences.mockResolvedValueOnce().mockResolvedValueOnce(); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce().mockResolvedValueOnce(); filesStorageService.copy.mockRejectedValueOnce(error); return { singleFileParams, copyFileParams, userId, fileRecord, error }; diff --git a/apps/server/src/modules/files-storage/uc/files-storage-delete.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-delete.uc.spec.ts index 9a870a7f4e1..a1aaf0342ee 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-delete.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-delete.uc.spec.ts @@ -4,11 +4,11 @@ import { HttpService } from '@nestjs/axios'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Counted, EntityId } from '@shared/domain'; -import { AntivirusService } from '@shared/infra/antivirus/antivirus.service'; +import { AntivirusService } from '@shared/infra/antivirus'; import { S3ClientAdapter } from '@shared/infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { AuthorizationService } from '@src/modules/authorization'; +import { AuthorizationReferenceService } from '@modules/authorization/domain'; import { FileRecordParams } from '../controller/dto'; import { FileRecord, FileRecordParentType } from '../entity'; import { FileStorageAuthorizationContext } from '../files-storage.const'; @@ -57,7 +57,7 @@ describe('FilesStorageUC delete methods', () => { let filesStorageUC: FilesStorageUC; let filesStorageService: DeepMocked; let previewService: DeepMocked; - let authorizationService: DeepMocked; + let authorizationReferenceService: DeepMocked; beforeAll(async () => { await setupEntities([FileRecord]); @@ -82,8 +82,8 @@ describe('FilesStorageUC delete methods', () => { useValue: createMock(), }, { - provide: AuthorizationService, - useValue: createMock(), + provide: AuthorizationReferenceService, + useValue: createMock(), }, { provide: HttpService, @@ -97,7 +97,7 @@ describe('FilesStorageUC delete methods', () => { }).compile(); filesStorageUC = module.get(FilesStorageUC); - authorizationService = module.get(AuthorizationService); + authorizationReferenceService = module.get(AuthorizationReferenceService); filesStorageService = module.get(FilesStorageService); previewService = module.get(PreviewService); }); @@ -122,7 +122,7 @@ describe('FilesStorageUC delete methods', () => { const fileRecord = fileRecords[0]; const mockedResult = [[fileRecord], 0] as Counted; - authorizationService.checkPermissionByReferences.mockResolvedValueOnce(); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); filesStorageService.deleteFilesOfParent.mockResolvedValueOnce(mockedResult); return { params, userId, mockedResult, requestParams, fileRecord }; @@ -134,7 +134,7 @@ describe('FilesStorageUC delete methods', () => { await filesStorageUC.deleteFilesOfParent(userId, requestParams); - expect(authorizationService.checkPermissionByReferences).toBeCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toBeCalledWith( userId, allowedType, requestParams.parentId, @@ -171,7 +171,7 @@ describe('FilesStorageUC delete methods', () => { const setup = () => { const { requestParams, userId } = createParams(); - authorizationService.checkPermissionByReferences.mockRejectedValueOnce(new ForbiddenException()); + authorizationReferenceService.checkPermissionByReferences.mockRejectedValueOnce(new ForbiddenException()); return { requestParams, userId }; }; @@ -192,7 +192,7 @@ describe('FilesStorageUC delete methods', () => { const { requestParams, userId } = createParams(); const error = new Error('test'); - authorizationService.checkPermissionByReferences.mockResolvedValueOnce(); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); filesStorageService.deleteFilesOfParent.mockRejectedValueOnce(error); return { requestParams, userId, error }; @@ -214,7 +214,7 @@ describe('FilesStorageUC delete methods', () => { const requestParams = { fileRecordId: fileRecord.id, parentType: fileRecord.parentType }; filesStorageService.getFileRecord.mockResolvedValueOnce(fileRecord); - authorizationService.checkPermissionByReferences.mockResolvedValueOnce(); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); filesStorageService.delete.mockResolvedValueOnce(); return { requestParams, userId, fileRecord }; @@ -227,7 +227,7 @@ describe('FilesStorageUC delete methods', () => { const allowedType = FilesStorageMapper.mapToAllowedAuthorizationEntityType(requestParams.parentType); - expect(authorizationService.checkPermissionByReferences).toBeCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toBeCalledWith( userId, allowedType, fileRecord.parentId, @@ -301,7 +301,7 @@ describe('FilesStorageUC delete methods', () => { const requestParams = { fileRecordId: fileRecord.id, parentType: fileRecord.parentType }; filesStorageService.getFileRecord.mockResolvedValueOnce(fileRecord); - authorizationService.checkPermissionByReferences.mockRejectedValueOnce(new ForbiddenException()); + authorizationReferenceService.checkPermissionByReferences.mockRejectedValueOnce(new ForbiddenException()); return { requestParams, userId }; }; @@ -322,7 +322,7 @@ describe('FilesStorageUC delete methods', () => { const error = new Error('test'); filesStorageService.getFileRecord.mockResolvedValueOnce(fileRecord); - authorizationService.checkPermissionByReferences.mockResolvedValueOnce(); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); filesStorageService.delete.mockRejectedValueOnce(error); return { requestParams, userId, error }; diff --git a/apps/server/src/modules/files-storage/uc/files-storage-download-preview.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-download-preview.uc.spec.ts index 2d62feaa08e..81b54553d1e 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-download-preview.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-download-preview.uc.spec.ts @@ -3,11 +3,11 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { HttpService } from '@nestjs/axios'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus/antivirus.service'; +import { AntivirusService } from '@shared/infra/antivirus'; import { S3ClientAdapter } from '@shared/infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { AuthorizationService } from '@src/modules/authorization'; +import { AuthorizationReferenceService } from '@modules/authorization/domain'; import { SingleFileParams } from '../controller/dto'; import { FileRecord } from '../entity'; import { FileStorageAuthorizationContext } from '../files-storage.const'; @@ -43,7 +43,7 @@ describe('FilesStorageUC', () => { let filesStorageUC: FilesStorageUC; let filesStorageService: DeepMocked; let previewService: DeepMocked; - let authorizationService: DeepMocked; + let authorizationReferenceService: DeepMocked; beforeAll(async () => { await setupEntities([FileRecord]); @@ -72,8 +72,8 @@ describe('FilesStorageUC', () => { useValue: createMock(), }, { - provide: AuthorizationService, - useValue: createMock(), + provide: AuthorizationReferenceService, + useValue: createMock(), }, { provide: HttpService, @@ -83,7 +83,7 @@ describe('FilesStorageUC', () => { }).compile(); filesStorageUC = module.get(FilesStorageUC); - authorizationService = module.get(AuthorizationService); + authorizationReferenceService = module.get(AuthorizationReferenceService); filesStorageService = module.get(FilesStorageService); previewService = module.get(PreviewService); }); @@ -143,7 +143,7 @@ describe('FilesStorageUC', () => { await filesStorageUC.downloadPreview(userId, fileDownloadParams, previewParams); const allowedType = FilesStorageMapper.mapToAllowedAuthorizationEntityType(fileRecord.parentType); - expect(authorizationService.checkPermissionByReferences).toHaveBeenCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenCalledWith( userId, allowedType, fileRecord.parentId, @@ -190,7 +190,7 @@ describe('FilesStorageUC', () => { filesStorageService.getFileRecord.mockResolvedValueOnce(fileRecord); const error = new ForbiddenException(); - authorizationService.checkPermissionByReferences.mockRejectedValueOnce(error); + authorizationReferenceService.checkPermissionByReferences.mockRejectedValueOnce(error); return { fileDownloadParams, userId, fileRecord, previewParams, error }; }; diff --git a/apps/server/src/modules/files-storage/uc/files-storage-download.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-download.uc.spec.ts index ee3eb1ecef3..51ad0fd0b77 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-download.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-download.uc.spec.ts @@ -3,11 +3,11 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { HttpService } from '@nestjs/axios'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus/antivirus.service'; +import { AntivirusService } from '@shared/infra/antivirus'; import { S3ClientAdapter } from '@shared/infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { AuthorizationService } from '@src/modules/authorization'; +import { AuthorizationReferenceService } from '@modules/authorization/domain'; import { SingleFileParams } from '../controller/dto'; import { FileRecord } from '../entity'; import { FileStorageAuthorizationContext } from '../files-storage.const'; @@ -34,7 +34,7 @@ describe('FilesStorageUC', () => { let module: TestingModule; let filesStorageUC: FilesStorageUC; let filesStorageService: DeepMocked; - let authorizationService: DeepMocked; + let authorizationReferenceService: DeepMocked; beforeAll(async () => { await setupEntities([FileRecord]); @@ -59,8 +59,8 @@ describe('FilesStorageUC', () => { useValue: createMock(), }, { - provide: AuthorizationService, - useValue: createMock(), + provide: AuthorizationReferenceService, + useValue: createMock(), }, { provide: HttpService, @@ -74,7 +74,7 @@ describe('FilesStorageUC', () => { }).compile(); filesStorageUC = module.get(FilesStorageUC); - authorizationService = module.get(AuthorizationService); + authorizationReferenceService = module.get(AuthorizationReferenceService); filesStorageService = module.get(FilesStorageService); }); @@ -99,7 +99,7 @@ describe('FilesStorageUC', () => { const fileResponse = createMock(); filesStorageService.getFileRecord.mockResolvedValueOnce(fileRecord); - authorizationService.checkPermissionByReferences.mockResolvedValue(); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValue(); filesStorageService.download.mockResolvedValueOnce(fileResponse); return { fileDownloadParams, userId, fileRecord, fileResponse }; @@ -121,7 +121,7 @@ describe('FilesStorageUC', () => { await filesStorageUC.download(userId, fileDownloadParams); const allowedType = FilesStorageMapper.mapToAllowedAuthorizationEntityType(fileRecord.parentType); - expect(authorizationService.checkPermissionByReferences).toHaveBeenCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenCalledWith( userId, allowedType, fileRecord.parentId, @@ -171,7 +171,7 @@ describe('FilesStorageUC', () => { const error = new ForbiddenException(); filesStorageService.getFileRecord.mockResolvedValueOnce(fileRecord); - authorizationService.checkPermissionByReferences.mockRejectedValueOnce(error); + authorizationReferenceService.checkPermissionByReferences.mockRejectedValueOnce(error); return { fileDownloadParams, userId, fileRecord }; }; @@ -190,7 +190,7 @@ describe('FilesStorageUC', () => { const error = new Error('test'); filesStorageService.getFileRecord.mockResolvedValueOnce(fileRecord); - authorizationService.checkPermissionByReferences.mockResolvedValue(); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValue(); filesStorageService.download.mockRejectedValueOnce(error); return { fileDownloadParams, userId, error }; diff --git a/apps/server/src/modules/files-storage/uc/files-storage-get.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-get.uc.spec.ts index 610f54d8d3b..60d3fdd1a64 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-get.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-get.uc.spec.ts @@ -2,11 +2,11 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { HttpService } from '@nestjs/axios'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus/antivirus.service'; +import { AntivirusService } from '@shared/infra/antivirus'; import { S3ClientAdapter } from '@shared/infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { AuthorizationService } from '@src/modules/authorization'; +import { AuthorizationReferenceService } from '@modules/authorization/domain'; import { FileRecordParams } from '../controller/dto'; import { FileRecord, FileRecordParentType } from '../entity'; import { FileStorageAuthorizationContext } from '../files-storage.const'; @@ -37,7 +37,7 @@ describe('FilesStorageUC', () => { let module: TestingModule; let filesStorageUC: FilesStorageUC; let filesStorageService: DeepMocked; - let authorizationService: DeepMocked; + let authorizationReferenceService: DeepMocked; beforeAll(async () => { await setupEntities([FileRecord]); @@ -62,8 +62,8 @@ describe('FilesStorageUC', () => { useValue: createMock(), }, { - provide: AuthorizationService, - useValue: createMock(), + provide: AuthorizationReferenceService, + useValue: createMock(), }, { provide: HttpService, @@ -77,7 +77,7 @@ describe('FilesStorageUC', () => { }).compile(); filesStorageUC = module.get(FilesStorageUC); - authorizationService = module.get(AuthorizationService); + authorizationReferenceService = module.get(AuthorizationReferenceService); filesStorageService = module.get(FilesStorageService); }); @@ -100,7 +100,7 @@ describe('FilesStorageUC', () => { const { fileRecords, params } = buildFileRecordsWithParams(); filesStorageService.getFileRecordsOfParent.mockResolvedValueOnce([fileRecords, fileRecords.length]); - authorizationService.checkPermissionByReferences.mockResolvedValueOnce(); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); return { userId, params, fileRecords }; }; @@ -110,7 +110,7 @@ describe('FilesStorageUC', () => { await filesStorageUC.getFileRecordsOfParent(userId, params); - expect(authorizationService.checkPermissionByReferences).toHaveBeenCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenCalledWith( userId, params.parentType, params.parentId, @@ -141,7 +141,7 @@ describe('FilesStorageUC', () => { const { fileRecords, params } = buildFileRecordsWithParams(); filesStorageService.getFileRecordsOfParent.mockResolvedValueOnce([fileRecords, fileRecords.length]); - authorizationService.checkPermissionByReferences.mockRejectedValueOnce(new Error('Bla')); + authorizationReferenceService.checkPermissionByReferences.mockRejectedValueOnce(new Error('Bla')); return { userId, params, fileRecords }; }; @@ -160,7 +160,7 @@ describe('FilesStorageUC', () => { const fileRecords = []; filesStorageService.getFileRecordsOfParent.mockResolvedValueOnce([fileRecords, fileRecords.length]); - authorizationService.checkPermissionByReferences.mockResolvedValueOnce(); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); return { userId, params, fileRecords }; }; diff --git a/apps/server/src/modules/files-storage/uc/files-storage-restore.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-restore.uc.spec.ts index 05961b33339..b66c9c8821d 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-restore.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-restore.uc.spec.ts @@ -3,11 +3,11 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { HttpService } from '@nestjs/axios'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus/antivirus.service'; +import { AntivirusService } from '@shared/infra/antivirus'; import { S3ClientAdapter } from '@shared/infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { AuthorizationService } from '@src/modules/authorization'; +import { AuthorizationReferenceService } from '@modules/authorization/domain'; import { FileRecordParams, SingleFileParams } from '../controller/dto'; import { FileRecord, FileRecordParentType } from '../entity'; import { FileStorageAuthorizationContext } from '../files-storage.const'; @@ -52,7 +52,7 @@ describe('FilesStorageUC', () => { let module: TestingModule; let filesStorageUC: FilesStorageUC; let filesStorageService: DeepMocked; - let authorizationService: DeepMocked; + let authorizationReferenceService: DeepMocked; beforeAll(async () => { await setupEntities([FileRecord]); @@ -77,8 +77,8 @@ describe('FilesStorageUC', () => { useValue: createMock(), }, { - provide: AuthorizationService, - useValue: createMock(), + provide: AuthorizationReferenceService, + useValue: createMock(), }, { provide: HttpService, @@ -92,7 +92,7 @@ describe('FilesStorageUC', () => { }).compile(); filesStorageUC = module.get(FilesStorageUC); - authorizationService = module.get(AuthorizationService); + authorizationReferenceService = module.get(AuthorizationReferenceService); filesStorageService = module.get(FilesStorageService); }); @@ -113,7 +113,7 @@ describe('FilesStorageUC', () => { const setup = () => { const { params, userId, fileRecords } = buildFileRecordsWithParams(); - authorizationService.checkPermissionByReferences.mockResolvedValueOnce(); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); filesStorageService.restoreFilesOfParent.mockResolvedValueOnce([fileRecords, fileRecords.length]); return { params, userId, fileRecords }; @@ -125,7 +125,7 @@ describe('FilesStorageUC', () => { await filesStorageUC.restoreFilesOfParent(userId, params); - expect(authorizationService.checkPermissionByReferences).toHaveBeenCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenCalledWith( userId, allowedType, params.parentId, @@ -153,7 +153,7 @@ describe('FilesStorageUC', () => { describe('WHEN user is not authorised ', () => { const setup = () => { const { params, userId } = buildFileRecordsWithParams(); - authorizationService.checkPermissionByReferences.mockRejectedValueOnce(new ForbiddenException()); + authorizationReferenceService.checkPermissionByReferences.mockRejectedValueOnce(new ForbiddenException()); return { params, userId }; }; @@ -189,7 +189,7 @@ describe('FilesStorageUC', () => { const { params, userId, fileRecord } = buildFileRecordWithParams(); filesStorageService.getFileRecordMarkedForDelete.mockResolvedValueOnce(fileRecord); - authorizationService.checkPermissionByReferences.mockResolvedValueOnce(); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); filesStorageService.restore.mockResolvedValueOnce(); return { params, userId, fileRecord }; @@ -209,7 +209,7 @@ describe('FilesStorageUC', () => { await filesStorageUC.restoreOneFile(userId, params); - expect(authorizationService.checkPermissionByReferences).toHaveBeenCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenCalledWith( userId, allowedType, fileRecord.parentId, @@ -239,7 +239,7 @@ describe('FilesStorageUC', () => { const { params, userId, fileRecord } = buildFileRecordWithParams(); filesStorageService.getFileRecordMarkedForDelete.mockResolvedValueOnce(fileRecord); - authorizationService.checkPermissionByReferences.mockRejectedValueOnce(new ForbiddenException()); + authorizationReferenceService.checkPermissionByReferences.mockRejectedValueOnce(new ForbiddenException()); return { params, userId }; }; @@ -276,7 +276,7 @@ describe('FilesStorageUC', () => { const error = new Error('test'); filesStorageService.getFileRecordMarkedForDelete.mockResolvedValueOnce(fileRecord); - authorizationService.checkPermissionByReferences.mockResolvedValueOnce(); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); filesStorageService.restore.mockRejectedValueOnce(error); return { params, userId, error }; diff --git a/apps/server/src/modules/files-storage/uc/files-storage-update.uc.spec.ts b/apps/server/src/modules/files-storage/uc/files-storage-update.uc.spec.ts index 75621cd82da..c59f37d2599 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage-update.uc.spec.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage-update.uc.spec.ts @@ -2,11 +2,11 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { HttpService } from '@nestjs/axios'; import { Test, TestingModule } from '@nestjs/testing'; -import { AntivirusService } from '@shared/infra/antivirus/antivirus.service'; +import { AntivirusService } from '@shared/infra/antivirus'; import { S3ClientAdapter } from '@shared/infra/s3-client'; import { fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { AuthorizationService } from '@src/modules/authorization'; +import { AuthorizationReferenceService } from '@modules/authorization/domain'; import { RenameFileParams, ScanResultParams, SingleFileParams } from '../controller/dto'; import { FileRecord } from '../entity'; import { FileStorageAuthorizationContext } from '../files-storage.const'; @@ -31,7 +31,7 @@ describe('FilesStorageUC', () => { let module: TestingModule; let filesStorageUC: FilesStorageUC; let filesStorageService: DeepMocked; - let authorizationService: DeepMocked; + let authorizationReferenceService: DeepMocked; beforeAll(async () => { await setupEntities([FileRecord]); @@ -56,8 +56,8 @@ describe('FilesStorageUC', () => { useValue: createMock(), }, { - provide: AuthorizationService, - useValue: createMock(), + provide: AuthorizationReferenceService, + useValue: createMock(), }, { provide: HttpService, @@ -71,7 +71,7 @@ describe('FilesStorageUC', () => { }).compile(); filesStorageUC = module.get(FilesStorageUC); - authorizationService = module.get(AuthorizationService); + authorizationReferenceService = module.get(AuthorizationReferenceService); filesStorageService = module.get(FilesStorageService); }); @@ -137,7 +137,7 @@ describe('FilesStorageUC', () => { const data: RenameFileParams = { fileName: 'test_new_name.txt' }; filesStorageService.getFileRecord.mockResolvedValueOnce(fileRecord); - authorizationService.checkPermissionByReferences.mockResolvedValueOnce(); + authorizationReferenceService.checkPermissionByReferences.mockResolvedValueOnce(); filesStorageService.patchFilename.mockResolvedValueOnce(fileRecord); return { userId, params, fileRecord, data }; @@ -155,7 +155,7 @@ describe('FilesStorageUC', () => { await filesStorageUC.patchFilename(userId, params, data); - expect(authorizationService.checkPermissionByReferences).toHaveBeenCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenCalledWith( userId, fileRecord.parentType, fileRecord.parentId, 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 78348078565..43d9e9b7750 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 @@ -4,11 +4,12 @@ import { HttpService } from '@nestjs/axios'; import { ForbiddenException, NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain'; -import { AntivirusService } from '@shared/infra/antivirus/antivirus.service'; +import { AntivirusService } from '@shared/infra/antivirus'; import { S3ClientAdapter } from '@shared/infra/s3-client'; import { AxiosHeadersKeyValue, axiosResponseFactory, fileRecordFactory, setupEntities } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { Action, AuthorizationService } from '@src/modules/authorization'; +import { Action } from '@modules/authorization'; +import { AuthorizationReferenceService } from '@modules/authorization/domain'; import { AxiosRequestConfig, AxiosResponse } from 'axios'; import { Request } from 'express'; import { of } from 'rxjs'; @@ -72,7 +73,7 @@ describe('FilesStorageUC upload methods', () => { let module: TestingModule; let filesStorageUC: FilesStorageUC; let filesStorageService: DeepMocked; - let authorizationService: DeepMocked; + let authorizationReferenceService: DeepMocked; let httpService: DeepMocked; beforeAll(async () => { @@ -98,8 +99,8 @@ describe('FilesStorageUC upload methods', () => { useValue: createMock(), }, { - provide: AuthorizationService, - useValue: createMock(), + provide: AuthorizationReferenceService, + useValue: createMock(), }, { provide: HttpService, @@ -113,7 +114,7 @@ describe('FilesStorageUC upload methods', () => { }).compile(); filesStorageUC = module.get(FilesStorageUC); - authorizationService = module.get(AuthorizationService); + authorizationReferenceService = module.get(AuthorizationReferenceService); httpService = module.get(HttpService); filesStorageService = module.get(FilesStorageService); }); @@ -171,7 +172,7 @@ describe('FilesStorageUC upload methods', () => { await filesStorageUC.uploadFromUrl(userId, uploadFromUrlParams); - expect(authorizationService.checkPermissionByReferences).toBeCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toBeCalledWith( userId, uploadFromUrlParams.parentType, uploadFromUrlParams.parentId, @@ -218,7 +219,7 @@ describe('FilesStorageUC upload methods', () => { const setup = () => { const { userId, uploadFromUrlParams } = createUploadFromUrlParams(); const error = new Error('test'); - authorizationService.checkPermissionByReferences.mockRejectedValueOnce(error); + authorizationReferenceService.checkPermissionByReferences.mockRejectedValueOnce(error); return { uploadFromUrlParams, userId, error }; }; @@ -300,7 +301,7 @@ describe('FilesStorageUC upload methods', () => { await filesStorageUC.upload(userId, params, request); const allowedType = FilesStorageMapper.mapToAllowedAuthorizationEntityType(params.parentType); - expect(authorizationService.checkPermissionByReferences).toHaveBeenCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenCalledWith( userId, allowedType, params.parentId, @@ -365,7 +366,7 @@ describe('FilesStorageUC upload methods', () => { const request = createRequest(); const error = new ForbiddenException(); - authorizationService.checkPermissionByReferences.mockRejectedValueOnce(error); + authorizationReferenceService.checkPermissionByReferences.mockRejectedValueOnce(error); return { params, userId, request, error }; }; diff --git a/apps/server/src/modules/files-storage/uc/files-storage.uc.ts b/apps/server/src/modules/files-storage/uc/files-storage.uc.ts index fa6a27202de..833d7575bdf 100644 --- a/apps/server/src/modules/files-storage/uc/files-storage.uc.ts +++ b/apps/server/src/modules/files-storage/uc/files-storage.uc.ts @@ -2,7 +2,8 @@ import { HttpService } from '@nestjs/axios'; import { Injectable, NotFoundException } from '@nestjs/common'; import { Counted, EntityId } from '@shared/domain'; import { LegacyLogger } from '@src/core/logger'; -import { AuthorizationContext, AuthorizationService } from '@src/modules/authorization'; +import { AuthorizationContext } from '@modules/authorization'; +import { AuthorizationReferenceService } from '@modules/authorization/domain'; import { AxiosRequestConfig, AxiosResponse } from 'axios'; import busboy from 'busboy'; import { Request } from 'express'; @@ -32,7 +33,7 @@ import { PreviewService } from '../service/preview.service'; export class FilesStorageUC { constructor( private logger: LegacyLogger, - private readonly authorizationService: AuthorizationService, + private readonly authorizationReferenceService: AuthorizationReferenceService, private readonly httpService: HttpService, private readonly filesStorageService: FilesStorageService, private readonly previewService: PreviewService @@ -47,7 +48,7 @@ export class FilesStorageUC { context: AuthorizationContext ) { const allowedType = FilesStorageMapper.mapToAllowedAuthorizationEntityType(parentType); - await this.authorizationService.checkPermissionByReferences(userId, allowedType, parentId, context); + await this.authorizationReferenceService.checkPermissionByReferences(userId, allowedType, parentId, context); } // upload diff --git a/apps/server/src/modules/files/entity/file.entity.spec.ts b/apps/server/src/modules/files/entity/file.entity.spec.ts index 1f6150b12c5..ea9649f4c66 100644 --- a/apps/server/src/modules/files/entity/file.entity.spec.ts +++ b/apps/server/src/modules/files/entity/file.entity.spec.ts @@ -1,6 +1,6 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { setupEntities, storageProviderFactory } from '@shared/testing'; -import { FileOwnerModel } from '@src/modules/files/domain'; +import { FileOwnerModel } from '@modules/files/domain'; import { fileEntityFactory, filePermissionEntityFactory } from './testing'; import { FileEntity } from './file.entity'; import { FileSecurityCheckEntity } from './file-security-check.entity'; 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 9f64e07129a..9eee09a30af 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 @@ -3,7 +3,7 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { INestApplication, NotFoundException } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { S3ClientAdapter } from '@shared/infra/s3-client'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { Readable } from 'stream'; import request from 'supertest'; import { FwuLearningContentsTestModule } from '../../fwu-learning-contents-test.module'; diff --git a/apps/server/src/modules/fwu-learning-contents/controller/fwu-learning-contents.controller.ts b/apps/server/src/modules/fwu-learning-contents/controller/fwu-learning-contents.controller.ts index 169d9d136b6..f9e7bd6a238 100644 --- a/apps/server/src/modules/fwu-learning-contents/controller/fwu-learning-contents.controller.ts +++ b/apps/server/src/modules/fwu-learning-contents/controller/fwu-learning-contents.controller.ts @@ -10,7 +10,7 @@ import { StreamableFile, } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { Authenticate } from '@src/modules/authentication/decorator/auth.decorator'; +import { Authenticate } from '@modules/authentication'; import { Request, Response } from 'express'; import { FwuLearningContentsUc } from '../uc/fwu-learning-contents.uc'; import { GetFwuLearningContentParams } from './dto/fwu-learning-contents.params'; 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 5b6efa3bbb1..62e25bef4e2 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 @@ -9,8 +9,8 @@ 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 { AuthenticationModule } from '@modules/authentication/authentication.module'; +import { AuthorizationModule } from '@modules/authorization'; import { FwuLearningContentsController } from './controller/fwu-learning-contents.controller'; import { config, s3Config } from './fwu-learning-contents.config'; import { FwuLearningContentsUc } from './uc/fwu-learning-contents.uc'; 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 2f4cd148d1b..b15c8a04054 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 @@ -9,7 +9,7 @@ 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 { AuthorizationModule } from '@src/modules/authorization'; +import { AuthorizationModule } from '@modules/authorization'; import { AuthenticationModule } from '../authentication/authentication.module'; import { FwuLearningContentsController } from './controller/fwu-learning-contents.controller'; import { config, s3Config } from './fwu-learning-contents.config'; diff --git a/apps/server/src/modules/group/controller/api-test/group.api.spec.ts b/apps/server/src/modules/group/controller/api-test/group.api.spec.ts index f0561518c0c..34a49c03a35 100644 --- a/apps/server/src/modules/group/controller/api-test/group.api.spec.ts +++ b/apps/server/src/modules/group/controller/api-test/group.api.spec.ts @@ -1,20 +1,23 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { Role, RoleName, SchoolEntity, SortOrder, SystemEntity, User } from '@shared/domain'; +import { Role, RoleName, SchoolEntity, SchoolYearEntity, SortOrder, SystemEntity, User } from '@shared/domain'; import { groupEntityFactory, roleFactory, schoolFactory, + schoolYearFactory, systemFactory, TestApiClient, UserAndAccountTestFactory, userFactory, } from '@shared/testing'; -import { ClassEntity } from '@src/modules/class/entity'; -import { classEntityFactory } from '@src/modules/class/entity/testing/factory/class.entity.factory'; -import { ServerTestModule } from '@src/modules/server'; +import { ClassEntity } from '@modules/class/entity'; +import { classEntityFactory } from '@modules/class/entity/testing/factory/class.entity.factory'; +import { ServerTestModule } from '@modules/server'; +import { ObjectId } from 'bson'; import { GroupEntity, GroupEntityTypes } from '../../entity'; +import { ClassRootType } from '../../uc/dto/class-root-type'; import { ClassInfoSearchListResponse, ClassSortBy } from '../dto'; const baseRouteName = '/groups'; @@ -39,7 +42,7 @@ describe('Group (API)', () => { await app.close(); }); - describe('findClassesForSchool', () => { + describe('[GET] /groups/class', () => { describe('when an admin requests a list of classes', () => { const setup = async () => { const school: SchoolEntity = schoolFactory.buildWithId(); @@ -48,11 +51,13 @@ describe('Group (API)', () => { const teacherRole: Role = roleFactory.buildWithId({ name: RoleName.TEACHER }); const teacherUser: User = userFactory.buildWithId({ school, roles: [teacherRole] }); const system: SystemEntity = systemFactory.buildWithId(); + const schoolYear: SchoolYearEntity = schoolYearFactory.buildWithId(); const clazz: ClassEntity = classEntityFactory.buildWithId({ name: 'Group A', schoolId: school._id, teacherIds: [teacherUser._id], source: undefined, + year: schoolYear.id, }); const group: GroupEntity = groupEntityFactory.buildWithId({ name: 'Group B', @@ -70,7 +75,17 @@ describe('Group (API)', () => { ], }); - await em.persistAndFlush([school, adminAccount, adminUser, teacherRole, teacherUser, system, clazz, group]); + await em.persistAndFlush([ + school, + adminAccount, + adminUser, + teacherRole, + teacherUser, + system, + clazz, + group, + schoolYear, + ]); em.clear(); const adminClient = await testApiClient.login(adminAccount); @@ -82,11 +97,12 @@ describe('Group (API)', () => { system, adminUser, teacherUser, + schoolYear, }; }; it('should return the classes of his school', async () => { - const { adminClient, group, clazz, system, adminUser, teacherUser } = await setup(); + const { adminClient, group, clazz, system, adminUser, teacherUser, schoolYear } = await setup(); const response = await adminClient.get(`/class`).query({ skip: 0, @@ -99,13 +115,19 @@ describe('Group (API)', () => { total: 2, data: [ { + id: group.id, + type: ClassRootType.GROUP, name: group.name, externalSourceName: system.displayName, teachers: [adminUser.lastName], }, { + id: clazz.id, + type: ClassRootType.CLASS, name: clazz.gradeLevel ? `${clazz.gradeLevel}${clazz.name}` : clazz.name, teachers: [teacherUser.lastName], + schoolYear: schoolYear.name, + isUpgradable: false, }, ], skip: 0, @@ -137,4 +159,119 @@ describe('Group (API)', () => { }); }); }); + + describe('[GET] /groups/:groupId', () => { + describe('when authorized user requests a group', () => { + describe('when group exists', () => { + const setup = async () => { + const school: SchoolEntity = schoolFactory.buildWithId(); + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }); + + const group: GroupEntity = groupEntityFactory.buildWithId({ + users: [ + { + user: teacherUser, + role: teacherUser.roles[0], + }, + ], + organization: school, + }); + + await em.persistAndFlush([teacherAccount, teacherUser, group]); + em.clear(); + + const loggedInClient = await testApiClient.login(teacherAccount); + + return { + loggedInClient, + group, + teacherUser, + }; + }; + + it('should return the group', async () => { + const { loggedInClient, group, teacherUser } = await setup(); + + const response = await loggedInClient.get(`${group.id}`); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body).toEqual({ + id: group.id, + name: group.name, + type: group.type, + users: [ + { + id: teacherUser.id, + firstName: teacherUser.firstName, + lastName: teacherUser.lastName, + role: teacherUser.roles[0].name, + }, + ], + externalSource: { + externalId: group.externalSource?.externalId, + systemId: group.externalSource?.system.id, + }, + }); + }); + }); + + describe('when group does not exist', () => { + const setup = async () => { + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); + + await em.persistAndFlush([teacherAccount, teacherUser]); + em.clear(); + + const loggedInClient = await testApiClient.login(teacherAccount); + + return { + loggedInClient, + }; + }; + + it('should return not found', async () => { + const { loggedInClient } = await setup(); + + const response = await loggedInClient.get(`${new ObjectId().toHexString()}`); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body).toEqual({ + code: HttpStatus.NOT_FOUND, + message: 'Not Found', + title: 'Not Found', + type: 'NOT_FOUND', + }); + }); + }); + }); + + describe('when unauthorized user requests a group', () => { + const setup = async () => { + const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); + + const group: GroupEntity = groupEntityFactory.buildWithId(); + + await em.persistAndFlush([studentAccount, studentUser, group]); + em.clear(); + + return { + groupId: group.id, + }; + }; + + it('should return unauthorized', async () => { + const { groupId } = await setup(); + + const response = await testApiClient.get(`${groupId}`); + + expect(response.status).toEqual(HttpStatus.UNAUTHORIZED); + expect(response.body).toEqual({ + code: HttpStatus.UNAUTHORIZED, + message: 'Unauthorized', + title: 'Unauthorized', + type: 'UNAUTHORIZED', + }); + }); + }); + }); }); diff --git a/apps/server/src/modules/group/controller/dto/request/group-id-params.ts b/apps/server/src/modules/group/controller/dto/request/group-id-params.ts new file mode 100644 index 00000000000..9423966009f --- /dev/null +++ b/apps/server/src/modules/group/controller/dto/request/group-id-params.ts @@ -0,0 +1,8 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsMongoId } from 'class-validator'; + +export class GroupIdParams { + @IsMongoId() + @ApiProperty({ nullable: false, required: true }) + groupId!: string; +} diff --git a/apps/server/src/modules/group/controller/dto/request/index.ts b/apps/server/src/modules/group/controller/dto/request/index.ts index 2255e9aac09..17ecd658b7d 100644 --- a/apps/server/src/modules/group/controller/dto/request/index.ts +++ b/apps/server/src/modules/group/controller/dto/request/index.ts @@ -1 +1,2 @@ export * from './class-sort-params'; +export * from './group-id-params'; diff --git a/apps/server/src/modules/group/controller/dto/response/class-info.response.ts b/apps/server/src/modules/group/controller/dto/response/class-info.response.ts index a2d71333c04..a62b8134158 100644 --- a/apps/server/src/modules/group/controller/dto/response/class-info.response.ts +++ b/apps/server/src/modules/group/controller/dto/response/class-info.response.ts @@ -1,6 +1,13 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ClassRootType } from '../../../uc/dto/class-root-type'; export class ClassInfoResponse { + @ApiProperty() + id: string; + + @ApiProperty({ enum: ClassRootType }) + type: ClassRootType; + @ApiProperty() name: string; @@ -10,9 +17,19 @@ export class ClassInfoResponse { @ApiProperty({ type: [String] }) teachers: string[]; + @ApiPropertyOptional() + schoolYear?: string; + + @ApiPropertyOptional() + isUpgradable?: boolean; + constructor(props: ClassInfoResponse) { + this.id = props.id; + this.type = props.type; this.name = props.name; this.externalSourceName = props.externalSourceName; this.teachers = props.teachers; + this.schoolYear = props.schoolYear; + this.isUpgradable = props.isUpgradable; } } diff --git a/apps/server/src/modules/group/controller/dto/response/external-source.response.ts b/apps/server/src/modules/group/controller/dto/response/external-source.response.ts new file mode 100644 index 00000000000..f03327c8a8c --- /dev/null +++ b/apps/server/src/modules/group/controller/dto/response/external-source.response.ts @@ -0,0 +1,14 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class ExternalSourceResponse { + @ApiProperty() + externalId: string; + + @ApiProperty() + systemId: string; + + constructor(props: ExternalSourceResponse) { + this.externalId = props.externalId; + this.systemId = props.systemId; + } +} diff --git a/apps/server/src/modules/group/controller/dto/response/group-type.response.ts b/apps/server/src/modules/group/controller/dto/response/group-type.response.ts new file mode 100644 index 00000000000..54c32148ca1 --- /dev/null +++ b/apps/server/src/modules/group/controller/dto/response/group-type.response.ts @@ -0,0 +1,3 @@ +export enum GroupTypeResponse { + CLASS = 'class', +} diff --git a/apps/server/src/modules/group/controller/dto/response/group-user.response.ts b/apps/server/src/modules/group/controller/dto/response/group-user.response.ts new file mode 100644 index 00000000000..000958c96cf --- /dev/null +++ b/apps/server/src/modules/group/controller/dto/response/group-user.response.ts @@ -0,0 +1,23 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { RoleName } from '@shared/domain'; + +export class GroupUserResponse { + @ApiProperty() + id: string; + + @ApiProperty() + firstName: string; + + @ApiProperty() + lastName: string; + + @ApiProperty({ enum: RoleName }) + role: RoleName; + + constructor(user: GroupUserResponse) { + this.id = user.id; + this.firstName = user.firstName; + this.lastName = user.lastName; + this.role = user.role; + } +} diff --git a/apps/server/src/modules/group/controller/dto/response/group.response.ts b/apps/server/src/modules/group/controller/dto/response/group.response.ts new file mode 100644 index 00000000000..1abb28a8a30 --- /dev/null +++ b/apps/server/src/modules/group/controller/dto/response/group.response.ts @@ -0,0 +1,33 @@ +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ExternalSourceResponse } from './external-source.response'; +import { GroupTypeResponse } from './group-type.response'; +import { GroupUserResponse } from './group-user.response'; + +export class GroupResponse { + @ApiProperty() + id: string; + + @ApiProperty() + name: string; + + @ApiProperty({ enum: GroupTypeResponse }) + type: GroupTypeResponse; + + @ApiProperty({ type: [GroupUserResponse] }) + users: GroupUserResponse[]; + + @ApiPropertyOptional() + externalSource?: ExternalSourceResponse; + + @ApiPropertyOptional() + organizationId?: string; + + constructor(group: GroupResponse) { + this.id = group.id; + this.name = group.name; + this.type = group.type; + this.users = group.users; + this.externalSource = group.externalSource; + this.organizationId = group.organizationId; + } +} diff --git a/apps/server/src/modules/group/controller/dto/response/index.ts b/apps/server/src/modules/group/controller/dto/response/index.ts index 1ec8a62f0d4..9593930f21e 100644 --- a/apps/server/src/modules/group/controller/dto/response/index.ts +++ b/apps/server/src/modules/group/controller/dto/response/index.ts @@ -1,2 +1,6 @@ export * from './class-info.response'; export * from './class-info-search-list.response'; +export * from './external-source.response'; +export * from './group.response'; +export * from './group-type.response'; +export * from './group-user.response'; diff --git a/apps/server/src/modules/group/controller/group.controller.ts b/apps/server/src/modules/group/controller/group.controller.ts index e810e200d85..9e5f4b3b51a 100644 --- a/apps/server/src/modules/group/controller/group.controller.ts +++ b/apps/server/src/modules/group/controller/group.controller.ts @@ -1,13 +1,12 @@ -import { Controller, Get, HttpStatus, Query } from '@nestjs/common'; +import { Controller, Get, HttpStatus, Param, Query } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { PaginationParams } from '@shared/controller'; import { Page } from '@shared/domain'; import { ErrorResponse } from '@src/core/error/dto'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; +import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; import { GroupUc } from '../uc'; -import { ClassInfoDto } from '../uc/dto'; -import { ClassInfoSearchListResponse, ClassSortParams } from './dto'; +import { ClassInfoDto, ResolvedGroupDto } from '../uc/dto'; +import { ClassInfoSearchListResponse, ClassSortParams, GroupIdParams, GroupResponse } from './dto'; import { GroupResponseMapper } from './mapper'; @ApiTags('Group') @@ -43,4 +42,20 @@ export class GroupController { return response; } + + @Get('/:groupId') + @ApiOperation({ summary: 'Get a group by id.' }) + @ApiResponse({ status: HttpStatus.OK, type: GroupResponse }) + @ApiResponse({ status: '4XX', type: ErrorResponse }) + @ApiResponse({ status: '5XX', type: ErrorResponse }) + public async getGroup( + @CurrentUser() currentUser: ICurrentUser, + @Param() params: GroupIdParams + ): Promise { + const group: ResolvedGroupDto = await this.groupUc.getGroup(currentUser.userId, params.groupId); + + const response: GroupResponse = GroupResponseMapper.mapToGroupResponse(group); + + return response; + } } diff --git a/apps/server/src/modules/group/controller/mapper/group-response.mapper.ts b/apps/server/src/modules/group/controller/mapper/group-response.mapper.ts index 6fbb0c6dc65..6efd02d899d 100644 --- a/apps/server/src/modules/group/controller/mapper/group-response.mapper.ts +++ b/apps/server/src/modules/group/controller/mapper/group-response.mapper.ts @@ -1,6 +1,18 @@ import { Page } from '@shared/domain'; -import { ClassInfoDto } from '../../uc/dto'; -import { ClassInfoResponse, ClassInfoSearchListResponse } from '../dto'; +import { GroupTypes } from '../../domain'; +import { ClassInfoDto, ResolvedGroupDto } from '../../uc/dto'; +import { + ClassInfoResponse, + ClassInfoSearchListResponse, + ExternalSourceResponse, + GroupResponse, + GroupTypeResponse, + GroupUserResponse, +} from '../dto'; + +const typeMapping: Record = { + [GroupTypes.CLASS]: GroupTypeResponse.CLASS, +}; export class GroupResponseMapper { static mapToClassInfosToListResponse( @@ -24,9 +36,39 @@ export class GroupResponseMapper { private static mapToClassInfoToResponse(classInfo: ClassInfoDto): ClassInfoResponse { const mapped = new ClassInfoResponse({ + id: classInfo.id, + type: classInfo.type, name: classInfo.name, externalSourceName: classInfo.externalSourceName, teachers: classInfo.teachers, + schoolYear: classInfo.schoolYear, + isUpgradable: classInfo.isUpgradable, + }); + + return mapped; + } + + static mapToGroupResponse(resolvedGroup: ResolvedGroupDto): GroupResponse { + const mapped: GroupResponse = new GroupResponse({ + id: resolvedGroup.id, + name: resolvedGroup.name, + type: typeMapping[resolvedGroup.type], + externalSource: resolvedGroup.externalSource + ? new ExternalSourceResponse({ + externalId: resolvedGroup.externalSource.externalId, + systemId: resolvedGroup.externalSource.systemId, + }) + : undefined, + users: resolvedGroup.users.map( + (user) => + new GroupUserResponse({ + id: user.user.id as string, + role: user.role.name, + firstName: user.user.firstName, + lastName: user.user.lastName, + }) + ), + organizationId: resolvedGroup.organizationId, }); return mapped; diff --git a/apps/server/src/modules/group/domain/group.spec.ts b/apps/server/src/modules/group/domain/group.spec.ts index b5ae8a03321..f2a22e1dc37 100644 --- a/apps/server/src/modules/group/domain/group.spec.ts +++ b/apps/server/src/modules/group/domain/group.spec.ts @@ -1,7 +1,7 @@ +import { RoleReference, UserDO } from '@shared/domain'; import { groupFactory, roleFactory, userDoFactory } from '@shared/testing'; import { ObjectId } from 'bson'; -import { RoleReference, UserDO } from '@shared/domain'; import { Group } from './group'; import { GroupUser } from './group-user'; @@ -135,4 +135,62 @@ describe('Group (Domain Object)', () => { }); }); }); + + describe('addUser', () => { + describe('when the user already exists in the group', () => { + const setup = () => { + const existingUser: GroupUser = new GroupUser({ + userId: new ObjectId().toHexString(), + roleId: new ObjectId().toHexString(), + }); + const group = groupFactory.build({ + users: [existingUser], + }); + + return { + group, + existingUser, + }; + }; + + it('should not add the user', () => { + const { group, existingUser } = setup(); + + group.addUser(existingUser); + + expect(group.users.length).toEqual(1); + }); + }); + + describe('when the user does not exist in the group', () => { + const setup = () => { + const newUser: GroupUser = new GroupUser({ + userId: new ObjectId().toHexString(), + roleId: new ObjectId().toHexString(), + }); + const group = groupFactory.build({ + users: [ + { + userId: new ObjectId().toHexString(), + roleId: new ObjectId().toHexString(), + }, + ], + }); + + return { + group, + newUser, + }; + }; + + it('should add the user', () => { + const { group, newUser } = setup(); + + group.addUser(newUser); + + expect(group.users).toContain(newUser); + expect(group.users.length).toEqual(2); + }); + }); + }); }); diff --git a/apps/server/src/modules/group/domain/group.ts b/apps/server/src/modules/group/domain/group.ts index 049043618ac..3d1f19bc312 100644 --- a/apps/server/src/modules/group/domain/group.ts +++ b/apps/server/src/modules/group/domain/group.ts @@ -38,6 +38,10 @@ export class Group extends DomainObject { return this.props.organizationId; } + get type(): GroupTypes { + return this.props.type; + } + removeUser(user: UserDO): void { this.props.users = this.props.users.filter((groupUser: GroupUser): boolean => groupUser.userId !== user.id); } @@ -45,4 +49,10 @@ export class Group extends DomainObject { isEmpty(): boolean { return this.props.users.length === 0; } + + addUser(user: GroupUser): void { + if (!this.users.find((u) => u.userId === user.userId)) { + this.users.push(user); + } + } } diff --git a/apps/server/src/modules/group/group-api.module.ts b/apps/server/src/modules/group/group-api.module.ts index 913fb2ef903..14a564b4741 100644 --- a/apps/server/src/modules/group/group-api.module.ts +++ b/apps/server/src/modules/group/group-api.module.ts @@ -1,10 +1,10 @@ import { Module } from '@nestjs/common'; -import { AuthorizationModule } from '@src/modules/authorization'; -import { ClassModule } from '@src/modules/class'; -import { RoleModule } from '@src/modules/role'; -import { LegacySchoolModule } from '@src/modules/legacy-school'; -import { SystemModule } from '@src/modules/system'; -import { UserModule } from '@src/modules/user'; +import { AuthorizationModule } from '@modules/authorization'; +import { ClassModule } from '@modules/class'; +import { RoleModule } from '@modules/role'; +import { LegacySchoolModule } from '@modules/legacy-school'; +import { SystemModule } from '@modules/system'; +import { UserModule } from '@modules/user'; import { GroupController } from './controller'; import { GroupModule } from './group.module'; import { GroupUc } from './uc'; diff --git a/apps/server/src/modules/group/service/group.service.ts b/apps/server/src/modules/group/service/group.service.ts index f3ce6a287e8..0ccded2442b 100644 --- a/apps/server/src/modules/group/service/group.service.ts +++ b/apps/server/src/modules/group/service/group.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { EntityId, type UserDO } from '@shared/domain'; -import { AuthorizationLoaderServiceGeneric } from '@src/modules/authorization'; +import { AuthorizationLoaderServiceGeneric } from '@modules/authorization'; import { Group } from '../domain'; import { GroupRepo } from '../repo'; diff --git a/apps/server/src/modules/group/uc/dto/class-info.dto.ts b/apps/server/src/modules/group/uc/dto/class-info.dto.ts index 0d2b5adaf68..8c564d9e106 100644 --- a/apps/server/src/modules/group/uc/dto/class-info.dto.ts +++ b/apps/server/src/modules/group/uc/dto/class-info.dto.ts @@ -1,13 +1,27 @@ +import { ClassRootType } from './class-root-type'; + export class ClassInfoDto { + id: string; + + type: ClassRootType; + name: string; externalSourceName?: string; teachers: string[]; + schoolYear?: string; + + isUpgradable?: boolean; + constructor(props: ClassInfoDto) { + this.id = props.id; + this.type = props.type; this.name = props.name; this.externalSourceName = props.externalSourceName; this.teachers = props.teachers; + this.schoolYear = props.schoolYear; + this.isUpgradable = props.isUpgradable; } } diff --git a/apps/server/src/modules/group/uc/dto/class-root-type.ts b/apps/server/src/modules/group/uc/dto/class-root-type.ts new file mode 100644 index 00000000000..b1a725a7ddc --- /dev/null +++ b/apps/server/src/modules/group/uc/dto/class-root-type.ts @@ -0,0 +1,4 @@ +export enum ClassRootType { + CLASS = 'class', + GROUP = 'group', +} diff --git a/apps/server/src/modules/group/uc/dto/index.ts b/apps/server/src/modules/group/uc/dto/index.ts index 389a31da162..d795f1c30d3 100644 --- a/apps/server/src/modules/group/uc/dto/index.ts +++ b/apps/server/src/modules/group/uc/dto/index.ts @@ -1,2 +1,3 @@ export * from './class-info.dto'; export * from './resolved-group-user'; +export * from './resolved-group.dto'; diff --git a/apps/server/src/modules/group/uc/dto/resolved-group-user.ts b/apps/server/src/modules/group/uc/dto/resolved-group-user.ts index 862abdba594..80db4973ef2 100644 --- a/apps/server/src/modules/group/uc/dto/resolved-group-user.ts +++ b/apps/server/src/modules/group/uc/dto/resolved-group-user.ts @@ -1,5 +1,5 @@ import { UserDO } from '@shared/domain'; -import { RoleDto } from '@src/modules/role/service/dto/role.dto'; +import { RoleDto } from '@modules/role/service/dto/role.dto'; export class ResolvedGroupUser { user: UserDO; diff --git a/apps/server/src/modules/group/uc/dto/resolved-group.dto.ts b/apps/server/src/modules/group/uc/dto/resolved-group.dto.ts new file mode 100644 index 00000000000..4d288f936a0 --- /dev/null +++ b/apps/server/src/modules/group/uc/dto/resolved-group.dto.ts @@ -0,0 +1,26 @@ +import { ExternalSource } from '@shared/domain'; +import { GroupTypes } from '../../domain'; +import { ResolvedGroupUser } from './resolved-group-user'; + +export class ResolvedGroupDto { + id: string; + + name: string; + + type: GroupTypes; + + users: ResolvedGroupUser[]; + + externalSource?: ExternalSource; + + organizationId?: string; + + constructor(group: ResolvedGroupDto) { + this.id = group.id; + this.name = group.name; + this.type = group.type; + this.users = group.users; + this.externalSource = group.externalSource; + this.organizationId = group.organizationId; + } +} diff --git a/apps/server/src/modules/group/uc/group.uc.spec.ts b/apps/server/src/modules/group/uc/group.uc.spec.ts index b4115d3739b..34cb55a1354 100644 --- a/apps/server/src/modules/group/uc/group.uc.spec.ts +++ b/apps/server/src/modules/group/uc/group.uc.spec.ts @@ -2,28 +2,31 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { ForbiddenException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { LegacySchoolDo, Page, Permission, SortOrder, User, UserDO } from '@shared/domain'; +import { NotFoundLoggableException } from '@shared/common/loggable-exception'; +import { LegacySchoolDo, Page, Permission, SchoolYearEntity, SortOrder, User, UserDO } from '@shared/domain'; import { groupFactory, legacySchoolDoFactory, roleDtoFactory, + schoolYearFactory, setupEntities, UserAndAccountTestFactory, userDoFactory, userFactory, } from '@shared/testing'; -import { Action, AuthorizationContext, AuthorizationService } from '@src/modules/authorization'; -import { ClassService } from '@src/modules/class'; -import { Class } from '@src/modules/class/domain'; -import { classFactory } from '@src/modules/class/domain/testing/factory/class.factory'; -import { LegacySchoolService } from '@src/modules/legacy-school'; -import { RoleService } from '@src/modules/role'; -import { RoleDto } from '@src/modules/role/service/dto/role.dto'; -import { SystemDto, SystemService } from '@src/modules/system'; -import { UserService } from '@src/modules/user'; -import { Group } from '../domain'; +import { Action, AuthorizationContext, AuthorizationService } from '@modules/authorization'; +import { ClassService } from '@modules/class'; +import { Class } from '@modules/class/domain'; +import { classFactory } from '@modules/class/domain/testing/factory/class.factory'; +import { LegacySchoolService, SchoolYearService } from '@modules/legacy-school'; +import { RoleService } from '@modules/role'; +import { RoleDto } from '@modules/role/service/dto/role.dto'; +import { SystemDto, SystemService } from '@modules/system'; +import { UserService } from '@modules/user'; +import { Group, GroupTypes } from '../domain'; import { GroupService } from '../service'; -import { ClassInfoDto } from './dto'; +import { ClassInfoDto, ResolvedGroupDto } from './dto'; +import { ClassRootType } from './dto/class-root-type'; import { GroupUc } from './group.uc'; describe('GroupUc', () => { @@ -37,6 +40,7 @@ describe('GroupUc', () => { let roleService: DeepMocked; let schoolService: DeepMocked; let authorizationService: DeepMocked; + let schoolYearService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -70,6 +74,10 @@ describe('GroupUc', () => { provide: AuthorizationService, useValue: createMock(), }, + { + provide: SchoolYearService, + useValue: createMock(), + }, ], }).compile(); @@ -81,6 +89,7 @@ describe('GroupUc', () => { roleService = module.get(RoleService); schoolService = module.get(LegacySchoolService); authorizationService = module.get(AuthorizationService); + schoolYearService = module.get(SchoolYearService); await setupEntities(); }); @@ -144,7 +153,13 @@ describe('GroupUc', () => { lastName: studentUser.lastName, roles: [{ id: studentUser.roles[0].id, name: studentUser.roles[0].name }], }); - const clazz: Class = classFactory.build({ name: 'A', teacherIds: [teacherUser.id], source: 'LDAP' }); + const schoolYear: SchoolYearEntity = schoolYearFactory.buildWithId(); + const clazz: Class = classFactory.build({ + name: 'A', + teacherIds: [teacherUser.id], + source: 'LDAP', + year: schoolYear.id, + }); const system: SystemDto = new SystemDto({ id: new ObjectId().toHexString(), displayName: 'External System', @@ -191,6 +206,7 @@ describe('GroupUc', () => { throw new Error(); }); + schoolYearService.findById.mockResolvedValue(schoolYear); return { teacherUser, @@ -199,10 +215,11 @@ describe('GroupUc', () => { group, groupWithSystem, system, + schoolYear, }; }; - it('should check the CLASS_LIST permission', async () => { + it('should check the required permissions', async () => { const { teacherUser, school } = setup(); await uc.findAllClassesForSchool(teacherUser.id, teacherUser.school.id); @@ -212,30 +229,38 @@ describe('GroupUc', () => { school, { action: Action.read, - requiredPermissions: [Permission.CLASS_LIST], + requiredPermissions: [Permission.CLASS_LIST, Permission.GROUP_LIST], } ); }); describe('when no pagination is given', () => { it('should return all classes sorted by name', async () => { - const { teacherUser, clazz, group, groupWithSystem, system } = setup(); + const { teacherUser, clazz, group, groupWithSystem, system, schoolYear } = setup(); const result: Page = await uc.findAllClassesForSchool(teacherUser.id, teacherUser.school.id); expect(result).toEqual>({ data: [ { + id: clazz.id, name: clazz.gradeLevel ? `${clazz.gradeLevel}${clazz.name}` : clazz.name, + type: ClassRootType.CLASS, externalSourceName: clazz.source, teachers: [teacherUser.lastName], + schoolYear: schoolYear.name, + isUpgradable: false, }, { + id: group.id, name: group.name, + type: ClassRootType.GROUP, teachers: [teacherUser.lastName], }, { + id: groupWithSystem.id, name: groupWithSystem.name, + type: ClassRootType.GROUP, externalSourceName: system.displayName, teachers: [teacherUser.lastName], }, @@ -247,7 +272,7 @@ describe('GroupUc', () => { describe('when sorting by external source name in descending order', () => { it('should return all classes sorted by external source name in descending order', async () => { - const { teacherUser, clazz, group, groupWithSystem, system } = setup(); + const { teacherUser, clazz, group, groupWithSystem, system, schoolYear } = setup(); const result: Page = await uc.findAllClassesForSchool( teacherUser.id, @@ -261,17 +286,25 @@ describe('GroupUc', () => { expect(result).toEqual>({ data: [ { + id: clazz.id, name: clazz.gradeLevel ? `${clazz.gradeLevel}${clazz.name}` : clazz.name, + type: ClassRootType.CLASS, externalSourceName: clazz.source, teachers: [teacherUser.lastName], + schoolYear: schoolYear.name, + isUpgradable: false, }, { + id: groupWithSystem.id, name: groupWithSystem.name, + type: ClassRootType.GROUP, externalSourceName: system.displayName, teachers: [teacherUser.lastName], }, { + id: group.id, name: group.name, + type: ClassRootType.GROUP, teachers: [teacherUser.lastName], }, ], @@ -296,7 +329,9 @@ describe('GroupUc', () => { expect(result).toEqual>({ data: [ { + id: group.id, name: group.name, + type: ClassRootType.GROUP, teachers: [teacherUser.lastName], }, ], @@ -306,4 +341,144 @@ describe('GroupUc', () => { }); }); }); + + describe('getGroup', () => { + describe('when the user has no permission', () => { + const setup = () => { + const user: User = userFactory.buildWithId(); + const error = new ForbiddenException(); + + authorizationService.getUserWithPermissions.mockResolvedValue(user); + authorizationService.checkPermission.mockImplementation(() => { + throw error; + }); + + return { + user, + error, + }; + }; + + it('should throw forbidden', async () => { + const { user, error } = setup(); + + const func = () => uc.getGroup(user.id, 'groupId'); + + await expect(func).rejects.toThrow(error); + }); + }); + + describe('when the group is not found', () => { + const setup = () => { + groupService.findById.mockRejectedValue(new NotFoundLoggableException(Group.name, 'id', 'groupId')); + const { teacherUser } = UserAndAccountTestFactory.buildTeacher(); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(teacherUser); + + return { + teacherId: teacherUser.id, + }; + }; + + it('should throw not found', async () => { + const { teacherId } = setup(); + + const func = () => uc.getGroup(teacherId, 'groupId'); + + await expect(func).rejects.toThrow(NotFoundLoggableException); + }); + }); + + describe('when the group is found', () => { + const setup = () => { + const { teacherUser } = UserAndAccountTestFactory.buildTeacher(); + const { studentUser } = UserAndAccountTestFactory.buildStudent(); + const group: Group = groupFactory.build({ + users: [ + { userId: teacherUser.id, roleId: teacherUser.roles[0].id }, + { userId: studentUser.id, roleId: studentUser.roles[0].id }, + ], + }); + const teacherRole: RoleDto = roleDtoFactory.build({ + id: teacherUser.roles[0].id, + name: teacherUser.roles[0].name, + }); + const studentRole: RoleDto = roleDtoFactory.build({ + id: studentUser.roles[0].id, + name: studentUser.roles[0].name, + }); + const teacherUserDo: UserDO = userDoFactory.build({ + id: teacherUser.id, + firstName: teacherUser.firstName, + lastName: teacherUser.lastName, + email: teacherUser.email, + roles: [{ id: teacherUser.roles[0].id, name: teacherUser.roles[0].name }], + }); + const studentUserDo: UserDO = userDoFactory.build({ + id: studentUser.id, + firstName: teacherUser.firstName, + lastName: studentUser.lastName, + email: studentUser.email, + roles: [{ id: studentUser.roles[0].id, name: studentUser.roles[0].name }], + }); + + groupService.findById.mockResolvedValueOnce(group); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(teacherUser); + userService.findById.mockResolvedValueOnce(teacherUserDo); + roleService.findById.mockResolvedValueOnce(teacherRole); + userService.findById.mockResolvedValueOnce(studentUserDo); + roleService.findById.mockResolvedValueOnce(studentRole); + + return { + teacherId: teacherUser.id, + teacherUser, + studentUser, + group, + expectedExternalId: group.externalSource?.externalId as string, + expectedSystemId: group.externalSource?.systemId as string, + }; + }; + + it('should return the resolved group', async () => { + const { teacherId, teacherUser, studentUser, group, expectedExternalId, expectedSystemId } = setup(); + + const result: ResolvedGroupDto = await uc.getGroup(teacherId, group.id); + + expect(result).toMatchObject({ + id: group.id, + name: group.name, + type: GroupTypes.CLASS, + externalSource: { + externalId: expectedExternalId, + systemId: expectedSystemId, + }, + users: [ + { + user: { + id: teacherUser.id, + firstName: teacherUser.firstName, + lastName: teacherUser.lastName, + email: teacherUser.email, + }, + role: { + id: teacherUser.roles[0].id, + name: teacherUser.roles[0].name, + }, + }, + { + user: { + id: studentUser.id, + firstName: studentUser.firstName, + lastName: studentUser.lastName, + email: studentUser.email, + }, + role: { + id: studentUser.roles[0].id, + name: studentUser.roles[0].name, + }, + }, + ], + }); + }); + }); + }); }); diff --git a/apps/server/src/modules/group/uc/group.uc.ts b/apps/server/src/modules/group/uc/group.uc.ts index a179b8cb352..2421e444e73 100644 --- a/apps/server/src/modules/group/uc/group.uc.ts +++ b/apps/server/src/modules/group/uc/group.uc.ts @@ -1,17 +1,17 @@ import { Injectable } from '@nestjs/common'; -import { EntityId, LegacySchoolDo, Page, Permission, SortOrder, User, UserDO } from '@shared/domain'; -import { AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; -import { ClassService } from '@src/modules/class'; -import { Class } from '@src/modules/class/domain'; -import { LegacySchoolService } from '@src/modules/legacy-school'; -import { RoleService } from '@src/modules/role'; -import { RoleDto } from '@src/modules/role/service/dto/role.dto'; -import { SystemDto, SystemService } from '@src/modules/system'; -import { UserService } from '@src/modules/user'; +import { EntityId, LegacySchoolDo, Page, Permission, SchoolYearEntity, SortOrder, User, UserDO } from '@shared/domain'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { ClassService } from '@modules/class'; +import { Class } from '@modules/class/domain'; +import { LegacySchoolService, SchoolYearService } from '@modules/legacy-school'; +import { RoleService } from '@modules/role'; +import { RoleDto } from '@modules/role/service/dto/role.dto'; +import { SystemDto, SystemService } from '@modules/system'; +import { UserService } from '@modules/user'; import { Group, GroupUser } from '../domain'; import { GroupService } from '../service'; import { SortHelper } from '../util'; -import { ClassInfoDto, ResolvedGroupUser } from './dto'; +import { ClassInfoDto, ResolvedGroupDto, ResolvedGroupUser } from './dto'; import { GroupUcMapper } from './mapper/group-uc.mapper'; @Injectable() @@ -23,7 +23,8 @@ export class GroupUc { private readonly userService: UserService, private readonly roleService: RoleService, private readonly schoolService: LegacySchoolService, - private readonly authorizationService: AuthorizationService + private readonly authorizationService: AuthorizationService, + private readonly schoolYearService: SchoolYearService ) {} public async findAllClassesForSchool( @@ -37,7 +38,11 @@ export class GroupUc { const school: LegacySchoolDo = await this.schoolService.getSchoolById(schoolId); const user: User = await this.authorizationService.getUserWithPermissions(userId); - this.authorizationService.checkPermission(user, school, AuthorizationContextBuilder.read([Permission.CLASS_LIST])); + this.authorizationService.checkPermission( + user, + school, + AuthorizationContextBuilder.read([Permission.CLASS_LIST, Permission.GROUP_LIST]) + ); const combinedClassInfo: ClassInfoDto[] = await this.findCombinedClassListForSchool(schoolId); @@ -72,7 +77,12 @@ export class GroupUc { clazz.teacherIds.map((teacherId: EntityId) => this.userService.findById(teacherId)) ); - const mapped: ClassInfoDto = GroupUcMapper.mapClassToClassInfoDto(clazz, teachers); + let schoolYear: SchoolYearEntity | undefined; + if (clazz.year) { + schoolYear = await this.schoolYearService.findById(clazz.year); + } + + const mapped: ClassInfoDto = GroupUcMapper.mapClassToClassInfoDto(clazz, teachers, schoolYear); return mapped; }) @@ -147,4 +157,24 @@ export class GroupUc { return page; } + + public async getGroup(userId: EntityId, groupId: EntityId): Promise { + const group: Group = await this.groupService.findById(groupId); + + await this.checkPermission(userId, group); + + const resolvedUsers: ResolvedGroupUser[] = await this.findUsersForGroup(group); + const resolvedGroup: ResolvedGroupDto = GroupUcMapper.mapToResolvedGroupDto(group, resolvedUsers); + + return resolvedGroup; + } + + private async checkPermission(userId: EntityId, group: Group): Promise { + const user: User = await this.authorizationService.getUserWithPermissions(userId); + return this.authorizationService.checkPermission( + user, + group, + AuthorizationContextBuilder.read([Permission.GROUP_VIEW]) + ); + } } diff --git a/apps/server/src/modules/group/uc/mapper/group-uc.mapper.ts b/apps/server/src/modules/group/uc/mapper/group-uc.mapper.ts index 1e1f11057ce..5ac11f0e0b6 100644 --- a/apps/server/src/modules/group/uc/mapper/group-uc.mapper.ts +++ b/apps/server/src/modules/group/uc/mapper/group-uc.mapper.ts @@ -1,8 +1,9 @@ -import { RoleName, UserDO } from '@shared/domain'; -import { Class } from '@src/modules/class/domain'; -import { SystemDto } from '@src/modules/system'; +import { RoleName, SchoolYearEntity, UserDO } from '@shared/domain'; +import { Class } from '@modules/class/domain'; +import { SystemDto } from '@modules/system'; import { Group } from '../../domain'; -import { ClassInfoDto, ResolvedGroupUser } from '../dto'; +import { ClassInfoDto, ResolvedGroupDto, ResolvedGroupUser } from '../dto'; +import { ClassRootType } from '../dto/class-root-type'; export class GroupUcMapper { public static mapGroupToClassInfoDto( @@ -11,6 +12,8 @@ export class GroupUcMapper { system?: SystemDto ): ClassInfoDto { const mapped: ClassInfoDto = new ClassInfoDto({ + id: group.id, + type: ClassRootType.GROUP, name: group.name, externalSourceName: system?.displayName, teachers: resolvedUsers @@ -21,13 +24,30 @@ export class GroupUcMapper { return mapped; } - public static mapClassToClassInfoDto(clazz: Class, teachers: UserDO[]): ClassInfoDto { + public static mapClassToClassInfoDto(clazz: Class, teachers: UserDO[], schoolYear?: SchoolYearEntity): ClassInfoDto { const name = clazz.gradeLevel ? `${clazz.gradeLevel}${clazz.name}` : clazz.name; + const isUpgradable = clazz.gradeLevel !== 13 && !clazz.successor; const mapped: ClassInfoDto = new ClassInfoDto({ + id: clazz.id, + type: ClassRootType.CLASS, name, externalSourceName: clazz.source, teachers: teachers.map((user: UserDO) => user.lastName), + schoolYear: schoolYear?.name, + isUpgradable, + }); + + return mapped; + } + + public static mapToResolvedGroupDto(group: Group, resolvedGroupUsers: ResolvedGroupUser[]): ResolvedGroupDto { + const mapped: ResolvedGroupDto = new ResolvedGroupDto({ + id: group.id, + name: group.name, + type: group.type, + externalSource: group.externalSource, + users: resolvedGroupUsers, }); return mapped; diff --git a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor.api.spec.ts b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor.api.spec.ts index f2e40310645..57a8a66b347 100644 --- a/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor.api.spec.ts +++ b/apps/server/src/modules/h5p-editor/controller/api-test/h5p-editor.api.spec.ts @@ -2,7 +2,7 @@ import { EntityManager } from '@mikro-orm/core'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test } from '@nestjs/testing'; import { TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; -import { H5PEditorTestModule } from '@src/modules/h5p-editor/h5p-editor-test.module'; +import { H5PEditorTestModule } from '@modules/h5p-editor/h5p-editor-test.module'; describe('H5PEditor Controller (api)', () => { let app: INestApplication; diff --git a/apps/server/src/modules/h5p-editor/controller/h5p-editor.controller.ts b/apps/server/src/modules/h5p-editor/controller/h5p-editor.controller.ts index 9f2a15c1a1d..519f96e75e1 100644 --- a/apps/server/src/modules/h5p-editor/controller/h5p-editor.controller.ts +++ b/apps/server/src/modules/h5p-editor/controller/h5p-editor.controller.ts @@ -1,7 +1,7 @@ import { BadRequestException, Controller, ForbiddenException, Get, InternalServerErrorException } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiValidationError } from '@shared/common'; -import { Authenticate } from '@src/modules/authentication/decorator/auth.decorator'; +import { Authenticate } from '@modules/authentication/decorator/auth.decorator'; // Dummy html response so we can test i-frame integration const dummyResponse = (title: string) => ` diff --git a/apps/server/src/modules/h5p-editor/h5p-editor-test.module.ts b/apps/server/src/modules/h5p-editor/h5p-editor-test.module.ts index dfe1b1ec846..fccb5e2841b 100644 --- a/apps/server/src/modules/h5p-editor/h5p-editor-test.module.ts +++ b/apps/server/src/modules/h5p-editor/h5p-editor-test.module.ts @@ -5,8 +5,8 @@ import { MongoDatabaseModuleOptions } from '@shared/infra/database/mongo-memory- import { RabbitMQWrapperTestModule } from '@shared/infra/rabbitmq'; 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 { AuthenticationModule } from '@modules/authentication/authentication.module'; +import { AuthorizationModule } from '@modules/authorization'; import { AuthenticationApiModule } from '../authentication/authentication-api.module'; import { H5PEditorModule } from './h5p-editor.module'; diff --git a/apps/server/src/modules/h5p-editor/h5p-editor.module.ts b/apps/server/src/modules/h5p-editor/h5p-editor.module.ts index 869f76d3a86..442f0a04409 100644 --- a/apps/server/src/modules/h5p-editor/h5p-editor.module.ts +++ b/apps/server/src/modules/h5p-editor/h5p-editor.module.ts @@ -6,7 +6,7 @@ import { Account, Role, SchoolEntity, SchoolYearEntity, SystemEntity, User } fro import { DB_PASSWORD, DB_URL, DB_USERNAME, createConfigModuleOptions } from '@src/config'; import { CoreModule } from '@src/core'; import { Logger } from '@src/core/logger'; -import { AuthorizationModule } from '@src/modules/authorization'; +import { AuthorizationModule } from '@modules/authorization'; import { AuthenticationModule } from '../authentication/authentication.module'; import { H5PEditorController } from './controller/h5p-editor.controller'; import { config } from './h5p-editor.config'; diff --git a/apps/server/src/modules/index.ts b/apps/server/src/modules/index.ts deleted file mode 100644 index 111a64d9f33..00000000000 --- a/apps/server/src/modules/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -export * from './account'; -export * from './authentication'; -export * from './authorization'; -export * from './board'; -export * from './collaborative-storage'; -export * from './files-storage'; -export * from './files-storage-client'; -export * from './fwu-learning-contents'; -export * from './learnroom'; -export * from './lesson'; -export * from './news'; -export * from './oauth'; -export * from './oauth-provider'; -export * from './provisioning'; -export * from './rocketchat'; -export * from './role'; -export * from './legacy-school'; -export * from './sharing'; -export * from './system'; -export * from './task'; -export * from './tool'; -export * from './user'; -export * from './user-import'; -export * from './user-login-migration'; -export * from './video-conference'; diff --git a/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts b/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts index 0d6518878f7..964df864abb 100644 --- a/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts +++ b/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts @@ -3,8 +3,8 @@ import { INestApplication, StreamableFile } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain'; import { cleanupCollections, courseFactory, UserAndAccountTestFactory, TestApiClient } from '@shared/testing'; -import { CourseMetadataListResponse } from '@src/modules/learnroom/controller/dto'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { CourseMetadataListResponse } from '@modules/learnroom/controller/dto'; +import { ServerTestModule } from '@modules/server/server.module'; const createStudent = () => { const { studentUser, studentAccount } = UserAndAccountTestFactory.buildStudent({}, [Permission.COURSE_VIEW]); diff --git a/apps/server/src/modules/learnroom/controller/api-test/dashboard.api.spec.ts b/apps/server/src/modules/learnroom/controller/api-test/dashboard.api.spec.ts index c3e9dc3d5e0..b6c27736d93 100644 --- a/apps/server/src/modules/learnroom/controller/api-test/dashboard.api.spec.ts +++ b/apps/server/src/modules/learnroom/controller/api-test/dashboard.api.spec.ts @@ -2,12 +2,12 @@ import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { DashboardEntity, GridElement, Permission, User, RoleName } from '@shared/domain'; -import { ICurrentUser } from '@src/modules/authentication'; +import { ICurrentUser } from '@modules/authentication'; import { IDashboardRepo } from '@shared/repo'; import { courseFactory, mapUserToCurrentUser, roleFactory, userFactory } from '@shared/testing'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { DashboardResponse } from '@src/modules/learnroom/controller/dto'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { DashboardResponse } from '@modules/learnroom/controller/dto'; +import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/learnroom/controller/api-test/rooms-copy-timeout.api.spec.ts b/apps/server/src/modules/learnroom/controller/api-test/rooms-copy-timeout.api.spec.ts index ed62a2b6ade..2941e185ca1 100644 --- a/apps/server/src/modules/learnroom/controller/api-test/rooms-copy-timeout.api.spec.ts +++ b/apps/server/src/modules/learnroom/controller/api-test/rooms-copy-timeout.api.spec.ts @@ -1,10 +1,9 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { IConfig } from '@hpi-schul-cloud/commons/lib/interfaces/IConfig'; import { EntityManager } from '@mikro-orm/mongodb'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain'; -import { ICurrentUser } from '@src/modules/authentication'; +import { ICurrentUser } from '@modules/authentication'; import { cleanupCollections, courseFactory, @@ -13,14 +12,18 @@ import { roleFactory, userFactory, } from '@shared/testing'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { Request } from 'express'; import request from 'supertest'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; +import { createMock } from '@golevelup/ts-jest'; +// config must be set outside before the server module is importat, otherwise the configuration is already set +const configBefore = Configuration.toObject({ plainSecrets: true }); Configuration.set('FEATURE_COPY_SERVICE_ENABLED', true); Configuration.set('INCOMING_REQUEST_TIMEOUT_COPY_API', 1); // eslint-disable-next-line import/first -import { ServerTestModule } from '@src/modules/server/server.module'; +import { ServerTestModule } from '@modules/server'; // This needs to be in a separate test file because of the above configuration. // When we find a way to mock the config, it should be moved alongside the other API tests. @@ -28,10 +31,8 @@ describe('Rooms copy (API)', () => { let app: INestApplication; let em: EntityManager; let currentUser: ICurrentUser; - let configBefore: IConfig; beforeAll(async () => { - configBefore = Configuration.toObject({ plainSecrets: true }); const moduleFixture: TestingModule = await Test.createTestingModule({ imports: [ServerTestModule], }) @@ -43,6 +44,8 @@ describe('Rooms copy (API)', () => { return true; }, }) + .overrideProvider(FilesStorageClientAdapterService) + .useValue(createMock()) .compile(); app = moduleFixture.createNestApplication(); diff --git a/apps/server/src/modules/learnroom/controller/api-test/rooms.api.spec.ts b/apps/server/src/modules/learnroom/controller/api-test/rooms.api.spec.ts index 2c62b74c9fd..6db5100405a 100644 --- a/apps/server/src/modules/learnroom/controller/api-test/rooms.api.spec.ts +++ b/apps/server/src/modules/learnroom/controller/api-test/rooms.api.spec.ts @@ -14,12 +14,12 @@ import { taskFactory, userFactory, } from '@shared/testing'; -import { ICurrentUser } from '@src/modules/authentication'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { CopyApiResponse } from '@src/modules/copy-helper'; -import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; -import { SingleColumnBoardResponse } from '@src/modules/learnroom/controller/dto'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { CopyApiResponse } from '@modules/copy-helper'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; +import { SingleColumnBoardResponse } from '@modules/learnroom/controller/dto'; +import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/learnroom/controller/course.controller.ts b/apps/server/src/modules/learnroom/controller/course.controller.ts index 14ae2b595f0..dfb4e920957 100644 --- a/apps/server/src/modules/learnroom/controller/course.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course.controller.ts @@ -1,8 +1,7 @@ import { Controller, Get, NotFoundException, Param, Query, Res, StreamableFile } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; +import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; import { PaginationParams } from '@shared/controller/'; -import { ICurrentUser } from '@src/modules/authentication'; import { Response } from 'express'; import { ConfigService } from '@nestjs/config'; import { CourseUc } from '../uc/course.uc'; diff --git a/apps/server/src/modules/learnroom/controller/dashboard.controller.spec.ts b/apps/server/src/modules/learnroom/controller/dashboard.controller.spec.ts index 59445f2c1f8..284f18ef862 100644 --- a/apps/server/src/modules/learnroom/controller/dashboard.controller.spec.ts +++ b/apps/server/src/modules/learnroom/controller/dashboard.controller.spec.ts @@ -7,7 +7,7 @@ import { LearnroomMetadata, LearnroomTypes, } from '@shared/domain'; -import { ICurrentUser } from '@src/modules/authentication'; +import { ICurrentUser } from '@modules/authentication'; import { DashboardUc } from '../uc/dashboard.uc'; import { DashboardController } from './dashboard.controller'; import { DashboardResponse } from './dto'; diff --git a/apps/server/src/modules/learnroom/controller/dashboard.controller.ts b/apps/server/src/modules/learnroom/controller/dashboard.controller.ts index dae1120be8a..224f6c41ca7 100644 --- a/apps/server/src/modules/learnroom/controller/dashboard.controller.ts +++ b/apps/server/src/modules/learnroom/controller/dashboard.controller.ts @@ -1,7 +1,6 @@ import { Body, Controller, Get, Param, Patch, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; +import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; import { DashboardMapper } from '../mapper/dashboard.mapper'; import { DashboardUc } from '../uc/dashboard.uc'; import { DashboardResponse, DashboardUrlParams, MoveElementParams, PatchGroupParams } from './dto'; diff --git a/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-element.response.ts b/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-element.response.ts index fb110ff95d9..4e9d139124b 100644 --- a/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-element.response.ts +++ b/apps/server/src/modules/learnroom/controller/dto/single-column-board/board-element.response.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { RoomBoardElementTypes } from '@src/modules/learnroom/types'; +import { RoomBoardElementTypes } from '@modules/learnroom/types'; import { BoardColumnBoardResponse } from './board-column-board.response'; import { BoardLessonResponse } from './board-lesson.response'; import { BoardTaskResponse } from './board-task.response'; diff --git a/apps/server/src/modules/learnroom/controller/rooms.controller.spec.ts b/apps/server/src/modules/learnroom/controller/rooms.controller.spec.ts index 5692cc3427b..c63e2e380ae 100644 --- a/apps/server/src/modules/learnroom/controller/rooms.controller.spec.ts +++ b/apps/server/src/modules/learnroom/controller/rooms.controller.spec.ts @@ -1,8 +1,8 @@ import { createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityId } from '@shared/domain'; -import { ICurrentUser } from '@src/modules/authentication'; -import { CopyApiResponse, CopyElementType, CopyStatus, CopyStatusEnum } from '@src/modules/copy-helper'; +import { ICurrentUser } from '@modules/authentication'; +import { CopyApiResponse, CopyElementType, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; import { RoomBoardResponseMapper } from '../mapper/room-board-response.mapper'; import { RoomBoardDTO } from '../types'; import { CourseCopyUC } from '../uc/course-copy.uc'; diff --git a/apps/server/src/modules/learnroom/controller/rooms.controller.ts b/apps/server/src/modules/learnroom/controller/rooms.controller.ts index bf0013dbc54..0e0b2f7c7a0 100644 --- a/apps/server/src/modules/learnroom/controller/rooms.controller.ts +++ b/apps/server/src/modules/learnroom/controller/rooms.controller.ts @@ -1,10 +1,9 @@ import { Body, Controller, Get, Param, Patch, Post } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { RequestTimeout } from '@shared/common'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; -import { CopyApiResponse, CopyMapper } from '@src/modules/copy-helper'; -import { serverConfig } from '@src/modules/server/server.config'; +import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; +import { CopyApiResponse, CopyMapper } from '@modules/copy-helper'; +import { serverConfig } from '@modules/server/server.config'; import { RoomBoardResponseMapper } from '../mapper/room-board-response.mapper'; import { CourseCopyUC } from '../uc/course-copy.uc'; import { LessonCopyUC } from '../uc/lesson-copy.uc'; diff --git a/apps/server/src/modules/learnroom/index.ts b/apps/server/src/modules/learnroom/index.ts index f2dc136ce5e..e4d907784d5 100644 --- a/apps/server/src/modules/learnroom/index.ts +++ b/apps/server/src/modules/learnroom/index.ts @@ -1,2 +1,3 @@ export * from './learnroom.module'; export * from './service/course-copy.service'; +export { CourseService } from './service'; diff --git a/apps/server/src/modules/learnroom/learnroom-api.module.ts b/apps/server/src/modules/learnroom/learnroom-api.module.ts index b72db2d7f59..5cfaada65b8 100644 --- a/apps/server/src/modules/learnroom/learnroom-api.module.ts +++ b/apps/server/src/modules/learnroom/learnroom-api.module.ts @@ -1,8 +1,9 @@ import { Module } from '@nestjs/common'; import { BoardRepo, CourseRepo, DashboardModelMapper, DashboardRepo, LessonRepo, UserRepo } from '@shared/repo'; -import { AuthorizationModule } from '@src/modules/authorization'; -import { CopyHelperModule } from '@src/modules/copy-helper'; -import { LessonModule } from '@src/modules/lesson'; +import { AuthorizationModule } from '@modules/authorization'; +import { AuthorizationReferenceModule } from '@modules/authorization/authorization-reference.module'; +import { CopyHelperModule } from '@modules/copy-helper'; +import { LessonModule } from '@modules/lesson'; import { CourseController } from './controller/course.controller'; import { DashboardController } from './controller/dashboard.controller'; import { RoomsController } from './controller/rooms.controller'; @@ -20,7 +21,7 @@ import { } from './uc'; @Module({ - imports: [AuthorizationModule, LessonModule, CopyHelperModule, LearnroomModule], + imports: [AuthorizationModule, LessonModule, CopyHelperModule, LearnroomModule, AuthorizationReferenceModule], controllers: [DashboardController, CourseController, RoomsController], providers: [ DashboardUc, diff --git a/apps/server/src/modules/learnroom/learnroom.module.ts b/apps/server/src/modules/learnroom/learnroom.module.ts index 1149b477c30..c84310ba05e 100644 --- a/apps/server/src/modules/learnroom/learnroom.module.ts +++ b/apps/server/src/modules/learnroom/learnroom.module.ts @@ -1,10 +1,10 @@ import { Module } from '@nestjs/common'; import { BoardRepo, CourseRepo, DashboardModelMapper, DashboardRepo, LessonRepo, UserRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; -import { BoardModule } from '@src/modules/board'; -import { CopyHelperModule } from '@src/modules/copy-helper'; -import { LessonModule } from '@src/modules/lesson'; -import { TaskModule } from '@src/modules/task'; +import { BoardModule } from '@modules/board'; +import { CopyHelperModule } from '@modules/copy-helper'; +import { LessonModule } from '@modules/lesson'; +import { TaskModule } from '@modules/task'; import { BoardCopyService, ColumnBoardTargetService, diff --git a/apps/server/src/modules/learnroom/service/board-copy.service.spec.ts b/apps/server/src/modules/learnroom/service/board-copy.service.spec.ts index 87542f7efb2..51ebc404d83 100644 --- a/apps/server/src/modules/learnroom/service/board-copy.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/board-copy.service.spec.ts @@ -17,10 +17,10 @@ import { userFactory, } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { ColumnBoardCopyService } from '@src/modules/board/service/column-board-copy.service'; -import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@src/modules/copy-helper'; -import { LessonCopyService } from '@src/modules/lesson/service'; -import { TaskCopyService } from '@src/modules/task/service'; +import { ColumnBoardCopyService } from '@modules/board/service/column-board-copy.service'; +import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; +import { LessonCopyService } from '@modules/lesson/service'; +import { TaskCopyService } from '@modules/task/service'; import { BoardCopyService } from './board-copy.service'; describe('board copy service', () => { diff --git a/apps/server/src/modules/learnroom/service/board-copy.service.ts b/apps/server/src/modules/learnroom/service/board-copy.service.ts index da31f4cef4e..f695dfd2c05 100644 --- a/apps/server/src/modules/learnroom/service/board-copy.service.ts +++ b/apps/server/src/modules/learnroom/service/board-copy.service.ts @@ -19,11 +19,11 @@ import { } from '@shared/domain'; import { BoardRepo } from '@shared/repo'; import { LegacyLogger } from '@src/core/logger'; -import { ColumnBoardCopyService } from '@src/modules/board/service/column-board-copy.service'; -import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@src/modules/copy-helper'; -import { getResolvedValues } from '@src/modules/files-storage/helper'; -import { LessonCopyService } from '@src/modules/lesson/service'; -import { TaskCopyService } from '@src/modules/task/service'; +import { ColumnBoardCopyService } from '@modules/board/service/column-board-copy.service'; +import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; +import { getResolvedValues } from '@modules/files-storage/helper'; +import { LessonCopyService } from '@modules/lesson/service'; +import { TaskCopyService } from '@modules/task/service'; import { sortBy } from 'lodash'; type BoardCopyParams = { diff --git a/apps/server/src/modules/learnroom/service/column-board-target.service.spec.ts b/apps/server/src/modules/learnroom/service/column-board-target.service.spec.ts index 59a6169a2a3..64db859ddac 100644 --- a/apps/server/src/modules/learnroom/service/column-board-target.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/column-board-target.service.spec.ts @@ -4,7 +4,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ColumnBoardTarget } from '@shared/domain'; import { MongoMemoryDatabaseModule } from '@shared/infra/database'; import { cleanupCollections, columnBoardTargetFactory } from '@shared/testing'; -import { ColumnBoardService } from '@src/modules/board'; +import { ColumnBoardService } from '@modules/board'; import { ColumnBoardTargetService } from './column-board-target.service'; describe(ColumnBoardTargetService.name, () => { diff --git a/apps/server/src/modules/learnroom/service/column-board-target.service.ts b/apps/server/src/modules/learnroom/service/column-board-target.service.ts index 8f72cb3e44e..79b476c4fe6 100644 --- a/apps/server/src/modules/learnroom/service/column-board-target.service.ts +++ b/apps/server/src/modules/learnroom/service/column-board-target.service.ts @@ -2,7 +2,7 @@ import { FilterQuery } from '@mikro-orm/core'; import { EntityManager } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; import { ColumnBoardTarget, EntityId } from '@shared/domain'; -import { ColumnBoardService } from '@src/modules/board'; +import { ColumnBoardService } from '@modules/board'; @Injectable() export class ColumnBoardTargetService { diff --git a/apps/server/src/modules/learnroom/service/common-cartridge-export.service.spec.ts b/apps/server/src/modules/learnroom/service/common-cartridge-export.service.spec.ts index e8e3d43f9bc..af1bc727d6a 100644 --- a/apps/server/src/modules/learnroom/service/common-cartridge-export.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/common-cartridge-export.service.spec.ts @@ -9,10 +9,10 @@ import { Task, } from '@shared/domain'; import { courseFactory, lessonFactory, setupEntities, taskFactory } from '@shared/testing'; -import { CommonCartridgeExportService } from '@src/modules/learnroom/service/common-cartridge-export.service'; -import { CourseService } from '@src/modules/learnroom/service'; -import { LessonService } from '@src/modules/lesson/service'; -import { TaskService } from '@src/modules/task/service/task.service'; +import { CommonCartridgeExportService } from '@modules/learnroom/service/common-cartridge-export.service'; +import { CourseService } from '@modules/learnroom/service'; +import { LessonService } from '@modules/lesson/service'; +import { TaskService } from '@modules/task/service/task.service'; import AdmZip from 'adm-zip'; import { CommonCartridgeVersion } from '../common-cartridge'; diff --git a/apps/server/src/modules/learnroom/service/common-cartridge-export.service.ts b/apps/server/src/modules/learnroom/service/common-cartridge-export.service.ts index 25077d84338..e25d2a62367 100644 --- a/apps/server/src/modules/learnroom/service/common-cartridge-export.service.ts +++ b/apps/server/src/modules/learnroom/service/common-cartridge-export.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { Course, EntityId, IComponentProperties, Task } from '@shared/domain'; -import { LessonService } from '@src/modules/lesson/service'; +import { LessonService } from '@modules/lesson/service'; import { ComponentType } from '@src/shared/domain/entity/lesson.entity'; -import { TaskService } from '@src/modules/task/service'; +import { TaskService } from '@modules/task/service'; import { CommonCartridgeFileBuilder, CommonCartridgeIntendedUseType, @@ -115,8 +115,18 @@ export class CommonCartridgeExportService { if (content.component === ComponentType.ETHERPAD) { return version === CommonCartridgeVersion.V_1_3_0 - ? { ...commonProps, type: CommonCartridgeResourceType.WEB_LINK_V3, url: content.content.url } - : { ...commonProps, type: CommonCartridgeResourceType.WEB_LINK_V1, url: content.content.url }; + ? { + ...commonProps, + type: CommonCartridgeResourceType.WEB_LINK_V3, + url: content.content.url, + title: content.content.description, + } + : { + ...commonProps, + type: CommonCartridgeResourceType.WEB_LINK_V1, + url: content.content.url, + title: content.content.description, + }; } return undefined; diff --git a/apps/server/src/modules/learnroom/service/course-copy.service.spec.ts b/apps/server/src/modules/learnroom/service/course-copy.service.spec.ts index 866ba655d97..969360dcc2e 100644 --- a/apps/server/src/modules/learnroom/service/course-copy.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/course-copy.service.spec.ts @@ -10,8 +10,8 @@ import { setupEntities, userFactory, } from '@shared/testing'; -import { CopyElementType, CopyHelperService, CopyStatusEnum } from '@src/modules/copy-helper'; -import { LessonCopyService } from '@src/modules/lesson/service'; +import { CopyElementType, CopyHelperService, CopyStatusEnum } from '@modules/copy-helper'; +import { LessonCopyService } from '@modules/lesson/service'; import { BoardCopyService } from './board-copy.service'; import { CourseCopyService } from './course-copy.service'; import { RoomsService } from './rooms.service'; diff --git a/apps/server/src/modules/learnroom/service/course-copy.service.ts b/apps/server/src/modules/learnroom/service/course-copy.service.ts index a8a0c236673..51f98bb436b 100644 --- a/apps/server/src/modules/learnroom/service/course-copy.service.ts +++ b/apps/server/src/modules/learnroom/service/course-copy.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { Course, EntityId, User } from '@shared/domain'; import { BoardRepo, CourseRepo, UserRepo } from '@shared/repo'; -import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@src/modules/copy-helper'; +import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; import { BoardCopyService } from './board-copy.service'; import { RoomsService } from './rooms.service'; diff --git a/apps/server/src/modules/learnroom/service/rooms.service.spec.ts b/apps/server/src/modules/learnroom/service/rooms.service.spec.ts index 789edcb9099..2358e2b2067 100644 --- a/apps/server/src/modules/learnroom/service/rooms.service.spec.ts +++ b/apps/server/src/modules/learnroom/service/rooms.service.spec.ts @@ -6,8 +6,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { BoardExternalReference, BoardExternalReferenceType, EntityId } from '@shared/domain'; import { BoardRepo, LessonRepo } from '@shared/repo'; import { boardFactory, courseFactory, lessonFactory, setupEntities, taskFactory, userFactory } from '@shared/testing'; -import { CardService, ColumnBoardService, ColumnService, ContentElementService } from '@src/modules/board'; -import { TaskService } from '@src/modules/task/service'; +import { CardService, ColumnBoardService, ColumnService, ContentElementService } from '@modules/board'; +import { TaskService } from '@modules/task/service'; import { ColumnBoardTargetService } from './column-board-target.service'; import { RoomsService } from './rooms.service'; diff --git a/apps/server/src/modules/learnroom/service/rooms.service.ts b/apps/server/src/modules/learnroom/service/rooms.service.ts index 24bffb03f4f..cc8b95e09b0 100644 --- a/apps/server/src/modules/learnroom/service/rooms.service.ts +++ b/apps/server/src/modules/learnroom/service/rooms.service.ts @@ -2,8 +2,8 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { Injectable } from '@nestjs/common'; import { Board, BoardExternalReferenceType, ColumnBoardTarget, EntityId } from '@shared/domain'; import { BoardRepo, LessonRepo } from '@shared/repo'; -import { ColumnBoardService } from '@src/modules/board'; -import { TaskService } from '@src/modules/task/service'; +import { ColumnBoardService } from '@modules/board'; +import { TaskService } from '@modules/task/service'; import { ColumnBoardTargetService } from './column-board-target.service'; @Injectable() diff --git a/apps/server/src/modules/learnroom/uc/course-copy.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course-copy.uc.spec.ts index 2e7e9f739ad..9e051dae6da 100644 --- a/apps/server/src/modules/learnroom/uc/course-copy.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course-copy.uc.spec.ts @@ -3,17 +3,17 @@ import { Configuration } from '@hpi-schul-cloud/commons'; import { ForbiddenException, InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain'; -import { boardFactory, courseFactory, setupEntities, userFactory } from '@shared/testing'; -import { Action, AuthorizableReferenceType } from '@src/modules/authorization'; -import { AuthorizationService } from '@src/modules/authorization/authorization.service'; -import { CopyElementType, CopyStatusEnum } from '@src/modules/copy-helper'; +import { courseFactory, setupEntities, userFactory } from '@shared/testing'; +import { AuthorizationContextBuilder } from '@modules/authorization'; +import { AuthorizationReferenceService, AuthorizableReferenceType } from '@modules/authorization/domain'; +import { CopyElementType, CopyStatusEnum } from '@modules/copy-helper'; import { CourseCopyService } from '../service'; import { CourseCopyUC } from './course-copy.uc'; describe('course copy uc', () => { let module: TestingModule; let uc: CourseCopyUC; - let authorization: DeepMocked; + let authorization: DeepMocked; let courseCopyService: DeepMocked; beforeAll(async () => { @@ -22,8 +22,8 @@ describe('course copy uc', () => { providers: [ CourseCopyUC, { - provide: AuthorizationService, - useValue: createMock(), + provide: AuthorizationReferenceService, + useValue: createMock(), }, { provide: CourseCopyService, @@ -33,7 +33,7 @@ describe('course copy uc', () => { }).compile(); uc = module.get(CourseCopyUC); - authorization = module.get(AuthorizationService); + authorization = module.get(AuthorizationReferenceService); courseCopyService = module.get(CourseCopyService); }); @@ -41,91 +41,99 @@ describe('course copy uc', () => { await module.close(); }); - beforeEach(() => { - Configuration.set('FEATURE_COPY_SERVICE_ENABLED', true); - }); + // Please be careful the Configuration.set is effects all tests !!! describe('copy course', () => { - const setup = () => { - const user = userFactory.buildWithId(); - const allCourses = courseFactory.buildList(3, { teachers: [user] }); - const course = allCourses[0]; - const originalBoard = boardFactory.build({ course }); - const courseCopy = courseFactory.buildWithId({ teachers: [user] }); - const boardCopy = boardFactory.build({ course: courseCopy }); - - authorization.getUserWithPermissions.mockResolvedValue(user); - const status = { - title: 'courseCopy', - type: CopyElementType.COURSE, - status: CopyStatusEnum.SUCCESS, - copyEntity: courseCopy, + describe('when authorization to course resolve with void and feature is deactivated', () => { + const setup = () => { + Configuration.set('FEATURE_COPY_SERVICE_ENABLED', false); + const user = userFactory.buildWithId(); + const course = courseFactory.buildWithId({ teachers: [user] }); + + return { + userId: user.id, + courseId: course.id, + }; }; - courseCopyService.copyCourse.mockResolvedValue(status); + it('should throw if copy feature is deactivated', async () => { + const { courseId, userId } = setup(); + + await expect(uc.copyCourse(userId, courseId)).rejects.toThrowError( + new InternalServerErrorException('Copy Feature not enabled') + ); + }); + }); - return { - user, - course, - originalBoard, - courseCopy, - boardCopy, - allCourses, - status, + describe('when authorization to course resolve with void and feature is activated', () => { + const setup = () => { + Configuration.set('FEATURE_COPY_SERVICE_ENABLED', true); + const user = userFactory.buildWithId(); + const course = courseFactory.buildWithId({ teachers: [user] }); + const courseCopy = courseFactory.buildWithId({ teachers: [user] }); + + const status = { + title: 'courseCopy', + type: CopyElementType.COURSE, + status: CopyStatusEnum.SUCCESS, + copyEntity: courseCopy, + }; + + authorization.checkPermissionByReferences.mockResolvedValueOnce(); + courseCopyService.copyCourse.mockResolvedValueOnce(status); + + return { + userId: user.id, + courseId: course.id, + status, + }; }; - }; - it('should throw if copy feature is deactivated', async () => { - Configuration.set('FEATURE_COPY_SERVICE_ENABLED', false); - const { course, user } = setup(); - await expect(uc.copyCourse(user.id, course.id)).rejects.toThrowError(InternalServerErrorException); - }); + it('should check permission to create a course', async () => { + const { courseId, userId } = setup(); - it('should check permission to create a course', async () => { - const { course, user } = setup(); - await uc.copyCourse(user.id, course.id); - expect(authorization.checkPermissionByReferences).toBeCalledWith( - user.id, - AuthorizableReferenceType.Course, - course.id, - { - action: Action.write, - requiredPermissions: [Permission.COURSE_CREATE], - } - ); - }); + await uc.copyCourse(userId, courseId); - it('should call course copy service', async () => { - const { course, user } = setup(); - await uc.copyCourse(user.id, course.id); - expect(courseCopyService.copyCourse).toBeCalledWith({ userId: user.id, courseId: course.id }); - }); + const context = AuthorizationContextBuilder.write([Permission.COURSE_CREATE]); + expect(authorization.checkPermissionByReferences).toBeCalledWith( + userId, + AuthorizableReferenceType.Course, + courseId, + context + ); + }); + + it('should call course copy service', async () => { + const { courseId, userId } = setup(); + + await uc.copyCourse(userId, courseId); + + expect(courseCopyService.copyCourse).toBeCalledWith({ userId, courseId }); + }); + + it('should return status', async () => { + const { courseId, userId, status } = setup(); + + const result = await uc.copyCourse(userId, courseId); - it('should return status', async () => { - const { course, user, status } = setup(); - const result = await uc.copyCourse(user.id, course.id); - expect(result).toEqual(status); + expect(result).toEqual(status); + }); }); - describe('when access to course is forbidden', () => { + describe('when authorization to course throw a forbidden exception', () => { const setupWithCourseForbidden = () => { + Configuration.set('FEATURE_COPY_SERVICE_ENABLED', true); const user = userFactory.buildWithId(); const course = courseFactory.buildWithId(); - authorization.checkPermissionByReferences.mockImplementation(() => { - throw new ForbiddenException(); - }); + authorization.checkPermissionByReferences.mockRejectedValueOnce(new ForbiddenException()); + return { user, course }; }; it('should throw ForbiddenException', async () => { const { course, user } = setupWithCourseForbidden(); - try { - await uc.copyCourse(user.id, course.id); - throw new Error('should have failed'); - } catch (err) { - expect(err).toBeInstanceOf(ForbiddenException); - } + await expect(uc.copyCourse(user.id, course.id)).rejects.toThrowError(new ForbiddenException()); }); }); }); diff --git a/apps/server/src/modules/learnroom/uc/course-copy.uc.ts b/apps/server/src/modules/learnroom/uc/course-copy.uc.ts index 0d806c36263..19b94a9c238 100644 --- a/apps/server/src/modules/learnroom/uc/course-copy.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course-copy.uc.ts @@ -1,24 +1,23 @@ import { Configuration } from '@hpi-schul-cloud/commons'; import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { EntityId, Permission } from '@shared/domain'; -import { Action, AuthorizableReferenceType, AuthorizationService } from '@src/modules/authorization'; -import { CopyStatus } from '@src/modules/copy-helper'; +import { AuthorizationContextBuilder } from '@modules/authorization'; +import { AuthorizationReferenceService, AuthorizableReferenceType } from '@modules/authorization/domain'; +import { CopyStatus } from '@modules/copy-helper'; import { CourseCopyService } from '../service'; @Injectable() export class CourseCopyUC { constructor( - private readonly authorization: AuthorizationService, + private readonly authorization: AuthorizationReferenceService, private readonly courseCopyService: CourseCopyService ) {} async copyCourse(userId: EntityId, courseId: EntityId): Promise { this.checkFeatureEnabled(); - await this.authorization.checkPermissionByReferences(userId, AuthorizableReferenceType.Course, courseId, { - action: Action.write, - requiredPermissions: [Permission.COURSE_CREATE], - }); + const context = AuthorizationContextBuilder.write([Permission.COURSE_CREATE]); + await this.authorization.checkPermissionByReferences(userId, AuthorizableReferenceType.Course, courseId, context); const result = await this.courseCopyService.copyCourse({ userId, courseId }); @@ -26,6 +25,7 @@ export class CourseCopyUC { } private checkFeatureEnabled() { + // @hpi-schul-cloud/commons is deprecated way to get envirements const enabled = Configuration.get('FEATURE_COPY_SERVICE_ENABLED') as boolean; if (!enabled) { throw new InternalServerErrorException('Copy Feature not enabled'); diff --git a/apps/server/src/modules/learnroom/uc/course-export.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course-export.uc.spec.ts index 3d93827f06d..8ca36158f5d 100644 --- a/apps/server/src/modules/learnroom/uc/course-export.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course-export.uc.spec.ts @@ -1,7 +1,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { CommonCartridgeExportService } from '@src/modules/learnroom/service/common-cartridge-export.service'; -import { AuthorizationService } from '@src/modules'; +import { CommonCartridgeExportService } from '@modules/learnroom/service/common-cartridge-export.service'; +import { AuthorizationReferenceService } from '@modules/authorization/domain'; +import { ObjectId } from 'bson'; +import { ForbiddenException } from '@nestjs/common'; import { CourseExportUc } from './course-export.uc'; import { CommonCartridgeVersion } from '../common-cartridge'; @@ -9,7 +11,7 @@ describe('CourseExportUc', () => { let module: TestingModule; let courseExportUc: CourseExportUc; let courseExportServiceMock: DeepMocked; - let authorizationServiceMock: DeepMocked; + let authorizationServiceMock: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -20,33 +22,86 @@ describe('CourseExportUc', () => { useValue: createMock(), }, { - provide: AuthorizationService, - useValue: createMock(), + provide: AuthorizationReferenceService, + useValue: createMock(), }, ], }).compile(); courseExportUc = module.get(CourseExportUc); courseExportServiceMock = module.get(CommonCartridgeExportService); - authorizationServiceMock = module.get(AuthorizationService); + authorizationServiceMock = module.get(AuthorizationReferenceService); }); afterAll(async () => { await module.close(); }); + afterEach(() => { + // is needed to solve buffer test isolation + jest.resetAllMocks(); + }); + describe('exportCourse', () => { - const version: CommonCartridgeVersion = CommonCartridgeVersion.V_1_1_0; - it('should check for permissions', async () => { - authorizationServiceMock.checkPermissionByReferences.mockResolvedValueOnce(); - await expect(courseExportUc.exportCourse('', '', version)).resolves.not.toThrow(); - expect(authorizationServiceMock.checkPermissionByReferences).toBeCalledTimes(1); + const setupParams = () => { + const courseId = new ObjectId().toHexString(); + const userId = new ObjectId().toHexString(); + const version: CommonCartridgeVersion = CommonCartridgeVersion.V_1_1_0; + + return { version, userId, courseId }; + }; + + describe('when authorization throw a error', () => { + const setup = () => { + authorizationServiceMock.checkPermissionByReferences.mockRejectedValueOnce(new ForbiddenException()); + courseExportServiceMock.exportCourse.mockResolvedValueOnce(Buffer.from('')); + + return setupParams(); + }; + + it('should pass this error', async () => { + const { courseId, userId, version } = setup(); + + await expect(courseExportUc.exportCourse(courseId, userId, version)).rejects.toThrowError( + new ForbiddenException() + ); + }); + }); + + describe('when course export service throw a error', () => { + const setup = () => { + authorizationServiceMock.checkPermissionByReferences.mockResolvedValueOnce(); + courseExportServiceMock.exportCourse.mockRejectedValueOnce(new Error()); + + return setupParams(); + }; + + it('should pass this error', async () => { + const { courseId, userId, version } = setup(); + + await expect(courseExportUc.exportCourse(courseId, userId, version)).rejects.toThrowError(new Error()); + }); }); - it('should return a binary file as buffer', async () => { - courseExportServiceMock.exportCourse.mockResolvedValueOnce(Buffer.from('')); - authorizationServiceMock.checkPermissionByReferences.mockResolvedValueOnce(); + describe('when authorization resolve', () => { + const setup = () => { + authorizationServiceMock.checkPermissionByReferences.mockResolvedValueOnce(); + courseExportServiceMock.exportCourse.mockResolvedValueOnce(Buffer.from('')); + + return setupParams(); + }; + + it('should check for permissions', async () => { + const { courseId, userId, version } = setup(); + + await expect(courseExportUc.exportCourse(courseId, userId, version)).resolves.not.toThrow(); + expect(authorizationServiceMock.checkPermissionByReferences).toBeCalledTimes(1); + }); + + it('should return a binary file as buffer', async () => { + const { courseId, userId, version } = setup(); - await expect(courseExportUc.exportCourse('', '', version)).resolves.toBeInstanceOf(Buffer); + await expect(courseExportUc.exportCourse(courseId, userId, version)).resolves.toBeInstanceOf(Buffer); + }); }); }); }); diff --git a/apps/server/src/modules/learnroom/uc/course-export.uc.ts b/apps/server/src/modules/learnroom/uc/course-export.uc.ts index 418812e0cd8..758b1b550d7 100644 --- a/apps/server/src/modules/learnroom/uc/course-export.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course-export.uc.ts @@ -1,6 +1,7 @@ import { Injectable } from '@nestjs/common'; import { EntityId, Permission } from '@shared/domain'; -import { Action, AuthorizableReferenceType, AuthorizationService } from '@src/modules/authorization'; +import { AuthorizationContextBuilder } from '@modules/authorization'; +import { AuthorizationReferenceService, AuthorizableReferenceType } from '@modules/authorization/domain'; import { CommonCartridgeVersion } from '../common-cartridge'; import { CommonCartridgeExportService } from '../service/common-cartridge-export.service'; @@ -8,14 +9,18 @@ import { CommonCartridgeExportService } from '../service/common-cartridge-export export class CourseExportUc { constructor( private readonly courseExportService: CommonCartridgeExportService, - private readonly authorizationService: AuthorizationService + private readonly authorizationService: AuthorizationReferenceService ) {} async exportCourse(courseId: EntityId, userId: EntityId, version: CommonCartridgeVersion): Promise { - await this.authorizationService.checkPermissionByReferences(userId, AuthorizableReferenceType.Course, courseId, { - action: Action.read, - requiredPermissions: [Permission.COURSE_EDIT], - }); + const context = AuthorizationContextBuilder.read([Permission.COURSE_EDIT]); + await this.authorizationService.checkPermissionByReferences( + userId, + AuthorizableReferenceType.Course, + courseId, + context + ); + return this.courseExportService.exportCourse(courseId, userId, version); } } diff --git a/apps/server/src/modules/learnroom/uc/lesson-copy.uc.spec.ts b/apps/server/src/modules/learnroom/uc/lesson-copy.uc.spec.ts index a00e0be6c26..49c7c53435c 100644 --- a/apps/server/src/modules/learnroom/uc/lesson-copy.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/lesson-copy.uc.spec.ts @@ -3,13 +3,12 @@ import { Configuration } from '@hpi-schul-cloud/commons'; import { ObjectId } from '@mikro-orm/mongodb'; import { ForbiddenException, InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BaseDO, Permission, User } from '@shared/domain'; +import { Permission } from '@shared/domain'; import { CourseRepo, LessonRepo, UserRepo } from '@shared/repo'; import { courseFactory, lessonFactory, setupEntities, userFactory } from '@shared/testing'; -import { Action, AuthorizableReferenceType, AuthorizationService } from '@src/modules/authorization'; -import { CopyElementType, CopyHelperService, CopyStatusEnum } from '@src/modules/copy-helper'; -import { EtherpadService, LessonCopyService } from '@src/modules/lesson/service'; -import { AuthorizableObject } from '@shared/domain/domain-object'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { CopyElementType, CopyHelperService, CopyStatusEnum } from '@modules/copy-helper'; +import { EtherpadService, LessonCopyService } from '@modules/lesson/service'; import { LessonCopyUC } from './lesson-copy.uc'; describe('lesson copy uc', () => { @@ -71,193 +70,286 @@ describe('lesson copy uc', () => { copyHelperService = module.get(CopyHelperService); }); - beforeEach(() => { - Configuration.set('FEATURE_COPY_SERVICE_ENABLED', true); + afterEach(() => { jest.resetAllMocks(); }); + // Please be careful the Configuration.set is effects all tests !!! + describe('copy lesson', () => { - const setup = () => { - const user = userFactory.buildWithId(); - const course = courseFactory.buildWithId({ teachers: [user] }); - const allLessons = lessonFactory.buildList(3, { course }); - const lesson = allLessons[0]; - - authorisation.getUserWithPermissions.mockResolvedValue(user); - lessonRepo.findById.mockResolvedValue(lesson); - lessonRepo.findAllByCourseIds.mockResolvedValue([allLessons, allLessons.length]); - lessonRepo.save.mockResolvedValue(undefined); - - courseRepo.findById.mockResolvedValue(course); - authorisation.hasPermission.mockReturnValue(true); - const copy = lessonFactory.buildWithId({ course }); - const status = { - title: 'lessonCopy', - type: CopyElementType.LESSON, - status: CopyStatusEnum.SUCCESS, - copyEntity: copy, - }; - lessonCopyService.copyLesson.mockResolvedValue(status); - lessonCopyService.updateCopiedEmbeddedTasks.mockReturnValue(status); - const lessonCopyName = 'Copy'; - copyHelperService.deriveCopyName.mockReturnValue(lessonCopyName); - - return { - user, - course, - lesson, - copy, - status, - lessonCopyName, - allLessons, - userId: user.id, + // missing tests + // when course repo is throw a error + // when lesson repo is throw a error + describe('when feature flag is disabled', () => { + const setup = () => { + Configuration.set('FEATURE_COPY_SERVICE_ENABLED', false); + + const user = userFactory.buildWithId(); + const course = courseFactory.buildWithId({ teachers: [user] }); + const lesson = lessonFactory.build({ course }); + + const parentParams = { courseId: course.id, userId: user.id }; + + return { + userId: user.id, + lessonId: lesson.id, + parentParams, + }; }; - }; - - it('should throw if copy feature is deactivated', async () => { - Configuration.set('FEATURE_COPY_SERVICE_ENABLED', false); - const { course, user, lesson, userId } = setup(); - await expect(uc.copyLesson(user.id, lesson.id, { courseId: course.id, userId })).rejects.toThrowError( - InternalServerErrorException - ); - }); - it('should fetch correct user', async () => { - const { course, user, lesson, userId } = setup(); - await uc.copyLesson(user.id, lesson.id, { courseId: course.id, userId }); - expect(authorisation.getUserWithPermissions).toBeCalledWith(user.id); - }); + it('should throw if copy feature is deactivated', async () => { + const { userId, lessonId, parentParams } = setup(); - it('should fetch correct lesson', async () => { - const { course, user, lesson, userId } = setup(); - await uc.copyLesson(user.id, lesson.id, { courseId: course.id, userId }); - expect(lessonRepo.findById).toBeCalledWith(lesson.id); + await expect(uc.copyLesson(userId, lessonId, parentParams)).rejects.toThrowError( + new InternalServerErrorException('Copy Feature not enabled') + ); + }); }); - it('should fetch destination course', async () => { - const { course, user, lesson, userId } = setup(); - await uc.copyLesson(user.id, lesson.id, { courseId: course.id, userId }); - expect(courseRepo.findById).toBeCalledWith(course.id); - }); + describe('when authorization resolve and no destination course is passed', () => { + const setup = () => { + Configuration.set('FEATURE_COPY_SERVICE_ENABLED', true); - it('should pass without destination course', async () => { - const { user, lesson, userId } = setup(); - await uc.copyLesson(user.id, lesson.id, { userId }); - expect(courseRepo.findById).not.toHaveBeenCalled(); - }); + const user = userFactory.buildWithId(); + const course = courseFactory.buildWithId({ teachers: [user] }); + const allLessons = lessonFactory.buildList(3, { course }); + const copy = lessonFactory.buildWithId({ course }); + + const lesson = allLessons[0]; + const status = { + title: 'lessonCopy', + type: CopyElementType.LESSON, + status: CopyStatusEnum.SUCCESS, + copyEntity: copy, + }; + const lessonCopyName = 'Copy'; + const parentParams = { userId: user.id }; + + authorisation.getUserWithPermissions.mockResolvedValueOnce(user); + authorisation.hasPermission.mockReturnValue(true); + + lessonRepo.findById.mockResolvedValueOnce(lesson); + lessonRepo.findAllByCourseIds.mockResolvedValueOnce([allLessons, allLessons.length]); + courseRepo.findById.mockResolvedValueOnce(course); + + lessonCopyService.copyLesson.mockResolvedValueOnce(status); + copyHelperService.deriveCopyName.mockReturnValueOnce(lessonCopyName); + + return { + user, + userId: user.id, + course, + courseId: course.id, + lessonId: lesson.id, + parentParams, + }; + }; + + it('should pass without destination course', async () => { + const { lessonId, userId, parentParams } = setup(); - it('should check authorisation for lesson', async () => { - const { course, user, lesson, userId } = setup(); - await uc.copyLesson(user.id, lesson.id, { courseId: course.id, userId }); - expect(authorisation.hasPermission).toBeCalledWith(user, lesson, { - action: Action.read, - requiredPermissions: [Permission.TOPIC_CREATE], + await uc.copyLesson(userId, lessonId, parentParams); + + expect(courseRepo.findById).not.toHaveBeenCalled(); }); - }); - it('should check authorisation for destination course', async () => { - const { course, user, lesson, userId } = setup(); - await uc.copyLesson(user.id, lesson.id, { courseId: course.id, userId }); - expect(authorisation.checkPermissionByReferences).toBeCalledWith( - user.id, - AuthorizableReferenceType.Course, - course.id, - { - action: Action.write, - requiredPermissions: [], - } - ); - }); + it('should pass authorisation check without destination course', async () => { + const { course, user, lessonId, userId, parentParams } = setup(); + + await uc.copyLesson(userId, lessonId, parentParams); - it('should pass authorisation check without destination course', async () => { - const { course, user, lesson, userId } = setup(); - await uc.copyLesson(user.id, lesson.id, { userId }); - expect(authorisation.hasPermission).not.toBeCalledWith(user, course, { - action: Action.write, - requiredPermissions: [], + const context = AuthorizationContextBuilder.write([]); + expect(authorisation.hasPermission).not.toBeCalledWith(user, course, context); }); }); - it('should call copy service', async () => { - const { course, user, lesson, lessonCopyName, userId } = setup(); - await uc.copyLesson(user.id, lesson.id, { courseId: course.id, userId }); - expect(lessonCopyService.copyLesson).toBeCalledWith({ - originalLessonId: lesson.id, - destinationCourse: course, - user, - copyName: lessonCopyName, + describe('when authorization resolve', () => { + const setup = () => { + Configuration.set('FEATURE_COPY_SERVICE_ENABLED', true); + + const user = userFactory.buildWithId(); + const course = courseFactory.buildWithId({ teachers: [user] }); + const allLessons = lessonFactory.buildList(3, { course }); + const copy = lessonFactory.buildWithId({ course }); + + const lesson = allLessons[0]; + const status = { + title: 'lessonCopy', + type: CopyElementType.LESSON, + status: CopyStatusEnum.SUCCESS, + copyEntity: copy, + }; + const lessonCopyName = 'Copy'; + const parentParams = { courseId: course.id, userId: user.id }; + + authorisation.getUserWithPermissions.mockResolvedValueOnce(user); + authorisation.hasPermission.mockReturnValue(true); + + lessonRepo.findById.mockResolvedValueOnce(lesson); + lessonRepo.findAllByCourseIds.mockResolvedValueOnce([allLessons, allLessons.length]); + courseRepo.findById.mockResolvedValueOnce(course); + + lessonCopyService.copyLesson.mockResolvedValueOnce(status); + // lessonCopyService.updateCopiedEmbeddedTasks.mockReturnValue(status); + copyHelperService.deriveCopyName.mockReturnValueOnce(lessonCopyName); + + return { + user, + userId: user.id, + course, + courseId: course.id, + lesson, + lessonId: lesson.id, + parentParams, + copy, + status, + lessonCopyName, + allLessons, + }; + }; + + it('should fetch correct user', async () => { + const { lessonId, userId, parentParams } = setup(); + + await uc.copyLesson(userId, lessonId, parentParams); + + expect(authorisation.getUserWithPermissions).toBeCalledWith(userId); }); - }); - it('should return status', async () => { - const { course, user, lesson, status, userId } = setup(); - const result = await uc.copyLesson(user.id, lesson.id, { courseId: course.id, userId }); - expect(result).toEqual(status); - }); + it('should fetch correct lesson', async () => { + const { lessonId, userId, parentParams } = setup(); - it('should use copyHelperService', async () => { - const { course, user, lesson, allLessons, userId } = setup(); - await uc.copyLesson(user.id, lesson.id, { courseId: course.id, userId }); - const existingNames = allLessons.map((l) => l.name); - expect(copyHelperService.deriveCopyName).toHaveBeenCalledWith(lesson.name, existingNames); - }); + await uc.copyLesson(userId, lessonId, parentParams); + + expect(lessonRepo.findById).toBeCalledWith(lessonId); + }); + + it('should fetch destination course', async () => { + const { course, lessonId, userId, parentParams } = setup(); + + await uc.copyLesson(userId, lessonId, parentParams); + + expect(courseRepo.findById).toBeCalledWith(course.id); + }); + + it('should check authorisation for lesson', async () => { + const { lessonId, userId, user, lesson, parentParams } = setup(); + + await uc.copyLesson(userId, lessonId, parentParams); + + const context = AuthorizationContextBuilder.read([Permission.TOPIC_CREATE]); + expect(authorisation.hasPermission).toBeCalledWith(user, lesson, context); + }); + + it('should check authorisation for destination course', async () => { + const { course, user, lessonId, userId, parentParams } = setup(); + + await uc.copyLesson(userId, lessonId, parentParams); + + const context = AuthorizationContextBuilder.write([]); + expect(authorisation.checkPermission).toBeCalledWith(user, course, context); + }); + + it('should call copy service', async () => { + const { course, user, lessonId, lessonCopyName, userId, parentParams } = setup(); + + await uc.copyLesson(userId, lessonId, parentParams); + + expect(lessonCopyService.copyLesson).toBeCalledWith({ + originalLessonId: lessonId, + destinationCourse: course, + user, + copyName: lessonCopyName, + }); + }); - it('should use findAllByCourseIds to determine existing lesson names', async () => { - const { course, user, lesson, userId } = setup(); - await uc.copyLesson(user.id, lesson.id, { courseId: course.id, userId }); - expect(lessonRepo.findAllByCourseIds).toHaveBeenCalledWith([course.id]); + it('should return status', async () => { + const { lessonId, status, userId, parentParams } = setup(); + + const result = await uc.copyLesson(userId, lessonId, parentParams); + + expect(result).toEqual(status); + }); + + it('should use copyHelperService', async () => { + const { lessonId, allLessons, userId, lesson, parentParams } = setup(); + + await uc.copyLesson(userId, lessonId, parentParams); + + const existingNames = allLessons.map((l) => l.name); + expect(copyHelperService.deriveCopyName).toHaveBeenCalledWith(lesson.name, existingNames); + }); + + it('should use findAllByCourseIds to determine existing lesson names', async () => { + const { courseId, userId, lessonId, parentParams } = setup(); + + await uc.copyLesson(userId, lessonId, parentParams); + + expect(lessonRepo.findAllByCourseIds).toHaveBeenCalledWith([courseId]); + }); }); - describe('when access to lesson is forbidden', () => { - const setupWithLessonForbidden = () => { + describe('when authorization of lesson throw forbidden exception', () => { + const setup = () => { + Configuration.set('FEATURE_COPY_SERVICE_ENABLED', true); + const user = userFactory.buildWithId(); const course = courseFactory.buildWithId(); const lesson = lessonFactory.buildWithId(); - userRepo.findById.mockResolvedValue(user); - lessonRepo.findById.mockResolvedValue(lesson); - // authorisation should not be mocked - authorisation.hasPermission.mockImplementation((u: User, e: AuthorizableObject | BaseDO) => e !== lesson); - return { user, course, lesson }; + const parentParams = { courseId: course.id, userId: new ObjectId().toHexString() }; + + userRepo.findById.mockResolvedValueOnce(user); + lessonRepo.findById.mockResolvedValueOnce(lesson); + courseRepo.findById.mockResolvedValueOnce(course); + authorisation.hasPermission.mockReturnValueOnce(false); + + return { + userId: user.id, + lessonId: lesson.id, + parentParams, + }; }; - it('should throw NotFoundException', async () => { - const { course, user, lesson } = setupWithLessonForbidden(); + it('should throw ForbiddenException', async () => { + const { parentParams, userId, lessonId } = setup(); - try { - await uc.copyLesson(user.id, lesson.id, { courseId: course.id, userId: new ObjectId().toHexString() }); - throw new Error('should have failed'); - } catch (err) { - expect(err).toBeInstanceOf(ForbiddenException); - } + await expect(uc.copyLesson(userId, lessonId, parentParams)).rejects.toThrowError( + new ForbiddenException('could not find lesson to copy') + ); }); }); - describe('when access to course is forbidden', () => { - const setupWithCourseForbidden = () => { + describe('when authorization of course throw with forbidden exception', () => { + const setup = () => { + Configuration.set('FEATURE_COPY_SERVICE_ENABLED', true); + const user = userFactory.buildWithId(); const course = courseFactory.buildWithId(); const lesson = lessonFactory.buildWithId(); - userRepo.findById.mockResolvedValue(user); - lessonRepo.findById.mockResolvedValue(lesson); - courseRepo.findById.mockResolvedValue(course); - authorisation.hasPermission.mockReturnValue(true); - authorisation.checkPermissionByReferences.mockImplementation(() => { + + const parentParams = { courseId: course.id, userId: new ObjectId().toHexString() }; + + userRepo.findById.mockResolvedValueOnce(user); + lessonRepo.findById.mockResolvedValueOnce(lesson); + courseRepo.findById.mockResolvedValueOnce(course); + authorisation.checkPermission.mockImplementationOnce(() => { throw new ForbiddenException(); }); - return { user, course, lesson }; + authorisation.hasPermission.mockReturnValueOnce(true); + + return { + userId: user.id, + lessonId: lesson.id, + parentParams, + }; }; - it('should throw Forbidden Exception', async () => { - const { course, user, lesson } = setupWithCourseForbidden(); + it('should pass the forbidden exception', async () => { + const { parentParams, userId, lessonId } = setup(); - try { - await uc.copyLesson(user.id, lesson.id, { courseId: course.id, userId: new ObjectId().toHexString() }); - throw new Error('should have failed'); - } catch (err) { - expect(err).toBeInstanceOf(ForbiddenException); - } + await expect(uc.copyLesson(userId, lessonId, parentParams)).rejects.toThrowError(new ForbiddenException()); }); }); }); diff --git a/apps/server/src/modules/learnroom/uc/lesson-copy.uc.ts b/apps/server/src/modules/learnroom/uc/lesson-copy.uc.ts index 753200a5718..ae41fb6881b 100644 --- a/apps/server/src/modules/learnroom/uc/lesson-copy.uc.ts +++ b/apps/server/src/modules/learnroom/uc/lesson-copy.uc.ts @@ -1,17 +1,12 @@ import { Configuration } from '@hpi-schul-cloud/commons'; import { ForbiddenException, Injectable, InternalServerErrorException } from '@nestjs/common'; -import { EntityId } from '@shared/domain'; +import { Course, EntityId, LessonEntity, User } from '@shared/domain'; import { Permission } from '@shared/domain/interface/permission.enum'; import { CourseRepo, LessonRepo } from '@shared/repo'; -import { - Action, - AuthorizableReferenceType, - AuthorizationContextBuilder, - AuthorizationService, -} from '@src/modules/authorization'; -import { CopyHelperService, CopyStatus } from '@src/modules/copy-helper'; -import { LessonCopyParentParams } from '@src/modules/lesson'; -import { LessonCopyService } from '@src/modules/lesson/service'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { CopyHelperService, CopyStatus } from '@modules/copy-helper'; +import { LessonCopyParentParams } from '@modules/lesson'; +import { LessonCopyService } from '@modules/lesson/service'; @Injectable() export class LessonCopyUC { @@ -24,27 +19,24 @@ export class LessonCopyUC { ) {} async copyLesson(userId: EntityId, lessonId: EntityId, parentParams: LessonCopyParentParams): Promise { - this.featureEnabled(); - const user = await this.authorisation.getUserWithPermissions(userId); - const originalLesson = await this.lessonRepo.findById(lessonId); - const context = AuthorizationContextBuilder.read([Permission.TOPIC_CREATE]); - if (!this.authorisation.hasPermission(user, originalLesson, context)) { - throw new ForbiddenException('could not find lesson to copy'); - } + this.checkFeatureEnabled(); + + const [user, originalLesson]: [User, LessonEntity] = await Promise.all([ + this.authorisation.getUserWithPermissions(userId), + this.lessonRepo.findById(lessonId), + ]); + this.checkOriginalLessonAuthorization(user, originalLesson); + + // should be a seperate private method const destinationCourse = parentParams.courseId ? await this.courseRepo.findById(parentParams.courseId) : originalLesson.course; - await this.authorisation.checkPermissionByReferences( - userId, - AuthorizableReferenceType.Course, - destinationCourse.id, - { - action: Action.write, - requiredPermissions: [], - } - ); + // --- + + this.checkDestinationCourseAuthorization(user, destinationCourse); + // should be a seperate private method const [existingLessons] = await this.lessonRepo.findAllByCourseIds([originalLesson.course.id]); const existingNames = existingLessons.map((l) => l.name); const copyName = this.copyHelperService.deriveCopyName(originalLesson.name, existingNames); @@ -55,11 +47,25 @@ export class LessonCopyUC { user, copyName, }); + // --- return copyStatus; } - private featureEnabled() { + private checkOriginalLessonAuthorization(user: User, originalLesson: LessonEntity): void { + const contextReadWithTopicCreate = AuthorizationContextBuilder.read([Permission.TOPIC_CREATE]); + if (!this.authorisation.hasPermission(user, originalLesson, contextReadWithTopicCreate)) { + // error message is not correct, switch to authorisation.checkPermission() makse sense for me + throw new ForbiddenException('could not find lesson to copy'); + } + } + + private checkDestinationCourseAuthorization(user: User, destinationCourse: Course): void { + const contextCanWrite = AuthorizationContextBuilder.write([]); + this.authorisation.checkPermission(user, destinationCourse, contextCanWrite); + } + + private checkFeatureEnabled() { const enabled = Configuration.get('FEATURE_COPY_SERVICE_ENABLED') as boolean; if (!enabled) { throw new InternalServerErrorException('Copy Feature not enabled'); diff --git a/apps/server/src/modules/learnroom/uc/room-board-dto.factory.spec.ts b/apps/server/src/modules/learnroom/uc/room-board-dto.factory.spec.ts index 9c7ddf0c742..f983342f4f8 100644 --- a/apps/server/src/modules/learnroom/uc/room-board-dto.factory.spec.ts +++ b/apps/server/src/modules/learnroom/uc/room-board-dto.factory.spec.ts @@ -13,7 +13,7 @@ import { taskFactory, userFactory, } from '@shared/testing'; -import { AuthorizationService } from '@src/modules/authorization'; +import { AuthorizationService } from '@modules/authorization'; import { LessonMetaData } from '../types'; import { RoomBoardDTOFactory } from './room-board-dto.factory'; import { RoomsAuthorisationService } from './rooms.authorisation.service'; diff --git a/apps/server/src/modules/learnroom/uc/room-board-dto.factory.ts b/apps/server/src/modules/learnroom/uc/room-board-dto.factory.ts index 98a0957f3d3..91bd043d472 100644 --- a/apps/server/src/modules/learnroom/uc/room-board-dto.factory.ts +++ b/apps/server/src/modules/learnroom/uc/room-board-dto.factory.ts @@ -14,8 +14,7 @@ import { TaskWithStatusVo, User, } from '@shared/domain'; -import { AuthorizationService } from '@src/modules/authorization/authorization.service'; -import { Action } from '@src/modules/authorization/types/action.enum'; +import { AuthorizationService, Action } from '@modules/authorization'; import { ColumnBoardMetaData, LessonMetaData, diff --git a/apps/server/src/modules/legacy-school/controller/legacy-school.controller.spec.ts b/apps/server/src/modules/legacy-school/controller/legacy-school.controller.spec.ts index 1777ad02faa..764d71b6abf 100644 --- a/apps/server/src/modules/legacy-school/controller/legacy-school.controller.spec.ts +++ b/apps/server/src/modules/legacy-school/controller/legacy-school.controller.spec.ts @@ -1,7 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; -import { ICurrentUser } from '@src/modules/authentication'; +import { ICurrentUser } from '@modules/authentication'; import { MigrationMapper } from '../mapper/migration.mapper'; import { OauthMigrationDto } from '../uc/dto/oauth-migration.dto'; import { LegacySchoolUc } from '../uc'; diff --git a/apps/server/src/modules/legacy-school/controller/legacy-school.controller.ts b/apps/server/src/modules/legacy-school/controller/legacy-school.controller.ts index a91d8662752..58b591faea1 100644 --- a/apps/server/src/modules/legacy-school/controller/legacy-school.controller.ts +++ b/apps/server/src/modules/legacy-school/controller/legacy-school.controller.ts @@ -6,8 +6,7 @@ import { ApiTags, ApiUnauthorizedResponse, } from '@nestjs/swagger'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; +import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; import { MigrationMapper } from '../mapper/migration.mapper'; import { OauthMigrationDto } from '../uc/dto/oauth-migration.dto'; import { LegacySchoolUc } from '../uc'; diff --git a/apps/server/src/modules/legacy-school/legacy-school-api.module.ts b/apps/server/src/modules/legacy-school/legacy-school-api.module.ts index 3072fa8f6ca..aaf1f6acad2 100644 --- a/apps/server/src/modules/legacy-school/legacy-school-api.module.ts +++ b/apps/server/src/modules/legacy-school/legacy-school-api.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; -import { AuthorizationModule } from '@src/modules/authorization'; +import { AuthorizationModule } from '@modules/authorization'; import { LoggerModule } from '@src/core/logger'; -import { UserLoginMigrationModule } from '@src/modules/user-login-migration'; +import { UserLoginMigrationModule } from '@modules/user-login-migration'; import { LegacySchoolUc } from './uc'; import { LegacySchoolModule } from './legacy-school.module'; import { LegacySchoolController } from './controller/legacy-school.controller'; diff --git a/apps/server/src/modules/legacy-school/service/school-year.service.spec.ts b/apps/server/src/modules/legacy-school/service/school-year.service.spec.ts index 00e47a6360f..041b80d41d1 100644 --- a/apps/server/src/modules/legacy-school/service/school-year.service.spec.ts +++ b/apps/server/src/modules/legacy-school/service/school-year.service.spec.ts @@ -1,10 +1,10 @@ -import { Test, TestingModule } from '@nestjs/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { SchoolYearEntity } from '@shared/domain'; import { setupEntities } from '@shared/testing'; import { schoolYearFactory } from '@shared/testing/factory/schoolyear.factory'; -import { SchoolYearEntity } from '@shared/domain'; -import { SchoolYearService } from './school-year.service'; import { SchoolYearRepo } from '../repo'; +import { SchoolYearService } from './school-year.service'; describe('SchoolYearService', () => { let module: TestingModule; @@ -57,4 +57,30 @@ describe('SchoolYearService', () => { }); }); }); + + describe('findById', () => { + const setup = () => { + jest.setSystemTime(new Date('2022-06-01').getTime()); + const schoolYear: SchoolYearEntity = schoolYearFactory.build({ + startDate: new Date('2021-09-01'), + endDate: new Date('2022-12-31'), + }); + + schoolYearRepo.findById.mockResolvedValue(schoolYear); + + return { + schoolYear, + }; + }; + + describe('when called', () => { + it('should return the current school year', async () => { + const { schoolYear } = setup(); + + const currentSchoolYear: SchoolYearEntity = await service.findById(schoolYear.id); + + expect(currentSchoolYear).toEqual(schoolYear); + }); + }); + }); }); diff --git a/apps/server/src/modules/legacy-school/service/school-year.service.ts b/apps/server/src/modules/legacy-school/service/school-year.service.ts index 16cae1c1cff..c153122e5d1 100644 --- a/apps/server/src/modules/legacy-school/service/school-year.service.ts +++ b/apps/server/src/modules/legacy-school/service/school-year.service.ts @@ -1,5 +1,5 @@ import { Injectable } from '@nestjs/common'; -import { SchoolYearEntity } from '@shared/domain'; +import { EntityId, SchoolYearEntity } from '@shared/domain'; import { SchoolYearRepo } from '../repo'; @Injectable() @@ -12,4 +12,10 @@ export class SchoolYearService { return current; } + + async findById(id: EntityId): Promise { + const year: SchoolYearEntity = await this.schoolYearRepo.findById(id); + + return year; + } } diff --git a/apps/server/src/modules/legacy-school/uc/legacy-school.uc.spec.ts b/apps/server/src/modules/legacy-school/uc/legacy-school.uc.spec.ts index 17dc2de5fd0..138bcd81a0a 100644 --- a/apps/server/src/modules/legacy-school/uc/legacy-school.uc.spec.ts +++ b/apps/server/src/modules/legacy-school/uc/legacy-school.uc.spec.ts @@ -3,14 +3,14 @@ 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 { AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/legacy-school/service'; -import { LegacySchoolUc } from '@src/modules/legacy-school/uc'; +import { AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school/service'; +import { LegacySchoolUc } from '@modules/legacy-school/uc'; import { SchoolMigrationService, UserLoginMigrationRevertService, UserLoginMigrationService, -} from '@src/modules/user-login-migration'; +} from '@modules/user-login-migration'; import { OauthMigrationDto } from './dto/oauth-migration.dto'; describe('LegacySchoolUc', () => { @@ -66,6 +66,9 @@ describe('LegacySchoolUc', () => { jest.resetAllMocks(); }); + // Tests with case of authService.checkPermission.mockImplementation(() => throw new ForbiddenException()); + // are missed for both methodes + describe('setMigration is called', () => { describe('when first starting the migration', () => { const setup = () => { @@ -77,7 +80,7 @@ describe('LegacySchoolUc', () => { }); userLoginMigrationService.findMigrationBySchool.mockResolvedValue(null); - authService.checkPermissionByReferences.mockImplementation(() => Promise.resolve()); + authService.checkPermission.mockReturnValueOnce(); schoolService.getSchoolById.mockResolvedValue(school); userLoginMigrationService.setMigration.mockResolvedValue(userLoginMigration); }; @@ -107,7 +110,7 @@ describe('LegacySchoolUc', () => { }); userLoginMigrationService.findMigrationBySchool.mockResolvedValue(userLoginMigration); - authService.checkPermissionByReferences.mockImplementation(() => Promise.resolve()); + authService.checkPermission.mockReturnValueOnce(); schoolService.getSchoolById.mockResolvedValue(school); userLoginMigrationService.setMigration.mockResolvedValue(updatedUserLoginMigration); schoolMigrationService.hasSchoolMigratedUser.mockResolvedValue(true); @@ -138,7 +141,7 @@ describe('LegacySchoolUc', () => { }); userLoginMigrationService.findMigrationBySchool.mockResolvedValue(userLoginMigration); - authService.checkPermissionByReferences.mockImplementation(() => Promise.resolve()); + authService.checkPermission.mockReturnValueOnce(); schoolService.getSchoolById.mockResolvedValue(school); userLoginMigrationService.setMigration.mockResolvedValue(updatedUserLoginMigration); schoolMigrationService.hasSchoolMigratedUser.mockResolvedValue(false); @@ -177,7 +180,7 @@ describe('LegacySchoolUc', () => { }); userLoginMigrationService.findMigrationBySchool.mockResolvedValue(userLoginMigration); - authService.checkPermissionByReferences.mockImplementation(() => Promise.resolve()); + authService.checkPermission.mockReturnValueOnce(); schoolService.getSchoolById.mockResolvedValue(school); userLoginMigrationService.setMigration.mockResolvedValue(updatedUserLoginMigration); }; @@ -208,7 +211,7 @@ describe('LegacySchoolUc', () => { }); userLoginMigrationService.findMigrationBySchool.mockResolvedValue(userLoginMigration); - authService.checkPermissionByReferences.mockImplementation(() => Promise.resolve()); + authService.checkPermission.mockReturnValueOnce(); schoolService.getSchoolById.mockResolvedValue(school); userLoginMigrationService.setMigration.mockResolvedValue(updatedUserLoginMigration); schoolMigrationService.validateGracePeriod.mockImplementation(() => { @@ -241,7 +244,7 @@ describe('LegacySchoolUc', () => { userLoginMigrationService.findMigrationBySchool.mockResolvedValue(userLoginMigration); schoolService.getSchoolById.mockResolvedValue(school); - authService.checkPermissionByReferences.mockImplementation(() => Promise.resolve()); + authService.checkPermission.mockReturnValueOnce(); }; it('should return a migration', async () => { @@ -265,7 +268,7 @@ describe('LegacySchoolUc', () => { userLoginMigrationService.findMigrationBySchool.mockResolvedValue(null); schoolService.getSchoolById.mockResolvedValue(school); - authService.checkPermissionByReferences.mockImplementation(() => Promise.resolve()); + authService.checkPermission.mockReturnValueOnce(); }; it('should return no migration information', async () => { diff --git a/apps/server/src/modules/legacy-school/uc/legacy-school.uc.ts b/apps/server/src/modules/legacy-school/uc/legacy-school.uc.ts index 2ccc9dc5698..50fd7faf5a5 100644 --- a/apps/server/src/modules/legacy-school/uc/legacy-school.uc.ts +++ b/apps/server/src/modules/legacy-school/uc/legacy-school.uc.ts @@ -1,11 +1,11 @@ import { Injectable } from '@nestjs/common'; -import { Permission, LegacySchoolDo, UserLoginMigrationDO } from '@shared/domain'; -import { Action, AuthorizableReferenceType, AuthorizationService } from '@src/modules/authorization'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { Permission, LegacySchoolDo, UserLoginMigrationDO, User } from '@shared/domain'; import { SchoolMigrationService, UserLoginMigrationRevertService, UserLoginMigrationService, -} from '@src/modules/user-login-migration'; +} from '@modules/user-login-migration'; import { LegacySchoolService } from '../service'; import { OauthMigrationDto } from './dto/oauth-migration.dto'; @@ -30,10 +30,12 @@ export class LegacySchoolUc { oauthMigrationFinished: boolean, userId: string ): Promise { - await this.authService.checkPermissionByReferences(userId, AuthorizableReferenceType.School, schoolId, { - action: Action.read, - requiredPermissions: [Permission.SCHOOL_EDIT], - }); + const [authorizableUser, school]: [User, LegacySchoolDo] = await Promise.all([ + this.authService.getUserWithPermissions(userId), + this.schoolService.getSchoolById(schoolId), + ]); + + this.checkSchoolAuthorization(authorizableUser, school); const existingUserLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationService.findMigrationBySchool(schoolId); @@ -61,8 +63,6 @@ export class LegacySchoolUc { await this.schoolMigrationService.unmarkOutdatedUsers(schoolId); } - const school: LegacySchoolDo = await this.schoolService.getSchoolById(schoolId); - const migrationDto: OauthMigrationDto = new OauthMigrationDto({ oauthMigrationPossible: !updatedUserLoginMigration.closedAt ? updatedUserLoginMigration.startedAt : undefined, oauthMigrationMandatory: updatedUserLoginMigration.mandatorySince, @@ -75,17 +75,17 @@ export class LegacySchoolUc { } async getMigration(schoolId: string, userId: string): Promise { - await this.authService.checkPermissionByReferences(userId, AuthorizableReferenceType.School, schoolId, { - action: Action.read, - requiredPermissions: [Permission.SCHOOL_EDIT], - }); + const [authorizableUser, school]: [User, LegacySchoolDo] = await Promise.all([ + this.authService.getUserWithPermissions(userId), + this.schoolService.getSchoolById(schoolId), + ]); + + this.checkSchoolAuthorization(authorizableUser, school); const userLoginMigration: UserLoginMigrationDO | null = await this.userLoginMigrationService.findMigrationBySchool( schoolId ); - const school: LegacySchoolDo = await this.schoolService.getSchoolById(schoolId); - const migrationDto: OauthMigrationDto = new OauthMigrationDto({ oauthMigrationPossible: userLoginMigration && !userLoginMigration.closedAt ? userLoginMigration.startedAt : undefined, @@ -97,4 +97,9 @@ export class LegacySchoolUc { return migrationDto; } + + private checkSchoolAuthorization(authorizableUser: User, school: LegacySchoolDo): void { + const context = AuthorizationContextBuilder.read([Permission.SCHOOL_EDIT]); + this.authService.checkPermission(authorizableUser, school, context); + } } diff --git a/apps/server/src/modules/lesson/controller/api-test/lesson-delete.api.spec.ts b/apps/server/src/modules/lesson/controller/api-test/lesson-delete.api.spec.ts index 96b9875cf15..d4a707420de 100644 --- a/apps/server/src/modules/lesson/controller/api-test/lesson-delete.api.spec.ts +++ b/apps/server/src/modules/lesson/controller/api-test/lesson-delete.api.spec.ts @@ -10,8 +10,8 @@ import { TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; -import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; -import { ServerTestModule } from '@src/modules/server'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; +import { ServerTestModule } from '@modules/server'; import { ObjectId } from 'bson'; describe('Lesson Controller (API) - delete', () => { diff --git a/apps/server/src/modules/lesson/controller/lesson.controller.ts b/apps/server/src/modules/lesson/controller/lesson.controller.ts index 8082060cbb7..14762a47d8c 100644 --- a/apps/server/src/modules/lesson/controller/lesson.controller.ts +++ b/apps/server/src/modules/lesson/controller/lesson.controller.ts @@ -1,7 +1,6 @@ import { Controller, Delete, Param } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; +import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; import { LessonUC } from '../uc'; import { LessonUrlParams } from './dto'; diff --git a/apps/server/src/modules/lesson/lesson-api.module.ts b/apps/server/src/modules/lesson/lesson-api.module.ts index 6185b5da2a4..1f17893f582 100644 --- a/apps/server/src/modules/lesson/lesson-api.module.ts +++ b/apps/server/src/modules/lesson/lesson-api.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { AuthorizationModule } from '@src/modules/authorization'; +import { AuthorizationModule } from '@modules/authorization'; import { LessonController } from './controller'; import { LessonModule } from './lesson.module'; import { LessonUC } from './uc'; diff --git a/apps/server/src/modules/lesson/lesson.module.ts b/apps/server/src/modules/lesson/lesson.module.ts index 021f4ab4efa..2e246c63211 100644 --- a/apps/server/src/modules/lesson/lesson.module.ts +++ b/apps/server/src/modules/lesson/lesson.module.ts @@ -2,9 +2,9 @@ import { Module } from '@nestjs/common'; import { FeathersServiceProvider } from '@shared/infra/feathers'; import { LessonRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; -import { CopyHelperModule } from '@src/modules/copy-helper'; -import { FilesStorageClientModule } from '@src/modules/files-storage-client'; -import { TaskModule } from '@src/modules/task'; +import { CopyHelperModule } from '@modules/copy-helper'; +import { FilesStorageClientModule } from '@modules/files-storage-client'; +import { TaskModule } from '@modules/task'; import { EtherpadService, LessonCopyService, LessonService, NexboardService } from './service'; @Module({ diff --git a/apps/server/src/modules/lesson/service/lesson-copy.service.spec.ts b/apps/server/src/modules/lesson/service/lesson-copy.service.spec.ts index 39f3f07882a..34392c91c0c 100644 --- a/apps/server/src/modules/lesson/service/lesson-copy.service.spec.ts +++ b/apps/server/src/modules/lesson/service/lesson-copy.service.spec.ts @@ -24,9 +24,9 @@ import { taskFactory, userFactory, } from '@shared/testing'; -import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@src/modules/copy-helper'; -import { CopyFilesService } from '@src/modules/files-storage-client'; -import { TaskCopyService } from '@src/modules/task/service'; +import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; +import { CopyFilesService } from '@modules/files-storage-client'; +import { TaskCopyService } from '@modules/task/service'; import { EtherpadService } from './etherpad.service'; import { LessonCopyService } from './lesson-copy.service'; import { NexboardService } from './nexboard.service'; @@ -531,6 +531,55 @@ describe('lesson copy service', () => { }); }); + describe('when lesson contains LernStore content element without set resource', () => { + const setup = () => { + const lernStoreContent: IComponentProperties = { + title: 'text component 1', + hidden: false, + component: ComponentType.LERNSTORE, + }; + const user = userFactory.build(); + const originalCourse = courseFactory.build({ school: user.school }); + const destinationCourse = courseFactory.build({ school: user.school, teachers: [user] }); + const originalLesson = lessonFactory.build({ + course: originalCourse, + contents: [lernStoreContent], + }); + lessonRepo.findById.mockResolvedValueOnce(originalLesson); + + return { user, originalCourse, destinationCourse, originalLesson, lernStoreContent }; + }; + + it('the content should be fully copied', async () => { + const { user, destinationCourse, originalLesson, lernStoreContent } = setup(); + + const status = await copyService.copyLesson({ + originalLessonId: originalLesson.id, + destinationCourse, + user, + }); + + const copiedLessonContents = (status.copyEntity as LessonEntity).contents as IComponentProperties[]; + expect(copiedLessonContents[0]).toEqual(lernStoreContent); + }); + + it('should set content type to LESSON_CONTENT_LERNSTORE', async () => { + const { user, destinationCourse, originalLesson } = setup(); + + const status = await copyService.copyLesson({ + originalLessonId: originalLesson.id, + destinationCourse, + user, + }); + const contentsStatus = status.elements?.find((el) => el.type === CopyElementType.LESSON_CONTENT_GROUP); + expect(contentsStatus).toBeDefined(); + if (contentsStatus?.elements) { + expect(contentsStatus.elements[0].type).toEqual(CopyElementType.LESSON_CONTENT_LERNSTORE); + expect(contentsStatus.elements[0].status).toEqual(CopyStatusEnum.SUCCESS); + } + }); + }); + describe('when lesson contains geoGebra content element', () => { const setup = () => { const geoGebraContent: IComponentProperties = { diff --git a/apps/server/src/modules/lesson/service/lesson-copy.service.ts b/apps/server/src/modules/lesson/service/lesson-copy.service.ts index 4a6835b05f2..b6d7e7849c7 100644 --- a/apps/server/src/modules/lesson/service/lesson-copy.service.ts +++ b/apps/server/src/modules/lesson/service/lesson-copy.service.ts @@ -12,16 +12,10 @@ import { Material, } from '@shared/domain'; import { LessonRepo } from '@shared/repo'; -import { - CopyDictionary, - CopyElementType, - CopyHelperService, - CopyStatus, - CopyStatusEnum, -} from '@src/modules/copy-helper'; -import { CopyFilesService } from '@src/modules/files-storage-client'; -import { FileUrlReplacement } from '@src/modules/files-storage-client/service/copy-files.service'; -import { TaskCopyService } from '@src/modules/task/service/task-copy.service'; +import { CopyDictionary, CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; +import { CopyFilesService } from '@modules/files-storage-client'; +import { FileUrlReplacement } from '@modules/files-storage-client/service/copy-files.service'; +import { TaskCopyService } from '@modules/task/service/task-copy.service'; import { randomBytes } from 'crypto'; import { LessonCopyParams } from '../types'; import { EtherpadService } from './etherpad.service'; @@ -266,29 +260,32 @@ export class LessonCopyService { } private copyLernStore(element: IComponentProperties): IComponentProperties { - const resources = ((element.content as IComponentLernstoreProperties).resources ?? []).map( - ({ client, description, merlinReference, title, url }) => { - const result = { - client, - description, - merlinReference, - title, - url, - }; - return result; - } - ); - - const lernstore = { + const lernstore: IComponentProperties = { title: element.title, hidden: element.hidden, component: ComponentType.LERNSTORE, user: element.user, // TODO should be params.user - but that made the server crash, but property is normally undefined - content: { - resources, - }, }; - return lernstore as IComponentProperties; + + if (element.content) { + const resources = ((element.content as IComponentLernstoreProperties).resources ?? []).map( + ({ client, description, merlinReference, title, url }) => { + const result = { + client, + description, + merlinReference, + title, + url, + }; + return result; + } + ); + + const lernstoreContent: IComponentLernstoreProperties = { resources }; + lernstore.content = lernstoreContent; + } + + return lernstore; } private static copyGeogebra(originalElement: IComponentProperties): IComponentProperties { diff --git a/apps/server/src/modules/lesson/service/lesson.service.spec.ts b/apps/server/src/modules/lesson/service/lesson.service.spec.ts index 7f0179640b5..a94ecfe9c8b 100644 --- a/apps/server/src/modules/lesson/service/lesson.service.spec.ts +++ b/apps/server/src/modules/lesson/service/lesson.service.spec.ts @@ -2,7 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { LessonRepo } from '@shared/repo'; import { lessonFactory, setupEntities } from '@shared/testing'; -import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; import { ObjectId } from '@mikro-orm/mongodb'; import { ComponentType, IComponentProperties } from '@shared/domain'; import { LessonService } from './lesson.service'; diff --git a/apps/server/src/modules/lesson/service/lesson.service.ts b/apps/server/src/modules/lesson/service/lesson.service.ts index 5a69a19f305..2dee6f05563 100644 --- a/apps/server/src/modules/lesson/service/lesson.service.ts +++ b/apps/server/src/modules/lesson/service/lesson.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { Counted, EntityId, IComponentProperties, LessonEntity } from '@shared/domain'; import { LessonRepo } from '@shared/repo'; -import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; @Injectable() export class LessonService { diff --git a/apps/server/src/modules/lesson/uc/lesson.uc.spec.ts b/apps/server/src/modules/lesson/uc/lesson.uc.spec.ts index cd6b347b048..72448f8b770 100644 --- a/apps/server/src/modules/lesson/uc/lesson.uc.spec.ts +++ b/apps/server/src/modules/lesson/uc/lesson.uc.spec.ts @@ -2,7 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain'; import { lessonFactory, setupEntities, userFactory } from '@shared/testing'; -import { AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { LessonService } from '../service'; import { LessonUC } from './lesson.uc'; diff --git a/apps/server/src/modules/lesson/uc/lesson.uc.ts b/apps/server/src/modules/lesson/uc/lesson.uc.ts index 68519a4e0e1..063a43ce9b9 100644 --- a/apps/server/src/modules/lesson/uc/lesson.uc.ts +++ b/apps/server/src/modules/lesson/uc/lesson.uc.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { EntityId, Permission } from '@shared/domain'; -import { AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { LessonService } from '../service'; @Injectable() diff --git a/apps/server/src/modules/management/controller/api-test/database-management.api.spec.ts b/apps/server/src/modules/management/controller/api-test/database-management.api.spec.ts index d4c9967dcf4..012ee8f3cf2 100644 --- a/apps/server/src/modules/management/controller/api-test/database-management.api.spec.ts +++ b/apps/server/src/modules/management/controller/api-test/database-management.api.spec.ts @@ -1,7 +1,7 @@ import { MikroORM } from '@mikro-orm/core'; import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { ManagementServerTestModule } from '@src/modules/management/management-server.module'; +import { ManagementServerTestModule } from '@modules/management/management-server.module'; import request from 'supertest'; describe('Database Management Controller (API)', () => { diff --git a/apps/server/src/modules/management/management.module.ts b/apps/server/src/modules/management/management.module.ts index 3fb5bf4e3c2..fc4c8bc08d3 100644 --- a/apps/server/src/modules/management/management.module.ts +++ b/apps/server/src/modules/management/management.module.ts @@ -8,7 +8,7 @@ import { FileSystemModule } from '@shared/infra/file-system'; import { KeycloakConfigurationModule } from '@shared/infra/identity-management/keycloak-configuration/keycloak-configuration.module'; import { createConfigModuleOptions } from '@src/config'; import { LoggerModule } from '@src/core/logger'; -import { serverConfig } from '@src/modules/server'; +import { serverConfig } from '@modules/server'; import { BoardManagementConsole } from './console/board-management.console'; import { DatabaseManagementConsole } from './console/database-management.console'; import { DatabaseManagementController } from './controller/database-management.controller'; diff --git a/apps/server/src/modules/news/controller/api-test/news.api.spec.ts b/apps/server/src/modules/news/controller/api-test/news.api.spec.ts index 644dde5a371..31e4a10af9a 100644 --- a/apps/server/src/modules/news/controller/api-test/news.api.spec.ts +++ b/apps/server/src/modules/news/controller/api-test/news.api.spec.ts @@ -3,10 +3,10 @@ import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityId, News, NewsTargetModel } from '@shared/domain'; import { API_VALIDATION_ERROR_TYPE } from '@src/core/error/server-error-types'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { FeathersAuthorizationService } from '@src/modules/authorization'; -import { CreateNewsParams, NewsListResponse, NewsResponse, UpdateNewsParams } from '@src/modules/news/controller/dto'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { FeathersAuthorizationService } from '@modules/authorization'; +import { CreateNewsParams, NewsListResponse, NewsResponse, UpdateNewsParams } from '@modules/news/controller/dto'; +import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import moment from 'moment'; import request from 'supertest'; diff --git a/apps/server/src/modules/news/controller/news.controller.ts b/apps/server/src/modules/news/controller/news.controller.ts index 2d2781fbef5..2f1c227401a 100644 --- a/apps/server/src/modules/news/controller/news.controller.ts +++ b/apps/server/src/modules/news/controller/news.controller.ts @@ -1,8 +1,7 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; import { PaginationParams } from '@shared/controller'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; +import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; import { NewsMapper } from '../mapper/news.mapper'; import { NewsUc } from '../uc/news.uc'; import { diff --git a/apps/server/src/modules/news/controller/team-news.controller.ts b/apps/server/src/modules/news/controller/team-news.controller.ts index 7fad91e88c2..2344d6f2ac9 100644 --- a/apps/server/src/modules/news/controller/team-news.controller.ts +++ b/apps/server/src/modules/news/controller/team-news.controller.ts @@ -1,10 +1,7 @@ import { Controller, Get, Param, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { ICurrentUser } from '@src/modules/authentication'; - +import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; import { PaginationParams } from '@shared/controller'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; - import { NewsMapper } from '../mapper/news.mapper'; import { NewsUc } from '../uc'; import { FilterNewsParams, NewsListResponse, TeamUrlParams } from './dto'; diff --git a/apps/server/src/modules/news/news.module.ts b/apps/server/src/modules/news/news.module.ts index 261a4b2b6ba..3766b26052e 100644 --- a/apps/server/src/modules/news/news.module.ts +++ b/apps/server/src/modules/news/news.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { NewsRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; -import { AuthorizationModule } from '@src/modules/authorization'; +import { AuthorizationModule } from '@modules/authorization'; import { NewsController } from './controller/news.controller'; import { TeamNewsController } from './controller/team-news.controller'; import { NewsUc } from './uc/news.uc'; diff --git a/apps/server/src/modules/news/uc/news.uc.spec.ts b/apps/server/src/modules/news/uc/news.uc.spec.ts index 39e4c9af9f7..d0671dd4259 100644 --- a/apps/server/src/modules/news/uc/news.uc.spec.ts +++ b/apps/server/src/modules/news/uc/news.uc.spec.ts @@ -6,7 +6,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ICreateNews, NewsTargetModel, Permission } from '@shared/domain'; import { NewsRepo } from '@shared/repo'; import { Logger } from '@src/core/logger'; -import { FeathersAuthorizationService } from '@src/modules/authorization'; +import { FeathersAuthorizationService } from '@modules/authorization'; import { NewsUc } from './news.uc'; describe('NewsUc', () => { diff --git a/apps/server/src/modules/news/uc/news.uc.ts b/apps/server/src/modules/news/uc/news.uc.ts index 39aeb05f03b..de0608b0234 100644 --- a/apps/server/src/modules/news/uc/news.uc.ts +++ b/apps/server/src/modules/news/uc/news.uc.ts @@ -14,7 +14,7 @@ import { import { NewsRepo, NewsTargetFilter } from '@shared/repo'; import { CrudOperation } from '@shared/types'; import { Logger } from '@src/core/logger'; -import { FeathersAuthorizationService } from '@src/modules/authorization'; +import { FeathersAuthorizationService } from '@modules/authorization'; import { NewsCrudOperationLoggable } from '../loggable/news-crud-operation.loggable'; type NewsPermission = Permission.NEWS_VIEW | Permission.NEWS_EDIT; diff --git a/apps/server/src/modules/oauth-provider/controller/dto/request/oauth-client.body.ts b/apps/server/src/modules/oauth-provider/controller/dto/request/oauth-client.body.ts index 735ebc02d5f..277922526f7 100644 --- a/apps/server/src/modules/oauth-provider/controller/dto/request/oauth-client.body.ts +++ b/apps/server/src/modules/oauth-provider/controller/dto/request/oauth-client.body.ts @@ -1,7 +1,7 @@ import { IsArray, IsEnum, IsOptional, IsString } from 'class-validator'; import { ApiProperty } from '@nestjs/swagger'; -import { SubjectTypeEnum } from '@src/modules/oauth-provider/interface/subject-type.enum'; -import { TokenAuthMethod } from '@src/modules/oauth-provider/interface/token-auth-method.enum'; +import { SubjectTypeEnum } from '@modules/oauth-provider/interface/subject-type.enum'; +import { TokenAuthMethod } from '@modules/oauth-provider/interface/token-auth-method.enum'; export class OauthClientBody { @IsString() diff --git a/apps/server/src/modules/oauth-provider/controller/dto/response/consent.response.ts b/apps/server/src/modules/oauth-provider/controller/dto/response/consent.response.ts index 3953273c2bf..a5c90fe11a2 100644 --- a/apps/server/src/modules/oauth-provider/controller/dto/response/consent.response.ts +++ b/apps/server/src/modules/oauth-provider/controller/dto/response/consent.response.ts @@ -1,7 +1,7 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsArray, IsOptional, IsString } from 'class-validator'; -import { OidcContextResponse } from '@src/modules/oauth-provider/controller/dto/response/oidc-context.response'; -import { OauthClientResponse } from '@src/modules/oauth-provider/controller/dto/response/oauth-client.response'; +import { OidcContextResponse } from '@modules/oauth-provider/controller/dto/response/oidc-context.response'; +import { OauthClientResponse } from '@modules/oauth-provider/controller/dto/response/oauth-client.response'; export class ConsentResponse { constructor(consentResponse: ConsentResponse) { diff --git a/apps/server/src/modules/oauth-provider/controller/dto/response/login.response.ts b/apps/server/src/modules/oauth-provider/controller/dto/response/login.response.ts index ddde211e219..761276bdc5c 100644 --- a/apps/server/src/modules/oauth-provider/controller/dto/response/login.response.ts +++ b/apps/server/src/modules/oauth-provider/controller/dto/response/login.response.ts @@ -1,6 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; -import { OauthClientResponse } from '@src/modules/oauth-provider/controller/dto/response/oauth-client.response'; -import { OidcContextResponse } from '@src/modules/oauth-provider/controller/dto/response/oidc-context.response'; +import { OauthClientResponse } from '@modules/oauth-provider/controller/dto/response/oauth-client.response'; +import { OidcContextResponse } from '@modules/oauth-provider/controller/dto/response/oidc-context.response'; import { IsArray, IsOptional, IsString } from 'class-validator'; export class LoginResponse { diff --git a/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.spec.ts b/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.spec.ts index 7edec7ece8d..83a3e3ac47b 100644 --- a/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.spec.ts +++ b/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.spec.ts @@ -1,8 +1,8 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { OauthProviderLogoutFlowUc } from '@src/modules/oauth-provider/uc/oauth-provider.logout-flow.uc'; +import { OauthProviderLogoutFlowUc } from '@modules/oauth-provider/uc/oauth-provider.logout-flow.uc'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { OauthProviderResponseMapper } from '@src/modules/oauth-provider/mapper/oauth-provider-response.mapper'; +import { OauthProviderResponseMapper } from '@modules/oauth-provider/mapper/oauth-provider-response.mapper'; import { AcceptQuery, ChallengeParams, @@ -14,16 +14,16 @@ import { OauthClientBody, OauthClientResponse, RedirectResponse, -} from '@src/modules/oauth-provider/controller/dto'; +} from '@modules/oauth-provider/controller/dto'; import { ProviderConsentResponse, ProviderConsentSessionResponse, ProviderLoginResponse, ProviderRedirectResponse, } from '@shared/infra/oauth-provider/dto'; -import { OauthProviderConsentFlowUc } from '@src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc'; -import { ICurrentUser } from '@src/modules/authentication'; -import { OauthProviderUc } from '@src/modules/oauth-provider/uc/oauth-provider.uc'; +import { OauthProviderConsentFlowUc } from '@modules/oauth-provider/uc/oauth-provider.consent-flow.uc'; +import { ICurrentUser } from '@modules/authentication'; +import { OauthProviderUc } from '@modules/oauth-provider/uc/oauth-provider.uc'; import { OauthProviderController } from './oauth-provider.controller'; import { OauthProviderClientCrudUc } from '../uc/oauth-provider.client-crud.uc'; import { OauthProviderLoginFlowUc } from '../uc/oauth-provider.login-flow.uc'; diff --git a/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.ts b/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.ts index b603739357a..054cca37ffa 100644 --- a/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.ts +++ b/apps/server/src/modules/oauth-provider/controller/oauth-provider.controller.ts @@ -1,22 +1,22 @@ -import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { Configuration } from '@hpi-schul-cloud/commons'; import { Body, Controller, Delete, Get, Param, Patch, Post, Put, Query } from '@nestjs/common'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; -import { OauthProviderLogoutFlowUc } from '@src/modules/oauth-provider/uc/oauth-provider.logout-flow.uc'; -import { OauthProviderLoginFlowUc } from '@src/modules/oauth-provider/uc/oauth-provider.login-flow.uc'; -import { OauthProviderResponseMapper } from '@src/modules/oauth-provider/mapper/oauth-provider-response.mapper'; -import { OauthProviderConsentFlowUc } from '@src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc'; +import { ICurrentUser, Authenticate, CurrentUser } from '@modules/authentication'; +// import should be @shared/infra/oauth-provider import { ProviderConsentResponse, ProviderLoginResponse, ProviderOauthClient, ProviderRedirectResponse, + ProviderConsentSessionResponse, } from '@shared/infra/oauth-provider/dto'; -import { ConsentResponse } from '@src/modules/oauth-provider/controller/dto/response/consent.response'; -import { ICurrentUser } from '@src/modules/authentication'; -import { OauthProviderClientCrudUc } from '@src/modules/oauth-provider/uc/oauth-provider.client-crud.uc'; -import { RedirectResponse } from '@src/modules/oauth-provider/controller/dto/response/redirect.response'; -import { ProviderConsentSessionResponse } from '@shared/infra/oauth-provider/dto/response/consent-session.response'; import { ApiTags } from '@nestjs/swagger'; +import { OauthProviderLogoutFlowUc } from '../uc/oauth-provider.logout-flow.uc'; +import { OauthProviderLoginFlowUc } from '../uc/oauth-provider.login-flow.uc'; +import { OauthProviderResponseMapper } from '../mapper/oauth-provider-response.mapper'; +import { OauthProviderConsentFlowUc } from '../uc/oauth-provider.consent-flow.uc'; +import { ConsentResponse } from './dto/response/consent.response'; +import { OauthProviderClientCrudUc } from '../uc/oauth-provider.client-crud.uc'; +import { RedirectResponse } from './dto/response/redirect.response'; import { OauthProviderUc } from '../uc/oauth-provider.uc'; import { AcceptQuery, diff --git a/apps/server/src/modules/oauth-provider/mapper/oauth-provider-request.mapper.ts b/apps/server/src/modules/oauth-provider/mapper/oauth-provider-request.mapper.ts index c44bda25da6..e0d4c4aaef4 100644 --- a/apps/server/src/modules/oauth-provider/mapper/oauth-provider-request.mapper.ts +++ b/apps/server/src/modules/oauth-provider/mapper/oauth-provider-request.mapper.ts @@ -1,5 +1,5 @@ import { AcceptLoginRequestBody } from '@shared/infra/oauth-provider/dto'; -import { LoginRequestBody } from '@src/modules/oauth-provider/controller/dto'; +import { LoginRequestBody } from '@modules/oauth-provider/controller/dto'; export class OauthProviderRequestMapper { static mapCreateAcceptLoginRequestBody( diff --git a/apps/server/src/modules/oauth-provider/mapper/oauth-provider-response.mapper.spec.ts b/apps/server/src/modules/oauth-provider/mapper/oauth-provider-response.mapper.spec.ts index 6e62e1ce6c7..13119635f75 100644 --- a/apps/server/src/modules/oauth-provider/mapper/oauth-provider-response.mapper.spec.ts +++ b/apps/server/src/modules/oauth-provider/mapper/oauth-provider-response.mapper.spec.ts @@ -1,4 +1,4 @@ -import { OauthProviderResponseMapper } from '@src/modules/oauth-provider/mapper/oauth-provider-response.mapper'; +import { OauthProviderResponseMapper } from '@modules/oauth-provider/mapper/oauth-provider-response.mapper'; import { ProviderConsentResponse, ProviderConsentSessionResponse, @@ -12,7 +12,7 @@ import { LoginResponse, OauthClientResponse, RedirectResponse, -} from '@src/modules/oauth-provider/controller/dto/'; +} from '@modules/oauth-provider/controller/dto/'; describe('OauthProviderResponseMapper', () => { let mapper: OauthProviderResponseMapper; diff --git a/apps/server/src/modules/oauth-provider/mapper/oauth-provider-response.mapper.ts b/apps/server/src/modules/oauth-provider/mapper/oauth-provider-response.mapper.ts index 19fef36d0f5..01038c23526 100644 --- a/apps/server/src/modules/oauth-provider/mapper/oauth-provider-response.mapper.ts +++ b/apps/server/src/modules/oauth-provider/mapper/oauth-provider-response.mapper.ts @@ -12,7 +12,7 @@ import { LoginResponse, OauthClientResponse, RedirectResponse, -} from '@src/modules/oauth-provider/controller/dto'; +} from '@modules/oauth-provider/controller/dto'; @Injectable() export class OauthProviderResponseMapper { diff --git a/apps/server/src/modules/oauth-provider/oauth-provider-api.module.ts b/apps/server/src/modules/oauth-provider/oauth-provider-api.module.ts index 907a28c1fc7..ccbd1566cda 100644 --- a/apps/server/src/modules/oauth-provider/oauth-provider-api.module.ts +++ b/apps/server/src/modules/oauth-provider/oauth-provider-api.module.ts @@ -1,9 +1,9 @@ import { Module } from '@nestjs/common'; import { OauthProviderServiceModule } from '@shared/infra/oauth-provider'; import { LoggerModule } from '@src/core/logger'; -import { AuthorizationModule } from '@src/modules/authorization'; -import { PseudonymModule } from '@src/modules/pseudonym'; -import { UserModule } from '@src/modules/user'; +import { AuthorizationModule } from '@modules/authorization'; +import { PseudonymModule } from '@modules/pseudonym'; +import { UserModule } from '@modules/user'; import { OauthProviderController } from './controller/oauth-provider.controller'; import { OauthProviderResponseMapper } from './mapper/oauth-provider-response.mapper'; import { OauthProviderModule } from './oauth-provider.module'; diff --git a/apps/server/src/modules/oauth-provider/oauth-provider.module.ts b/apps/server/src/modules/oauth-provider/oauth-provider.module.ts index d963d247a93..4289644d29e 100644 --- a/apps/server/src/modules/oauth-provider/oauth-provider.module.ts +++ b/apps/server/src/modules/oauth-provider/oauth-provider.module.ts @@ -2,11 +2,11 @@ import { Module } from '@nestjs/common'; import { OauthProviderServiceModule } from '@shared/infra/oauth-provider'; import { TeamsRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; -import { LtiToolModule } from '@src/modules/lti-tool'; -import { PseudonymModule } from '@src/modules/pseudonym'; -import { ToolModule } from '@src/modules/tool'; -import { ToolConfigModule } from '@src/modules/tool/tool-config.module'; -import { UserModule } from '@src/modules/user'; +import { LtiToolModule } from '@modules/lti-tool'; +import { PseudonymModule } from '@modules/pseudonym'; +import { ToolModule } from '@modules/tool'; +import { ToolConfigModule } from '@modules/tool/tool-config.module'; +import { UserModule } from '@modules/user'; import { IdTokenService } from './service/id-token.service'; import { OauthProviderLoginFlowService } from './service/oauth-provider.login-flow.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 48f21de4077..4f8eff19c80 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 @@ -4,12 +4,12 @@ import { Pseudonym, TeamEntity, UserDO } from '@shared/domain'; import { TeamsRepo } from '@shared/repo'; 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 { ExternalTool } from '@src/modules/tool/external-tool/domain'; -import { UserService } from '@src/modules/user/service/user.service'; +import { IdToken } from '@modules/oauth-provider/interface/id-token'; +import { OauthScope } from '@modules/oauth-provider/interface/oauth-scope.enum'; +import { IdTokenService } from '@modules/oauth-provider/service/id-token.service'; +import { PseudonymService } from '@modules/pseudonym/service'; +import { ExternalTool } from '@modules/tool/external-tool/domain'; +import { UserService } from '@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; diff --git a/apps/server/src/modules/oauth-provider/service/id-token.service.ts b/apps/server/src/modules/oauth-provider/service/id-token.service.ts index dbe1b2c54fc..998ff46f15f 100644 --- a/apps/server/src/modules/oauth-provider/service/id-token.service.ts +++ b/apps/server/src/modules/oauth-provider/service/id-token.service.ts @@ -1,9 +1,9 @@ import { Injectable } from '@nestjs/common'; import { LtiToolDO, Pseudonym, TeamEntity, UserDO } from '@shared/domain'; import { TeamsRepo } from '@shared/repo'; -import { PseudonymService } from '@src/modules/pseudonym'; -import { ExternalTool } from '@src/modules/tool/external-tool/domain'; -import { UserService } from '@src/modules/user'; +import { PseudonymService } from '@modules/pseudonym'; +import { ExternalTool } from '@modules/tool/external-tool/domain'; +import { UserService } from '@modules/user'; import { IdTokenCreationLoggableException } from '../error/id-token-creation-exception.loggable'; import { GroupNameIdTuple, IdToken, OauthScope } from '../interface'; import { OauthProviderLoginFlowService } from './oauth-provider.login-flow.service'; diff --git a/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.spec.ts b/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.spec.ts index 3d382fffe3b..3c85dde6c62 100644 --- a/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.spec.ts +++ b/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.spec.ts @@ -3,10 +3,10 @@ import { NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { LtiToolDO } from '@shared/domain'; import { externalToolFactory, ltiToolDOFactory, setupEntities } from '@shared/testing'; -import { LtiToolService } from '@src/modules/lti-tool'; -import { ExternalTool } from '@src/modules/tool/external-tool/domain'; -import { ExternalToolService } from '@src/modules/tool/external-tool/service'; -import { IToolFeatures, ToolFeatures } from '@src/modules/tool/tool-config'; +import { LtiToolService } from '@modules/lti-tool'; +import { ExternalTool } from '@modules/tool/external-tool/domain'; +import { ExternalToolService } from '@modules/tool/external-tool/service'; +import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; import { OauthProviderLoginFlowService } from './oauth-provider.login-flow.service'; describe('OauthProviderLoginFlowService', () => { diff --git a/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.ts b/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.ts index 367efdcf6f7..0b1948baa28 100644 --- a/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.ts +++ b/apps/server/src/modules/oauth-provider/service/oauth-provider.login-flow.service.ts @@ -2,10 +2,10 @@ import { Inject } from '@nestjs/common'; import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; import { NotFoundException } from '@nestjs/common/exceptions/not-found.exception'; import { LtiToolDO } from '@shared/domain/domainobject/ltitool.do'; -import { LtiToolService } from '@src/modules/lti-tool/service'; -import { ExternalTool } from '@src/modules/tool/external-tool/domain'; -import { ExternalToolService } from '@src/modules/tool/external-tool/service'; -import { IToolFeatures, ToolFeatures } from '@src/modules/tool/tool-config'; +import { LtiToolService } from '@modules/lti-tool/service'; +import { ExternalTool } from '@modules/tool/external-tool/domain'; +import { ExternalToolService } from '@modules/tool/external-tool/service'; +import { IToolFeatures, ToolFeatures } from '@modules/tool/tool-config'; @Injectable() export class OauthProviderLoginFlowService { diff --git a/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.spec.ts b/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.spec.ts index b9d2abc6117..6ce203ab5b7 100644 --- a/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.spec.ts +++ b/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.spec.ts @@ -5,8 +5,8 @@ import { Permission, User } from '@shared/domain'; import { OauthProviderService } from '@shared/infra/oauth-provider'; import { ProviderOauthClient } from '@shared/infra/oauth-provider/dto'; import { setupEntities, userFactory } from '@shared/testing'; -import { AuthorizationService } from '@src/modules'; -import { ICurrentUser } from '@src/modules/authentication'; +import { AuthorizationService } from '@modules/authorization'; +import { ICurrentUser } from '@modules/authentication'; import { OauthProviderClientCrudUc } from './oauth-provider.client-crud.uc'; import resetAllMocks = jest.resetAllMocks; diff --git a/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.ts b/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.ts index b344f1b6cdc..3595f00679b 100644 --- a/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.ts +++ b/apps/server/src/modules/oauth-provider/uc/oauth-provider.client-crud.uc.ts @@ -1,9 +1,9 @@ import { Injectable } from '@nestjs/common'; import { OauthProviderService } from '@shared/infra/oauth-provider/index'; import { Permission, User } from '@shared/domain/index'; -import { AuthorizationService } from '@src/modules/authorization/authorization.service'; +import { AuthorizationService } from '@modules/authorization'; import { ProviderOauthClient } from '@shared/infra/oauth-provider/dto'; -import { ICurrentUser } from '@src/modules/authentication'; +import { ICurrentUser } from '@modules/authentication'; @Injectable() export class OauthProviderClientCrudUc { diff --git a/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.spec.ts b/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.spec.ts index 9abb5c8564c..b397b048dd4 100644 --- a/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.spec.ts +++ b/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.spec.ts @@ -1,17 +1,17 @@ import { Test, TestingModule } from '@nestjs/testing'; import { OauthProviderService } from '@shared/infra/oauth-provider/index'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { AcceptQuery, ConsentRequestBody } from '@src/modules/oauth-provider/controller/dto'; +import { AcceptQuery, ConsentRequestBody } from '@modules/oauth-provider/controller/dto'; import { AcceptConsentRequestBody, ProviderConsentResponse, ProviderRedirectResponse, } from '@shared/infra/oauth-provider/dto'; -import { OauthProviderConsentFlowUc } from '@src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc'; -import { ICurrentUser } from '@src/modules/authentication'; +import { OauthProviderConsentFlowUc } from '@modules/oauth-provider/uc/oauth-provider.consent-flow.uc'; +import { ICurrentUser } from '@modules/authentication'; import { ForbiddenException } from '@nestjs/common'; -import { IdTokenService } from '@src/modules/oauth-provider/service/id-token.service'; -import { IdToken } from '@src/modules/oauth-provider/interface/id-token'; +import { IdTokenService } from '@modules/oauth-provider/service/id-token.service'; +import { IdToken } from '@modules/oauth-provider/interface/id-token'; describe('OauthProviderConsentFlowUc', () => { let module: TestingModule; diff --git a/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.ts b/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.ts index 3fcf82687f6..eb91d8132fe 100644 --- a/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.ts +++ b/apps/server/src/modules/oauth-provider/uc/oauth-provider.consent-flow.uc.ts @@ -4,12 +4,12 @@ import { ProviderRedirectResponse, RejectRequestBody, } from '@shared/infra/oauth-provider/dto'; -import { AcceptQuery, ConsentRequestBody } from '@src/modules/oauth-provider/controller/dto'; -import { ICurrentUser } from '@src/modules/authentication'; +import { AcceptQuery, ConsentRequestBody } from '@modules/oauth-provider/controller/dto'; +import { ICurrentUser } from '@modules/authentication'; import { ForbiddenException, Injectable } from '@nestjs/common'; -import { IdTokenService } from '@src/modules/oauth-provider/service/id-token.service'; +import { IdTokenService } from '@modules/oauth-provider/service/id-token.service'; import { OauthProviderService } from '@shared/infra/oauth-provider'; -import { IdToken } from '@src/modules/oauth-provider/interface/id-token'; +import { IdToken } from '@modules/oauth-provider/interface/id-token'; @Injectable() export class OauthProviderConsentFlowUc { diff --git a/apps/server/src/modules/oauth-provider/uc/oauth-provider.login-flow.uc.spec.ts b/apps/server/src/modules/oauth-provider/uc/oauth-provider.login-flow.uc.spec.ts index e95aea26594..a9225031d04 100644 --- a/apps/server/src/modules/oauth-provider/uc/oauth-provider.login-flow.uc.spec.ts +++ b/apps/server/src/modules/oauth-provider/uc/oauth-provider.login-flow.uc.spec.ts @@ -12,10 +12,10 @@ import { userDoFactory, userFactory, } from '@shared/testing'; -import { AuthorizationService } from '@src/modules/authorization'; -import { PseudonymService } from '@src/modules/pseudonym'; -import { ExternalTool } from '@src/modules/tool/external-tool/domain'; -import { UserService } from '@src/modules/user'; +import { AuthorizationService } from '@modules/authorization'; +import { PseudonymService } from '@modules/pseudonym'; +import { ExternalTool } from '@modules/tool/external-tool/domain'; +import { UserService } from '@modules/user'; import { AcceptQuery, LoginRequestBody, OAuthRejectableBody } from '../controller/dto'; import { OauthProviderLoginFlowService } from '../service/oauth-provider.login-flow.service'; import { OauthProviderLoginFlowUc } from './oauth-provider.login-flow.uc'; diff --git a/apps/server/src/modules/oauth-provider/uc/oauth-provider.login-flow.uc.ts b/apps/server/src/modules/oauth-provider/uc/oauth-provider.login-flow.uc.ts index 8901b506f99..dade1cb3f07 100644 --- a/apps/server/src/modules/oauth-provider/uc/oauth-provider.login-flow.uc.ts +++ b/apps/server/src/modules/oauth-provider/uc/oauth-provider.login-flow.uc.ts @@ -7,12 +7,12 @@ import { ProviderLoginResponse, ProviderRedirectResponse, } from '@shared/infra/oauth-provider/dto'; -import { AuthorizationService } from '@src/modules/authorization'; -import { AcceptQuery, LoginRequestBody, OAuthRejectableBody } from '@src/modules/oauth-provider/controller/dto'; -import { OauthProviderRequestMapper } from '@src/modules/oauth-provider/mapper/oauth-provider-request.mapper'; -import { PseudonymService } from '@src/modules/pseudonym/service'; -import { ExternalTool, Oauth2ToolConfig } from '@src/modules/tool/external-tool/domain'; -import { UserService } from '@src/modules/user'; +import { AuthorizationService } from '@modules/authorization'; +import { AcceptQuery, LoginRequestBody, OAuthRejectableBody } from '@modules/oauth-provider/controller/dto'; +import { OauthProviderRequestMapper } from '@modules/oauth-provider/mapper/oauth-provider-request.mapper'; +import { PseudonymService } from '@modules/pseudonym/service'; +import { ExternalTool, Oauth2ToolConfig } from '@modules/tool/external-tool/domain'; +import { UserService } from '@modules/user'; import { OauthProviderLoginFlowService } from '../service/oauth-provider.login-flow.service'; @Injectable() diff --git a/apps/server/src/modules/oauth-provider/uc/oauth-provider.logout-flow.uc.spec.ts b/apps/server/src/modules/oauth-provider/uc/oauth-provider.logout-flow.uc.spec.ts index 7814ee90a7a..778112840b2 100644 --- a/apps/server/src/modules/oauth-provider/uc/oauth-provider.logout-flow.uc.spec.ts +++ b/apps/server/src/modules/oauth-provider/uc/oauth-provider.logout-flow.uc.spec.ts @@ -1,5 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { OauthProviderLogoutFlowUc } from '@src/modules/oauth-provider/uc/oauth-provider.logout-flow.uc'; +import { OauthProviderLogoutFlowUc } from '@modules/oauth-provider/uc/oauth-provider.logout-flow.uc'; import { OauthProviderService } from '@shared/infra/oauth-provider/index'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; diff --git a/apps/server/src/modules/oauth-provider/uc/oauth-provider.uc.spec.ts b/apps/server/src/modules/oauth-provider/uc/oauth-provider.uc.spec.ts index 90a1e5262d8..f1205db3d28 100644 --- a/apps/server/src/modules/oauth-provider/uc/oauth-provider.uc.spec.ts +++ b/apps/server/src/modules/oauth-provider/uc/oauth-provider.uc.spec.ts @@ -1,5 +1,5 @@ import { Test, TestingModule } from '@nestjs/testing'; -import { OauthProviderUc } from '@src/modules/oauth-provider/uc/oauth-provider.uc'; +import { OauthProviderUc } from '@modules/oauth-provider/uc/oauth-provider.uc'; import { OauthProviderService } from '@shared/infra/oauth-provider/index'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ProviderConsentSessionResponse } from '@shared/infra/oauth-provider/dto'; 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 7ddecbbaa54..eaaf07f4500 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 @@ -16,10 +16,10 @@ import { } from '@shared/testing'; 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/strategy/sanis/response'; -import { ServerTestModule } from '@src/modules/server'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { SanisResponse, SanisRole } from '@modules/provisioning/strategy/sanis/response'; +import { ServerTestModule } from '@modules/server'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { UUID } from 'bson'; diff --git a/apps/server/src/modules/oauth/controller/oauth-sso.controller.spec.ts b/apps/server/src/modules/oauth/controller/oauth-sso.controller.spec.ts index ee86f8e1b25..3d1a470e227 100644 --- a/apps/server/src/modules/oauth/controller/oauth-sso.controller.spec.ts +++ b/apps/server/src/modules/oauth/controller/oauth-sso.controller.spec.ts @@ -3,8 +3,8 @@ import { Configuration } from '@hpi-schul-cloud/commons'; import { UnauthorizedException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { LegacyLogger } from '@src/core/logger'; -import { ICurrentUser } from '@src/modules/authentication'; -import { HydraOauthUc } from '@src/modules/oauth/uc/hydra-oauth.uc'; +import { ICurrentUser } from '@modules/authentication'; +import { HydraOauthUc } from '@modules/oauth/uc/hydra-oauth.uc'; import { Request } from 'express'; import { OauthSSOController } from './oauth-sso.controller'; import { StatelessAuthorizationParams } from './dto/stateless-authorization.params'; diff --git a/apps/server/src/modules/oauth/controller/oauth-sso.controller.ts b/apps/server/src/modules/oauth/controller/oauth-sso.controller.ts index 934f7c26dc6..5ff7e7cae02 100644 --- a/apps/server/src/modules/oauth/controller/oauth-sso.controller.ts +++ b/apps/server/src/modules/oauth/controller/oauth-sso.controller.ts @@ -14,14 +14,13 @@ import { import { ApiOkResponse, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ISession } from '@shared/domain/types/session'; import { LegacyLogger } from '@src/core/logger'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser, JWT } from '@src/modules/authentication/decorator/auth.decorator'; -import { UserMigrationResponse } from '@src/modules/oauth/controller/dto/user-migration.response'; -import { HydraOauthUc } from '@src/modules/oauth/uc/hydra-oauth.uc'; -import { OAuthMigrationError } from '@src/modules/user-login-migration/error/oauth-migration.error'; -import { MigrationDto } from '@src/modules/user-login-migration/service/dto'; +import { ICurrentUser, Authenticate, CurrentUser, JWT } from '@modules/authentication'; +import { OAuthMigrationError } from '@modules/user-login-migration/error/oauth-migration.error'; +import { MigrationDto } from '@modules/user-login-migration/service/dto'; import { CookieOptions, Request, Response } from 'express'; -import { OAuthSSOError } from '../error/oauth-sso.error'; +import { HydraOauthUc } from '../uc/hydra-oauth.uc'; +import { UserMigrationResponse } from './dto/user-migration.response'; +import { OAuthSSOError } from '../loggable/oauth-sso.error'; import { OAuthTokenDto } from '../interface'; import { OauthLoginStateMapper } from '../mapper/oauth-login-state.mapper'; import { UserMigrationMapper } from '../mapper/user-migration.mapper'; diff --git a/apps/server/src/modules/oauth/error/index.ts b/apps/server/src/modules/oauth/loggable/index.ts similarity index 100% rename from apps/server/src/modules/oauth/error/index.ts rename to apps/server/src/modules/oauth/loggable/index.ts diff --git a/apps/server/src/modules/oauth/error/oauth-sso.error.spec.ts b/apps/server/src/modules/oauth/loggable/oauth-sso.error.spec.ts similarity index 100% rename from apps/server/src/modules/oauth/error/oauth-sso.error.spec.ts rename to apps/server/src/modules/oauth/loggable/oauth-sso.error.spec.ts diff --git a/apps/server/src/modules/oauth/error/oauth-sso.error.ts b/apps/server/src/modules/oauth/loggable/oauth-sso.error.ts similarity index 100% rename from apps/server/src/modules/oauth/error/oauth-sso.error.ts rename to apps/server/src/modules/oauth/loggable/oauth-sso.error.ts diff --git a/apps/server/src/modules/oauth/error/sso-error-code.enum.ts b/apps/server/src/modules/oauth/loggable/sso-error-code.enum.ts similarity index 100% rename from apps/server/src/modules/oauth/error/sso-error-code.enum.ts rename to apps/server/src/modules/oauth/loggable/sso-error-code.enum.ts diff --git a/apps/server/src/modules/oauth/error/user-not-found-after-provisioning.loggable-exception.spec.ts b/apps/server/src/modules/oauth/loggable/user-not-found-after-provisioning.loggable-exception.spec.ts similarity index 100% rename from apps/server/src/modules/oauth/error/user-not-found-after-provisioning.loggable-exception.spec.ts rename to apps/server/src/modules/oauth/loggable/user-not-found-after-provisioning.loggable-exception.spec.ts diff --git a/apps/server/src/modules/oauth/error/user-not-found-after-provisioning.loggable-exception.ts b/apps/server/src/modules/oauth/loggable/user-not-found-after-provisioning.loggable-exception.ts similarity index 100% rename from apps/server/src/modules/oauth/error/user-not-found-after-provisioning.loggable-exception.ts rename to apps/server/src/modules/oauth/loggable/user-not-found-after-provisioning.loggable-exception.ts diff --git a/apps/server/src/modules/oauth/mapper/user-migration.mapper.ts b/apps/server/src/modules/oauth/mapper/user-migration.mapper.ts index a6c7694aa60..42134d0b4d2 100644 --- a/apps/server/src/modules/oauth/mapper/user-migration.mapper.ts +++ b/apps/server/src/modules/oauth/mapper/user-migration.mapper.ts @@ -1,4 +1,4 @@ -import { MigrationDto } from '@src/modules/user-login-migration/service/dto'; +import { MigrationDto } from '@modules/user-login-migration/service/dto'; import { UserMigrationResponse } from '../controller/dto'; export class UserMigrationMapper { diff --git a/apps/server/src/modules/oauth/oauth-api.module.ts b/apps/server/src/modules/oauth/oauth-api.module.ts index 4bde06ce6a0..98e62d87eca 100644 --- a/apps/server/src/modules/oauth/oauth-api.module.ts +++ b/apps/server/src/modules/oauth/oauth-api.module.ts @@ -1,12 +1,12 @@ import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; -import { AuthenticationModule } from '@src/modules/authentication/authentication.module'; -import { AuthorizationModule } from '@src/modules/authorization'; -import { ProvisioningModule } from '@src/modules/provisioning'; -import { LegacySchoolModule } from '@src/modules/legacy-school'; -import { SystemModule } from '@src/modules/system'; -import { UserModule } from '@src/modules/user'; -import { UserLoginMigrationModule } from '@src/modules/user-login-migration'; +import { AuthenticationModule } from '@modules/authentication/authentication.module'; +import { AuthorizationModule } from '@modules/authorization'; +import { ProvisioningModule } from '@modules/provisioning'; +import { LegacySchoolModule } from '@modules/legacy-school'; +import { SystemModule } from '@modules/system'; +import { UserModule } from '@modules/user'; +import { UserLoginMigrationModule } from '@modules/user-login-migration'; import { OauthSSOController } from './controller/oauth-sso.controller'; import { OauthModule } from './oauth.module'; import { HydraOauthUc, OauthUc } from './uc'; diff --git a/apps/server/src/modules/oauth/oauth.module.ts b/apps/server/src/modules/oauth/oauth.module.ts index b8519e3eaa2..273a099159b 100644 --- a/apps/server/src/modules/oauth/oauth.module.ts +++ b/apps/server/src/modules/oauth/oauth.module.ts @@ -4,12 +4,12 @@ import { CacheWrapperModule } from '@shared/infra/cache'; import { EncryptionModule } from '@shared/infra/encryption'; import { LtiToolRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; -import { AuthorizationModule } from '@src/modules/authorization'; -import { ProvisioningModule } from '@src/modules/provisioning'; -import { LegacySchoolModule } from '@src/modules/legacy-school'; -import { SystemModule } from '@src/modules/system'; -import { UserModule } from '@src/modules/user'; -import { UserLoginMigrationModule } from '@src/modules/user-login-migration'; +import { AuthorizationModule } from '@modules/authorization'; +import { ProvisioningModule } from '@modules/provisioning'; +import { LegacySchoolModule } from '@modules/legacy-school'; +import { SystemModule } from '@modules/system'; +import { UserModule } from '@modules/user'; +import { UserLoginMigrationModule } from '@modules/user-login-migration'; import { HydraSsoService } from './service/hydra.service'; import { OauthAdapterService } from './service/oauth-adapter.service'; import { OAuthService } from './service/oauth.service'; diff --git a/apps/server/src/modules/oauth/service/dto/hydra.redirect.dto.ts b/apps/server/src/modules/oauth/service/dto/hydra.redirect.dto.ts index fe4a244de4e..4a1ead7c157 100644 --- a/apps/server/src/modules/oauth/service/dto/hydra.redirect.dto.ts +++ b/apps/server/src/modules/oauth/service/dto/hydra.redirect.dto.ts @@ -1,4 +1,4 @@ -import { CookiesDto } from '@src/modules/oauth/service/dto/cookies.dto'; +import { CookiesDto } from '@modules/oauth/service/dto/cookies.dto'; import { AxiosRequestConfig, AxiosResponse } from 'axios'; export class HydraRedirectDto { 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 4912bc039ca..3886aa40a58 100644 --- a/apps/server/src/modules/oauth/service/hydra.service.spec.ts +++ b/apps/server/src/modules/oauth/service/hydra.service.spec.ts @@ -10,9 +10,9 @@ import { DefaultEncryptionService, SymetricKeyEncryptionService } from '@shared/ 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 { CookiesDto } from '@modules/oauth/service/dto/cookies.dto'; +import { HydraRedirectDto } from '@modules/oauth/service/dto/hydra.redirect.dto'; +import { HydraSsoService } from '@modules/oauth/service/hydra.service'; import { AxiosResponse } from 'axios'; import { of } from 'rxjs'; import { StatelessAuthorizationParams } from '../controller/dto/stateless-authorization.params'; diff --git a/apps/server/src/modules/oauth/service/hydra.service.ts b/apps/server/src/modules/oauth/service/hydra.service.ts index 94ab8c66ff8..9b335604825 100644 --- a/apps/server/src/modules/oauth/service/hydra.service.ts +++ b/apps/server/src/modules/oauth/service/hydra.service.ts @@ -7,9 +7,9 @@ import { LtiToolDO } from '@shared/domain/domainobject/ltitool.do'; 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 { AuthorizationParams } from '@modules/oauth/controller/dto/authorization.params'; +import { CookiesDto } from '@modules/oauth/service/dto/cookies.dto'; +import { HydraRedirectDto } from '@modules/oauth/service/dto/hydra.redirect.dto'; import { AxiosRequestConfig, AxiosResponse } from 'axios'; import { nanoid } from 'nanoid'; import QueryString from 'qs'; 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 897ca989c04..12c0a381d8b 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 @@ -2,10 +2,9 @@ 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 { of, throwError } from 'rxjs'; -import { OAuthSSOError } from '../error/oauth-sso.error'; import { OAuthGrantType } from '../interface/oauth-grant-type.enum'; +import { OAuthSSOError } from '../loggable'; import { AuthenticationCodeGrantTokenRequest, OauthTokenResponse } from './dto'; import { OauthAdapterService } from './oauth-adapter.service'; @@ -44,10 +43,6 @@ describe('OauthAdapterServive', () => { provide: HttpService, useValue: createMock(), }, - { - provide: LegacyLogger, - useValue: createMock(), - }, ], }).compile(); service = module.get(OauthAdapterService); diff --git a/apps/server/src/modules/oauth/service/oauth-adapter.service.ts b/apps/server/src/modules/oauth/service/oauth-adapter.service.ts index f48982e051e..6b008b610cf 100644 --- a/apps/server/src/modules/oauth/service/oauth-adapter.service.ts +++ b/apps/server/src/modules/oauth/service/oauth-adapter.service.ts @@ -1,18 +1,15 @@ import { HttpService } from '@nestjs/axios'; import { Injectable } from '@nestjs/common/decorators'; -import { LegacyLogger } from '@src/core/logger'; import { AxiosResponse } from 'axios'; import JwksRsa from 'jwks-rsa'; import QueryString from 'qs'; import { lastValueFrom, Observable } from 'rxjs'; -import { OAuthSSOError } from '../error/oauth-sso.error'; +import { OAuthSSOError } from '../loggable'; import { AuthenticationCodeGrantTokenRequest, OauthTokenResponse } from './dto'; @Injectable() export class OauthAdapterService { - constructor(private readonly httpService: HttpService, private readonly logger: LegacyLogger) { - this.logger.setContext(OauthAdapterService.name); - } + constructor(private readonly httpService: HttpService) {} async getPublicKey(jwksUri: string): Promise { const client: JwksRsa.JwksClient = JwksRsa({ 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 5394070d273..2743037e214 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.spec.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.spec.ts @@ -7,16 +7,16 @@ import { SystemProvisioningStrategy } from '@shared/domain/interface/system-prov import { DefaultEncryptionService, IEncryptionService, SymetricKeyEncryptionService } from '@shared/infra/encryption'; import { legacySchoolDoFactory, setupEntities, systemFactory, userDoFactory } from '@shared/testing'; 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/legacy-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'; -import { UserService } from '@src/modules/user'; -import { MigrationCheckService, UserMigrationService } from '@src/modules/user-login-migration'; +import { ProvisioningDto, ProvisioningService } from '@modules/provisioning'; +import { ExternalSchoolDto, ExternalUserDto, OauthDataDto, ProvisioningSystemDto } from '@modules/provisioning/dto'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { OauthConfigDto } from '@modules/system/service'; +import { SystemDto } from '@modules/system/service/dto/system.dto'; +import { SystemService } from '@modules/system/service/system.service'; +import { UserService } from '@modules/user'; +import { MigrationCheckService, UserMigrationService } from '@modules/user-login-migration'; import jwt, { JwtPayload } from 'jsonwebtoken'; -import { OAuthSSOError, UserNotFoundAfterProvisioningLoggableException } from '../error'; +import { OAuthSSOError, UserNotFoundAfterProvisioningLoggableException } from '../loggable'; import { OAuthTokenDto } from '../interface'; import { OauthTokenResponse } from './dto'; import { OauthAdapterService } from './oauth-adapter.service'; diff --git a/apps/server/src/modules/oauth/service/oauth.service.ts b/apps/server/src/modules/oauth/service/oauth.service.ts index afef7f113e5..28a24c0534a 100644 --- a/apps/server/src/modules/oauth/service/oauth.service.ts +++ b/apps/server/src/modules/oauth/service/oauth.service.ts @@ -4,15 +4,15 @@ import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator' import { EntityId, LegacySchoolDo, OauthConfig, SchoolFeatures, UserDO } from '@shared/domain'; 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/legacy-school'; -import { SystemService } from '@src/modules/system'; -import { SystemDto } from '@src/modules/system/service'; -import { UserService } from '@src/modules/user'; -import { MigrationCheckService, UserMigrationService } from '@src/modules/user-login-migration'; +import { ProvisioningService } from '@modules/provisioning'; +import { OauthDataDto } from '@modules/provisioning/dto'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { SystemService } from '@modules/system'; +import { SystemDto } from '@modules/system/service'; +import { UserService } from '@modules/user'; +import { MigrationCheckService, UserMigrationService } from '@modules/user-login-migration'; import jwt, { JwtPayload } from 'jsonwebtoken'; -import { OAuthSSOError, SSOErrorCode, UserNotFoundAfterProvisioningLoggableException } from '../error'; +import { OAuthSSOError, SSOErrorCode, UserNotFoundAfterProvisioningLoggableException } from '../loggable'; import { OAuthTokenDto } from '../interface'; import { TokenRequestMapper } from '../mapper/token-request.mapper'; import { AuthenticationCodeGrantTokenRequest, OauthTokenResponse } from './dto'; @@ -172,7 +172,7 @@ export class OAuthService { const system: SystemDto = await this.systemService.findById(systemId); let redirect: string; - if (system.oauthConfig?.provider === 'iserv') { + if (system.oauthConfig?.provider === 'iserv' && system.oauthConfig?.logoutEndpoint) { const iservLogoutUrl: URL = new URL(system.oauthConfig.logoutEndpoint); iservLogoutUrl.searchParams.append('id_token_hint', idToken); iservLogoutUrl.searchParams.append('post_logout_redirect_uri', postLoginRedirect || dashboardUrl.toString()); 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 ffcc9abc393..3d42b0e977f 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 @@ -6,14 +6,14 @@ 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 { HydraRedirectDto } from '@modules/oauth/service/dto/hydra.redirect.dto'; +import { HydraSsoService } from '@modules/oauth/service/hydra.service'; +import { OAuthService } from '@modules/oauth/service/oauth.service'; import { AxiosResponse } from 'axios'; import { HydraOauthUc } from '.'; import { AuthorizationParams } from '../controller/dto'; import { StatelessAuthorizationParams } from '../controller/dto/stateless-authorization.params'; -import { OAuthSSOError } from '../error/oauth-sso.error'; +import { OAuthSSOError } from '../loggable/oauth-sso.error'; import { OAuthTokenDto } from '../interface'; class HydraOauthUcSpec extends HydraOauthUc { diff --git a/apps/server/src/modules/oauth/uc/hydra-oauth.uc.ts b/apps/server/src/modules/oauth/uc/hydra-oauth.uc.ts index d042c037df5..905cd3c8802 100644 --- a/apps/server/src/modules/oauth/uc/hydra-oauth.uc.ts +++ b/apps/server/src/modules/oauth/uc/hydra-oauth.uc.ts @@ -2,10 +2,10 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { OauthConfig } from '@shared/domain'; import { LegacyLogger } from '@src/core/logger'; -import { HydraRedirectDto } from '@src/modules/oauth/service/dto/hydra.redirect.dto'; +import { HydraRedirectDto } from '@modules/oauth/service/dto/hydra.redirect.dto'; import { AxiosRequestConfig, AxiosResponse } from 'axios'; import { AuthorizationParams } from '../controller/dto'; -import { OAuthSSOError } from '../error/oauth-sso.error'; +import { OAuthSSOError } from '../loggable/oauth-sso.error'; import { OAuthTokenDto } from '../interface'; import { HydraSsoService } from '../service/hydra.service'; import { OAuthService } from '../service/oauth.service'; 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 dba6cca003c..4323cd5bc85 100644 --- a/apps/server/src/modules/oauth/uc/oauth.uc.spec.ts +++ b/apps/server/src/modules/oauth/uc/oauth.uc.spec.ts @@ -6,20 +6,20 @@ import { SystemProvisioningStrategy } from '@shared/domain/interface/system-prov import { ISession } from '@shared/domain/types/session'; import { legacySchoolDoFactory, 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'; -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/legacy-school'; -import { SystemService } from '@src/modules/system'; -import { OauthConfigDto, SystemDto } from '@src/modules/system/service'; -import { UserService } from '@src/modules/user'; -import { UserMigrationService } from '@src/modules/user-login-migration'; -import { OAuthMigrationError } from '@src/modules/user-login-migration/error/oauth-migration.error'; -import { SchoolMigrationService } from '@src/modules/user-login-migration/service'; -import { MigrationDto } from '@src/modules/user-login-migration/service/dto'; +import { ICurrentUser } from '@modules/authentication'; +import { AuthenticationService } from '@modules/authentication/services/authentication.service'; +import { OauthUc } from '@modules/oauth/uc/oauth.uc'; +import { ProvisioningService } from '@modules/provisioning'; +import { ExternalUserDto, OauthDataDto, ProvisioningSystemDto } from '@modules/provisioning/dto'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { SystemService } from '@modules/system'; +import { OauthConfigDto, SystemDto } from '@modules/system/service'; +import { UserService } from '@modules/user'; +import { UserMigrationService } from '@modules/user-login-migration'; +import { OAuthMigrationError } from '@modules/user-login-migration/error/oauth-migration.error'; +import { SchoolMigrationService } from '@modules/user-login-migration/service'; +import { MigrationDto } from '@modules/user-login-migration/service/dto'; +import { OAuthSSOError } from '../loggable/oauth-sso.error'; import { AuthorizationParams } from '../controller/dto'; import { OAuthTokenDto } from '../interface'; import { OAuthProcessDto } from '../service/dto'; diff --git a/apps/server/src/modules/oauth/uc/oauth.uc.ts b/apps/server/src/modules/oauth/uc/oauth.uc.ts index e4dd7e68264..53d986bf029 100644 --- a/apps/server/src/modules/oauth/uc/oauth.uc.ts +++ b/apps/server/src/modules/oauth/uc/oauth.uc.ts @@ -2,16 +2,16 @@ import { Injectable, UnauthorizedException, UnprocessableEntityException } from import { EntityId, LegacySchoolDo, UserDO } from '@shared/domain'; import { ISession } from '@shared/domain/types/session'; import { LegacyLogger } from '@src/core/logger'; -import { ICurrentUser } from '@src/modules/authentication'; -import { AuthenticationService } from '@src/modules/authentication/services/authentication.service'; -import { ProvisioningService } from '@src/modules/provisioning'; -import { OauthDataDto } from '@src/modules/provisioning/dto'; -import { SystemService } from '@src/modules/system'; -import { SystemDto } from '@src/modules/system/service/dto/system.dto'; -import { UserService } from '@src/modules/user'; -import { UserMigrationService } from '@src/modules/user-login-migration'; -import { SchoolMigrationService } from '@src/modules/user-login-migration/service'; -import { MigrationDto } from '@src/modules/user-login-migration/service/dto'; +import { ICurrentUser } from '@modules/authentication'; +import { AuthenticationService } from '@modules/authentication/services/authentication.service'; +import { ProvisioningService } from '@modules/provisioning'; +import { OauthDataDto } from '@modules/provisioning/dto'; +import { SystemService } from '@modules/system'; +import { SystemDto } from '@modules/system/service/dto/system.dto'; +import { UserService } from '@modules/user'; +import { UserMigrationService } from '@modules/user-login-migration'; +import { SchoolMigrationService } from '@modules/user-login-migration/service'; +import { MigrationDto } from '@modules/user-login-migration/service/dto'; import { nanoid } from 'nanoid'; import { AuthorizationParams } from '../controller/dto'; import { OAuthTokenDto } from '../interface'; diff --git a/apps/server/src/modules/provisioning/dto/external-group.dto.ts b/apps/server/src/modules/provisioning/dto/external-group.dto.ts index 57cdc78e44c..e01093fa4ea 100644 --- a/apps/server/src/modules/provisioning/dto/external-group.dto.ts +++ b/apps/server/src/modules/provisioning/dto/external-group.dto.ts @@ -1,4 +1,4 @@ -import { GroupTypes } from '@src/modules/group'; +import { GroupTypes } from '@modules/group'; import { ExternalGroupUserDto } from './external-group-user.dto'; export class ExternalGroupDto { diff --git a/apps/server/src/modules/provisioning/mapper/provisioning-system-input.mapper.spec.ts b/apps/server/src/modules/provisioning/mapper/provisioning-system-input.mapper.spec.ts index fe7d866e2c9..f25054ce1ac 100644 --- a/apps/server/src/modules/provisioning/mapper/provisioning-system-input.mapper.spec.ts +++ b/apps/server/src/modules/provisioning/mapper/provisioning-system-input.mapper.spec.ts @@ -1,5 +1,5 @@ import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { SystemDto } from '@src/modules/system/service/dto/system.dto'; +import { SystemDto } from '@modules/system/service/dto/system.dto'; import { ProvisioningSystemDto } from '../dto'; import { ProvisioningSystemInputMapper } from './provisioning-system-input.mapper'; diff --git a/apps/server/src/modules/provisioning/mapper/provisioning-system-input.mapper.ts b/apps/server/src/modules/provisioning/mapper/provisioning-system-input.mapper.ts index b0b6a81a5af..9668bbfffea 100644 --- a/apps/server/src/modules/provisioning/mapper/provisioning-system-input.mapper.ts +++ b/apps/server/src/modules/provisioning/mapper/provisioning-system-input.mapper.ts @@ -1,5 +1,5 @@ import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { SystemDto } from '@src/modules/system/service/dto/system.dto'; +import { SystemDto } from '@modules/system/service/dto/system.dto'; import { ProvisioningSystemDto } from '../dto'; export class ProvisioningSystemInputMapper { diff --git a/apps/server/src/modules/provisioning/provisioning.module.ts b/apps/server/src/modules/provisioning/provisioning.module.ts index 854d1834387..185516e3f38 100644 --- a/apps/server/src/modules/provisioning/provisioning.module.ts +++ b/apps/server/src/modules/provisioning/provisioning.module.ts @@ -1,12 +1,12 @@ import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; -import { AccountModule } from '@src/modules/account/account.module'; -import { RoleModule } from '@src/modules/role'; -import { LegacySchoolModule } from '@src/modules/legacy-school'; -import { SystemModule } from '@src/modules/system/system.module'; -import { UserModule } from '@src/modules/user'; -import { GroupModule } from '@src/modules/group'; +import { AccountModule } from '@modules/account/account.module'; +import { RoleModule } from '@modules/role'; +import { LegacySchoolModule } from '@modules/legacy-school'; +import { SystemModule } from '@modules/system/system.module'; +import { UserModule } from '@modules/user'; +import { GroupModule } from '@modules/group'; import { ProvisioningService } from './service/provisioning.service'; import { IservProvisioningStrategy, OidcMockProvisioningStrategy, SanisProvisioningStrategy } from './strategy'; import { OidcProvisioningService } from './strategy/oidc/service/oidc-provisioning.service'; diff --git a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts index fee48a9b457..1d80c6c7b90 100644 --- a/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/service/provisioning.service.spec.ts @@ -2,8 +2,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { SystemDto } from '@src/modules/system/service/dto/system.dto'; -import { SystemService } from '@src/modules/system/service/system.service'; +import { SystemDto } from '@modules/system/service/dto/system.dto'; +import { SystemService } from '@modules/system/service/system.service'; import { ExternalUserDto, OauthDataDto, diff --git a/apps/server/src/modules/provisioning/service/provisioning.service.ts b/apps/server/src/modules/provisioning/service/provisioning.service.ts index 015c0d02fc9..50ee527001c 100644 --- a/apps/server/src/modules/provisioning/service/provisioning.service.ts +++ b/apps/server/src/modules/provisioning/service/provisioning.service.ts @@ -1,7 +1,7 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { SystemService } from '@src/modules/system'; -import { SystemDto } from '@src/modules/system/service/dto/system.dto'; +import { SystemService } from '@modules/system'; +import { SystemDto } from '@modules/system/service/dto/system.dto'; import { OauthDataDto, OauthDataStrategyInputDto, ProvisioningDto, ProvisioningSystemDto } from '../dto'; import { ProvisioningSystemInputMapper } from '../mapper/provisioning-system-input.mapper'; import { 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 0926ae7e3a7..e91ec8ca159 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 @@ -4,10 +4,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { LegacySchoolDo, RoleName, User, UserDO } from '@shared/domain'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { legacySchoolDoFactory, schoolFactory, setupEntities, userDoFactory, userFactory } from '@shared/testing'; -import { OAuthSSOError } from '@src/modules/oauth/error/oauth-sso.error'; -import { LegacySchoolService } from '@src/modules/legacy-school'; -import { UserService } from '@src/modules/user'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; import jwt from 'jsonwebtoken'; +import { OAuthSSOError } from '@modules/oauth/loggable'; import { RoleDto } from '../../../role/service/dto/role.dto'; import { ExternalSchoolDto, 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 71ea2972b54..8d5e1b5378f 100644 --- a/apps/server/src/modules/provisioning/strategy/iserv/iserv.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/iserv/iserv.strategy.ts @@ -1,10 +1,10 @@ import { Injectable } from '@nestjs/common'; import { LegacySchoolDo, RoleName, RoleReference, User, UserDO } from '@shared/domain'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { OAuthSSOError } from '@src/modules/oauth/error/oauth-sso.error'; -import { LegacySchoolService } from '@src/modules/legacy-school'; -import { UserService } from '@src/modules/user'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; import jwt, { JwtPayload } from 'jsonwebtoken'; +import { OAuthSSOError } from '@modules/oauth/loggable'; import { ExternalSchoolDto, ExternalUserDto, diff --git a/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.spec.ts index 37ca542a6d7..f9235522100 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { OAuthSSOError } from '@src/modules/oauth/error/oauth-sso.error'; import jwt from 'jsonwebtoken'; +import { OAuthSSOError } from '@modules/oauth/loggable'; import { ExternalUserDto, OauthDataDto, diff --git a/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.ts b/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.ts index 1d944bb0c8f..e505386ca97 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc-mock/oidc-mock.strategy.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { OAuthSSOError } from '@src/modules/oauth/error/oauth-sso.error'; import jwt, { JwtPayload } from 'jsonwebtoken'; +import { OAuthSSOError } from '@modules/oauth/loggable'; import { ExternalUserDto, OauthDataDto, OauthDataStrategyInputDto, ProvisioningDto } from '../../dto'; import { ProvisioningStrategy } from '../base.strategy'; 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 c4b27cac34b..09e253dddbf 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 @@ -14,13 +14,13 @@ import { roleFactory, } 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/legacy-school'; -import { UserService } from '@src/modules/user'; +import { AccountService } from '@modules/account/services/account.service'; +import { AccountSaveDto } from '@modules/account/services/dto'; +import { Group, GroupService } from '@modules/group'; +import { RoleService } from '@modules/role'; +import { RoleDto } from '@modules/role/service/dto/role.dto'; +import { FederalStateService, LegacySchoolService, SchoolYearService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; import CryptoJS from 'crypto-js'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { ExternalGroupDto, ExternalSchoolDto, ExternalUserDto } from '../../../dto'; @@ -661,7 +661,7 @@ describe('OidcProvisioningService', () => { describe('when provision group', () => { const setup = () => { - const group: Group = groupFactory.build(); + const group: Group = groupFactory.build({ users: [] }); groupService.findByExternalSource.mockResolvedValue(group); const school: LegacySchoolDo = legacySchoolDoFactory.build({ id: 'schoolId' }); 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 0aef3fdecb9..66c243e6457 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 @@ -2,14 +2,14 @@ import { Injectable, UnprocessableEntityException } from '@nestjs/common'; import { EntityId, ExternalSource, FederalStateEntity, SchoolFeatures, SchoolYearEntity } from '@shared/domain'; import { LegacySchoolDo, RoleReference, UserDO } from '@shared/domain/domainobject'; 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 { FederalStateService, LegacySchoolService, SchoolYearService } from '@src/modules/legacy-school'; -import { FederalStateNames } from '@src/modules/legacy-school/types'; -import { RoleService } from '@src/modules/role'; -import { RoleDto } from '@src/modules/role/service/dto/role.dto'; -import { UserService } from '@src/modules/user'; +import { AccountService } from '@modules/account/services/account.service'; +import { AccountSaveDto } from '@modules/account/services/dto'; +import { Group, GroupService, GroupUser } from '@modules/group'; +import { FederalStateService, LegacySchoolService, SchoolYearService } from '@modules/legacy-school'; +import { FederalStateNames } from '@modules/legacy-school/types'; +import { RoleService } from '@modules/role'; +import { RoleDto } from '@modules/role/service/dto/role.dto'; +import { UserService } from '@modules/user'; import { ObjectId } from 'bson'; import CryptoJS from 'crypto-js'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; @@ -119,10 +119,6 @@ export class OidcProvisioningService { } async provisionExternalGroup(externalGroup: ExternalGroupDto, systemId: EntityId): Promise { - if (externalGroup.users.length === 0) { - return; - } - const existingGroup: Group | null = await this.groupService.findByExternalSource( externalGroup.externalId, systemId @@ -145,6 +141,10 @@ export class OidcProvisioningService { const users: GroupUser[] = await this.getFilteredGroupUsers(externalGroup, systemId); + if (!users.length) { + return; + } + const group: Group = new Group({ id: existingGroup ? existingGroup.id : new ObjectId().toHexString(), name: externalGroup.name, @@ -156,8 +156,9 @@ export class OidcProvisioningService { organizationId, validFrom: externalGroup.from, validUntil: externalGroup.until, - users, + users: existingGroup ? existingGroup.users : [], }); + users.forEach((user: GroupUser) => group.addUser(user)); await this.groupService.save(group); } @@ -168,7 +169,7 @@ export class OidcProvisioningService { 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) { + if (!user?.id || roles.length !== 1 || !roles[0].id) { this.logger.info(new UserForGroupNotFoundLoggable(externalGroupUser)); return null; } 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 902e2e850f2..2fe68c0163b 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 @@ -2,7 +2,7 @@ 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 { GroupTypes } from '@modules/group'; import { UUID } from 'bson'; import { ExternalGroupDto, ExternalSchoolDto, ExternalUserDto } from '../../dto'; import { @@ -171,10 +171,6 @@ describe('SanisResponseMapper', () => { 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, @@ -206,9 +202,7 @@ describe('SanisResponseMapper', () => { 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, - ]; + sanisResponse.personenkontexte[0].gruppen![0]!.gruppenzugehoerigkeit.rollen = [SanisGroupRole.SCHOOL_SUPPORT]; return { sanisResponse, @@ -220,7 +214,7 @@ describe('SanisResponseMapper', () => { const result: ExternalGroupDto[] | undefined = mapper.mapToExternalGroupDtos(sanisResponse); - expect(result![0].users).toHaveLength(1); + expect(result![0].users).toHaveLength(0); }); }); 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 34a7ff4029c..23d9b15fbdc 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,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { RoleName } from '@shared/domain'; import { Logger } from '@src/core/logger'; -import { GroupTypes } from '@src/modules/group'; +import { GroupTypes } from '@modules/group'; import { ExternalGroupDto, ExternalGroupUserDto, ExternalSchoolDto, ExternalUserDto } from '../../dto'; import { GroupRoleUnknownLoggable } from '../../loggable'; import { @@ -66,7 +66,7 @@ export class SanisResponseMapper { } mapToExternalGroupDtos(source: SanisResponse): ExternalGroupDto[] | undefined { - const groups: SanisGruppenResponse[] | undefined = source.personenkontexte[0].gruppen; + const groups: SanisGruppenResponse[] | undefined = source.personenkontexte[0]?.gruppen; if (!groups) { return undefined; @@ -81,12 +81,11 @@ export class SanisResponseMapper { } const sanisGroupUsers: SanisSonstigeGruppenzugehoerigeResponse[] = [ - ...(group.sonstige_gruppenzugehoerige ?? []), { ktid: source.personenkontexte[0].id, rollen: group.gruppenzugehoerigkeit.rollen, }, - ].sort((a, b) => a.ktid.localeCompare(b.ktid)); + ].filter((sanisGroupUser) => sanisGroupUser.ktid && sanisGroupUser.rollen); const gruppenzugehoerigkeiten: ExternalGroupUserDto[] = sanisGroupUsers .map((relation): ExternalGroupUserDto | null => this.mapToExternalGroupUser(relation)) 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 b1324cbf74b..f0ea97f89fd 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts @@ -6,7 +6,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { RoleName } from '@shared/domain'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { axiosResponseFactory, setupEntities } from '@shared/testing'; -import { GroupTypes } from '@src/modules/group'; +import { GroupTypes } from '@modules/group'; import { UUID } from 'bson'; import { of } from 'rxjs'; import { diff --git a/apps/server/src/modules/pseudonym/controller/api-test/pseudonym.api.spec.ts b/apps/server/src/modules/pseudonym/controller/api-test/pseudonym.api.spec.ts index 05bd5d4f2f5..fa72cf82075 100644 --- a/apps/server/src/modules/pseudonym/controller/api-test/pseudonym.api.spec.ts +++ b/apps/server/src/modules/pseudonym/controller/api-test/pseudonym.api.spec.ts @@ -11,8 +11,8 @@ import { import { Test, TestingModule } from '@nestjs/testing'; import { Response } from 'supertest'; import { SchoolEntity } from '@shared/domain'; -import { ServerTestModule } from '@src/modules/server'; -import { ExternalToolEntity } from '@src/modules/tool/external-tool/entity'; +import { ServerTestModule } from '@modules/server'; +import { ExternalToolEntity } from '@modules/tool/external-tool/entity'; import { UUID } from 'bson'; import { ExternalToolPseudonymEntity } from '../../entity'; import { PseudonymResponse } from '../dto'; diff --git a/apps/server/src/modules/pseudonym/controller/pseudonym.controller.ts b/apps/server/src/modules/pseudonym/controller/pseudonym.controller.ts index 02aade8a446..f7e378e050a 100644 --- a/apps/server/src/modules/pseudonym/controller/pseudonym.controller.ts +++ b/apps/server/src/modules/pseudonym/controller/pseudonym.controller.ts @@ -7,8 +7,7 @@ import { ApiUnauthorizedResponse, } from '@nestjs/swagger'; import { Pseudonym } from '@shared/domain'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; -import { ICurrentUser } from '@src/modules/authentication'; +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { PseudonymMapper } from '../mapper/pseudonym.mapper'; import { PseudonymUc } from '../uc'; import { PseudonymResponse } from './dto'; diff --git a/apps/server/src/modules/pseudonym/pseudonym-api.module.ts b/apps/server/src/modules/pseudonym/pseudonym-api.module.ts index 8ba18f1cb72..2b9fdd3afed 100644 --- a/apps/server/src/modules/pseudonym/pseudonym-api.module.ts +++ b/apps/server/src/modules/pseudonym/pseudonym-api.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; -import { AuthorizationModule } from '@src/modules/authorization'; -import { LegacySchoolModule } from '@src/modules/legacy-school'; +import { AuthorizationModule } from '@modules/authorization'; +import { LegacySchoolModule } from '@modules/legacy-school'; import { PseudonymModule } from './pseudonym.module'; import { PseudonymController } from './controller/pseudonym.controller'; import { PseudonymUc } from './uc'; diff --git a/apps/server/src/modules/pseudonym/pseudonym.module.ts b/apps/server/src/modules/pseudonym/pseudonym.module.ts index 21da4ef3c59..d282c5dd9fe 100644 --- a/apps/server/src/modules/pseudonym/pseudonym.module.ts +++ b/apps/server/src/modules/pseudonym/pseudonym.module.ts @@ -1,9 +1,9 @@ import { forwardRef, Module } from '@nestjs/common'; import { LegacyLogger } from '@src/core/logger'; -import { LearnroomModule } from '@src/modules/learnroom'; -import { UserModule } from '@src/modules/user'; -import { ToolModule } from '@src/modules/tool'; -import { AuthorizationModule } from '@src/modules/authorization'; +import { LearnroomModule } from '@modules/learnroom'; +import { UserModule } from '@modules/user'; +import { ToolModule } from '@modules/tool'; +import { AuthorizationModule } from '@modules/authorization'; import { ExternalToolPseudonymRepo, PseudonymsRepo } from './repo'; import { FeathersRosterService, PseudonymService } from './service'; diff --git a/apps/server/src/modules/pseudonym/service/feathers-roster.service.spec.ts b/apps/server/src/modules/pseudonym/service/feathers-roster.service.spec.ts index 6c55067552d..59de8663ea6 100644 --- a/apps/server/src/modules/pseudonym/service/feathers-roster.service.spec.ts +++ b/apps/server/src/modules/pseudonym/service/feathers-roster.service.spec.ts @@ -2,28 +2,28 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { DatabaseObjectNotFoundException } from '@mikro-orm/core'; import { Test, TestingModule } from '@nestjs/testing'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; -import { Course, Pseudonym, RoleName, LegacySchoolDo, UserDO, SchoolEntity } from '@shared/domain'; +import { Course, LegacySchoolDo, Pseudonym, RoleName, SchoolEntity, UserDO } from '@shared/domain'; import { contextExternalToolFactory, courseFactory, externalToolFactory, - pseudonymFactory, legacySchoolDoFactory, + pseudonymFactory, schoolExternalToolFactory, schoolFactory, setupEntities, UserAndAccountTestFactory, userDoFactory, } from '@shared/testing'; -import { CourseService } from '@src/modules/learnroom/service/course.service'; -import { ToolContextType } from '@src/modules/tool/common/enum'; -import { ContextExternalTool, ContextRef } from '@src/modules/tool/context-external-tool/domain'; -import { ContextExternalToolService } from '@src/modules/tool/context-external-tool/service'; -import { ExternalTool } from '@src/modules/tool/external-tool/domain'; -import { ExternalToolService } from '@src/modules/tool/external-tool/service'; -import { SchoolExternalTool } from '@src/modules/tool/school-external-tool/domain'; -import { SchoolExternalToolService } from '@src/modules/tool/school-external-tool/service'; -import { UserService } from '@src/modules/user'; +import { CourseService } from '@modules/learnroom/service/course.service'; +import { ToolContextType } from '@modules/tool/common/enum'; +import { ContextExternalTool, ContextRef } from '@modules/tool/context-external-tool/domain'; +import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; +import { ExternalTool } from '@modules/tool/external-tool/domain'; +import { ExternalToolService } from '@modules/tool/external-tool/service'; +import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; +import { SchoolExternalToolService } from '@modules/tool/school-external-tool/service'; +import { UserService } from '@modules/user'; import { ObjectId } from 'bson'; import { FeathersRosterService } from './feathers-roster.service'; import { PseudonymService } from './pseudonym.service'; @@ -249,10 +249,10 @@ describe('FeathersRosterService', () => { ]); contextExternalToolService.findAllByContext.mockResolvedValueOnce([otherContextExternalTool]); contextExternalToolService.findAllByContext.mockResolvedValueOnce([]); - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValueOnce(schoolExternalTool); - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValueOnce(otherSchoolExternalTool); - externalToolService.findExternalToolById.mockResolvedValueOnce(externalTool); - externalToolService.findExternalToolById.mockResolvedValueOnce(otherExternalTool); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); + schoolExternalToolService.findById.mockResolvedValueOnce(otherSchoolExternalTool); + externalToolService.findById.mockResolvedValueOnce(externalTool); + externalToolService.findById.mockResolvedValueOnce(otherExternalTool); return { pseudonym, @@ -299,7 +299,7 @@ describe('FeathersRosterService', () => { await service.getUserGroups(pseudonym.pseudonym, clientId); - expect(schoolExternalToolService.getSchoolExternalToolById.mock.calls).toEqual([ + expect(schoolExternalToolService.findById.mock.calls).toEqual([ [schoolExternalTool.id], [otherSchoolExternalTool.id], ]); @@ -310,7 +310,7 @@ describe('FeathersRosterService', () => { await service.getUserGroups(pseudonym.pseudonym, clientId); - expect(externalToolService.findExternalToolById.mock.calls).toEqual([[externalToolId], [otherExternalTool.id]]); + expect(externalToolService.findById.mock.calls).toEqual([[externalToolId], [otherExternalTool.id]]); }); it('should return a group for each course where the tool of the users pseudonym is used', async () => { @@ -424,6 +424,8 @@ describe('FeathersRosterService', () => { students: [studentUser, studentUser2], teachers: [teacherUser], substitutionTeachers: [substitutionTeacherUser], + classes: [], + groups: [], }); courseService.findById.mockResolvedValue(courseA); diff --git a/apps/server/src/modules/pseudonym/service/feathers-roster.service.ts b/apps/server/src/modules/pseudonym/service/feathers-roster.service.ts index a5fd359b6c1..a6b512d13c2 100644 --- a/apps/server/src/modules/pseudonym/service/feathers-roster.service.ts +++ b/apps/server/src/modules/pseudonym/service/feathers-roster.service.ts @@ -1,15 +1,15 @@ import { Injectable } from '@nestjs/common'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { Course, EntityId, Pseudonym, RoleName, RoleReference, UserDO } from '@shared/domain'; -import { CourseService } from '@src/modules/learnroom/service'; -import { ToolContextType } from '@src/modules/tool/common/enum'; -import { ContextExternalTool, ContextRef } from '@src/modules/tool/context-external-tool/domain'; -import { ContextExternalToolService } from '@src/modules/tool/context-external-tool/service'; -import { ExternalTool } from '@src/modules/tool/external-tool/domain'; -import { ExternalToolService } from '@src/modules/tool/external-tool/service'; -import { SchoolExternalTool } from '@src/modules/tool/school-external-tool/domain'; -import { SchoolExternalToolService } from '@src/modules/tool/school-external-tool/service'; -import { UserService } from '@src/modules/user'; +import { CourseService } from '@modules/learnroom/service'; +import { ToolContextType } from '@modules/tool/common/enum'; +import { ContextExternalTool, ContextRef } from '@modules/tool/context-external-tool/domain'; +import { ContextExternalToolService } from '@modules/tool/context-external-tool/service'; +import { ExternalTool } from '@modules/tool/external-tool/domain'; +import { ExternalToolService } from '@modules/tool/external-tool/service'; +import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; +import { SchoolExternalToolService } from '@modules/tool/school-external-tool/service'; +import { UserService } from '@modules/user'; import { PseudonymService } from './pseudonym.service'; interface UserMetdata { @@ -179,12 +179,10 @@ export class FeathersRosterService { ); for await (const contextExternalTool of contextExternalTools) { - const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.getSchoolExternalToolById( + const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById( contextExternalTool.schoolToolRef.schoolToolId ); - const externalTool: ExternalTool = await this.externalToolService.findExternalToolById( - schoolExternalTool.toolId - ); + const externalTool: ExternalTool = await this.externalToolService.findById(schoolExternalTool.toolId); const isRequiredTool: boolean = externalTool.id === externalToolId; if (isRequiredTool) { 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 e2fbb6e1b1f..026d28d039f 100644 --- a/apps/server/src/modules/pseudonym/service/pseudonym.service.spec.ts +++ b/apps/server/src/modules/pseudonym/service/pseudonym.service.spec.ts @@ -4,7 +4,7 @@ import { InternalServerErrorException, NotFoundException } from '@nestjs/common' import { Test, TestingModule } from '@nestjs/testing'; import { IFindOptions, LtiToolDO, Page, Pseudonym, UserDO } from '@shared/domain'; import { externalToolFactory, ltiToolDOFactory, pseudonymFactory, userDoFactory } from '@shared/testing/factory'; -import { ExternalTool } from '@src/modules/tool/external-tool/domain'; +import { ExternalTool } from '@modules/tool/external-tool/domain'; import { PseudonymSearchQuery } from '../domain'; import { ExternalToolPseudonymRepo, PseudonymsRepo } from '../repo'; import { PseudonymService } from './pseudonym.service'; diff --git a/apps/server/src/modules/pseudonym/service/pseudonym.service.ts b/apps/server/src/modules/pseudonym/service/pseudonym.service.ts index 23819f2fd3b..6d15d6a1ec9 100644 --- a/apps/server/src/modules/pseudonym/service/pseudonym.service.ts +++ b/apps/server/src/modules/pseudonym/service/pseudonym.service.ts @@ -2,7 +2,7 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { ObjectId } from '@mikro-orm/mongodb'; import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { IFindOptions, LtiToolDO, Page, Pseudonym, UserDO } from '@shared/domain'; -import { ExternalTool } from '@src/modules/tool/external-tool/domain'; +import { ExternalTool } from '@modules/tool/external-tool/domain'; import { v4 as uuidv4 } from 'uuid'; import { PseudonymSearchQuery } from '../domain'; import { ExternalToolPseudonymRepo, PseudonymsRepo } from '../repo'; diff --git a/apps/server/src/modules/pseudonym/uc/pseudonym.uc.spec.ts b/apps/server/src/modules/pseudonym/uc/pseudonym.uc.spec.ts index 87fbfcbb526..26140da8a28 100644 --- a/apps/server/src/modules/pseudonym/uc/pseudonym.uc.spec.ts +++ b/apps/server/src/modules/pseudonym/uc/pseudonym.uc.spec.ts @@ -3,8 +3,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { LegacySchoolDo, Pseudonym, SchoolEntity, User } from '@shared/domain'; import { legacySchoolDoFactory, pseudonymFactory, schoolFactory, setupEntities, userFactory } from '@shared/testing'; import { ForbiddenException } from '@nestjs/common'; -import { Action, AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/legacy-school'; +import { Action, AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school'; import { PseudonymService } from '../service'; import { PseudonymUc } from './pseudonym.uc'; diff --git a/apps/server/src/modules/pseudonym/uc/pseudonym.uc.ts b/apps/server/src/modules/pseudonym/uc/pseudonym.uc.ts index a960a33bc3c..4c0ecb19a36 100644 --- a/apps/server/src/modules/pseudonym/uc/pseudonym.uc.ts +++ b/apps/server/src/modules/pseudonym/uc/pseudonym.uc.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { EntityId, LegacySchoolDo, Pseudonym, User } from '@shared/domain'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; -import { AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/legacy-school'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school'; import { PseudonymService } from '../service'; @Injectable() diff --git a/apps/server/src/modules/role/mapper/role.mapper.spec.ts b/apps/server/src/modules/role/mapper/role.mapper.spec.ts index 806d21d01e8..c7152cfd949 100644 --- a/apps/server/src/modules/role/mapper/role.mapper.spec.ts +++ b/apps/server/src/modules/role/mapper/role.mapper.spec.ts @@ -1,7 +1,7 @@ import { Permission, Role } from '@shared/domain'; import { roleFactory, setupEntities } from '@shared/testing'; -import { RoleMapper } from '@src/modules/role/mapper/role.mapper'; -import { RoleDto } from '@src/modules/role/service/dto/role.dto'; +import { RoleMapper } from '@modules/role/mapper/role.mapper'; +import { RoleDto } from '@modules/role/service/dto/role.dto'; describe('RoleMapper', () => { beforeAll(async () => { diff --git a/apps/server/src/modules/role/mapper/role.mapper.ts b/apps/server/src/modules/role/mapper/role.mapper.ts index a805098544b..7b427bb809d 100644 --- a/apps/server/src/modules/role/mapper/role.mapper.ts +++ b/apps/server/src/modules/role/mapper/role.mapper.ts @@ -1,5 +1,5 @@ import { Role } from '@shared/domain'; -import { RoleDto } from '@src/modules/role/service/dto/role.dto'; +import { RoleDto } from '@modules/role/service/dto/role.dto'; export class RoleMapper { static mapFromEntityToDto(entity: Role): RoleDto { diff --git a/apps/server/src/modules/role/role.module.ts b/apps/server/src/modules/role/role.module.ts index 9189d3ab191..42063b400e3 100644 --- a/apps/server/src/modules/role/role.module.ts +++ b/apps/server/src/modules/role/role.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { RoleRepo } from '@shared/repo'; -import { RoleService } from '@src/modules/role/service/role.service'; -import { RoleUc } from '@src/modules/role/uc/role.uc'; +import { RoleService } from '@modules/role/service/role.service'; +import { RoleUc } from '@modules/role/uc/role.uc'; @Module({ providers: [RoleRepo, RoleService, RoleUc], diff --git a/apps/server/src/modules/role/uc/role.uc.spec.ts b/apps/server/src/modules/role/uc/role.uc.spec.ts index 8be660bcffb..b026687af3f 100644 --- a/apps/server/src/modules/role/uc/role.uc.spec.ts +++ b/apps/server/src/modules/role/uc/role.uc.spec.ts @@ -1,9 +1,9 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { RoleName } from '@shared/domain'; -import { RoleDto } from '@src/modules/role/service/dto/role.dto'; -import { RoleService } from '@src/modules/role/service/role.service'; -import { RoleUc } from '@src/modules/role/uc/role.uc'; +import { RoleDto } from '@modules/role/service/dto/role.dto'; +import { RoleService } from '@modules/role/service/role.service'; +import { RoleUc } from '@modules/role/uc/role.uc'; describe('RoleUc', () => { let module: TestingModule; diff --git a/apps/server/src/modules/role/uc/role.uc.ts b/apps/server/src/modules/role/uc/role.uc.ts index faa29b7df9b..a1b5ba0b649 100644 --- a/apps/server/src/modules/role/uc/role.uc.ts +++ b/apps/server/src/modules/role/uc/role.uc.ts @@ -1,5 +1,5 @@ -import { RoleService } from '@src/modules/role/service/role.service'; -import { RoleDto } from '@src/modules/role/service/dto/role.dto'; +import { RoleService } from '@modules/role/service/role.service'; +import { RoleDto } from '@modules/role/service/dto/role.dto'; import { Injectable } from '@nestjs/common'; import { RoleName } from '@shared/domain'; diff --git a/apps/server/src/modules/server/controller/api-test/server.api.spec.ts b/apps/server/src/modules/server/controller/api-test/server.api.spec.ts index e730fe016d6..a6e42141c6a 100644 --- a/apps/server/src/modules/server/controller/api-test/server.api.spec.ts +++ b/apps/server/src/modules/server/controller/api-test/server.api.spec.ts @@ -1,6 +1,6 @@ import { INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { ServerTestModule } from '@src/modules/server'; +import { ServerTestModule } from '@modules/server'; import request from 'supertest'; describe('Server Controller (API)', () => { diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index 1f3222b0250..09dd2210928 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -1,8 +1,10 @@ import { Configuration } from '@hpi-schul-cloud/commons'; import type { IIdentityManagementConfig } from '@shared/infra/identity-management'; import type { ICoreModuleConfig } from '@src/core'; -import type { IAccountConfig, IFilesStorageClientConfig, IUserConfig } from '@src/modules/'; -import type { ICommonCartridgeConfig } from '@src/modules/learnroom/common-cartridge'; +import type { IAccountConfig } from '@modules/account'; +import type { IFilesStorageClientConfig } from '@modules/files-storage-client'; +import type { IUserConfig } from '@modules/user'; +import type { ICommonCartridgeConfig } from '@modules/learnroom/common-cartridge'; export enum NodeEnvType { TEST = 'test', diff --git a/apps/server/src/modules/server/server.module.ts b/apps/server/src/modules/server/server.module.ts index 20b346929cd..084ca72fca3 100644 --- a/apps/server/src/modules/server/server.module.ts +++ b/apps/server/src/modules/server/server.module.ts @@ -11,32 +11,32 @@ import { REDIS_CLIENT, RedisModule } from '@shared/infra/redis'; import { createConfigModuleOptions, DB_PASSWORD, DB_URL, DB_USERNAME } from '@src/config'; import { CoreModule } from '@src/core'; import { LegacyLogger, LoggerModule } from '@src/core/logger'; -import { AccountApiModule } from '@src/modules/account/account-api.module'; -import { AuthenticationApiModule } from '@src/modules/authentication/authentication-api.module'; -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'; -import { OauthProviderApiModule } from '@src/modules/oauth-provider'; -import { OauthApiModule } from '@src/modules/oauth/oauth-api.module'; -import { RocketChatModule } from '@src/modules/rocketchat'; -import { LegacySchoolApiModule } from '@src/modules/legacy-school/legacy-school-api.module'; -import { SharingApiModule } from '@src/modules/sharing/sharing.module'; -import { SystemApiModule } from '@src/modules/system/system-api.module'; -import { TaskApiModule } from '@src/modules/task/task-api.module'; -import { ToolApiModule } from '@src/modules/tool/tool-api.module'; -import { ImportUserModule } from '@src/modules/user-import'; -import { UserLoginMigrationApiModule } from '@src/modules/user-login-migration/user-login-migration-api.module'; -import { UserApiModule } from '@src/modules/user/user-api.module'; -import { VideoConferenceApiModule } from '@src/modules/video-conference/video-conference-api.module'; +import { AccountApiModule } from '@modules/account/account-api.module'; +import { AuthenticationApiModule } from '@modules/authentication/authentication-api.module'; +import { BoardApiModule } from '@modules/board/board-api.module'; +import { CollaborativeStorageModule } from '@modules/collaborative-storage'; +import { FilesStorageClientModule } from '@modules/files-storage-client'; +import { GroupApiModule } from '@modules/group/group-api.module'; +import { LearnroomApiModule } from '@modules/learnroom/learnroom-api.module'; +import { LessonApiModule } from '@modules/lesson/lesson-api.module'; +import { NewsModule } from '@modules/news'; +import { OauthProviderApiModule } from '@modules/oauth-provider'; +import { OauthApiModule } from '@modules/oauth/oauth-api.module'; +import { RocketChatModule } from '@modules/rocketchat'; +import { LegacySchoolApiModule } from '@modules/legacy-school/legacy-school-api.module'; +import { SharingApiModule } from '@modules/sharing/sharing.module'; +import { SystemApiModule } from '@modules/system/system-api.module'; +import { TaskApiModule } from '@modules/task/task-api.module'; +import { ToolApiModule } from '@modules/tool/tool-api.module'; +import { ImportUserModule } from '@modules/user-import'; +import { UserLoginMigrationApiModule } from '@modules/user-login-migration/user-login-migration-api.module'; +import { UserApiModule } from '@modules/user/user-api.module'; +import { VideoConferenceApiModule } from '@modules/video-conference/video-conference-api.module'; import connectRedis from 'connect-redis'; import session from 'express-session'; import { RedisClient } from 'redis'; -import { TeamsApiModule } from '@src/modules/teams/teams-api.module'; -import { PseudonymApiModule } from '@src/modules/pseudonym/pseudonym-api.module'; +import { TeamsApiModule } from '@modules/teams/teams-api.module'; +import { PseudonymApiModule } from '@modules/pseudonym/pseudonym-api.module'; import { ServerController } from './controller/server.controller'; import { serverConfig } from './server.config'; diff --git a/apps/server/src/modules/sharing/controller/api-test/sharing-create-token.api.spec.ts b/apps/server/src/modules/sharing/controller/api-test/sharing-create-token.api.spec.ts index 68c3a141f9b..7890f778f86 100644 --- a/apps/server/src/modules/sharing/controller/api-test/sharing-create-token.api.spec.ts +++ b/apps/server/src/modules/sharing/controller/api-test/sharing-create-token.api.spec.ts @@ -1,12 +1,12 @@ import { Request } from 'express'; import request from 'supertest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { EntityManager } from '@mikro-orm/mongodb'; -import { ExecutionContext, INestApplication } from '@nestjs/common'; +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { ExecutionContext, HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { Permission } from '@shared/domain'; -import { ICurrentUser } from '@src/modules/authentication'; +import { ICurrentUser } from '@modules/authentication'; import { cleanupCollections, courseFactory, @@ -15,8 +15,8 @@ import { schoolFactory, userFactory, } from '@shared/testing'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; import { ShareTokenBodyParams, ShareTokenResponse } from '../dto'; import { ShareTokenParentType } from '../../domainobject/share-token.do'; @@ -101,27 +101,27 @@ describe(`share token creation (api)`, () => { const response = await api.post({ parentId: course.id, parentType: ShareTokenParentType.Course }); - expect(response.status).toEqual(500); + expect(response.status).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); }); }); - describe('with ivalid request data', () => { + describe('with invalid request data', () => { it('should return status 400 on empty parent id', async () => { const response = await api.post({ parentId: '', parentType: ShareTokenParentType.Course, }); - expect(response.status).toEqual(400); + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); }); - it('should return status 403 when parent id is not found', async () => { + it('should return status 404 when parent id is not found', async () => { const response = await api.post({ - parentId: '000011112222333344445555', + parentId: new ObjectId().toHexString(), parentType: ShareTokenParentType.Course, }); - expect(response.status).toEqual(403); + expect(response.status).toEqual(HttpStatus.NOT_FOUND); }); it('should return status 400 on invalid parent id', async () => { @@ -130,7 +130,7 @@ describe(`share token creation (api)`, () => { parentType: ShareTokenParentType.Course, }); - expect(response.status).toEqual(400); + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); }); it('should return status 400 on invalid parent type', async () => { @@ -142,7 +142,7 @@ describe(`share token creation (api)`, () => { parentType: 'invalid', }); - expect(response.status).toEqual(400); + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); }); it('should return status 400 when expiresInDays is invalid integer', async () => { @@ -155,7 +155,7 @@ describe(`share token creation (api)`, () => { expiresInDays: 'foo', }); - expect(response.status).toEqual(400); + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); }); it('should return status 400 when expiresInDays is negative', async () => { @@ -167,7 +167,7 @@ describe(`share token creation (api)`, () => { expiresInDays: -10, }); - expect(response.status).toEqual(400); + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); }); it('should return status 400 when expiresInDays is not an integer', async () => { @@ -179,7 +179,7 @@ describe(`share token creation (api)`, () => { expiresInDays: 2.5, }); - expect(response.status).toEqual(400); + expect(response.status).toEqual(HttpStatus.BAD_REQUEST); }); }); @@ -189,7 +189,7 @@ describe(`share token creation (api)`, () => { const response = await api.post({ parentId: course.id, parentType: ShareTokenParentType.Course }); - expect(response.status).toEqual(201); + expect(response.status).toEqual(HttpStatus.CREATED); }); it('should return a valid result', async () => { @@ -216,7 +216,7 @@ describe(`share token creation (api)`, () => { schoolExclusive: true, }); - expect(response.status).toEqual(201); + expect(response.status).toEqual(HttpStatus.CREATED); }); it('should return a valid result', async () => { @@ -248,7 +248,7 @@ describe(`share token creation (api)`, () => { expiresInDays: 5, }); - expect(response.status).toEqual(201); + expect(response.status).toEqual(HttpStatus.CREATED); }); it('should return a valid result containg the expiration timestamp', async () => { diff --git a/apps/server/src/modules/sharing/controller/api-test/sharing-import-token.api.spec.ts b/apps/server/src/modules/sharing/controller/api-test/sharing-import-token.api.spec.ts index e58940addda..f19dea681f2 100644 --- a/apps/server/src/modules/sharing/controller/api-test/sharing-import-token.api.spec.ts +++ b/apps/server/src/modules/sharing/controller/api-test/sharing-import-token.api.spec.ts @@ -1,10 +1,10 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { EntityManager } from '@mikro-orm/mongodb'; -import { ExecutionContext, INestApplication } from '@nestjs/common'; +import { ExecutionContext, HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { Permission } from '@shared/domain'; -import { ICurrentUser } from '@src/modules/authentication'; +import { ICurrentUser } from '@modules/authentication'; import { cleanupCollections, courseFactory, @@ -13,9 +13,9 @@ import { schoolFactory, userFactory, } from '@shared/testing'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { CopyApiResponse, CopyElementType, CopyStatusEnum } from '@src/modules/copy-helper'; -import { ServerTestModule } from '@src/modules/server'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { CopyApiResponse, CopyElementType, CopyStatusEnum } from '@modules/copy-helper'; +import { ServerTestModule } from '@modules/server'; import { Request } from 'express'; import request from 'supertest'; import { ShareTokenContext, ShareTokenContextType, ShareTokenParentType } from '../../domainobject/share-token.do'; @@ -118,16 +118,17 @@ describe(`share token import (api)`, () => { const response = await api.post({ token }, { newName: 'NewName' }); - expect(response.status).toEqual(500); + expect(response.status).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); }); }); describe('with a valid token', () => { it('should return status 201', async () => { const { token } = await setup(); + const response = await api.post({ token }, { newName: 'NewName' }); - expect(response.status).toEqual(201); + expect(response.status).toEqual(HttpStatus.CREATED); }); it('should return a valid result', async () => { @@ -149,23 +150,53 @@ describe(`share token import (api)`, () => { describe('with invalid token', () => { it('should return status 404', async () => { await setup(); + const response = await api.post({ token: 'invalid_token' }, { newName: 'NewName' }); - expect(response.status).toEqual(404); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); }); }); describe('with invalid context', () => { - it('should return status 403', async () => { + const setup2 = async () => { + const school = schoolFactory.build(); const otherSchool = schoolFactory.build(); - await em.persistAndFlush(otherSchool); - em.clear(); + const roles = roleFactory.buildList(1, { + permissions: [Permission.COURSE_CREATE], + }); - const { token } = await setup({ + const user = userFactory.build({ school, roles }); + const course = courseFactory.build({ teachers: [user] }); + await em.persistAndFlush([user, course, otherSchool]); + + const context = { contextType: ShareTokenContextType.School, contextId: otherSchool.id, - }); - const response = await api.post({ token }, { newName: 'NewName' }); - expect(response.status).toEqual(403); + }; + + const shareToken = await shareTokenService.createToken( + { + parentType: ShareTokenParentType.Course, + parentId: course.id, + }, + { context } + ); + + em.clear(); + + currentUser = mapUserToCurrentUser(user); + + return { + shareTokenFromDifferentCourse: shareToken.token, + }; + }; + + it('should return status 403', async () => { + const { shareTokenFromDifferentCourse } = await setup2(); + + const response = await api.post({ token: shareTokenFromDifferentCourse }, { newName: 'NewName' }); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); }); }); @@ -175,7 +206,7 @@ describe(`share token import (api)`, () => { // @ts-expect-error invalid new name const response = await api.post({ token }, { newName: 42 }); - expect(response.status).toEqual(501); + expect(response.status).toEqual(HttpStatus.NOT_IMPLEMENTED); }); }); }); diff --git a/apps/server/src/modules/sharing/controller/api-test/sharing-lookup-token.api.spec.ts b/apps/server/src/modules/sharing/controller/api-test/sharing-lookup-token.api.spec.ts index a5c1304a730..88f23408be4 100644 --- a/apps/server/src/modules/sharing/controller/api-test/sharing-lookup-token.api.spec.ts +++ b/apps/server/src/modules/sharing/controller/api-test/sharing-lookup-token.api.spec.ts @@ -1,167 +1,210 @@ -import { Request } from 'express'; -import request from 'supertest'; -import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { Configuration } from '@hpi-schul-cloud/commons'; import { EntityManager } from '@mikro-orm/mongodb'; -import { ExecutionContext, INestApplication } from '@nestjs/common'; +import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { ApiValidationError } from '@shared/common'; import { Permission } from '@shared/domain'; -import { ICurrentUser } from '@src/modules/authentication'; -import { - cleanupCollections, - courseFactory, - mapUserToCurrentUser, - roleFactory, - schoolFactory, - userFactory, -} from '@shared/testing'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@src/modules/server'; +import { TestApiClient, UserAndAccountTestFactory, courseFactory, schoolFactory } from '@shared/testing'; +import { ServerTestModule } from '@modules/server'; import { ShareTokenService } from '../../service'; -import { ShareTokenInfoResponse, ShareTokenResponse, ShareTokenUrlParams } from '../dto'; -import { ShareTokenContext, ShareTokenContextType, ShareTokenParentType } from '../../domainobject/share-token.do'; - -const baseRouteName = '/sharetoken'; - -class API { - app: INestApplication; - - constructor(app: INestApplication) { - this.app = app; - } - - async get(urlParams: ShareTokenUrlParams) { - const response = await request(this.app.getHttpServer()) - .get(`${baseRouteName}/${urlParams.token}`) - .set('Accept', 'application/json'); - - return { - result: response.body as ShareTokenResponse, - error: response.body as ApiValidationError, - status: response.status, - }; - } -} +import { ShareTokenInfoResponse } from '../dto'; +import { ShareTokenContextType, ShareTokenParentType } from '../../domainobject/share-token.do'; describe(`share token lookup (api)`, () => { let app: INestApplication; let em: EntityManager; - let currentUser: ICurrentUser; let shareTokenService: ShareTokenService; - let api: API; + let testApiClient: TestApiClient; beforeAll(async () => { const module: TestingModule = await Test.createTestingModule({ imports: [ServerTestModule], - }) - .overrideGuard(JwtAuthGuard) - .useValue({ - canActivate(context: ExecutionContext) { - const req: Request = context.switchToHttp().getRequest(); - req.user = currentUser; - return true; - }, - }) - .compile(); + }).compile(); app = module.createNestApplication(); await app.init(); em = module.get(EntityManager); shareTokenService = module.get(ShareTokenService); - api = new API(app); + testApiClient = new TestApiClient(app, 'sharetoken'); }); afterAll(async () => { await app.close(); }); - beforeEach(() => { - Configuration.set('FEATURE_COURSE_SHARE_NEW', true); - }); + describe('with the feature disabled', () => { + const setup = async () => { + Configuration.set('FEATURE_COURSE_SHARE_NEW', false); - const setup = async (context?: ShareTokenContext) => { - await cleanupCollections(em); - const school = schoolFactory.build(); - const roles = roleFactory.buildList(1, { - permissions: [Permission.COURSE_CREATE], - }); - const user = userFactory.build({ school, roles }); - const course = courseFactory.build({ teachers: [user] }); - await em.persistAndFlush([user, course]); - - const shareToken = await shareTokenService.createToken( - { - parentType: ShareTokenParentType.Course, - parentId: course.id, - }, - { context } - ); - - em.clear(); - - currentUser = mapUserToCurrentUser(user); - - return { - parentType: ShareTokenParentType.Course, - parentName: course.getMetadata().title, - token: shareToken.token, + const parentType = ShareTokenParentType.Course; + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({}, [Permission.COURSE_CREATE]); + const course = courseFactory.build({ teachers: [teacherUser] }); + + await em.persistAndFlush([course, teacherAccount, teacherUser]); + em.clear(); + + const shareToken = await shareTokenService.createToken( + { + parentType, + parentId: course.id, + }, + undefined + ); + + const loggedInClient = await testApiClient.login(teacherAccount); + + return { + token: shareToken.token, + loggedInClient, + }; }; - }; - describe('with the feature disabled', () => { it('should return status 500', async () => { - Configuration.set('FEATURE_COURSE_SHARE_NEW', false); - const { token } = await setup(); + const { token, loggedInClient } = await setup(); - const response = await api.get({ token }); + const response = await loggedInClient.get(token); - expect(response.status).toEqual(500); + expect(response.status).toEqual(HttpStatus.INTERNAL_SERVER_ERROR); + expect(response.body).toEqual({ + code: 500, + message: 'Import Course Feature not enabled', + title: 'Internal Server Error', + type: 'INTERNAL_SERVER_ERROR', + }); }); }); + // test and setup for other feature flags are missed + describe('with a valid token', () => { - it('should return status 200', async () => { - const { token } = await setup(); - const response = await api.get({ token }); + const setup = async () => { + Configuration.set('FEATURE_COURSE_SHARE_NEW', true); - expect(response.status).toEqual(200); - }); + const parentType = ShareTokenParentType.Course; + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({}, [Permission.COURSE_CREATE]); + const course = courseFactory.build({ teachers: [teacherUser] }); - it('should return a valid result', async () => { - const { parentType, parentName, token } = await setup(); - const response = await api.get({ token }); + await em.persistAndFlush([course, teacherAccount, teacherUser]); + em.clear(); + + const shareToken = await shareTokenService.createToken( + { + parentType, + parentId: course.id, + }, + undefined + ); + + const loggedInClient = await testApiClient.login(teacherAccount); const expectedResult: ShareTokenInfoResponse = { - token, + token: shareToken.token, parentType, - parentName, + parentName: course.getMetadata().title, }; - expect(response.result).toEqual(expectedResult); + return { + expectedResult, + token: shareToken.token, + loggedInClient, + }; + }; + + it('should return status 200 with correct formated body', async () => { + const { token, loggedInClient, expectedResult } = await setup(); + + const response = await loggedInClient.get(token); + + expect(response.status).toEqual(HttpStatus.OK); + expect(response.body).toEqual(expectedResult); }); }); describe('with invalid token', () => { + const setup = async () => { + Configuration.set('FEATURE_COURSE_SHARE_NEW', true); + + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({}, [Permission.COURSE_CREATE]); + const course = courseFactory.build({ teachers: [teacherUser] }); + + await em.persistAndFlush([course, teacherAccount, teacherUser]); + em.clear(); + + const loggedInClient = await testApiClient.login(teacherAccount); + + return { + invalidToken: 'invalid_token', + loggedInClient, + }; + }; + it('should return status 404', async () => { - await setup(); - const response = await api.get({ token: 'invalid_token' }); - expect(response.status).toEqual(404); + const { invalidToken, loggedInClient } = await setup(); + + const response = await loggedInClient.get(invalidToken); + + expect(response.status).toEqual(HttpStatus.NOT_FOUND); + expect(response.body).toEqual({ + code: 404, + message: 'The requested ShareToken: [object Object] has not been found.', + title: 'Not Found', + type: 'NOT_FOUND', + }); }); }); describe('with invalid context', () => { - it('should return status 403', async () => { + const setup = async () => { + Configuration.set('FEATURE_COURSE_SHARE_NEW', true); + + const parentType = ShareTokenParentType.Course; + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({}, [Permission.COURSE_CREATE]); const otherSchool = schoolFactory.build(); - await em.persistAndFlush(otherSchool); + const course = courseFactory.build({ teachers: [teacherUser] }); + + await em.persistAndFlush([course, teacherAccount, teacherUser, otherSchool]); em.clear(); - const { token } = await setup({ + const context = { contextType: ShareTokenContextType.School, contextId: otherSchool.id, + }; + + const shareToken = await shareTokenService.createToken( + { + parentType, + parentId: course.id, + }, + { context } + ); + + const loggedInClient = await testApiClient.login(teacherAccount); + + const expectedResult: ShareTokenInfoResponse = { + token: shareToken.token, + parentType, + parentName: course.getMetadata().title, + }; + + return { + expectedResult, + token: shareToken.token, + loggedInClient, + }; + }; + + it('should return status 403', async () => { + const { token, loggedInClient } = await setup(); + + const response = await loggedInClient.get(token); + + expect(response.status).toEqual(HttpStatus.FORBIDDEN); + expect(response.body).toEqual({ + code: 403, + message: 'Forbidden', + title: 'Forbidden', + type: 'FORBIDDEN', }); - const response = await api.get({ token }); - expect(response.status).toEqual(403); }); }); }); diff --git a/apps/server/src/modules/sharing/controller/share-token.controller.spec.ts b/apps/server/src/modules/sharing/controller/share-token.controller.spec.ts index b694c60731a..7d6f51fa471 100644 --- a/apps/server/src/modules/sharing/controller/share-token.controller.spec.ts +++ b/apps/server/src/modules/sharing/controller/share-token.controller.spec.ts @@ -1,8 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { courseFactory, setupEntities, shareTokenFactory } from '@shared/testing'; -import { ICurrentUser } from '@src/modules/authentication'; -import { CopyElementType, CopyStatus, CopyStatusEnum } from '@src/modules/copy-helper'; +import { ICurrentUser } from '@modules/authentication'; +import { CopyElementType, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; import { ShareTokenParentType } from '../domainobject/share-token.do'; import { ShareTokenUC } from '../uc'; import { ShareTokenInfoDto } from '../uc/dto'; diff --git a/apps/server/src/modules/sharing/controller/share-token.controller.ts b/apps/server/src/modules/sharing/controller/share-token.controller.ts index bd627977aad..373e1169600 100644 --- a/apps/server/src/modules/sharing/controller/share-token.controller.ts +++ b/apps/server/src/modules/sharing/controller/share-token.controller.ts @@ -11,10 +11,10 @@ import { } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { ApiValidationError, RequestTimeout } from '@shared/common'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; -import { CopyApiResponse, CopyMapper } from '@src/modules/copy-helper'; -import { serverConfig } from '@src/modules/server/server.config'; +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; +import { CopyApiResponse, CopyMapper } from '@modules/copy-helper'; +// invalid import can produce dependency cycles +import { serverConfig } from '@modules/server/server.config'; import { ShareTokenInfoResponseMapper, ShareTokenResponseMapper } from '../mapper'; import { ShareTokenUC } from '../uc'; import { diff --git a/apps/server/src/modules/sharing/mapper/context-type.mapper.spec.ts b/apps/server/src/modules/sharing/mapper/context-type.mapper.spec.ts index 9684a1dbb58..6f106c3e90a 100644 --- a/apps/server/src/modules/sharing/mapper/context-type.mapper.spec.ts +++ b/apps/server/src/modules/sharing/mapper/context-type.mapper.spec.ts @@ -1,5 +1,5 @@ import { NotImplementedException } from '@nestjs/common'; -import { AuthorizableReferenceType } from '@src/modules/authorization'; +import { AuthorizableReferenceType } from '@modules/authorization/domain'; import { ShareTokenContextType } from '../domainobject/share-token.do'; import { ShareTokenContextTypeMapper } from './context-type.mapper'; diff --git a/apps/server/src/modules/sharing/mapper/context-type.mapper.ts b/apps/server/src/modules/sharing/mapper/context-type.mapper.ts index 7c9b4c8bb1d..fbb6526eac9 100644 --- a/apps/server/src/modules/sharing/mapper/context-type.mapper.ts +++ b/apps/server/src/modules/sharing/mapper/context-type.mapper.ts @@ -1,5 +1,5 @@ import { NotImplementedException } from '@nestjs/common'; -import { AuthorizableReferenceType } from '@src/modules/authorization'; +import { AuthorizableReferenceType } from '@modules/authorization/domain'; import { ShareTokenContextType } from '../domainobject/share-token.do'; export class ShareTokenContextTypeMapper { @@ -12,6 +12,7 @@ export class ShareTokenContextTypeMapper { if (!res) { throw new NotImplementedException(); } + return res; } } diff --git a/apps/server/src/modules/sharing/mapper/parent-type.mapper.spec.ts b/apps/server/src/modules/sharing/mapper/parent-type.mapper.spec.ts index c6d8669bc70..99a9ea19453 100644 --- a/apps/server/src/modules/sharing/mapper/parent-type.mapper.spec.ts +++ b/apps/server/src/modules/sharing/mapper/parent-type.mapper.spec.ts @@ -1,5 +1,5 @@ import { NotImplementedException } from '@nestjs/common'; -import { AuthorizableReferenceType } from '@src/modules/authorization'; +import { AuthorizableReferenceType } from '@modules/authorization/domain'; import { ShareTokenParentType } from '../domainobject/share-token.do'; import { ShareTokenParentTypeMapper } from './parent-type.mapper'; diff --git a/apps/server/src/modules/sharing/mapper/parent-type.mapper.ts b/apps/server/src/modules/sharing/mapper/parent-type.mapper.ts index 54d8ceb0470..7f2f0fdea60 100644 --- a/apps/server/src/modules/sharing/mapper/parent-type.mapper.ts +++ b/apps/server/src/modules/sharing/mapper/parent-type.mapper.ts @@ -1,5 +1,5 @@ import { NotImplementedException } from '@nestjs/common'; -import { AuthorizableReferenceType } from '@src/modules/authorization'; +import { AuthorizableReferenceType } from '@modules/authorization/domain'; import { ShareTokenParentType } from '../domainobject/share-token.do'; export class ShareTokenParentTypeMapper { diff --git a/apps/server/src/modules/sharing/service/share-token.service.spec.ts b/apps/server/src/modules/sharing/service/share-token.service.spec.ts index c3436fb54ce..08680427058 100644 --- a/apps/server/src/modules/sharing/service/share-token.service.spec.ts +++ b/apps/server/src/modules/sharing/service/share-token.service.spec.ts @@ -2,9 +2,9 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { courseFactory, lessonFactory, setupEntities, shareTokenFactory, taskFactory } from '@shared/testing'; -import { CourseService } from '@src/modules/learnroom/service'; -import { LessonService } from '@src/modules/lesson/service'; -import { TaskService } from '@src/modules/task/service'; +import { CourseService } from '@modules/learnroom/service'; +import { LessonService } from '@modules/lesson/service'; +import { TaskService } from '@modules/task/service'; import { ObjectId } from 'bson'; import { ShareTokenContextType, ShareTokenParentType } from '../domainobject/share-token.do'; import { ShareTokenRepo } from '../repo/share-token.repo'; diff --git a/apps/server/src/modules/sharing/service/share-token.service.ts b/apps/server/src/modules/sharing/service/share-token.service.ts index e6431d64ed5..befdc580b2b 100644 --- a/apps/server/src/modules/sharing/service/share-token.service.ts +++ b/apps/server/src/modules/sharing/service/share-token.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; -import { CourseService } from '@src/modules/learnroom/service'; -import { LessonService } from '@src/modules/lesson/service'; -import { TaskService } from '@src/modules/task/service'; +import { CourseService } from '@modules/learnroom/service'; +import { LessonService } from '@modules/lesson/service'; +import { TaskService } from '@modules/task/service'; import { ShareTokenContext, ShareTokenDO, diff --git a/apps/server/src/modules/sharing/sharing.module.ts b/apps/server/src/modules/sharing/sharing.module.ts index f09214e9cf8..183141e19a7 100644 --- a/apps/server/src/modules/sharing/sharing.module.ts +++ b/apps/server/src/modules/sharing/sharing.module.ts @@ -1,6 +1,7 @@ import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; -import { AuthorizationModule } from '@src/modules/authorization'; +import { AuthorizationModule } from '@modules/authorization'; +import { AuthorizationReferenceModule } from '@modules/authorization/authorization-reference.module'; import { ShareTokenController } from './controller/share-token.controller'; import { ShareTokenUC } from './uc'; import { ShareTokenService, TokenGenerator } from './service'; @@ -10,7 +11,7 @@ import { LearnroomModule } from '../learnroom'; import { TaskModule } from '../task'; @Module({ - imports: [AuthorizationModule, LoggerModule, LearnroomModule, LessonModule, TaskModule], + imports: [AuthorizationModule, AuthorizationReferenceModule, LoggerModule, LearnroomModule, LessonModule, TaskModule], controllers: [], providers: [ShareTokenService, TokenGenerator, ShareTokenRepo], exports: [ShareTokenService], @@ -18,7 +19,15 @@ import { TaskModule } from '../task'; export class SharingModule {} @Module({ - imports: [SharingModule, AuthorizationModule, LearnroomModule, LessonModule, TaskModule, LoggerModule], + imports: [ + SharingModule, + AuthorizationModule, + AuthorizationReferenceModule, + LearnroomModule, + LessonModule, + TaskModule, + LoggerModule, + ], controllers: [ShareTokenController], providers: [ShareTokenUC], }) diff --git a/apps/server/src/modules/sharing/uc/share-token.uc.spec.ts b/apps/server/src/modules/sharing/uc/share-token.uc.spec.ts index 73234960794..72d2a824327 100644 --- a/apps/server/src/modules/sharing/uc/share-token.uc.spec.ts +++ b/apps/server/src/modules/sharing/uc/share-token.uc.spec.ts @@ -15,12 +15,13 @@ import { userFactory, } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { Action, AuthorizableReferenceType, AuthorizationService } from '@src/modules/authorization'; -import { CopyElementType, CopyStatus, CopyStatusEnum } from '@src/modules/copy-helper'; -import { CourseCopyService } from '@src/modules/learnroom'; -import { CourseService } from '@src/modules/learnroom/service'; -import { LessonCopyService } from '@src/modules/lesson/service'; -import { TaskCopyService } from '@src/modules/task/service'; +import { Action, AuthorizationService } from '@modules/authorization'; +import { AuthorizableReferenceType, AuthorizationReferenceService } from '@modules/authorization/domain'; +import { CopyElementType, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; +import { CourseCopyService } from '@modules/learnroom'; +import { CourseService } from '@modules/learnroom/service'; +import { LessonCopyService } from '@modules/lesson/service'; +import { TaskCopyService } from '@modules/task/service'; import { ShareTokenContextType, ShareTokenParentType, ShareTokenPayload } from '../domainobject/share-token.do'; import { ShareTokenService } from '../service'; import { ShareTokenUC } from './share-token.uc'; @@ -33,6 +34,7 @@ describe('ShareTokenUC', () => { let lessonCopyService: DeepMocked; let taskCopyService: DeepMocked; let authorization: DeepMocked; + let authorizationReferenceService: DeepMocked; let courseService: DeepMocked; let lessonRepo: DeepMocked; @@ -48,6 +50,10 @@ describe('ShareTokenUC', () => { provide: AuthorizationService, useValue: createMock(), }, + { + provide: AuthorizationReferenceService, + useValue: createMock(), + }, { provide: CourseCopyService, useValue: createMock(), @@ -81,8 +87,10 @@ describe('ShareTokenUC', () => { lessonCopyService = module.get(LessonCopyService); taskCopyService = module.get(TaskCopyService); authorization = module.get(AuthorizationService); + authorizationReferenceService = module.get(AuthorizationReferenceService); courseService = module.get(CourseService); lessonRepo = module.get(LessonRepo); + await setupEntities(); }); @@ -93,6 +101,7 @@ describe('ShareTokenUC', () => { beforeEach(() => { jest.resetAllMocks(); jest.clearAllMocks(); + // configuration sets must be part of the setup functions and part of the describe when ...and feature x is activated Configuration.set('FEATURE_COURSE_SHARE_NEW', true); Configuration.set('FEATURE_LESSON_SHARE', true); Configuration.set('FEATURE_TASK_SHARE', true); @@ -129,7 +138,7 @@ describe('ShareTokenUC', () => { parentType: ShareTokenParentType.Course, }); - expect(authorization.checkPermissionByReferences).toHaveBeenCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenCalledWith( user.id, AuthorizableReferenceType.Course, course.id, @@ -148,7 +157,7 @@ describe('ShareTokenUC', () => { parentType: ShareTokenParentType.Course, }); - expect(authorization.checkPermissionByReferences).toHaveBeenCalledTimes(1); + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenCalledTimes(1); }); it('should call the service', async () => { @@ -190,7 +199,7 @@ describe('ShareTokenUC', () => { parentType: ShareTokenParentType.Lesson, }); - expect(authorization.checkPermissionByReferences).toHaveBeenCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenCalledWith( user.id, AuthorizableReferenceType.Lesson, lesson.id, @@ -209,7 +218,7 @@ describe('ShareTokenUC', () => { parentType: ShareTokenParentType.Lesson, }); - expect(authorization.checkPermissionByReferences).toHaveBeenCalledTimes(1); + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenCalledTimes(1); }); it('should call the service', async () => { @@ -251,7 +260,7 @@ describe('ShareTokenUC', () => { parentType: ShareTokenParentType.Task, }); - expect(authorization.checkPermissionByReferences).toHaveBeenCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenCalledWith( user.id, AuthorizableReferenceType.Task, task.id, @@ -270,7 +279,7 @@ describe('ShareTokenUC', () => { parentType: ShareTokenParentType.Task, }); - expect(authorization.checkPermissionByReferences).toHaveBeenCalledTimes(1); + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenCalledTimes(1); }); it('should call the service', async () => { @@ -309,7 +318,7 @@ describe('ShareTokenUC', () => { } ); - expect(authorization.checkPermissionByReferences).toHaveBeenCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenCalledWith( user.id, AuthorizableReferenceType.Course, course.id, @@ -337,7 +346,7 @@ describe('ShareTokenUC', () => { } ); - expect(authorization.checkPermissionByReferences).toHaveBeenCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenCalledWith( user.id, AuthorizableReferenceType.School, school.id, @@ -574,7 +583,7 @@ describe('ShareTokenUC', () => { await uc.lookupShareToken(user.id, shareToken.token); - expect(authorization.checkPermissionByReferences).toHaveBeenCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenCalledWith( user.id, AuthorizableReferenceType.School, school.id, @@ -601,7 +610,7 @@ describe('ShareTokenUC', () => { await uc.lookupShareToken(user.id, shareToken.token); - expect(authorization.checkPermissionByReferences).not.toHaveBeenCalled(); + expect(authorizationReferenceService.checkPermissionByReferences).not.toHaveBeenCalled(); }); }); }); @@ -686,7 +695,7 @@ describe('ShareTokenUC', () => { await uc.importShareToken(user.id, shareToken.token, 'NewName'); - expect(authorization.checkPermissionByReferences).toHaveBeenCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenCalledWith( user.id, AuthorizableReferenceType.School, school.id, @@ -706,7 +715,7 @@ describe('ShareTokenUC', () => { await uc.importShareToken(user.id, shareToken.token, 'NewName'); - expect(authorization.checkPermissionByReferences).not.toHaveBeenCalled(); + expect(authorizationReferenceService.checkPermissionByReferences).not.toHaveBeenCalled(); }); }); }); @@ -803,7 +812,7 @@ describe('ShareTokenUC', () => { await uc.importShareToken(user.id, shareToken.token, 'NewName'); - expect(authorization.checkPermissionByReferences).toHaveBeenCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenCalledWith( user.id, AuthorizableReferenceType.School, school.id, @@ -823,7 +832,7 @@ describe('ShareTokenUC', () => { await uc.importShareToken(user.id, shareToken.token, 'NewName'); - expect(authorization.checkPermissionByReferences).not.toHaveBeenCalled(); + expect(authorizationReferenceService.checkPermissionByReferences).not.toHaveBeenCalled(); }); }); }); @@ -919,7 +928,7 @@ describe('ShareTokenUC', () => { await uc.importShareToken(user.id, shareToken.token, 'NewName'); - expect(authorization.checkPermissionByReferences).toHaveBeenCalledWith( + expect(authorizationReferenceService.checkPermissionByReferences).toHaveBeenCalledWith( user.id, AuthorizableReferenceType.School, school.id, @@ -939,7 +948,7 @@ describe('ShareTokenUC', () => { await uc.importShareToken(user.id, shareToken.token, 'NewName'); - expect(authorization.checkPermissionByReferences).not.toHaveBeenCalled(); + expect(authorizationReferenceService.checkPermissionByReferences).not.toHaveBeenCalled(); }); }); }); @@ -962,6 +971,7 @@ describe('ShareTokenUC', () => { service.lookupToken.mockResolvedValue(shareToken); jest.spyOn(ShareTokenUC.prototype as any, 'checkFeatureEnabled').mockReturnValue(undefined); jest.spyOn(ShareTokenUC.prototype as any, 'checkCreatePermission').mockReturnValue(undefined); + await expect(uc.importShareToken('userId', shareToken.token, 'NewName')).rejects.toThrowError( NotImplementedException ); diff --git a/apps/server/src/modules/sharing/uc/share-token.uc.ts b/apps/server/src/modules/sharing/uc/share-token.uc.ts index 0b72be7ce31..c2a12054cb7 100644 --- a/apps/server/src/modules/sharing/uc/share-token.uc.ts +++ b/apps/server/src/modules/sharing/uc/share-token.uc.ts @@ -2,12 +2,13 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { BadRequestException, Injectable, InternalServerErrorException, NotImplementedException } from '@nestjs/common'; import { EntityId, Permission } from '@shared/domain'; import { LegacyLogger } from '@src/core/logger'; -import { Action, AuthorizationService } from '@src/modules/authorization'; -import { CopyStatus } from '@src/modules/copy-helper'; -import { CourseCopyService } from '@src/modules/learnroom'; -import { CourseService } from '@src/modules/learnroom/service'; -import { LessonCopyService } from '@src/modules/lesson/service'; -import { TaskCopyService } from '@src/modules/task/service'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { AuthorizationReferenceService } from '@modules/authorization/domain'; +import { CopyStatus } from '@modules/copy-helper'; +import { CourseCopyService } from '@modules/learnroom'; +import { CourseService } from '@modules/learnroom/service'; +import { LessonCopyService } from '@modules/lesson/service'; +import { TaskCopyService } from '@modules/task/service'; import { ShareTokenContext, ShareTokenContextType, @@ -24,6 +25,7 @@ export class ShareTokenUC { constructor( private readonly shareTokenService: ShareTokenService, private readonly authorizationService: AuthorizationService, + private readonly authorizationReferenceService: AuthorizationReferenceService, private readonly courseCopyService: CourseCopyService, private readonly lessonCopyService: LessonCopyService, private readonly courseService: CourseService, @@ -177,18 +179,26 @@ export class ShareTokenUC { requiredPermissions = [Permission.HOMEWORK_CREATE]; } - await this.authorizationService.checkPermissionByReferences(userId, allowedParentType, payload.parentId, { - action: Action.write, - requiredPermissions, - }); + const authorizationContext = AuthorizationContextBuilder.write(requiredPermissions); + + await this.authorizationReferenceService.checkPermissionByReferences( + userId, + allowedParentType, + payload.parentId, + authorizationContext + ); } private async checkContextReadPermission(userId: EntityId, context: ShareTokenContext) { const allowedContextType = ShareTokenContextTypeMapper.mapToAllowedAuthorizationEntityType(context.contextType); - await this.authorizationService.checkPermissionByReferences(userId, allowedContextType, context.contextId, { - action: Action.read, - requiredPermissions: [], - }); + const authorizationContext = AuthorizationContextBuilder.read([]); + + await this.authorizationReferenceService.checkPermissionByReferences( + userId, + allowedContextType, + context.contextId, + authorizationContext + ); } private async checkCreatePermission(userId: EntityId, parentType: ShareTokenParentType) { @@ -221,16 +231,19 @@ export class ShareTokenUC { private checkFeatureEnabled(parentType: ShareTokenParentType) { switch (parentType) { case ShareTokenParentType.Course: + // Configuration.get is the deprecated way to read envirment variables if (!(Configuration.get('FEATURE_COURSE_SHARE_NEW') as boolean)) { throw new InternalServerErrorException('Import Course Feature not enabled'); } break; case ShareTokenParentType.Lesson: + // Configuration.get is the deprecated way to read envirment variables if (!(Configuration.get('FEATURE_LESSON_SHARE') as boolean)) { throw new InternalServerErrorException('Import Lesson Feature not enabled'); } break; case ShareTokenParentType.Task: + // Configuration.get is the deprecated way to read envirment variables if (!(Configuration.get('FEATURE_TASK_SHARE') as boolean)) { throw new InternalServerErrorException('Import Task Feature not enabled'); } diff --git a/apps/server/src/modules/system/controller/api-test/system.api.spec.ts b/apps/server/src/modules/system/controller/api-test/system.api.spec.ts index 823b0d82abf..d067abf1889 100644 --- a/apps/server/src/modules/system/controller/api-test/system.api.spec.ts +++ b/apps/server/src/modules/system/controller/api-test/system.api.spec.ts @@ -3,9 +3,9 @@ import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { OauthConfig, SystemEntity } from '@shared/domain'; import { cleanupCollections, systemFactory } from '@shared/testing'; -import { ICurrentUser } from '@src/modules/authentication'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@src/modules/server'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server'; import { Request } from 'express'; import request, { Response } from 'supertest'; import { PublicSystemListResponse } from '../dto/public-system-list.response'; diff --git a/apps/server/src/modules/system/controller/dto/oauth-config.response.ts b/apps/server/src/modules/system/controller/dto/oauth-config.response.ts index 1d5f98a7c6d..f2649b75f9a 100644 --- a/apps/server/src/modules/system/controller/dto/oauth-config.response.ts +++ b/apps/server/src/modules/system/controller/dto/oauth-config.response.ts @@ -66,10 +66,10 @@ export class OauthConfigResponse { @ApiProperty({ description: 'Logout endpoint', - required: true, + required: false, nullable: false, }) - logoutEndpoint: string; + logoutEndpoint?: string; @ApiProperty({ description: 'Issuer', @@ -95,7 +95,7 @@ export class OauthConfigResponse { jwksEndpoint: string; authEndpoint: string; scope: string; - logoutEndpoint: string; + logoutEndpoint?: string; grantType: string; issuer: string; }) { diff --git a/apps/server/src/modules/system/controller/dto/public-system-response.ts b/apps/server/src/modules/system/controller/dto/public-system-response.ts index 2d37338273c..b00055a1849 100644 --- a/apps/server/src/modules/system/controller/dto/public-system-response.ts +++ b/apps/server/src/modules/system/controller/dto/public-system-response.ts @@ -1,5 +1,5 @@ import { ApiProperty } from '@nestjs/swagger'; -import { OauthConfigResponse } from '@src/modules/system/controller/dto/oauth-config.response'; +import { OauthConfigResponse } from '@modules/system/controller/dto/oauth-config.response'; export class PublicSystemResponse { @ApiProperty({ diff --git a/apps/server/src/modules/system/controller/mapper/system-response.mapper.ts b/apps/server/src/modules/system/controller/mapper/system-response.mapper.ts index f996a59af99..20a215a3dbe 100644 --- a/apps/server/src/modules/system/controller/mapper/system-response.mapper.ts +++ b/apps/server/src/modules/system/controller/mapper/system-response.mapper.ts @@ -1,6 +1,6 @@ -import { OauthConfigResponse } from '@src/modules/system/controller/dto/oauth-config.response'; -import { OauthConfigDto } from '@src/modules/system/service/dto/oauth-config.dto'; -import { SystemDto } from '@src/modules/system/service/dto/system.dto'; +import { OauthConfigResponse } from '@modules/system/controller/dto/oauth-config.response'; +import { OauthConfigDto } from '@modules/system/service/dto/oauth-config.dto'; +import { SystemDto } from '@modules/system/service/dto/system.dto'; import { PublicSystemListResponse } from '../dto/public-system-list.response'; import { PublicSystemResponse } from '../dto/public-system-response'; diff --git a/apps/server/src/modules/system/controller/system.controller.ts b/apps/server/src/modules/system/controller/system.controller.ts index 95881cad907..bbeca71b83c 100644 --- a/apps/server/src/modules/system/controller/system.controller.ts +++ b/apps/server/src/modules/system/controller/system.controller.ts @@ -1,6 +1,6 @@ import { Controller, Get, Param, Query } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { SystemFilterParams } from '@src/modules/system/controller/dto/system.filter.params'; +import { SystemFilterParams } from '@modules/system/controller/dto/system.filter.params'; import { SystemDto } from '../service'; import { SystemUc } from '../uc/system.uc'; import { PublicSystemListResponse } from './dto/public-system-list.response'; diff --git a/apps/server/src/modules/system/mapper/system-oidc.mapper.spec.ts b/apps/server/src/modules/system/mapper/system-oidc.mapper.spec.ts index 5cc88f020ad..2486d4a1872 100644 --- a/apps/server/src/modules/system/mapper/system-oidc.mapper.spec.ts +++ b/apps/server/src/modules/system/mapper/system-oidc.mapper.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SystemEntity } from '@shared/domain'; import { systemFactory } from '@shared/testing'; -import { SystemOidcMapper } from '@src/modules/system/mapper/system-oidc.mapper'; +import { SystemOidcMapper } from '@modules/system/mapper/system-oidc.mapper'; describe('SystemOidcMapper', () => { let module: TestingModule; diff --git a/apps/server/src/modules/system/mapper/system-oidc.mapper.ts b/apps/server/src/modules/system/mapper/system-oidc.mapper.ts index f62a2e022c9..8726ce09a78 100644 --- a/apps/server/src/modules/system/mapper/system-oidc.mapper.ts +++ b/apps/server/src/modules/system/mapper/system-oidc.mapper.ts @@ -1,5 +1,5 @@ import { OidcConfig, SystemEntity } from '@shared/domain'; -import { OidcConfigDto } from '@src/modules/system/service/dto/oidc-config.dto'; +import { OidcConfigDto } from '@modules/system/service/dto/oidc-config.dto'; export class SystemOidcMapper { static mapFromEntityToDto(entity: SystemEntity): OidcConfigDto | undefined { diff --git a/apps/server/src/modules/system/mapper/system.mapper.spec.ts b/apps/server/src/modules/system/mapper/system.mapper.spec.ts index 21e9cf5f9b3..54c20cc0cff 100644 --- a/apps/server/src/modules/system/mapper/system.mapper.spec.ts +++ b/apps/server/src/modules/system/mapper/system.mapper.spec.ts @@ -1,7 +1,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SystemEntity } from '@shared/domain'; import { systemFactory } from '@shared/testing'; -import { SystemMapper } from '@src/modules/system/mapper/system.mapper'; +import { SystemMapper } from '@modules/system/mapper/system.mapper'; describe('SystemMapper', () => { let module: TestingModule; diff --git a/apps/server/src/modules/system/mapper/system.mapper.ts b/apps/server/src/modules/system/mapper/system.mapper.ts index b464e54f263..ae29fea67c8 100644 --- a/apps/server/src/modules/system/mapper/system.mapper.ts +++ b/apps/server/src/modules/system/mapper/system.mapper.ts @@ -1,6 +1,6 @@ import { OauthConfig, SystemEntity } from '@shared/domain'; -import { OauthConfigDto } from '@src/modules/system/service/dto/oauth-config.dto'; -import { SystemDto } from '@src/modules/system/service/dto/system.dto'; +import { OauthConfigDto } from '@modules/system/service/dto/oauth-config.dto'; +import { SystemDto } from '@modules/system/service/dto/system.dto'; export class SystemMapper { static mapFromEntityToDto(entity: SystemEntity): SystemDto { diff --git a/apps/server/src/modules/system/service/dto/oauth-config.dto.ts b/apps/server/src/modules/system/service/dto/oauth-config.dto.ts index 8ee1605bee2..7af97200971 100644 --- a/apps/server/src/modules/system/service/dto/oauth-config.dto.ts +++ b/apps/server/src/modules/system/service/dto/oauth-config.dto.ts @@ -19,7 +19,10 @@ export class OauthConfigDto { provider: string; - logoutEndpoint: string; + /** + * If this is set it will be used to redirect the user after login to the logout endpoint of the identity provider. + */ + logoutEndpoint?: string; issuer: string; diff --git a/apps/server/src/modules/system/service/dto/system.dto.ts b/apps/server/src/modules/system/service/dto/system.dto.ts index dbb5b7f7315..1ea7e4a84ee 100644 --- a/apps/server/src/modules/system/service/dto/system.dto.ts +++ b/apps/server/src/modules/system/service/dto/system.dto.ts @@ -1,6 +1,6 @@ import { EntityId } from '@shared/domain'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { OauthConfigDto } from '@src/modules/system/service/dto/oauth-config.dto'; +import { OauthConfigDto } from '@modules/system/service/dto/oauth-config.dto'; export class SystemDto { id?: EntityId; diff --git a/apps/server/src/modules/system/service/system-oidc.service.ts b/apps/server/src/modules/system/service/system-oidc.service.ts index a4987ff47d8..c1f1cf0a4c1 100644 --- a/apps/server/src/modules/system/service/system-oidc.service.ts +++ b/apps/server/src/modules/system/service/system-oidc.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@nestjs/common'; import { EntityNotFoundError } from '@shared/common'; import { EntityId, SystemEntity, SystemTypeEnum } from '@shared/domain'; import { SystemRepo } from '@shared/repo'; -import { SystemOidcMapper } from '@src/modules/system/mapper/system-oidc.mapper'; +import { SystemOidcMapper } from '@modules/system/mapper/system-oidc.mapper'; import { OidcConfigDto } from './dto'; @Injectable() diff --git a/apps/server/src/modules/system/service/system.service.ts b/apps/server/src/modules/system/service/system.service.ts index ec15c7d8bb3..960c15f7945 100644 --- a/apps/server/src/modules/system/service/system.service.ts +++ b/apps/server/src/modules/system/service/system.service.ts @@ -3,8 +3,8 @@ import { EntityNotFoundError } from '@shared/common'; import { EntityId, SystemEntity, SystemTypeEnum } from '@shared/domain'; import { IdentityManagementOauthService } from '@shared/infra/identity-management/identity-management-oauth.service'; import { SystemRepo } from '@shared/repo'; -import { SystemMapper } from '@src/modules/system/mapper/system.mapper'; -import { SystemDto } from '@src/modules/system/service/dto/system.dto'; +import { SystemMapper } from '@modules/system/mapper/system.mapper'; +import { SystemDto } from '@modules/system/service/dto/system.dto'; @Injectable() export class SystemService { diff --git a/apps/server/src/modules/system/system-api.module.ts b/apps/server/src/modules/system/system-api.module.ts index 27c14327e1f..e7213c1fac7 100644 --- a/apps/server/src/modules/system/system-api.module.ts +++ b/apps/server/src/modules/system/system-api.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; -import { SystemController } from '@src/modules/system/controller/system.controller'; -import { SystemUc } from '@src/modules/system/uc/system.uc'; +import { SystemController } from '@modules/system/controller/system.controller'; +import { SystemUc } from '@modules/system/uc/system.uc'; import { SystemModule } from './system.module'; @Module({ diff --git a/apps/server/src/modules/system/system.module.ts b/apps/server/src/modules/system/system.module.ts index f72c73855a0..64caef0df61 100644 --- a/apps/server/src/modules/system/system.module.ts +++ b/apps/server/src/modules/system/system.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { IdentityManagementModule } from '@shared/infra/identity-management/identity-management.module'; import { SystemRepo } from '@shared/repo'; -import { SystemService } from '@src/modules/system/service/system.service'; +import { SystemService } from '@modules/system/service/system.service'; import { SystemOidcService } from './service/system-oidc.service'; @Module({ diff --git a/apps/server/src/modules/system/uc/system.uc.spec.ts b/apps/server/src/modules/system/uc/system.uc.spec.ts index 21fc452c134..45bd65694d9 100644 --- a/apps/server/src/modules/system/uc/system.uc.spec.ts +++ b/apps/server/src/modules/system/uc/system.uc.spec.ts @@ -3,10 +3,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { EntityNotFoundError } from '@shared/common'; import { EntityId, SystemEntity, SystemTypeEnum } from '@shared/domain'; import { systemFactory } from '@shared/testing'; -import { SystemMapper } from '@src/modules/system/mapper/system.mapper'; -import { SystemDto } from '@src/modules/system/service/dto/system.dto'; -import { SystemService } from '@src/modules/system/service/system.service'; -import { SystemUc } from '@src/modules/system/uc/system.uc'; +import { SystemMapper } from '@modules/system/mapper/system.mapper'; +import { SystemDto } from '@modules/system/service/dto/system.dto'; +import { SystemService } from '@modules/system/service/system.service'; +import { SystemUc } from '@modules/system/uc/system.uc'; describe('SystemUc', () => { let module: TestingModule; diff --git a/apps/server/src/modules/system/uc/system.uc.ts b/apps/server/src/modules/system/uc/system.uc.ts index 89953cd29dc..00665191da7 100644 --- a/apps/server/src/modules/system/uc/system.uc.ts +++ b/apps/server/src/modules/system/uc/system.uc.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { EntityNotFoundError } from '@shared/common'; import { EntityId, SystemEntity, SystemType, SystemTypeEnum } from '@shared/domain'; -import { SystemDto } from '@src/modules/system/service/dto/system.dto'; -import { SystemService } from '@src/modules/system/service/system.service'; +import { SystemDto } from '@modules/system/service/dto/system.dto'; +import { SystemService } from '@modules/system/service/system.service'; @Injectable() export class SystemUc { diff --git a/apps/server/src/modules/task/controller/api-test/submission.api.spec.ts b/apps/server/src/modules/task/controller/api-test/submission.api.spec.ts index f01b3f5baf0..6aea7ac2521 100644 --- a/apps/server/src/modules/task/controller/api-test/submission.api.spec.ts +++ b/apps/server/src/modules/task/controller/api-test/submission.api.spec.ts @@ -4,7 +4,7 @@ import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { Permission, Submission } from '@shared/domain'; -import { ICurrentUser } from '@src/modules/authentication'; +import { ICurrentUser } from '@modules/authentication'; import { cleanupCollections, courseGroupFactory, @@ -14,10 +14,10 @@ import { taskFactory, userFactory, } from '@shared/testing'; -import { FilesStorageClientAdapterService } from '@src/modules'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@src/modules/server/server.module'; -import { SubmissionStatusListResponse } from '@src/modules/task/controller/dto/submission.response'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; +import { SubmissionStatusListResponse } from '@modules/task/controller/dto/submission.response'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/task/controller/api-test/task-copy-timeout.api.spec.ts b/apps/server/src/modules/task/controller/api-test/task-copy-timeout.api.spec.ts index f8e431b4d09..446a0fab032 100644 --- a/apps/server/src/modules/task/controller/api-test/task-copy-timeout.api.spec.ts +++ b/apps/server/src/modules/task/controller/api-test/task-copy-timeout.api.spec.ts @@ -3,7 +3,7 @@ import { IConfig } from '@hpi-schul-cloud/commons/lib/interfaces/IConfig'; import { EntityManager } from '@mikro-orm/mongodb'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { ICurrentUser } from '@src/modules/authentication'; +import { ICurrentUser } from '@modules/authentication'; import { cleanupCollections, courseFactory, @@ -12,14 +12,14 @@ import { taskFactory, userFactory, } from '@shared/testing'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; import { Request } from 'express'; import request from 'supertest'; Configuration.set('FEATURE_COPY_SERVICE_ENABLED', true); Configuration.set('INCOMING_REQUEST_TIMEOUT_COPY_API', 1); // eslint-disable-next-line import/first -import { ServerTestModule } from '@src/modules/server/server.module'; +import { ServerTestModule } from '@modules/server/server.module'; // This needs to be in a separate test file because of the above configuration. // When we find a way to mock the config, it should be moved alongside the other API tests. diff --git a/apps/server/src/modules/task/controller/api-test/task-delete.api.spec.ts b/apps/server/src/modules/task/controller/api-test/task-delete.api.spec.ts index a85474c7862..efdc78d53b2 100644 --- a/apps/server/src/modules/task/controller/api-test/task-delete.api.spec.ts +++ b/apps/server/src/modules/task/controller/api-test/task-delete.api.spec.ts @@ -10,8 +10,8 @@ import { TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; -import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; -import { ServerTestModule } from '@src/modules/server'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; +import { ServerTestModule } from '@modules/server'; const createStudent = () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent({}, [ diff --git a/apps/server/src/modules/task/controller/api-test/task-finish.api.spec.ts b/apps/server/src/modules/task/controller/api-test/task-finish.api.spec.ts index 43597913adb..acf66c36a1d 100644 --- a/apps/server/src/modules/task/controller/api-test/task-finish.api.spec.ts +++ b/apps/server/src/modules/task/controller/api-test/task-finish.api.spec.ts @@ -9,7 +9,7 @@ import { TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; -import { ServerTestModule } from '@src/modules/server'; +import { ServerTestModule } from '@modules/server'; const createStudent = () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent({}, [ diff --git a/apps/server/src/modules/task/controller/api-test/task-finished.api.spec.ts b/apps/server/src/modules/task/controller/api-test/task-finished.api.spec.ts index 9d48217bcde..93c79e4015d 100644 --- a/apps/server/src/modules/task/controller/api-test/task-finished.api.spec.ts +++ b/apps/server/src/modules/task/controller/api-test/task-finished.api.spec.ts @@ -2,7 +2,7 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { ExecutionContext, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission } from '@shared/domain'; -import { ICurrentUser } from '@src/modules/authentication'; +import { ICurrentUser } from '@modules/authentication'; import { cleanupCollections, courseFactory, @@ -12,9 +12,9 @@ import { taskFactory, userFactory, } from '@shared/testing'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@src/modules/server/server.module'; -import { TaskListResponse } from '@src/modules/task/controller/dto'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; +import { TaskListResponse } from '@modules/task/controller/dto'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/task/controller/api-test/task-restore.api.spec.ts b/apps/server/src/modules/task/controller/api-test/task-restore.api.spec.ts index 221d53f0f75..9d8f50e0950 100644 --- a/apps/server/src/modules/task/controller/api-test/task-restore.api.spec.ts +++ b/apps/server/src/modules/task/controller/api-test/task-restore.api.spec.ts @@ -9,7 +9,7 @@ import { TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; -import { ServerTestModule } from '@src/modules/server'; +import { ServerTestModule } from '@modules/server'; const createStudent = () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent({}, [ diff --git a/apps/server/src/modules/task/controller/api-test/task-revert-published.api.spec.ts b/apps/server/src/modules/task/controller/api-test/task-revert-published.api.spec.ts index b26555c7c7c..d4e6d4dee64 100644 --- a/apps/server/src/modules/task/controller/api-test/task-revert-published.api.spec.ts +++ b/apps/server/src/modules/task/controller/api-test/task-revert-published.api.spec.ts @@ -9,7 +9,7 @@ import { TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; -import { ServerTestModule } from '@src/modules/server'; +import { ServerTestModule } from '@modules/server'; const createStudent = () => { const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent({}, [ diff --git a/apps/server/src/modules/task/controller/api-test/task.api.spec.ts b/apps/server/src/modules/task/controller/api-test/task.api.spec.ts index 369216b0d50..b7afee9c257 100644 --- a/apps/server/src/modules/task/controller/api-test/task.api.spec.ts +++ b/apps/server/src/modules/task/controller/api-test/task.api.spec.ts @@ -10,8 +10,8 @@ import { submissionFactory, taskFactory, } from '@shared/testing'; -import { ServerTestModule } from '@src/modules/server/server.module'; -import { TaskListResponse } from '@src/modules/task/controller/dto'; +import { ServerTestModule } from '@modules/server/server.module'; +import { TaskListResponse } from '@modules/task/controller/dto'; const tomorrow = new Date(Date.now() + 86400000); diff --git a/apps/server/src/modules/task/controller/submission.controller.ts b/apps/server/src/modules/task/controller/submission.controller.ts index df1d08f9953..64d3a23296f 100644 --- a/apps/server/src/modules/task/controller/submission.controller.ts +++ b/apps/server/src/modules/task/controller/submission.controller.ts @@ -1,7 +1,6 @@ import { Controller, Delete, Get, Param } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { SubmissionMapper } from '../mapper'; import { SubmissionUc } from '../uc'; import { SubmissionStatusListResponse, SubmissionUrlParams, TaskUrlParams } from './dto'; diff --git a/apps/server/src/modules/task/controller/task.controller.spec.ts b/apps/server/src/modules/task/controller/task.controller.spec.ts index 652f4797fa8..e44deee06c3 100644 --- a/apps/server/src/modules/task/controller/task.controller.spec.ts +++ b/apps/server/src/modules/task/controller/task.controller.spec.ts @@ -1,8 +1,8 @@ import { createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; -import { ICurrentUser } from '@src/modules/authentication'; -import { CopyElementType, CopyStatus, CopyStatusEnum } from '@src/modules/copy-helper'; -import { CopyApiResponse } from '@src/modules/copy-helper/dto/copy.response'; +import { ICurrentUser } from '@modules/authentication'; +import { CopyElementType, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; +import { CopyApiResponse } from '@modules/copy-helper/dto/copy.response'; import { TaskCopyUC, TaskUC } from '../uc'; import { TaskController } from './task.controller'; diff --git a/apps/server/src/modules/task/controller/task.controller.ts b/apps/server/src/modules/task/controller/task.controller.ts index b62400d0426..44911973ffd 100644 --- a/apps/server/src/modules/task/controller/task.controller.ts +++ b/apps/server/src/modules/task/controller/task.controller.ts @@ -2,10 +2,10 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestj import { ApiTags } from '@nestjs/swagger'; import { RequestTimeout } from '@shared/common'; import { PaginationParams } from '@shared/controller/'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; -import { CopyApiResponse, CopyMapper } from '@src/modules/copy-helper'; -import { serverConfig } from '@src/modules/server/server.config'; +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; +import { CopyApiResponse, CopyMapper } from '@modules/copy-helper'; +// invalid import can produce dependency cycles +import { serverConfig } from '@modules/server/server.config'; import { TaskMapper } from '../mapper'; import { TaskCopyUC } from '../uc/task-copy.uc'; import { TaskUC } from '../uc/task.uc'; diff --git a/apps/server/src/modules/task/service/submission.service.spec.ts b/apps/server/src/modules/task/service/submission.service.spec.ts index 97267be74df..4d7373570cf 100644 --- a/apps/server/src/modules/task/service/submission.service.spec.ts +++ b/apps/server/src/modules/task/service/submission.service.spec.ts @@ -4,7 +4,7 @@ import { Counted, Submission } from '@shared/domain'; import { FileRecordParentType } from '@shared/infra/rabbitmq'; import { SubmissionRepo } from '@shared/repo'; import { setupEntities, submissionFactory, taskFactory } from '@shared/testing'; -import { FileDto, FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; +import { FileDto, FilesStorageClientAdapterService } from '@modules/files-storage-client'; import { SubmissionService } from './submission.service'; describe('Submission Service', () => { diff --git a/apps/server/src/modules/task/service/submission.service.ts b/apps/server/src/modules/task/service/submission.service.ts index 378bd29c67e..c451dc8ad95 100644 --- a/apps/server/src/modules/task/service/submission.service.ts +++ b/apps/server/src/modules/task/service/submission.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { Counted, EntityId, Submission } from '@shared/domain'; import { SubmissionRepo } from '@shared/repo'; -import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; @Injectable() export class SubmissionService { diff --git a/apps/server/src/modules/task/service/task-copy.service.spec.ts b/apps/server/src/modules/task/service/task-copy.service.spec.ts index a54ac5ca002..ca320ac60d6 100644 --- a/apps/server/src/modules/task/service/task-copy.service.spec.ts +++ b/apps/server/src/modules/task/service/task-copy.service.spec.ts @@ -11,8 +11,8 @@ import { userFactory, legacyFileEntityMockFactory, } from '@shared/testing'; -import { CopyElementType, CopyHelperService, CopyStatusEnum } from '@src/modules/copy-helper'; -import { CopyFilesService } from '@src/modules/files-storage-client'; +import { CopyElementType, CopyHelperService, CopyStatusEnum } from '@modules/copy-helper'; +import { CopyFilesService } from '@modules/files-storage-client'; import { TaskCopyService } from './task-copy.service'; describe('task copy service', () => { diff --git a/apps/server/src/modules/task/service/task-copy.service.ts b/apps/server/src/modules/task/service/task-copy.service.ts index 024ed123f4b..f7faa684e8d 100644 --- a/apps/server/src/modules/task/service/task-copy.service.ts +++ b/apps/server/src/modules/task/service/task-copy.service.ts @@ -2,9 +2,9 @@ import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain'; import { Course, LessonEntity, Task, User } from '@shared/domain/entity'; import { TaskRepo } from '@shared/repo'; -import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@src/modules/copy-helper'; -import { CopyFilesService } from '@src/modules/files-storage-client'; -import { FileUrlReplacement } from '@src/modules/files-storage-client/service/copy-files.service'; +import { CopyElementType, CopyHelperService, CopyStatus, CopyStatusEnum } from '@modules/copy-helper'; +import { CopyFilesService } from '@modules/files-storage-client'; +import { FileUrlReplacement } from '@modules/files-storage-client/service/copy-files.service'; type TaskCopyParams = { originalTaskId: EntityId; diff --git a/apps/server/src/modules/task/service/task.service.spec.ts b/apps/server/src/modules/task/service/task.service.spec.ts index 3183337a540..4cf950964bd 100644 --- a/apps/server/src/modules/task/service/task.service.spec.ts +++ b/apps/server/src/modules/task/service/task.service.spec.ts @@ -2,7 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { TaskRepo } from '@shared/repo'; import { setupEntities, submissionFactory, taskFactory } from '@shared/testing'; -import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; import { SubmissionService } from './submission.service'; import { TaskService } from './task.service'; diff --git a/apps/server/src/modules/task/service/task.service.ts b/apps/server/src/modules/task/service/task.service.ts index c7e433ae73b..78f9dca9a51 100644 --- a/apps/server/src/modules/task/service/task.service.ts +++ b/apps/server/src/modules/task/service/task.service.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { Counted, EntityId, IFindOptions, Task } from '@shared/domain'; import { TaskRepo } from '@shared/repo'; -import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; import { SubmissionService } from './submission.service'; @Injectable() diff --git a/apps/server/src/modules/task/task-api.module.ts b/apps/server/src/modules/task/task-api.module.ts index 442b5d2d111..cdb998eab4a 100644 --- a/apps/server/src/modules/task/task-api.module.ts +++ b/apps/server/src/modules/task/task-api.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { CourseRepo, LessonRepo, TaskRepo } from '@shared/repo'; -import { AuthorizationModule } from '@src/modules/authorization'; -import { CopyHelperModule } from '@src/modules/copy-helper/copy-helper.module'; +import { AuthorizationModule } from '@modules/authorization'; +import { CopyHelperModule } from '@modules/copy-helper/copy-helper.module'; import { SubmissionController, TaskController } from './controller'; import { TaskModule } from './task.module'; import { SubmissionUc, TaskCopyUC, TaskUC } from './uc'; diff --git a/apps/server/src/modules/task/task.module.ts b/apps/server/src/modules/task/task.module.ts index 0d95bcc19b0..696d608d0a3 100644 --- a/apps/server/src/modules/task/task.module.ts +++ b/apps/server/src/modules/task/task.module.ts @@ -1,8 +1,8 @@ import { forwardRef, Module } from '@nestjs/common'; import { CourseRepo, LessonRepo, SubmissionRepo, TaskRepo } from '@shared/repo'; -import { AuthorizationModule } from '@src/modules/authorization'; -import { CopyHelperModule } from '@src/modules/copy-helper'; -import { FilesStorageClientModule } from '@src/modules/files-storage-client'; +import { AuthorizationModule } from '@modules/authorization'; +import { CopyHelperModule } from '@modules/copy-helper'; +import { FilesStorageClientModule } from '@modules/files-storage-client'; import { SubmissionService, TaskCopyService, TaskService } from './service'; @Module({ diff --git a/apps/server/src/modules/task/uc/submission.uc.spec.ts b/apps/server/src/modules/task/uc/submission.uc.spec.ts index 5747e0c6cc8..63bac0f52f1 100644 --- a/apps/server/src/modules/task/uc/submission.uc.spec.ts +++ b/apps/server/src/modules/task/uc/submission.uc.spec.ts @@ -2,7 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { Counted, Permission, Submission } from '@shared/domain'; import { setupEntities, submissionFactory, taskFactory, userFactory } from '@shared/testing'; -import { AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { SubmissionService } from '../service/submission.service'; import { SubmissionUc } from './submission.uc'; diff --git a/apps/server/src/modules/task/uc/submission.uc.ts b/apps/server/src/modules/task/uc/submission.uc.ts index 2edc26a97a3..50c2430e8de 100644 --- a/apps/server/src/modules/task/uc/submission.uc.ts +++ b/apps/server/src/modules/task/uc/submission.uc.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { EntityId, Permission, Submission, User } from '@shared/domain'; -import { AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { SubmissionService } from '../service'; @Injectable() diff --git a/apps/server/src/modules/task/uc/task-copy.uc.spec.ts b/apps/server/src/modules/task/uc/task-copy.uc.spec.ts index a7666d479a9..dc381cda22e 100644 --- a/apps/server/src/modules/task/uc/task-copy.uc.spec.ts +++ b/apps/server/src/modules/task/uc/task-copy.uc.spec.ts @@ -3,15 +3,14 @@ import { Configuration } from '@hpi-schul-cloud/commons'; import { ObjectId } from '@mikro-orm/mongodb'; import { ForbiddenException, InternalServerErrorException, NotFoundException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { BaseDO, User } from '@shared/domain'; import { CourseRepo, LessonRepo, TaskRepo, UserRepo } from '@shared/repo'; import { courseFactory, lessonFactory, setupEntities, taskFactory, userFactory } from '@shared/testing'; -import { Action, AuthorizableReferenceType, AuthorizationService } from '@src/modules/authorization'; -import { CopyElementType, CopyHelperService, CopyStatusEnum } from '@src/modules/copy-helper'; -import { FilesStorageClientAdapterService } from '@src/modules/files-storage-client'; -import { AuthorizableObject } from '@shared/domain/domain-object'; +import { Action, AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { CopyElementType, CopyHelperService, CopyStatusEnum } from '@modules/copy-helper'; +import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; import { TaskCopyService } from '../service'; import { TaskCopyUC } from './task-copy.uc'; +import { TaskCopyParentParams } from '../types'; describe('task copy uc', () => { let uc: TaskCopyUC; @@ -92,14 +91,8 @@ describe('task copy uc', () => { const lesson = lessonFactory.buildWithId({ course }); const allTasks = taskFactory.buildList(3, { course }); const task = allTasks[0]; - authorisation.getUserWithPermissions.mockResolvedValue(user); - taskRepo.findById.mockResolvedValue(task); - lessonRepo.findById.mockResolvedValue(lesson); - taskRepo.findBySingleParent.mockResolvedValue([allTasks, allTasks.length]); - courseRepo.findById.mockResolvedValue(course); - authorisation.hasPermission.mockReturnValue(true); const copyName = 'name of the copy'; - copyHelperService.deriveCopyName.mockReturnValue(copyName); + const copy = taskFactory.buildWithId({ creator: user, course }); const status = { title: 'taskCopy', @@ -108,9 +101,16 @@ describe('task copy uc', () => { copyEntity: copy, originalEntity: task, }; - taskCopyService.copyTask.mockResolvedValue(status); - taskRepo.save.mockResolvedValue(undefined); - const userId = user.id; + + authorisation.getUserWithPermissions.mockResolvedValueOnce(user); + taskRepo.findById.mockResolvedValueOnce(task); + lessonRepo.findById.mockResolvedValueOnce(lesson); + taskRepo.findBySingleParent.mockResolvedValueOnce([allTasks, allTasks.length]); + courseRepo.findById.mockResolvedValueOnce(course); + authorisation.hasPermission.mockReturnValueOnce(true).mockReturnValueOnce(true); + copyHelperService.deriveCopyName.mockReturnValueOnce(copyName); + taskCopyService.copyTask.mockResolvedValueOnce(status); + taskRepo.save.mockResolvedValueOnce(); return { user, @@ -121,15 +121,16 @@ describe('task copy uc', () => { copy, allTasks, status, - userId, + userId: user.id, }; }; describe('feature is deactivated', () => { it('should throw InternalServerErrorException', async () => { + const { course, user, task, userId } = setup(); Configuration.set('FEATURE_COPY_SERVICE_ENABLED', false); - await expect(uc.copyTask('user.id', 'task.id', { courseId: 'course.id', userId: 'test' })).rejects.toThrowError( + await expect(uc.copyTask(user.id, task.id, { courseId: course.id, userId })).rejects.toThrowError( InternalServerErrorException ); }); @@ -214,15 +215,9 @@ describe('task copy uc', () => { const { course, user, task, userId } = setup(); await uc.copyTask(user.id, task.id, { courseId: course.id, userId }); - expect(authorisation.checkPermissionByReferences).toBeCalledWith( - user.id, - AuthorizableReferenceType.Course, - course.id, - { - action: Action.write, - requiredPermissions: [], - } - ); + + const context = AuthorizationContextBuilder.write([]); + expect(authorisation.checkPermission).toBeCalledWith(user, course, context); }); it('should pass authorisation check without destination course', async () => { @@ -230,10 +225,8 @@ describe('task copy uc', () => { await uc.copyTask(user.id, task.id, { userId }); - expect(authorisation.hasPermission).not.toBeCalledWith(user, course, { - action: Action.write, - requiredPermissions: [], - }); + const context = AuthorizationContextBuilder.write([]); + expect(authorisation.hasPermission).not.toBeCalledWith(user, course, context); }); it('should check authorisation for destination lesson', async () => { @@ -260,57 +253,64 @@ describe('task copy uc', () => { describe('when access to task is forbidden', () => { const setupWithTaskForbidden = () => { + Configuration.set('FEATURE_COPY_SERVICE_ENABLED', true); + const user = userFactory.buildWithId(); const course = courseFactory.buildWithId(); const lesson = lessonFactory.buildWithId({ course }); const task = taskFactory.buildWithId(); - userRepo.findById.mockResolvedValue(user); - taskRepo.findById.mockResolvedValue(task); - // authorisation should not be mocked - authorisation.hasPermission.mockImplementation((u: User, e: AuthorizableObject | BaseDO) => e !== task); - return { user, course, lesson, task }; + + userRepo.findById.mockResolvedValueOnce(user); + taskRepo.findById.mockResolvedValueOnce(task); + authorisation.hasPermission.mockReturnValueOnce(false); + + const parentParams: TaskCopyParentParams = { + courseId: course.id, + lessonId: lesson.id, + userId: new ObjectId().toHexString(), + }; + + return { user, course, lesson, task, parentParams }; }; it('should throw NotFoundException', async () => { - const { course, lesson, user, task } = setupWithTaskForbidden(); - - try { - await uc.copyTask(user.id, task.id, { - courseId: course.id, - lessonId: lesson.id, - userId: new ObjectId().toHexString(), - }); - throw new Error('should have failed'); - } catch (err) { - expect(err).toBeInstanceOf(NotFoundException); - } + const { user, task, parentParams } = setupWithTaskForbidden(); + + await expect(uc.copyTask(user.id, task.id, parentParams)).rejects.toThrowError( + new NotFoundException('could not find task to copy') + ); }); }); describe('when access to course is forbidden', () => { const setupWithCourseForbidden = () => { + Configuration.set('FEATURE_COPY_SERVICE_ENABLED', true); + const user = userFactory.buildWithId(); const course = courseFactory.buildWithId(); const task = taskFactory.buildWithId(); - userRepo.findById.mockResolvedValue(user); - taskRepo.findById.mockResolvedValue(task); - // authorisation should not be mocked - authorisation.hasPermission.mockImplementation((u: User, e: AuthorizableObject | BaseDO) => e !== course); - authorisation.checkPermissionByReferences.mockImplementation(() => { + + userRepo.findById.mockResolvedValueOnce(user); + taskRepo.findById.mockResolvedValueOnce(task); + courseRepo.findById.mockResolvedValueOnce(course); + authorisation.hasPermission.mockReturnValueOnce(true).mockReturnValueOnce(true); + authorisation.checkPermission.mockImplementationOnce(() => { throw new ForbiddenException(); }); - return { user, course, task }; + + const parentParams: TaskCopyParentParams = { courseId: course.id, userId: new ObjectId().toHexString() }; + + return { + userId: user.id, + taskId: task.id, + parentParams, + }; }; it('should throw Forbidden Exception', async () => { - const { course, user, task } = setupWithCourseForbidden(); - - try { - await uc.copyTask(user.id, task.id, { courseId: course.id, userId: new ObjectId().toHexString() }); - throw new Error('should have failed'); - } catch (err) { - expect(err).toBeInstanceOf(ForbiddenException); - } + const { userId, taskId, parentParams } = setupWithCourseForbidden(); + + await expect(uc.copyTask(userId, taskId, parentParams)).rejects.toThrowError(new ForbiddenException()); }); }); }); @@ -355,32 +355,35 @@ describe('task copy uc', () => { describe('when access to lesson is forbidden', () => { const setupWithLessonForbidden = () => { + Configuration.set('FEATURE_COPY_SERVICE_ENABLED', true); + const user = userFactory.buildWithId(); const course = courseFactory.buildWithId(); const lesson = lessonFactory.buildWithId({ course }); const task = taskFactory.buildWithId(); - userRepo.findById.mockResolvedValue(user); - taskRepo.findById.mockResolvedValue(task); - courseRepo.findById.mockResolvedValue(course); - lessonRepo.findById.mockResolvedValue(lesson); - // Authorisation should not be mocked - authorisation.hasPermission.mockImplementation((u: User, e: AuthorizableObject | BaseDO) => { - if (e === lesson) return false; - return true; - }); - return { user, lesson, task }; + userRepo.findById.mockResolvedValueOnce(user); + taskRepo.findById.mockResolvedValueOnce(task); + courseRepo.findById.mockResolvedValueOnce(course); + lessonRepo.findById.mockResolvedValueOnce(lesson); + // first canReadTask > second canWriteLesson + authorisation.hasPermission.mockReturnValueOnce(true).mockReturnValueOnce(false); + + const parentParams: TaskCopyParentParams = { lessonId: lesson.id, userId: new ObjectId().toHexString() }; + + return { + userId: user.id, + taskId: task.id, + parentParams, + }; }; it('should throw Forbidden Exception', async () => { - const { lesson, user, task } = setupWithLessonForbidden(); - - try { - await uc.copyTask(user.id, task.id, { lessonId: lesson.id, userId: new ObjectId().toHexString() }); - throw new Error('should have failed'); - } catch (err) { - expect(err).toBeInstanceOf(ForbiddenException); - } + const { userId, taskId, parentParams } = setupWithLessonForbidden(); + + await expect(uc.copyTask(userId, taskId, parentParams)).rejects.toThrowError( + new ForbiddenException('you dont have permission to add to this lesson') + ); }); }); }); diff --git a/apps/server/src/modules/task/uc/task-copy.uc.ts b/apps/server/src/modules/task/uc/task-copy.uc.ts index b1cdd212919..69fd99e224f 100644 --- a/apps/server/src/modules/task/uc/task-copy.uc.ts +++ b/apps/server/src/modules/task/uc/task-copy.uc.ts @@ -1,14 +1,9 @@ import { Configuration } from '@hpi-schul-cloud/commons'; import { ForbiddenException, Injectable, InternalServerErrorException, NotFoundException } from '@nestjs/common'; -import { Course, EntityId, LessonEntity, User } from '@shared/domain'; +import { Course, EntityId, Task, LessonEntity, User } from '@shared/domain'; import { CourseRepo, LessonRepo, TaskRepo } from '@shared/repo'; -import { - Action, - AuthorizableReferenceType, - AuthorizationContextBuilder, - AuthorizationService, -} from '@src/modules/authorization'; -import { CopyHelperService, CopyStatus } from '@src/modules/copy-helper'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { CopyHelperService, CopyStatus } from '@modules/copy-helper'; import { TaskCopyService } from '../service'; import { TaskCopyParentParams } from '../types'; @@ -24,46 +19,73 @@ export class TaskCopyUC { ) {} async copyTask(userId: EntityId, taskId: EntityId, parentParams: TaskCopyParentParams): Promise { - this.featureEnabled(); - const user = await this.authorisation.getUserWithPermissions(userId); - const originalTask = await this.taskRepo.findById(taskId); - if (!this.authorisation.hasPermission(user, originalTask, AuthorizationContextBuilder.read([]))) { - throw new NotFoundException('could not find task to copy'); - } + this.checkFeatureEnabled(); + + // i put it to promise all, it do not look like any more information can be expose over errors if it is called between the authorizations + // TODO: Add try catch around it with throw BadRequest invalid data + const [authorizableUser, originalTask, destinationCourse]: [User, Task, Course | undefined] = await Promise.all([ + this.authorisation.getUserWithPermissions(userId), + this.taskRepo.findById(taskId), + this.getDestinationCourse(parentParams.courseId), + ]); - const destinationCourse = await this.getDestinationCourse(parentParams.courseId); - if (parentParams.courseId) { - await this.authorisation.checkPermissionByReferences( - userId, - AuthorizableReferenceType.Course, - parentParams.courseId, - { - action: Action.write, - requiredPermissions: [], - } - ); + this.checkOriginalTaskAuthorization(authorizableUser, originalTask); + + if (destinationCourse) { + this.checkDestinationCourseAuthorisation(authorizableUser, destinationCourse); } - const destinationLesson = await this.getDestinationLesson(parentParams.lessonId, user); - const copyName = await this.getCopyName(originalTask.name, parentParams.courseId); + // i think getDestinationLesson can also to a promise.all on top + // then getCopyName can be put into if (destinationCourse) { + // but then the test need to cleanup + const [destinationLesson, copyName]: [LessonEntity | undefined, string | undefined] = await Promise.all([ + this.getDestinationLesson(parentParams.lessonId), + this.getCopyName(originalTask.name, parentParams.courseId), + ]); + + if (destinationLesson) { + this.checkDestinationLessonAuthorization(authorizableUser, destinationLesson); + } const status = await this.taskCopyService.copyTask({ originalTaskId: originalTask.id, destinationCourse, destinationLesson, - user, + user: authorizableUser, copyName, }); return status; } + private checkOriginalTaskAuthorization(authorizableUser: User, originalTask: Task): void { + const context = AuthorizationContextBuilder.read([]); + if (!this.authorisation.hasPermission(authorizableUser, originalTask, context)) { + // error message and erorr type are not correct + throw new NotFoundException('could not find task to copy'); + } + } + + private checkDestinationCourseAuthorisation(authorizableUser: User, destinationCourse: Course): void { + const context = AuthorizationContextBuilder.write([]); + this.authorisation.checkPermission(authorizableUser, destinationCourse, context); + } + + private checkDestinationLessonAuthorization(authorizableUser: User, destinationLesson: LessonEntity): void { + const context = AuthorizationContextBuilder.write([]); + if (!this.authorisation.hasPermission(authorizableUser, destinationLesson, context)) { + throw new ForbiddenException('you dont have permission to add to this lesson'); + } + } + private async getCopyName(originalTaskName: string, parentCourseId: EntityId | undefined) { let existingNames: string[] = []; if (parentCourseId) { + // It should really get an task where the creatorId === '' ? const [existingTasks] = await this.taskRepo.findBySingleParent('', parentCourseId); existingNames = existingTasks.map((t) => t.name); } + return this.copyHelperService.deriveCopyName(originalTaskName, existingNames); } @@ -77,19 +99,18 @@ export class TaskCopyUC { return destinationCourse; } - private async getDestinationLesson(lessonId: string | undefined, user: User): Promise { + private async getDestinationLesson(lessonId: string | undefined): Promise { if (lessonId === undefined) { return undefined; } const destinationLesson = await this.lessonRepo.findById(lessonId); - if (!this.authorisation.hasPermission(user, destinationLesson, AuthorizationContextBuilder.write([]))) { - throw new ForbiddenException('you dont have permission to add to this lesson'); - } + return destinationLesson; } - private featureEnabled() { + private checkFeatureEnabled() { + // This is the deprecated way to read envirement variables const enabled = Configuration.get('FEATURE_COPY_SERVICE_ENABLED') as boolean; if (!enabled) { throw new InternalServerErrorException('Copy Feature not enabled'); diff --git a/apps/server/src/modules/task/uc/task.uc.spec.ts b/apps/server/src/modules/task/uc/task.uc.spec.ts index e190db57d74..90bb29db444 100644 --- a/apps/server/src/modules/task/uc/task.uc.spec.ts +++ b/apps/server/src/modules/task/uc/task.uc.spec.ts @@ -13,7 +13,7 @@ import { taskFactory, userFactory, } from '@shared/testing'; -import { Action, AuthorizationService } from '@src/modules/authorization'; +import { Action, AuthorizationService } from '@modules/authorization'; import { TaskService } from '../service'; import { TaskUC } from './task.uc'; diff --git a/apps/server/src/modules/task/uc/task.uc.ts b/apps/server/src/modules/task/uc/task.uc.ts index 47186c304fc..a6e40dd3b6d 100644 --- a/apps/server/src/modules/task/uc/task.uc.ts +++ b/apps/server/src/modules/task/uc/task.uc.ts @@ -12,7 +12,7 @@ import { User, } from '@shared/domain'; import { CourseRepo, LessonRepo, TaskRepo } from '@shared/repo'; -import { Action, AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; +import { Action, AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; import { TaskService } from '../service'; @Injectable() diff --git a/apps/server/src/modules/teams/teams-api.module.ts b/apps/server/src/modules/teams/teams-api.module.ts index 5fc620fe76a..e5df732a951 100644 --- a/apps/server/src/modules/teams/teams-api.module.ts +++ b/apps/server/src/modules/teams/teams-api.module.ts @@ -1,5 +1,5 @@ import { Module } from '@nestjs/common'; -import { TeamsModule } from '@src/modules/teams/teams.module'; +import { TeamsModule } from '@modules/teams/teams.module'; @Module({ imports: [TeamsModule], diff --git a/apps/server/src/modules/tool/common/common-tool.module.ts b/apps/server/src/modules/tool/common/common-tool.module.ts index cc7f5f86f00..57375c67e96 100644 --- a/apps/server/src/modules/tool/common/common-tool.module.ts +++ b/apps/server/src/modules/tool/common/common-tool.module.ts @@ -1,27 +1,13 @@ -import { forwardRef, Module } from '@nestjs/common'; +import { LegacySchoolModule } from '@modules/legacy-school'; +import { Module } from '@nestjs/common'; import { ContextExternalToolRepo, SchoolExternalToolRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; -import { AuthorizationModule } from '@src/modules/authorization'; -import { LegacySchoolModule } from '@src/modules/legacy-school'; import { CommonToolService, CommonToolValidationService } from './service'; -import { ToolPermissionHelper } from './uc/tool-permission-helper'; @Module({ - imports: [LoggerModule, forwardRef(() => AuthorizationModule), LegacySchoolModule], + imports: [LoggerModule, LegacySchoolModule], // TODO: make deletion of entities cascading, adjust ExternalToolService.deleteExternalTool and remove the repos from here - providers: [ - CommonToolService, - CommonToolValidationService, - ToolPermissionHelper, - SchoolExternalToolRepo, - ContextExternalToolRepo, - ], - exports: [ - CommonToolService, - CommonToolValidationService, - ToolPermissionHelper, - SchoolExternalToolRepo, - ContextExternalToolRepo, - ], + providers: [CommonToolService, CommonToolValidationService, SchoolExternalToolRepo, ContextExternalToolRepo], + exports: [CommonToolService, CommonToolValidationService, SchoolExternalToolRepo, ContextExternalToolRepo], }) export class CommonToolModule {} diff --git a/apps/server/src/modules/tool/common/enum/tool-context-type.enum.ts b/apps/server/src/modules/tool/common/enum/tool-context-type.enum.ts index 20e57d7bd60..4c930b57397 100644 --- a/apps/server/src/modules/tool/common/enum/tool-context-type.enum.ts +++ b/apps/server/src/modules/tool/common/enum/tool-context-type.enum.ts @@ -1,3 +1,4 @@ export enum ToolContextType { COURSE = 'course', + BOARD_ELEMENT = 'board-element', } diff --git a/apps/server/src/modules/tool/common/mapper/context-type.mapper.ts b/apps/server/src/modules/tool/common/mapper/context-type.mapper.ts deleted file mode 100644 index 00da0a8b36d..00000000000 --- a/apps/server/src/modules/tool/common/mapper/context-type.mapper.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { AuthorizableReferenceType } from '@src/modules/authorization/types'; -import { ToolContextType } from '../enum'; - -const typeMapping: Record = { - [ToolContextType.COURSE]: AuthorizableReferenceType.Course, -}; - -export class ContextTypeMapper { - static mapContextTypeToAllowedAuthorizationEntityType(type: ToolContextType): AuthorizableReferenceType { - return typeMapping[type]; - } -} diff --git a/apps/server/src/modules/tool/common/mapper/index.ts b/apps/server/src/modules/tool/common/mapper/index.ts index 3da6b0fa28b..b6be27cdc1f 100644 --- a/apps/server/src/modules/tool/common/mapper/index.ts +++ b/apps/server/src/modules/tool/common/mapper/index.ts @@ -1 +1 @@ -export * from './context-type.mapper'; +export * from './tool-status-response.mapper'; diff --git a/apps/server/src/modules/tool/common/mapper/tool-status-response.mapper.ts b/apps/server/src/modules/tool/common/mapper/tool-status-response.mapper.ts new file mode 100644 index 00000000000..0c16ca50e9a --- /dev/null +++ b/apps/server/src/modules/tool/common/mapper/tool-status-response.mapper.ts @@ -0,0 +1,14 @@ +import { ToolConfigurationStatusResponse } from '../../context-external-tool/controller/dto'; +import { ToolConfigurationStatus } from '../enum'; + +export const statusMapping: Record = { + [ToolConfigurationStatus.LATEST]: ToolConfigurationStatusResponse.LATEST, + [ToolConfigurationStatus.OUTDATED]: ToolConfigurationStatusResponse.OUTDATED, + [ToolConfigurationStatus.UNKNOWN]: ToolConfigurationStatusResponse.UNKNOWN, +}; + +export class ToolStatusResponseMapper { + static mapToResponse(status: ToolConfigurationStatus): ToolConfigurationStatusResponse { + return statusMapping[status]; + } +} 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 dc4f339b4ab..525c8c5d3b6 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,16 +1,25 @@ +import { AuthorizationContext, AuthorizationService, ForbiddenLoggableException } from '@modules/authorization'; +import { AuthorizableReferenceType } from '@modules/authorization/domain'; +import { BoardDoAuthorizableService, ContentElementService } from '@modules/board'; +import { CourseService } from '@modules/learnroom'; +import { LegacySchoolService } from '@modules/legacy-school'; import { forwardRef, Inject, Injectable } from '@nestjs/common'; -import { EntityId, LegacySchoolDo, User } from '@shared/domain'; -import { AuthorizableReferenceType, AuthorizationContext, AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/legacy-school'; +import { BoardDoAuthorizable, Course, EntityId, LegacySchoolDo, User } from '@shared/domain'; import { ContextExternalTool } from '../../context-external-tool/domain'; import { SchoolExternalTool } from '../../school-external-tool/domain'; -import { ContextTypeMapper } from '../mapper'; +import { ToolContextType } from '../enum'; @Injectable() export class ToolPermissionHelper { constructor( - @Inject(forwardRef(() => AuthorizationService)) private authorizationService: AuthorizationService, - private readonly schoolService: LegacySchoolService + @Inject(forwardRef(() => AuthorizationService)) private readonly authorizationService: AuthorizationService, + private readonly schoolService: LegacySchoolService, + // invalid dependency on this place it is in UC layer in a other module + // loading of ressources should be part of service layer + // if it must resolve different loadings based on the request it can be added in own service and use in UC + private readonly courseService: CourseService, + private readonly boardElementService: ContentElementService, + private readonly boardService: BoardDoAuthorizableService ) {} // TODO build interface to get contextDO by contextType @@ -19,21 +28,24 @@ export class ToolPermissionHelper { contextExternalTool: ContextExternalTool, context: AuthorizationContext ): Promise { - if (contextExternalTool.id) { - await this.authorizationService.checkPermissionByReferences( - userId, - AuthorizableReferenceType.ContextExternalToolEntity, - contextExternalTool.id, - context - ); - } + const authorizableUser = await this.authorizationService.getUserWithPermissions(userId); + + this.authorizationService.checkPermission(authorizableUser, contextExternalTool, context); + + if (contextExternalTool.contextRef.type === ToolContextType.COURSE) { + // loading of ressources should be part of the UC -> unnessasary awaits + const course: Course = await this.courseService.findById(contextExternalTool.contextRef.id); + + this.authorizationService.checkPermission(authorizableUser, course, context); + } else if (contextExternalTool.contextRef.type === ToolContextType.BOARD_ELEMENT) { + const boardElement = await this.boardElementService.findById(contextExternalTool.contextRef.id); - await this.authorizationService.checkPermissionByReferences( - userId, - ContextTypeMapper.mapContextTypeToAllowedAuthorizationEntityType(contextExternalTool.contextRef.type), - contextExternalTool.contextRef.id, - context - ); + const board: BoardDoAuthorizable = await this.boardService.getBoardAuthorizable(boardElement); + + this.authorizationService.checkPermission(authorizableUser, board, context); + } else { + throw new ForbiddenLoggableException(userId, AuthorizableReferenceType.ContextExternalToolEntity, context); + } } public async ensureSchoolPermissions( @@ -41,8 +53,12 @@ export class ToolPermissionHelper { schoolExternalTool: SchoolExternalTool, context: AuthorizationContext ): Promise { - const user: User = await this.authorizationService.getUserWithPermissions(userId); - const school: LegacySchoolDo = await this.schoolService.getSchoolById(schoolExternalTool.schoolId); + // loading of ressources should be part of the UC -> unnessasary awaits + const [user, school]: [User, LegacySchoolDo] = await Promise.all([ + this.authorizationService.getUserWithPermissions(userId), + 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 b1567693130..aace35579db 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,17 +1,31 @@ -import { Test, TestingModule } from '@nestjs/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { + AuthorizationContext, + AuthorizationContextBuilder, + AuthorizationService, + ForbiddenLoggableException, +} from '@modules/authorization'; +import { AuthorizableReferenceType } from '@modules/authorization/domain'; +import { BoardDoAuthorizableService, ContentElementService } from '@modules/board'; +import { CourseService } from '@modules/learnroom'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { ForbiddenException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { BoardDoAuthorizable, ExternalToolElement, LegacySchoolDo, Permission } from '@shared/domain'; import { contextExternalToolFactory, + courseFactory, + externalToolElementFactory, legacySchoolDoFactory, schoolExternalToolFactory, setupEntities, + userFactory, } from '@shared/testing'; -import { Permission, LegacySchoolDo } from '@shared/domain'; -import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/legacy-school'; -import { ContextExternalTool } from '../../context-external-tool/domain'; -import { ToolPermissionHelper } from './tool-permission-helper'; +import { boardDoAuthorizableFactory } from '@shared/testing/factory/domainobject/board/board-do-authorizable.factory'; +import { ContextExternalTool, ContextRef } from '../../context-external-tool/domain'; import { SchoolExternalTool } from '../../school-external-tool/domain'; +import { ToolContextType } from '../enum'; +import { ToolPermissionHelper } from './tool-permission-helper'; describe('ToolPermissionHelper', () => { let module: TestingModule; @@ -19,6 +33,9 @@ describe('ToolPermissionHelper', () => { let authorizationService: DeepMocked; let schoolService: DeepMocked; + let courseService: DeepMocked; + let contentElementService: DeepMocked; + let boardDoAuthorizableService: DeepMocked; beforeAll(async () => { await setupEntities(); @@ -33,12 +50,27 @@ describe('ToolPermissionHelper', () => { provide: LegacySchoolService, useValue: createMock(), }, + { + provide: CourseService, + useValue: createMock(), + }, + { + provide: ContentElementService, + useValue: createMock(), + }, + { + provide: BoardDoAuthorizableService, + useValue: createMock(), + }, ], }).compile(); helper = module.get(ToolPermissionHelper); authorizationService = module.get(AuthorizationService); schoolService = module.get(LegacySchoolService); + courseService = module.get(CourseService); + contentElementService = module.get(ContentElementService); + boardDoAuthorizableService = module.get(BoardDoAuthorizableService); }); afterAll(async () => { @@ -50,29 +82,129 @@ describe('ToolPermissionHelper', () => { }); describe('ensureContextPermissions', () => { - describe('when context external tool is given', () => { + describe('when a context external tool for context "course" is given', () => { const setup = () => { - const userId = 'userId'; - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build(); + const user = userFactory.buildWithId(); + const course = courseFactory.buildWithId(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ + contextRef: new ContextRef({ + id: course.id, + type: ToolContextType.COURSE, + }), + }); const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + courseService.findById.mockResolvedValueOnce(course); + return { - userId, + user, + course, contextExternalTool, context, }; }; it('should check permission for context external tool', async () => { - const { userId, contextExternalTool, context } = setup(); + const { user, course, contextExternalTool, context } = setup(); + + await helper.ensureContextPermissions(user.id, contextExternalTool, context); + + expect(authorizationService.checkPermission).toHaveBeenCalledTimes(2); + expect(authorizationService.checkPermission).toHaveBeenNthCalledWith(1, user, contextExternalTool, context); + expect(authorizationService.checkPermission).toHaveBeenNthCalledWith(2, user, course, context); + }); + }); - await helper.ensureContextPermissions(userId, contextExternalTool, context); + describe('when a context external tool for context "board element" is given', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const externalToolElement: ExternalToolElement = externalToolElementFactory.build(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ + contextRef: new ContextRef({ + id: externalToolElement.id, + type: ToolContextType.BOARD_ELEMENT, + }), + }); + const board: BoardDoAuthorizable = boardDoAuthorizableFactory.build(); + const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]); + + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + contentElementService.findById.mockResolvedValueOnce(externalToolElement); + boardDoAuthorizableService.getBoardAuthorizable.mockResolvedValueOnce(board); + + return { + user, + board, + contextExternalTool, + context, + }; + }; + + it('should check permission for context external tool', async () => { + const { user, board, contextExternalTool, context } = setup(); + + await helper.ensureContextPermissions(user.id, contextExternalTool, context); + + expect(authorizationService.checkPermission).toHaveBeenCalledTimes(2); + expect(authorizationService.checkPermission).toHaveBeenNthCalledWith(1, user, contextExternalTool, context); + expect(authorizationService.checkPermission).toHaveBeenNthCalledWith(2, user, board, context); + }); + }); - expect(authorizationService.checkPermissionByReferences).toHaveBeenCalledWith( - userId, - 'courses', - contextExternalTool.contextRef.id, - context + describe('when the context external tool has an unkown context', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ + contextRef: { + type: 'unknown type' as unknown as ToolContextType, + }, + }); + const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]); + + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + + return { + user, + contextExternalTool, + context, + }; + }; + + it('should throw a forbidden loggable exception', async () => { + const { user, contextExternalTool, context } = setup(); + + await expect(helper.ensureContextPermissions(user.id, contextExternalTool, context)).rejects.toThrowError( + new ForbiddenLoggableException(user.id, AuthorizableReferenceType.ContextExternalToolEntity, context) + ); + }); + }); + + describe('when user is unauthorized', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId(); + const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]); + const error = new ForbiddenException(); + + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + authorizationService.checkPermission.mockImplementationOnce(() => { + throw error; + }); + + return { + user, + contextExternalTool, + context, + error, + }; + }; + + it('should check permission for context external tool and fail', async () => { + const { user, contextExternalTool, context, error } = setup(); + + await expect(helper.ensureContextPermissions(user.id, contextExternalTool, context)).rejects.toThrowError( + error ); }); }); @@ -81,15 +213,16 @@ describe('ToolPermissionHelper', () => { describe('ensureSchoolPermissions', () => { describe('when school external tool is given', () => { const setup = () => { - const userId = 'userId'; + const user = userFactory.buildWithId(); const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId(); const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.SCHOOL_TOOL_ADMIN]); const school: LegacySchoolDo = legacySchoolDoFactory.build({ id: schoolExternalTool.schoolId }); schoolService.getSchoolById.mockResolvedValue(school); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); return { - userId, + user, schoolExternalTool, school, context, @@ -97,11 +230,20 @@ describe('ToolPermissionHelper', () => { }; it('should check permission for school external tool', async () => { - const { userId, schoolExternalTool, context, school } = setup(); + const { user, schoolExternalTool, context, school } = setup(); + + await helper.ensureSchoolPermissions(user.id, schoolExternalTool, context); + + expect(authorizationService.checkPermission).toHaveBeenCalledTimes(1); + expect(authorizationService.checkPermission).toHaveBeenCalledWith(user, school, context); + }); + + it('should return undefined', async () => { + const { user, schoolExternalTool, context } = setup(); - await helper.ensureSchoolPermissions(userId, schoolExternalTool, context); + const result = await helper.ensureSchoolPermissions(user.id, schoolExternalTool, context); - expect(authorizationService.checkPermission).toHaveBeenCalledWith(userId, school, context); + expect(result).toBeUndefined(); }); }); }); diff --git a/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts b/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts index e3319512fc5..1afd639f1e7 100644 --- a/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts +++ b/apps/server/src/modules/tool/context-external-tool/context-external-tool.module.ts @@ -1,25 +1,28 @@ -import { forwardRef, Module } from '@nestjs/common'; +import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; -import { AuthorizationModule } from '@src/modules/authorization'; +import { CommonToolModule } from '../common'; import { ExternalToolModule } from '../external-tool'; import { SchoolExternalToolModule } from '../school-external-tool'; import { ContextExternalToolAuthorizableService, ContextExternalToolService, ContextExternalToolValidationService, + ToolReferenceService, } from './service'; -import { CommonToolModule } from '../common'; @Module({ - // TODO: remove authorization module here N21-1055 - imports: [ - CommonToolModule, - ExternalToolModule, - SchoolExternalToolModule, - LoggerModule, - forwardRef(() => AuthorizationModule), + imports: [CommonToolModule, ExternalToolModule, SchoolExternalToolModule, LoggerModule], + providers: [ + ContextExternalToolService, + ContextExternalToolValidationService, + ContextExternalToolAuthorizableService, + ToolReferenceService, + ], + exports: [ + ContextExternalToolService, + ContextExternalToolValidationService, + ContextExternalToolAuthorizableService, + ToolReferenceService, ], - providers: [ContextExternalToolService, ContextExternalToolValidationService, ContextExternalToolAuthorizableService], - exports: [ContextExternalToolService, ContextExternalToolValidationService, ContextExternalToolAuthorizableService], }) export class ContextExternalToolModule {} diff --git a/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-context.api.spec.ts b/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-context.api.spec.ts index 6e8ca18253f..eb570130c4e 100644 --- a/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-context.api.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-context.api.spec.ts @@ -1,4 +1,5 @@ import { EntityManager, MikroORM } from '@mikro-orm/core'; +import { ServerTestModule } from '@modules/server'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Account, Course, Permission, SchoolEntity, User } from '@shared/domain'; @@ -15,18 +16,17 @@ import { UserAndAccountTestFactory, userFactory, } from '@shared/testing'; -import { ServerTestModule } from '@src/modules/server'; import { ObjectId } from 'bson'; import { CustomParameterScope, ToolContextType } from '../../../common/enum'; +import { ExternalToolEntity } from '../../../external-tool/entity'; +import { CustomParameterEntryResponse } from '../../../school-external-tool/controller/dto'; import { SchoolExternalToolEntity } from '../../../school-external-tool/entity'; +import { ContextExternalToolEntity, ContextExternalToolType } from '../../entity'; import { ContextExternalToolPostParams, ContextExternalToolResponse, ContextExternalToolSearchListResponse, } from '../dto'; -import { ContextExternalToolEntity, ContextExternalToolType } from '../../entity'; -import { ExternalToolEntity } from '../../../external-tool/entity'; -import { CustomParameterEntryResponse } from '../../../school-external-tool/controller/dto'; describe('ToolContextController (API)', () => { let app: INestApplication; @@ -114,29 +114,29 @@ describe('ToolContextController (API)', () => { }); }); - describe('when creation of contextExternalTool failed', () => { + describe('when user is not authorized for the requested context', () => { const setup = async () => { - const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); - - const course: Course = courseFactory.buildWithId({ teachers: [teacherUser] }); - - const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ + const school = schoolFactory.build(); + const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }); + const course = courseFactory.build({ teachers: [teacherUser] }); + const otherCourse = courseFactory.build(); + const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.build({ schoolParameters: [], toolVersion: 1, + school, }); - const randomTestId = new ObjectId().toString(); + await em.persistAndFlush([course, otherCourse, school, teacherUser, teacherAccount, schoolExternalToolEntity]); + em.clear(); + const postParams: ContextExternalToolPostParams = { - schoolToolId: randomTestId, - contextId: randomTestId, + schoolToolId: schoolExternalToolEntity.id, + contextId: otherCourse.id, contextType: ToolContextType.COURSE, parameters: [], toolVersion: 1, }; - await em.persistAndFlush([course, teacherUser, teacherAccount, schoolExternalToolEntity]); - em.clear(); - const loggedInClient: TestApiClient = await testApiClient.login(teacherAccount); return { @@ -145,12 +145,13 @@ describe('ToolContextController (API)', () => { }; }; - it('when user is not authorized, it should return forbidden', async () => { + it('it should return forbidden', async () => { const { postParams, loggedInClient } = await setup(); const response = await loggedInClient.post().send(postParams); expect(response.statusCode).toEqual(HttpStatus.FORBIDDEN); + // expected body is missed }); }); }); @@ -204,23 +205,26 @@ describe('ToolContextController (API)', () => { describe('when deletion of contextExternalTool failed', () => { const setup = async () => { const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher(); - - const course: Course = courseFactory.buildWithId({ teachers: [teacherUser] }); - - const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ + const course = courseFactory.buildWithId({ teachers: [teacherUser] }); + const schoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ toolVersion: 1, }); - - const contextExternalToolEntity: ContextExternalToolEntity = contextExternalToolEntityFactory.buildWithId({ + const contextExternalToolEntity = contextExternalToolEntityFactory.buildWithId({ schoolTool: schoolExternalToolEntity, + contextId: course.id, toolVersion: 1, }); - em.persist([course, teacherUser, teacherAccount, schoolExternalToolEntity, contextExternalToolEntity]); - await em.flush(); + await em.persistAndFlush([ + course, + teacherUser, + teacherAccount, + schoolExternalToolEntity, + contextExternalToolEntity, + ]); em.clear(); - const loggedInClient: TestApiClient = await testApiClient.login(teacherAccount); + const loggedInClient = await testApiClient.login(teacherAccount); return { contextExternalToolEntity, @@ -234,6 +238,7 @@ describe('ToolContextController (API)', () => { const result = await loggedInClient.delete(`${contextExternalToolEntity.id}`); expect(result.statusCode).toEqual(HttpStatus.FORBIDDEN); + // result.body is missed }); }); }); @@ -543,37 +548,29 @@ describe('ToolContextController (API)', () => { describe('when user has not the required permission', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); - + const school = schoolFactory.build(); const { studentUser, studentAccount } = UserAndAccountTestFactory.buildStudent({ school }); - - const course: Course = courseFactory.buildWithId({ + const course = courseFactory.build({ teachers: [studentUser], school, }); - - const externalTool: ExternalToolEntity = externalToolEntityFactory.buildWithId(); - const schoolExternalTool: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ + const externalTool: ExternalToolEntity = externalToolEntityFactory.build(); + const schoolExternalTool: SchoolExternalToolEntity = schoolExternalToolEntityFactory.build({ school, tool: externalTool, toolVersion: 1, }); - const contextExternalTool: ContextExternalToolEntity = contextExternalToolEntityFactory.buildWithId({ + + await em.persistAndFlush([school, course, externalTool, schoolExternalTool, studentAccount, studentUser]); + + const contextExternalTool: ContextExternalToolEntity = contextExternalToolEntityFactory.build({ contextId: course.id, schoolTool: schoolExternalTool, toolVersion: 1, contextType: ContextExternalToolType.COURSE, }); - await em.persistAndFlush([ - school, - course, - externalTool, - schoolExternalTool, - contextExternalTool, - studentAccount, - studentUser, - ]); + await em.persistAndFlush([contextExternalTool]); em.clear(); const loggedInClient: TestApiClient = await testApiClient.login(studentAccount); @@ -591,6 +588,7 @@ describe('ToolContextController (API)', () => { const response = await loggedInClient.get(`${contextExternalTool.id}`); expect(response.status).toEqual(HttpStatus.FORBIDDEN); + // body check }); }); }); @@ -688,34 +686,37 @@ describe('ToolContextController (API)', () => { describe('when the user is not authorized', () => { const setup = async () => { - const roleWithoutPermission = roleFactory.buildWithId(); - const { teacherUser, teacherAccount } = UserAndAccountTestFactory.buildTeacher(); - + const roleWithoutPermission = roleFactory.build(); teacherUser.roles.set([roleWithoutPermission]); - const school: SchoolEntity = schoolFactory.buildWithId(); - - const course: Course = courseFactory.buildWithId({ teachers: [teacherUser], school }); - + const school = schoolFactory.build(); + const course = courseFactory.build({ teachers: [teacherUser], school }); const contextParameter = customParameterEntityFactory.build({ scope: CustomParameterScope.CONTEXT, regex: 'testValue123', }); - - const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId({ + const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.build({ parameters: [contextParameter], version: 2, }); - - const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ + const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.build({ tool: externalToolEntity, school, schoolParameters: [], toolVersion: 2, }); - const contextExternalToolEntity: ContextExternalToolEntity = contextExternalToolEntityFactory.buildWithId({ + await em.persistAndFlush([ + course, + school, + teacherUser, + teacherAccount, + externalToolEntity, + schoolExternalToolEntity, + ]); + + const contextExternalToolEntity: ContextExternalToolEntity = contextExternalToolEntityFactory.build({ schoolTool: schoolExternalToolEntity, contextId: course.id, contextType: ContextExternalToolType.COURSE, @@ -724,6 +725,10 @@ describe('ToolContextController (API)', () => { toolVersion: 1, }); + await em.persistAndFlush([contextExternalToolEntity]); + + em.clear(); + const postParams: ContextExternalToolPostParams = { schoolToolId: schoolExternalToolEntity.id, contextId: course.id, @@ -738,17 +743,6 @@ describe('ToolContextController (API)', () => { toolVersion: 2, }; - await em.persistAndFlush([ - course, - school, - teacherUser, - teacherAccount, - externalToolEntity, - schoolExternalToolEntity, - contextExternalToolEntity, - ]); - em.clear(); - const loggedInClient: TestApiClient = await testApiClient.login(teacherAccount); return { loggedInClient, postParams, contextExternalToolEntity }; @@ -760,6 +754,7 @@ describe('ToolContextController (API)', () => { const response = await loggedInClient.put(`${contextExternalToolEntity.id}`).send(postParams); expect(response.status).toEqual(HttpStatus.FORBIDDEN); + // body missed }); }); diff --git a/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-reference.api.spec.ts b/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-reference.api.spec.ts new file mode 100644 index 00000000000..9eae3c7298e --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/controller/api-test/tool-reference.api.spec.ts @@ -0,0 +1,287 @@ +import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Course, Permission, SchoolEntity } from '@shared/domain'; +import { + cleanupCollections, + contextExternalToolEntityFactory, + courseFactory, + externalToolEntityFactory, + schoolExternalToolEntityFactory, + schoolFactory, + TestApiClient, + UserAndAccountTestFactory, +} from '@shared/testing'; +import { ServerTestModule } from '@modules/server'; +import { Response } from 'supertest'; +import { ToolContextType } from '../../../common/enum'; +import { ExternalToolEntity } from '../../../external-tool/entity'; +import { SchoolExternalToolEntity } from '../../../school-external-tool/entity'; +import { ContextExternalToolEntity, ContextExternalToolType } from '../../entity'; +import { ContextExternalToolContextParams, ToolReferenceListResponse, ToolReferenceResponse } from '../dto'; +import { ToolConfigurationStatusResponse } from '../dto/tool-configuration-status.response'; + +describe('ToolReferenceController (API)', () => { + let app: INestApplication; + let em: EntityManager; + + let testApiClient: TestApiClient; + + beforeAll(async () => { + const moduleRef: TestingModule = await Test.createTestingModule({ + imports: [ServerTestModule], + }).compile(); + + app = moduleRef.createNestApplication(); + + await app.init(); + + em = app.get(EntityManager); + testApiClient = new TestApiClient(app, 'tools/tool-references'); + }); + + afterAll(async () => { + await app.close(); + }); + + afterEach(async () => { + await cleanupCollections(em); + }); + + describe('[GET] tools/tool-references/:contextType/:contextId', () => { + describe('when user is not authenticated', () => { + it('should return unauthorized', async () => { + const response: Response = await testApiClient.get(`contextType/${new ObjectId().toHexString()}`); + + expect(response.statusCode).toEqual(HttpStatus.UNAUTHORIZED); + }); + }); + + describe('when user has no access to a tool', () => { + const setup = async () => { + const schoolWithoutTool: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolFactory.buildWithId(); + const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school: schoolWithoutTool }); + const course: Course = courseFactory.buildWithId({ school, teachers: [adminUser] }); + const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId(); + const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ + school, + tool: externalToolEntity, + }); + const contextExternalToolEntity: ContextExternalToolEntity = contextExternalToolEntityFactory.buildWithId({ + schoolTool: schoolExternalToolEntity, + contextId: course.id, + contextType: ContextExternalToolType.COURSE, + }); + + await em.persistAndFlush([ + school, + adminAccount, + adminUser, + course, + externalToolEntity, + schoolExternalToolEntity, + contextExternalToolEntity, + ]); + em.clear(); + + const params: ContextExternalToolContextParams = { + contextId: course.id, + contextType: ToolContextType.COURSE, + }; + + const loggedInClient: TestApiClient = await testApiClient.login(adminAccount); + + return { loggedInClient, params }; + }; + + it('should filter out the tool', async () => { + const { loggedInClient, params } = await setup(); + + const response: Response = await loggedInClient.get(`${params.contextType}/${params.contextId}`); + + expect(response.statusCode).toEqual(HttpStatus.OK); + expect(response.body).toEqual({ data: [] }); + }); + }); + + describe('when user has access for a tool', () => { + const setup = async () => { + const school: SchoolEntity = schoolFactory.buildWithId(); + const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school }, [ + Permission.CONTEXT_TOOL_USER, + ]); + const course: Course = courseFactory.buildWithId({ school, teachers: [adminUser] }); + const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId({ + logoBase64: 'logoBase64', + }); + const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ + school, + tool: externalToolEntity, + toolVersion: externalToolEntity.version, + }); + const contextExternalToolEntity: ContextExternalToolEntity = contextExternalToolEntityFactory.buildWithId({ + schoolTool: schoolExternalToolEntity, + contextId: course.id, + contextType: ContextExternalToolType.COURSE, + displayName: 'This is a test tool', + toolVersion: schoolExternalToolEntity.toolVersion, + }); + + await em.persistAndFlush([ + school, + adminAccount, + adminUser, + course, + externalToolEntity, + schoolExternalToolEntity, + contextExternalToolEntity, + ]); + em.clear(); + + const params: ContextExternalToolContextParams = { + contextId: course.id, + contextType: ToolContextType.COURSE, + }; + + const loggedInClient: TestApiClient = await testApiClient.login(adminAccount); + + return { loggedInClient, params, contextExternalToolEntity, externalToolEntity }; + }; + + it('should return an ToolReferenceListResponse with data', async () => { + const { loggedInClient, params, contextExternalToolEntity, externalToolEntity } = await setup(); + + const response: Response = await loggedInClient.get(`${params.contextType}/${params.contextId}`); + + expect(response.statusCode).toEqual(HttpStatus.OK); + expect(response.body).toEqual({ + data: [ + { + contextToolId: contextExternalToolEntity.id, + displayName: contextExternalToolEntity.displayName as string, + status: ToolConfigurationStatusResponse.LATEST, + logoUrl: `http://localhost:3030/api/v3/tools/external-tools/${externalToolEntity.id}/logo`, + openInNewTab: externalToolEntity.openNewTab, + }, + ], + }); + }); + }); + }); + + describe('[GET] tools/tool-references/context-external-tools/:contextExternalToolId', () => { + describe('when user is not authenticated', () => { + it('should return unauthorized', async () => { + const response: Response = await testApiClient.get(`context-external-tools/${new ObjectId().toHexString()}`); + + expect(response.statusCode).toEqual(HttpStatus.UNAUTHORIZED); + }); + }); + + describe('when user has no access to a tool', () => { + const setup = async () => { + const schoolWithoutTool: SchoolEntity = schoolFactory.buildWithId(); + const school: SchoolEntity = schoolFactory.buildWithId(); + const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school: schoolWithoutTool }); + const course: Course = courseFactory.buildWithId({ school, teachers: [adminUser] }); + const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId(); + const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ + school, + tool: externalToolEntity, + }); + const contextExternalToolEntity: ContextExternalToolEntity = contextExternalToolEntityFactory.buildWithId({ + schoolTool: schoolExternalToolEntity, + contextId: course.id, + contextType: ContextExternalToolType.COURSE, + }); + + await em.persistAndFlush([ + school, + adminAccount, + adminUser, + course, + externalToolEntity, + schoolExternalToolEntity, + contextExternalToolEntity, + ]); + em.clear(); + + const loggedInClient: TestApiClient = await testApiClient.login(adminAccount); + + return { + loggedInClient, + contextExternalToolId: contextExternalToolEntity.id, + }; + }; + + it('should filter out the tool', async () => { + const { loggedInClient, contextExternalToolId } = await setup(); + + const response: Response = await loggedInClient.get(`context-external-tools/${contextExternalToolId}`); + + expect(response.statusCode).toEqual(HttpStatus.FORBIDDEN); + }); + }); + + describe('when user has access for a tool', () => { + const setup = async () => { + const school: SchoolEntity = schoolFactory.buildWithId(); + const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school }, [ + Permission.CONTEXT_TOOL_USER, + ]); + const course: Course = courseFactory.buildWithId({ school, teachers: [adminUser] }); + const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId({ + logoBase64: 'logoBase64', + }); + const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ + school, + tool: externalToolEntity, + toolVersion: externalToolEntity.version, + }); + const contextExternalToolEntity: ContextExternalToolEntity = contextExternalToolEntityFactory.buildWithId({ + schoolTool: schoolExternalToolEntity, + contextId: course.id, + contextType: ContextExternalToolType.COURSE, + displayName: 'This is a test tool', + toolVersion: schoolExternalToolEntity.toolVersion, + }); + + await em.persistAndFlush([ + school, + adminAccount, + adminUser, + course, + externalToolEntity, + schoolExternalToolEntity, + contextExternalToolEntity, + ]); + em.clear(); + + const loggedInClient: TestApiClient = await testApiClient.login(adminAccount); + + return { + loggedInClient, + contextExternalToolId: contextExternalToolEntity.id, + contextExternalToolEntity, + externalToolEntity, + }; + }; + + it('should return an ToolReferenceListResponse with data', async () => { + const { loggedInClient, contextExternalToolId, contextExternalToolEntity, externalToolEntity } = await setup(); + + const response: Response = await loggedInClient.get(`context-external-tools/${contextExternalToolId}`); + + expect(response.statusCode).toEqual(HttpStatus.OK); + expect(response.body).toEqual({ + contextToolId: contextExternalToolEntity.id, + displayName: contextExternalToolEntity.displayName as string, + status: ToolConfigurationStatusResponse.LATEST, + logoUrl: `http://localhost:3030/api/v3/tools/external-tools/${externalToolEntity.id}/logo`, + openInNewTab: externalToolEntity.openNewTab, + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/context-external-tool/controller/dto/context-external-tool-context.params.ts b/apps/server/src/modules/tool/context-external-tool/controller/dto/context-external-tool-context.params.ts index 7d20deef026..63850daa22b 100644 --- a/apps/server/src/modules/tool/context-external-tool/controller/dto/context-external-tool-context.params.ts +++ b/apps/server/src/modules/tool/context-external-tool/controller/dto/context-external-tool-context.params.ts @@ -8,6 +8,12 @@ export class ContextExternalToolContextParams { contextId!: string; @IsEnum(ToolContextType) - @ApiProperty({ nullable: false, required: true, example: ToolContextType.COURSE }) + @ApiProperty({ + enum: ToolContextType, + enumName: 'ToolContextType', + nullable: false, + required: true, + example: ToolContextType.COURSE, + }) contextType!: ToolContextType; } diff --git a/apps/server/src/modules/tool/context-external-tool/controller/dto/index.ts b/apps/server/src/modules/tool/context-external-tool/controller/dto/index.ts index dfe16d84244..e6da4bb909f 100644 --- a/apps/server/src/modules/tool/context-external-tool/controller/dto/index.ts +++ b/apps/server/src/modules/tool/context-external-tool/controller/dto/index.ts @@ -3,3 +3,6 @@ export * from './context-external-tool-id.params'; export * from './context-external-tool-search-list.response'; export * from './context-external-tool-context.params'; export * from './context-external-tool.response'; +export * from './tool-reference-list.response'; +export * from './tool-reference.response'; +export * from './tool-configuration-status.response'; diff --git a/apps/server/src/modules/tool/external-tool/controller/dto/response/tool-configuration-status.response.ts b/apps/server/src/modules/tool/context-external-tool/controller/dto/tool-configuration-status.response.ts similarity index 100% rename from apps/server/src/modules/tool/external-tool/controller/dto/response/tool-configuration-status.response.ts rename to apps/server/src/modules/tool/context-external-tool/controller/dto/tool-configuration-status.response.ts diff --git a/apps/server/src/modules/tool/external-tool/controller/dto/response/tool-reference-list.response.ts b/apps/server/src/modules/tool/context-external-tool/controller/dto/tool-reference-list.response.ts similarity index 100% rename from apps/server/src/modules/tool/external-tool/controller/dto/response/tool-reference-list.response.ts rename to apps/server/src/modules/tool/context-external-tool/controller/dto/tool-reference-list.response.ts diff --git a/apps/server/src/modules/tool/external-tool/controller/dto/response/tool-reference.response.ts b/apps/server/src/modules/tool/context-external-tool/controller/dto/tool-reference.response.ts similarity index 96% rename from apps/server/src/modules/tool/external-tool/controller/dto/response/tool-reference.response.ts rename to apps/server/src/modules/tool/context-external-tool/controller/dto/tool-reference.response.ts index 24844d8bd2a..0ccefffa6ae 100644 --- a/apps/server/src/modules/tool/external-tool/controller/dto/response/tool-reference.response.ts +++ b/apps/server/src/modules/tool/context-external-tool/controller/dto/tool-reference.response.ts @@ -20,6 +20,7 @@ export class ToolReferenceResponse { @ApiProperty({ enum: ToolConfigurationStatusResponse, + enumName: 'ToolConfigurationStatusResponse', nullable: false, required: true, description: 'The status of the tool', diff --git a/apps/server/src/modules/tool/context-external-tool/controller/tool-context.controller.ts b/apps/server/src/modules/tool/context-external-tool/controller/tool-context.controller.ts index 491e7d9f2d6..20d8b96f795 100644 --- a/apps/server/src/modules/tool/context-external-tool/controller/tool-context.controller.ts +++ b/apps/server/src/modules/tool/context-external-tool/controller/tool-context.controller.ts @@ -1,3 +1,4 @@ +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common'; import { ApiCreatedResponse, @@ -12,8 +13,6 @@ import { } from '@nestjs/swagger'; import { ValidationError } from '@shared/common'; import { LegacyLogger } from '@src/core/logger'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; import { ContextExternalTool } from '../domain'; import { ContextExternalToolRequestMapper, ContextExternalToolResponseMapper } from '../mapper'; import { ContextExternalToolUc } from '../uc'; @@ -51,6 +50,7 @@ export class ToolContextController { const createdTool: ContextExternalTool = await this.contextExternalToolUc.createContextExternalTool( currentUser.userId, + currentUser.schoolId, contextExternalTool ); @@ -58,6 +58,7 @@ export class ToolContextController { ContextExternalToolResponseMapper.mapContextExternalToolResponse(createdTool); this.logger.debug(`ContextExternalTool with id ${response.id} was created by user with id ${currentUser.userId}`); + return response; } @@ -152,6 +153,7 @@ export class ToolContextController { const updatedTool: ContextExternalTool = await this.contextExternalToolUc.updateContextExternalTool( currentUser.userId, + currentUser.schoolId, params.contextExternalToolId, contextExternalTool ); diff --git a/apps/server/src/modules/tool/context-external-tool/controller/tool-reference.controller.ts b/apps/server/src/modules/tool/context-external-tool/controller/tool-reference.controller.ts new file mode 100644 index 00000000000..3c00d30e6e2 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/controller/tool-reference.controller.ts @@ -0,0 +1,67 @@ +import { Controller, Get, Param } from '@nestjs/common'; +import { ApiForbiddenResponse, ApiOkResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; +import { ToolReference } from '../domain'; +import { ContextExternalToolResponseMapper } from '../mapper'; +import { ToolReferenceUc } from '../uc'; +import { + ContextExternalToolContextParams, + ContextExternalToolIdParams, + ToolReferenceListResponse, + ToolReferenceResponse, +} from './dto'; + +@ApiTags('Tool') +@Authenticate('jwt') +@Controller('tools/tool-references') +export class ToolReferenceController { + constructor(private readonly toolReferenceUc: ToolReferenceUc) {} + + @Get('context-external-tools/:contextExternalToolId') + @ApiOperation({ summary: 'Get ExternalTool Reference for a given context external tool' }) + @ApiOkResponse({ + description: 'The Tool Reference has been successfully fetched.', + type: ToolReferenceResponse, + }) + @ApiForbiddenResponse({ description: 'User is not allowed to access this resource.' }) + @ApiUnauthorizedResponse({ description: 'User is not logged in.' }) + async getToolReference( + @CurrentUser() currentUser: ICurrentUser, + @Param() params: ContextExternalToolIdParams + ): Promise { + const toolReference: ToolReference = await this.toolReferenceUc.getToolReference( + currentUser.userId, + params.contextExternalToolId + ); + + const toolReferenceResponse: ToolReferenceResponse = + ContextExternalToolResponseMapper.mapToToolReferenceResponse(toolReference); + + return toolReferenceResponse; + } + + @Get('/:contextType/:contextId') + @ApiOperation({ summary: 'Get ExternalTool References for a given context' }) + @ApiOkResponse({ + description: 'The Tool References has been successfully fetched.', + type: ToolReferenceListResponse, + }) + @ApiForbiddenResponse({ description: 'User is not allowed to access this resource.' }) + @ApiUnauthorizedResponse({ description: 'User is not logged in.' }) + async getToolReferencesForContext( + @CurrentUser() currentUser: ICurrentUser, + @Param() params: ContextExternalToolContextParams + ): Promise { + const toolReferences: ToolReference[] = await this.toolReferenceUc.getToolReferencesForContext( + currentUser.userId, + params.contextType, + params.contextId + ); + + const toolReferenceResponses: ToolReferenceResponse[] = + ContextExternalToolResponseMapper.mapToToolReferenceResponses(toolReferences); + const toolReferenceListResponse = new ToolReferenceListResponse(toolReferenceResponses); + + return toolReferenceListResponse; + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/domain/index.ts b/apps/server/src/modules/tool/context-external-tool/domain/index.ts index a012e1d4002..557bc04788c 100644 --- a/apps/server/src/modules/tool/context-external-tool/domain/index.ts +++ b/apps/server/src/modules/tool/context-external-tool/domain/index.ts @@ -1,2 +1,3 @@ export * from './context-external-tool.do'; export * from './context-ref'; +export * from './tool-reference'; diff --git a/apps/server/src/modules/tool/external-tool/domain/tool-reference.ts b/apps/server/src/modules/tool/context-external-tool/domain/tool-reference.ts similarity index 100% rename from apps/server/src/modules/tool/external-tool/domain/tool-reference.ts rename to apps/server/src/modules/tool/context-external-tool/domain/tool-reference.ts diff --git a/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool-type.enum.ts b/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool-type.enum.ts index 4d40ca6e84c..56753c354dc 100644 --- a/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool-type.enum.ts +++ b/apps/server/src/modules/tool/context-external-tool/entity/context-external-tool-type.enum.ts @@ -1,3 +1,4 @@ export enum ContextExternalToolType { COURSE = 'course', + BOARD_ELEMENT = 'boardElement', } diff --git a/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-request.mapper.ts b/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-request.mapper.ts index 45f912bf52c..951559afbcc 100644 --- a/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-request.mapper.ts +++ b/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-request.mapper.ts @@ -1,12 +1,11 @@ -import { ContextExternalToolPostParams } from '../controller/dto'; +import { CustomParameterEntry } from '../../common/domain'; import { CustomParameterEntryParam } from '../../school-external-tool/controller/dto'; +import { ContextExternalToolPostParams } from '../controller/dto'; import { ContextExternalToolDto } from '../uc/dto/context-external-tool.types'; -import { CustomParameterEntry } from '../../common/domain'; export class ContextExternalToolRequestMapper { static mapContextExternalToolRequest(request: ContextExternalToolPostParams): ContextExternalToolDto { return { - id: '', schoolToolRef: { schoolToolId: request.schoolToolId, }, diff --git a/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-response.mapper.ts b/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-response.mapper.ts index 601c960299d..07610a7a508 100644 --- a/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-response.mapper.ts +++ b/apps/server/src/modules/tool/context-external-tool/mapper/context-external-tool-response.mapper.ts @@ -1,6 +1,7 @@ +import { ToolStatusResponseMapper } from '../../common/mapper/tool-status-response.mapper'; import { CustomParameterEntryParam, CustomParameterEntryResponse } from '../../school-external-tool/controller/dto'; -import { ContextExternalToolResponse } from '../controller/dto'; -import { ContextExternalTool } from '../domain'; +import { ContextExternalToolResponse, ToolReferenceResponse } from '../controller/dto'; +import { ContextExternalTool, ToolReference } from '../domain'; export class ContextExternalToolResponseMapper { static mapContextExternalToolResponse(contextExternalTool: ContextExternalTool): ContextExternalToolResponse { @@ -33,4 +34,24 @@ export class ContextExternalToolResponseMapper { return mapped; } + + static mapToToolReferenceResponses(toolReferences: ToolReference[]): ToolReferenceResponse[] { + const toolReferenceResponses: ToolReferenceResponse[] = toolReferences.map((toolReference: ToolReference) => + this.mapToToolReferenceResponse(toolReference) + ); + + return toolReferenceResponses; + } + + static mapToToolReferenceResponse(toolReference: ToolReference): ToolReferenceResponse { + const response = new ToolReferenceResponse({ + contextToolId: toolReference.contextToolId, + displayName: toolReference.displayName, + logoUrl: toolReference.logoUrl, + openInNewTab: toolReference.openInNewTab, + status: ToolStatusResponseMapper.mapToResponse(toolReference.status), + }); + + return response; + } } diff --git a/apps/server/src/modules/tool/context-external-tool/mapper/index.ts b/apps/server/src/modules/tool/context-external-tool/mapper/index.ts index 1987491c4e0..427f02a713a 100644 --- a/apps/server/src/modules/tool/context-external-tool/mapper/index.ts +++ b/apps/server/src/modules/tool/context-external-tool/mapper/index.ts @@ -1,2 +1,3 @@ export * from './context-external-tool-request.mapper'; export * from './context-external-tool-response.mapper'; +export * from './tool-reference.mapper'; diff --git a/apps/server/src/modules/tool/external-tool/mapper/tool-reference.mapper.ts b/apps/server/src/modules/tool/context-external-tool/mapper/tool-reference.mapper.ts similarity index 80% rename from apps/server/src/modules/tool/external-tool/mapper/tool-reference.mapper.ts rename to apps/server/src/modules/tool/context-external-tool/mapper/tool-reference.mapper.ts index ec982467578..be6e6b8ab12 100644 --- a/apps/server/src/modules/tool/external-tool/mapper/tool-reference.mapper.ts +++ b/apps/server/src/modules/tool/context-external-tool/mapper/tool-reference.mapper.ts @@ -1,6 +1,6 @@ -import { ExternalTool, ToolReference } from '../domain'; -import { ContextExternalTool } from '../../context-external-tool/domain'; import { ToolConfigurationStatus } from '../../common/enum'; +import { ExternalTool } from '../../external-tool/domain'; +import { ContextExternalTool, ToolReference } from '../domain'; export class ToolReferenceMapper { static mapToToolReference( diff --git a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-authorizable.service.ts b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-authorizable.service.ts index 9f0606c6e14..20dbc16fff8 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-authorizable.service.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-authorizable.service.ts @@ -1,4 +1,4 @@ -import { AuthorizationLoaderService } from '@src/modules/authorization'; +import { AuthorizationLoaderService } from '@modules/authorization'; import { EntityId } from '@shared/domain'; import { ContextExternalToolRepo } from '@shared/repo'; import { Injectable } from '@nestjs/common'; diff --git a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.spec.ts b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.spec.ts index c419154c020..41e849b3d79 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.spec.ts @@ -1,8 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ValidationError } from '@mikro-orm/core'; import { UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { contextExternalToolFactory, externalToolFactory } from '@shared/testing'; -import { ValidationError } from '@mikro-orm/core'; import { CommonToolValidationService } from '../../common/service'; import { ExternalTool } from '../../external-tool/domain'; import { ExternalToolService } from '../../external-tool/service'; @@ -62,7 +62,7 @@ describe('ContextExternalToolValidationService', () => { describe('when no tool with the name exists in the context', () => { const setup = () => { const externalTool: ExternalTool = externalToolFactory.buildWithId(); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + externalToolService.findById.mockResolvedValue(externalTool); const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ displayName: 'Tool 1', @@ -93,9 +93,7 @@ describe('ContextExternalToolValidationService', () => { await service.validate(contextExternalTool); - expect(schoolExternalToolService.getSchoolExternalToolById).toBeCalledWith( - contextExternalTool.schoolToolRef.schoolToolId - ); + expect(schoolExternalToolService.findById).toBeCalledWith(contextExternalTool.schoolToolRef.schoolToolId); }); it('should call commonToolValidationService.checkCustomParameterEntries', async () => { diff --git a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.ts b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.ts index af6d36840f7..2cce83a08b2 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool-validation.service.ts @@ -6,7 +6,6 @@ import { ExternalToolService } from '../../external-tool/service'; import { SchoolExternalTool } from '../../school-external-tool/domain'; import { SchoolExternalToolService } from '../../school-external-tool/service'; import { ContextExternalTool } from '../domain'; -import { ContextExternalToolDto } from '../uc/dto/context-external-tool.types'; import { ContextExternalToolService } from './context-external-tool.service'; @Injectable() @@ -18,18 +17,14 @@ export class ContextExternalToolValidationService { private readonly commonToolValidationService: CommonToolValidationService ) {} - async validate(toValidate: ContextExternalToolDto): Promise { - const contextExternalTool: ContextExternalTool = new ContextExternalTool(toValidate); - + async validate(contextExternalTool: ContextExternalTool): Promise { await this.checkDuplicateInContext(contextExternalTool); - const loadedSchoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.getSchoolExternalToolById( + const loadedSchoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById( contextExternalTool.schoolToolRef.schoolToolId ); - const loadedExternalTool: ExternalTool = await this.externalToolService.findExternalToolById( - loadedSchoolExternalTool.toolId - ); + const loadedExternalTool: ExternalTool = await this.externalToolService.findById(loadedSchoolExternalTool.toolId); this.commonToolValidationService.checkCustomParameterEntries(loadedExternalTool, contextExternalTool); } 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..60275c71d0d 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 @@ -7,7 +7,7 @@ import { legacySchoolDoFactory, schoolExternalToolFactory, } from '@shared/testing/factory/domainobject'; -import { AuthorizationService } from '@src/modules/authorization'; +import { AuthorizationService } from '@modules/authorization'; import { ToolContextType } from '../../common/enum'; import { SchoolExternalTool } from '../../school-external-tool/domain'; import { ContextExternalTool, ContextRef } from '../domain'; @@ -130,7 +130,7 @@ describe('ContextExternalToolService', () => { }); }); - describe('getContextExternalToolById', () => { + describe('findById', () => { describe('when contextExternalToolId is given', () => { const setup = () => { const schoolId: string = legacySchoolDoFactory.buildWithId().id as string; @@ -151,7 +151,7 @@ describe('ContextExternalToolService', () => { it('should return a contextExternalTool', async () => { const { contextExternalTool } = setup(); - const result: ContextExternalTool = await service.getContextExternalToolById(contextExternalTool.id as string); + const result: ContextExternalTool = await service.findById(contextExternalTool.id as string); expect(result).toEqual(contextExternalTool); }); @@ -165,7 +165,7 @@ describe('ContextExternalToolService', () => { it('should throw a not found exception', async () => { setup(); - const func = () => service.getContextExternalToolById('unknownContextExternalToolId'); + const func = () => service.findById('unknownContextExternalToolId'); await expect(func()).rejects.toThrow(NotFoundException); }); diff --git a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.ts b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.ts index 011e6db2f7a..63618191810 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/context-external-tool.service.ts @@ -1,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain'; import { ContextExternalToolRepo } from '@shared/repo'; -import { ContextExternalToolQuery } from '../uc/dto/context-external-tool.types'; import { ContextExternalTool, ContextRef } from '../domain'; +import { ContextExternalToolQuery } from '../uc/dto/context-external-tool.types'; @Injectable() export class ContextExternalToolService { @@ -14,7 +14,7 @@ export class ContextExternalToolService { return contextExternalTools; } - async getContextExternalToolById(contextExternalToolId: EntityId): Promise { + async findById(contextExternalToolId: EntityId): Promise { const tool: ContextExternalTool = await this.contextExternalToolRepo.findById(contextExternalToolId); return tool; diff --git a/apps/server/src/modules/tool/context-external-tool/service/index.ts b/apps/server/src/modules/tool/context-external-tool/service/index.ts index 887cfbe7d9d..31fedbe42af 100644 --- a/apps/server/src/modules/tool/context-external-tool/service/index.ts +++ b/apps/server/src/modules/tool/context-external-tool/service/index.ts @@ -1,3 +1,4 @@ export * from './context-external-tool.service'; export * from './context-external-tool-validation.service'; export * from './context-external-tool-authorizable.service'; +export * from './tool-reference.service'; diff --git a/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.spec.ts b/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.spec.ts new file mode 100644 index 00000000000..e434ab49527 --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.spec.ts @@ -0,0 +1,132 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { Test, TestingModule } from '@nestjs/testing'; +import { contextExternalToolFactory, externalToolFactory, schoolExternalToolFactory } from '@shared/testing'; +import { ToolConfigurationStatus } from '../../common/enum'; +import { CommonToolService } from '../../common/service'; +import { ExternalToolLogoService, ExternalToolService } from '../../external-tool/service'; +import { SchoolExternalToolService } from '../../school-external-tool/service'; +import { ToolReference } from '../domain'; +import { ContextExternalToolService } from './context-external-tool.service'; +import { ToolReferenceService } from './tool-reference.service'; + +describe('ToolReferenceService', () => { + let module: TestingModule; + let service: ToolReferenceService; + + let externalToolService: DeepMocked; + let schoolExternalToolService: DeepMocked; + let contextExternalToolService: DeepMocked; + let commonToolService: DeepMocked; + let externalToolLogoService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + ToolReferenceService, + { + provide: ExternalToolService, + useValue: createMock(), + }, + { + provide: SchoolExternalToolService, + useValue: createMock(), + }, + { + provide: ContextExternalToolService, + useValue: createMock(), + }, + { + provide: CommonToolService, + useValue: createMock(), + }, + { + provide: ExternalToolLogoService, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(ToolReferenceService); + externalToolService = module.get(ExternalToolService); + schoolExternalToolService = module.get(SchoolExternalToolService); + contextExternalToolService = module.get(ContextExternalToolService); + commonToolService = module.get(CommonToolService); + externalToolLogoService = module.get(ExternalToolLogoService); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe('getToolReference', () => { + describe('when a context external tool id is provided', () => { + const setup = () => { + const contextExternalToolId = new ObjectId().toHexString(); + const externalTool = externalToolFactory.buildWithId(); + const schoolExternalTool = schoolExternalToolFactory.buildWithId({ + toolId: externalTool.id as string, + }); + const contextExternalTool = contextExternalToolFactory + .withSchoolExternalToolRef(schoolExternalTool.id as string) + .buildWithId(undefined, contextExternalToolId); + const logoUrl = 'logoUrl'; + + contextExternalToolService.findById.mockResolvedValueOnce(contextExternalTool); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); + externalToolService.findById.mockResolvedValueOnce(externalTool); + commonToolService.determineToolConfigurationStatus.mockReturnValue(ToolConfigurationStatus.OUTDATED); + externalToolLogoService.buildLogoUrl.mockReturnValue(logoUrl); + + return { + contextExternalToolId, + externalTool, + schoolExternalTool, + contextExternalTool, + logoUrl, + }; + }; + + it('should determine the tool status', async () => { + const { contextExternalToolId, externalTool, schoolExternalTool, contextExternalTool } = setup(); + + await service.getToolReference(contextExternalToolId); + + expect(commonToolService.determineToolConfigurationStatus).toHaveBeenCalledWith( + externalTool, + schoolExternalTool, + contextExternalTool + ); + }); + + it('should build the logo url', async () => { + const { contextExternalToolId, externalTool } = setup(); + + await service.getToolReference(contextExternalToolId); + + expect(externalToolLogoService.buildLogoUrl).toHaveBeenCalledWith( + '/v3/tools/external-tools/{id}/logo', + externalTool + ); + }); + + it('should return the tool reference', async () => { + const { contextExternalToolId, logoUrl, contextExternalTool, externalTool } = setup(); + + const result: ToolReference = await service.getToolReference(contextExternalToolId); + + expect(result).toEqual({ + logoUrl, + displayName: contextExternalTool.displayName as string, + openInNewTab: externalTool.openNewTab, + status: ToolConfigurationStatus.OUTDATED, + contextToolId: contextExternalToolId, + }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.ts b/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.ts new file mode 100644 index 00000000000..02c6a08677e --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/service/tool-reference.service.ts @@ -0,0 +1,50 @@ +import { Injectable } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { ToolConfigurationStatus } from '../../common/enum'; +import { CommonToolService } from '../../common/service'; +import { ExternalTool } from '../../external-tool/domain'; +import { ExternalToolLogoService, ExternalToolService } from '../../external-tool/service'; +import { SchoolExternalTool } from '../../school-external-tool/domain'; +import { SchoolExternalToolService } from '../../school-external-tool/service'; +import { ContextExternalTool, ToolReference } from '../domain'; +import { ToolReferenceMapper } from '../mapper'; +import { ContextExternalToolService } from './context-external-tool.service'; + +@Injectable() +export class ToolReferenceService { + constructor( + private readonly externalToolService: ExternalToolService, + private readonly schoolExternalToolService: SchoolExternalToolService, + private readonly contextExternalToolService: ContextExternalToolService, + private readonly commonToolService: CommonToolService, + private readonly externalToolLogoService: ExternalToolLogoService + ) {} + + async getToolReference(contextExternalToolId: EntityId): Promise { + const contextExternalTool: ContextExternalTool = await this.contextExternalToolService.findById( + contextExternalToolId + ); + const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById( + contextExternalTool.schoolToolRef.schoolToolId + ); + const externalTool: ExternalTool = await this.externalToolService.findById(schoolExternalTool.toolId); + + const status: ToolConfigurationStatus = this.commonToolService.determineToolConfigurationStatus( + externalTool, + schoolExternalTool, + contextExternalTool + ); + + const toolReference: ToolReference = ToolReferenceMapper.mapToToolReference( + externalTool, + contextExternalTool, + status + ); + toolReference.logoUrl = this.externalToolLogoService.buildLogoUrl( + '/v3/tools/external-tools/{id}/logo', + externalTool + ); + + return toolReference; + } +} diff --git a/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.spec.ts b/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.spec.ts index 7dcdea9d16b..0fcca404049 100644 --- a/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.spec.ts +++ b/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.spec.ts @@ -1,22 +1,29 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; +import { + Action, + AuthorizationContextBuilder, + AuthorizationService, + ForbiddenLoggableException, +} from '@modules/authorization'; +import { AuthorizableReferenceType } from '@modules/authorization/domain'; import { ForbiddenException, UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityId, Permission, User } from '@shared/domain'; -import { contextExternalToolFactory, setupEntities, userFactory } from '@shared/testing'; -import { LegacyLogger } from '@src/core/logger'; -import { Action, AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; -import { ForbiddenLoggableException } from '@src/modules/authorization/errors/forbidden.loggable-exception'; +import { contextExternalToolFactory, schoolExternalToolFactory, setupEntities, userFactory } from '@shared/testing'; import { ToolContextType } from '../../common/enum'; +import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; +import { SchoolExternalTool } from '../../school-external-tool/domain'; +import { SchoolExternalToolService } from '../../school-external-tool/service'; import { ContextExternalTool } from '../domain'; import { ContextExternalToolService, ContextExternalToolValidationService } from '../service'; import { ContextExternalToolUc } from './context-external-tool.uc'; -import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; describe('ContextExternalToolUc', () => { let module: TestingModule; let uc: ContextExternalToolUc; + let schoolExternalToolService: DeepMocked; let contextExternalToolService: DeepMocked; let contextExternalToolValidationService: DeepMocked; let toolPermissionHelper: DeepMocked; @@ -27,6 +34,10 @@ describe('ContextExternalToolUc', () => { module = await Test.createTestingModule({ providers: [ ContextExternalToolUc, + { + provide: SchoolExternalToolService, + useValue: createMock(), + }, { provide: ContextExternalToolService, useValue: createMock(), @@ -35,10 +46,6 @@ describe('ContextExternalToolUc', () => { provide: ContextExternalToolValidationService, useValue: createMock(), }, - { - provide: LegacyLogger, - useValue: createMock(), - }, { provide: ToolPermissionHelper, useValue: createMock(), @@ -51,6 +58,7 @@ describe('ContextExternalToolUc', () => { }).compile(); uc = module.get(ContextExternalToolUc); + schoolExternalToolService = module.get(SchoolExternalToolService); contextExternalToolService = module.get(ContextExternalToolService); contextExternalToolValidationService = module.get(ContextExternalToolValidationService); toolPermissionHelper = module.get(ToolPermissionHelper); @@ -69,36 +77,46 @@ describe('ContextExternalToolUc', () => { describe('when contextExternalTool is given and user has permission ', () => { const setup = () => { const userId: EntityId = 'userId'; + const schoolId: EntityId = new ObjectId().toHexString(); + + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId({ + schoolId, + }); const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ displayName: 'Course', + schoolToolRef: { + schoolToolId: schoolExternalTool.id, + schoolId, + }, contextRef: { id: 'contextId', type: ToolContextType.COURSE, }, }); - toolPermissionHelper.ensureContextPermissions.mockResolvedValue(Promise.resolve()); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); contextExternalToolService.saveContextExternalTool.mockResolvedValue(contextExternalTool); return { contextExternalTool, userId, + schoolId, }; }; it('should call contextExternalToolService', async () => { - const { contextExternalTool, userId } = setup(); + const { contextExternalTool, userId, schoolId } = setup(); - await uc.createContextExternalTool(userId, contextExternalTool); + await uc.createContextExternalTool(userId, schoolId, contextExternalTool); expect(contextExternalToolService.saveContextExternalTool).toHaveBeenCalledWith(contextExternalTool); }); it('should call contextExternalToolService to ensure permissions', async () => { - const { contextExternalTool, userId } = setup(); + const { contextExternalTool, userId, schoolId } = setup(); - await uc.createContextExternalTool(userId, contextExternalTool); + await uc.createContextExternalTool(userId, schoolId, contextExternalTool); expect(toolPermissionHelper.ensureContextPermissions).toHaveBeenCalledWith( userId, @@ -108,28 +126,82 @@ describe('ContextExternalToolUc', () => { }); it('should call contextExternalToolValidationService', async () => { - const { contextExternalTool, userId } = setup(); + const { contextExternalTool, userId, schoolId } = setup(); - await uc.createContextExternalTool(userId, contextExternalTool); + await uc.createContextExternalTool(userId, schoolId, contextExternalTool); expect(contextExternalToolValidationService.validate).toHaveBeenCalledWith(contextExternalTool); }); it('should return the saved object', async () => { - const { contextExternalTool, userId } = setup(); + const { contextExternalTool, userId, schoolId } = setup(); - const result = await uc.createContextExternalTool(userId, contextExternalTool); + const result = await uc.createContextExternalTool(userId, schoolId, contextExternalTool); expect(result).toEqual(contextExternalTool); }); }); + describe('when the user is from a different school than the school external tool', () => { + const setup = () => { + const userId: EntityId = 'userId'; + const schoolId: EntityId = new ObjectId().toHexString(); + + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId({ + schoolId, + }); + + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ + displayName: 'Course', + schoolToolRef: { + schoolToolId: schoolExternalTool.id, + schoolId, + }, + contextRef: { + id: 'contextId', + type: ToolContextType.COURSE, + }, + }); + + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); + + return { + contextExternalTool, + userId, + }; + }; + + it('should return UnprocessableEntity and not save', async () => { + const { contextExternalTool, userId } = setup(); + + const func = () => uc.createContextExternalTool(userId, new ObjectId().toHexString(), contextExternalTool); + + await expect(func).rejects.toThrow( + new ForbiddenLoggableException( + userId, + AuthorizableReferenceType.ContextExternalToolEntity, + AuthorizationContextBuilder.write([Permission.CONTEXT_TOOL_ADMIN]) + ) + ); + expect(contextExternalToolService.saveContextExternalTool).not.toHaveBeenCalled(); + }); + }); + describe('when the user does not have permission', () => { const setup = () => { const userId: EntityId = 'userId'; + const schoolId: EntityId = new ObjectId().toHexString(); + + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId({ + schoolId, + }); const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ displayName: 'Course', + schoolToolRef: { + schoolToolId: schoolExternalTool.id, + schoolId, + }, contextRef: { id: 'contextId', type: ToolContextType.COURSE, @@ -138,19 +210,21 @@ describe('ContextExternalToolUc', () => { const error = new ForbiddenException(); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); toolPermissionHelper.ensureContextPermissions.mockRejectedValue(error); return { contextExternalTool, userId, + schoolId, error, }; }; it('should return forbidden and not save', async () => { - const { contextExternalTool, userId, error } = setup(); + const { contextExternalTool, userId, error, schoolId } = setup(); - const func = () => uc.createContextExternalTool(userId, contextExternalTool); + const func = () => uc.createContextExternalTool(userId, schoolId, contextExternalTool); await expect(func).rejects.toThrow(error); expect(contextExternalToolService.saveContextExternalTool).not.toHaveBeenCalled(); @@ -160,9 +234,18 @@ describe('ContextExternalToolUc', () => { describe('when the validation fails', () => { const setup = () => { const userId: EntityId = 'userId'; + const schoolId: EntityId = new ObjectId().toHexString(); + + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId({ + schoolId, + }); const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ displayName: 'Course', + schoolToolRef: { + schoolToolId: schoolExternalTool.id, + schoolId, + }, contextRef: { id: 'contextId', type: ToolContextType.COURSE, @@ -171,19 +254,21 @@ describe('ContextExternalToolUc', () => { const error = new UnprocessableEntityException(); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); contextExternalToolValidationService.validate.mockRejectedValue(error); return { contextExternalTool, userId, + schoolId, error, }; }; it('should return UnprocessableEntity and not save', async () => { - const { contextExternalTool, userId, error } = setup(); + const { contextExternalTool, userId, error, schoolId } = setup(); - const func = () => uc.createContextExternalTool(userId, contextExternalTool); + const func = () => uc.createContextExternalTool(userId, schoolId, contextExternalTool); await expect(func).rejects.toThrow(error); expect(contextExternalToolService.saveContextExternalTool).not.toHaveBeenCalled(); @@ -195,40 +280,48 @@ describe('ContextExternalToolUc', () => { describe('when contextExternalTool is given and user has permission ', () => { const setup = () => { const userId: EntityId = 'userId'; + const schoolId: EntityId = new ObjectId().toHexString(); + + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId({ + schoolId, + }); - const contextExternalToolId = new ObjectId().toHexString(); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId( - { - displayName: 'Course', - contextRef: { - id: 'contextId', - type: ToolContextType.COURSE, - }, + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ + displayName: 'Course', + schoolToolRef: { + schoolToolId: schoolExternalTool.id, + schoolId, }, - contextExternalToolId - ); + contextRef: { + id: 'contextId', + type: ToolContextType.COURSE, + }, + }); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); contextExternalToolService.saveContextExternalTool.mockResolvedValue(contextExternalTool); + contextExternalToolService.findById.mockResolvedValueOnce(contextExternalTool); return { contextExternalTool, - contextExternalToolId, + contextExternalToolId: contextExternalTool.id as string, userId, + schoolId, }; }; it('should call contextExternalToolService', async () => { - const { contextExternalTool, contextExternalToolId, userId } = setup(); + const { contextExternalTool, userId, schoolId, contextExternalToolId } = setup(); - await uc.updateContextExternalTool(userId, contextExternalToolId, contextExternalTool); + await uc.updateContextExternalTool(userId, schoolId, contextExternalToolId, contextExternalTool); expect(contextExternalToolService.saveContextExternalTool).toHaveBeenCalledWith(contextExternalTool); }); it('should call contextExternalToolService to ensure permissions', async () => { - const { contextExternalTool, contextExternalToolId, userId } = setup(); + const { contextExternalTool, userId, schoolId, contextExternalToolId } = setup(); - await uc.updateContextExternalTool(userId, contextExternalToolId, contextExternalTool); + await uc.updateContextExternalTool(userId, schoolId, contextExternalToolId, contextExternalTool); expect(toolPermissionHelper.ensureContextPermissions).toHaveBeenCalledWith( userId, @@ -238,54 +331,114 @@ describe('ContextExternalToolUc', () => { }); it('should call contextExternalToolValidationService', async () => { - const { contextExternalTool, contextExternalToolId, userId } = setup(); + const { contextExternalTool, userId, schoolId, contextExternalToolId } = setup(); - await uc.updateContextExternalTool(userId, contextExternalToolId, contextExternalTool); + await uc.updateContextExternalTool(userId, schoolId, contextExternalToolId, contextExternalTool); expect(contextExternalToolValidationService.validate).toHaveBeenCalledWith(contextExternalTool); }); it('should return the saved object', async () => { - const { contextExternalTool, contextExternalToolId, userId } = setup(); + const { contextExternalTool, userId, schoolId, contextExternalToolId } = setup(); - const result = await uc.updateContextExternalTool(userId, contextExternalToolId, contextExternalTool); + const result = await uc.updateContextExternalTool(userId, schoolId, contextExternalToolId, contextExternalTool); expect(result).toEqual(contextExternalTool); }); }); - describe('when the user does not have permission', () => { + describe('when the user is from a different school than the school external tool', () => { const setup = () => { const userId: EntityId = 'userId'; + const schoolId: EntityId = new ObjectId().toHexString(); + + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId({ + schoolId, + }); - const contextExternalToolId = new ObjectId().toHexString(); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId( - { - displayName: 'Course', - contextRef: { - id: 'contextId', - type: ToolContextType.COURSE, - }, + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ + displayName: 'Course', + schoolToolRef: { + schoolToolId: schoolExternalTool.id, + schoolId, + }, + contextRef: { + id: 'contextId', + type: ToolContextType.COURSE, }, - contextExternalToolId + }); + + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); + + return { + contextExternalTool, + contextExternalToolId: contextExternalTool.id as string, + userId, + }; + }; + + it('should return UnprocessableEntity and not save', async () => { + const { contextExternalTool, userId, contextExternalToolId } = setup(); + + const func = () => + uc.updateContextExternalTool( + userId, + new ObjectId().toHexString(), + contextExternalToolId, + contextExternalTool + ); + + await expect(func).rejects.toThrow( + new ForbiddenLoggableException( + userId, + AuthorizableReferenceType.ContextExternalToolEntity, + AuthorizationContextBuilder.write([Permission.CONTEXT_TOOL_ADMIN]) + ) ); + expect(contextExternalToolService.saveContextExternalTool).not.toHaveBeenCalled(); + }); + }); + + describe('when the user does not have permission', () => { + const setup = () => { + const userId: EntityId = 'userId'; + const schoolId: EntityId = new ObjectId().toHexString(); + + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId({ + schoolId, + }); + + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ + displayName: 'Course', + schoolToolRef: { + schoolToolId: schoolExternalTool.id, + schoolId, + }, + contextRef: { + id: 'contextId', + type: ToolContextType.COURSE, + }, + }); const error = new ForbiddenException(); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); + contextExternalToolService.findById.mockResolvedValueOnce(contextExternalTool); toolPermissionHelper.ensureContextPermissions.mockRejectedValue(error); return { contextExternalTool, - contextExternalToolId, + contextExternalToolId: contextExternalTool.id as string, userId, + schoolId, error, }; }; it('should return forbidden and not save', async () => { - const { contextExternalTool, contextExternalToolId, userId, error } = setup(); + const { contextExternalTool, userId, error, schoolId, contextExternalToolId } = setup(); - const func = () => uc.updateContextExternalTool(userId, contextExternalToolId, contextExternalTool); + const func = () => uc.updateContextExternalTool(userId, schoolId, contextExternalToolId, contextExternalTool); await expect(func).rejects.toThrow(error); expect(contextExternalToolService.saveContextExternalTool).not.toHaveBeenCalled(); @@ -295,35 +448,43 @@ describe('ContextExternalToolUc', () => { describe('when the validation fails', () => { const setup = () => { const userId: EntityId = 'userId'; + const schoolId: EntityId = new ObjectId().toHexString(); - const contextExternalToolId = new ObjectId().toHexString(); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId( - { - displayName: 'Course', - contextRef: { - id: 'contextId', - type: ToolContextType.COURSE, - }, + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.buildWithId({ + schoolId, + }); + + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId({ + displayName: 'Course', + schoolToolRef: { + schoolToolId: schoolExternalTool.id, + schoolId, }, - contextExternalToolId - ); + contextRef: { + id: 'contextId', + type: ToolContextType.COURSE, + }, + }); const error = new UnprocessableEntityException(); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); + contextExternalToolService.findById.mockResolvedValueOnce(contextExternalTool); contextExternalToolValidationService.validate.mockRejectedValue(error); return { contextExternalTool, - contextExternalToolId, + contextExternalToolId: contextExternalTool.id as string, userId, + schoolId, error, }; }; it('should return UnprocessableEntity and not save', async () => { - const { contextExternalTool, contextExternalToolId, userId, error } = setup(); + const { contextExternalTool, userId, error, schoolId, contextExternalToolId } = setup(); - const func = () => uc.updateContextExternalTool(userId, contextExternalToolId, contextExternalTool); + const func = () => uc.updateContextExternalTool(userId, schoolId, contextExternalToolId, contextExternalTool); await expect(func).rejects.toThrow(error); expect(contextExternalToolService.saveContextExternalTool).not.toHaveBeenCalled(); @@ -339,7 +500,7 @@ describe('ContextExternalToolUc', () => { const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId(); toolPermissionHelper.ensureContextPermissions.mockResolvedValue(); - contextExternalToolService.getContextExternalToolById.mockResolvedValue(contextExternalTool); + contextExternalToolService.findById.mockResolvedValue(contextExternalTool); return { contextExternalTool, @@ -496,7 +657,7 @@ describe('ContextExternalToolUc', () => { }, }); - contextExternalToolService.getContextExternalToolById.mockResolvedValue(contextExternalTool); + contextExternalToolService.findById.mockResolvedValue(contextExternalTool); toolPermissionHelper.ensureContextPermissions.mockResolvedValue(Promise.resolve()); return { @@ -524,7 +685,7 @@ describe('ContextExternalToolUc', () => { await uc.getContextExternalTool(userId, contextExternalTool.id as string); - expect(contextExternalToolService.getContextExternalToolById).toHaveBeenCalledWith(contextExternalTool.id); + expect(contextExternalToolService.findById).toHaveBeenCalledWith(contextExternalTool.id); }); }); @@ -542,7 +703,7 @@ describe('ContextExternalToolUc', () => { }, }); - contextExternalToolService.getContextExternalToolById.mockResolvedValue(contextExternalTool); + contextExternalToolService.findById.mockResolvedValue(contextExternalTool); toolPermissionHelper.ensureContextPermissions.mockRejectedValue( new ForbiddenLoggableException( userId, diff --git a/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.ts b/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.ts index 903b8197251..587ecb01c64 100644 --- a/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.ts +++ b/apps/server/src/modules/tool/context-external-tool/uc/context-external-tool.uc.ts @@ -1,33 +1,50 @@ +import { + AuthorizationContext, + AuthorizationContextBuilder, + AuthorizationService, + ForbiddenLoggableException, +} from '@modules/authorization'; +import { AuthorizableReferenceType } from '@modules/authorization/domain'; import { Injectable } from '@nestjs/common'; import { EntityId, Permission, User } from '@shared/domain'; -import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; -import { LegacyLogger } from '@src/core/logger'; -import { ContextExternalToolService, ContextExternalToolValidationService } from '../service'; -import { ContextExternalToolDto } from './dto/context-external-tool.types'; -import { ContextExternalTool, ContextRef } from '../domain'; import { ToolContextType } from '../../common/enum'; import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; +import { SchoolExternalTool } from '../../school-external-tool/domain'; +import { SchoolExternalToolService } from '../../school-external-tool/service'; +import { ContextExternalTool, ContextRef } from '../domain'; +import { ContextExternalToolService, ContextExternalToolValidationService } from '../service'; +import { ContextExternalToolDto } from './dto/context-external-tool.types'; @Injectable() export class ContextExternalToolUc { constructor( private readonly toolPermissionHelper: ToolPermissionHelper, + private readonly schoolExternalToolService: SchoolExternalToolService, private readonly contextExternalToolService: ContextExternalToolService, private readonly contextExternalToolValidationService: ContextExternalToolValidationService, - private readonly authorizationService: AuthorizationService, - private readonly logger: LegacyLogger + private readonly authorizationService: AuthorizationService ) {} async createContextExternalTool( userId: EntityId, + schoolId: EntityId, contextExternalToolDto: ContextExternalToolDto ): Promise { - const contextExternalTool = new ContextExternalTool(contextExternalToolDto); const context: AuthorizationContext = AuthorizationContextBuilder.write([Permission.CONTEXT_TOOL_ADMIN]); + const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById( + contextExternalToolDto.schoolToolRef.schoolToolId + ); + + if (schoolExternalTool.schoolId !== schoolId) { + throw new ForbiddenLoggableException(userId, AuthorizableReferenceType.ContextExternalToolEntity, context); + } + + contextExternalToolDto.schoolToolRef.schoolId = schoolId; + const contextExternalTool = new ContextExternalTool(contextExternalToolDto); await this.toolPermissionHelper.ensureContextPermissions(userId, contextExternalTool, context); - await this.contextExternalToolValidationService.validate(contextExternalToolDto); + await this.contextExternalToolValidationService.validate(contextExternalTool); const createdTool: ContextExternalTool = await this.contextExternalToolService.saveContextExternalTool( contextExternalTool @@ -38,43 +55,54 @@ export class ContextExternalToolUc { async updateContextExternalTool( userId: EntityId, + schoolId: EntityId, contextExternalToolId: EntityId, contextExternalToolDto: ContextExternalToolDto ): Promise { - const contextExternalTool: ContextExternalTool = new ContextExternalTool(contextExternalToolDto); + const context: AuthorizationContext = AuthorizationContextBuilder.write([Permission.CONTEXT_TOOL_ADMIN]); + const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById( + contextExternalToolDto.schoolToolRef.schoolToolId + ); - await this.toolPermissionHelper.ensureContextPermissions( - userId, - contextExternalTool, - AuthorizationContextBuilder.write([Permission.CONTEXT_TOOL_ADMIN]) + if (schoolExternalTool.schoolId !== schoolId) { + throw new ForbiddenLoggableException(userId, AuthorizableReferenceType.ContextExternalToolEntity, context); + } + + let contextExternalTool: ContextExternalTool = await this.contextExternalToolService.findById( + contextExternalToolId ); - const updated: ContextExternalTool = new ContextExternalTool({ - ...contextExternalTool, - id: contextExternalToolId, + contextExternalTool = new ContextExternalTool({ + ...contextExternalToolDto, + id: contextExternalTool.id, }); + contextExternalTool.schoolToolRef.schoolId = schoolId; + + await this.toolPermissionHelper.ensureContextPermissions(userId, contextExternalTool, context); - await this.contextExternalToolValidationService.validate(updated); + await this.contextExternalToolValidationService.validate(contextExternalTool); - const saved: ContextExternalTool = await this.contextExternalToolService.saveContextExternalTool(updated); + const updatedTool: ContextExternalTool = await this.contextExternalToolService.saveContextExternalTool( + contextExternalTool + ); - return saved; + return updatedTool; } - async deleteContextExternalTool(userId: EntityId, contextExternalToolId: EntityId): Promise { - const tool: ContextExternalTool = await this.contextExternalToolService.getContextExternalToolById( - contextExternalToolId - ); - const context: AuthorizationContext = AuthorizationContextBuilder.write([Permission.CONTEXT_TOOL_ADMIN]); + public async deleteContextExternalTool(userId: EntityId, contextExternalToolId: EntityId): Promise { + const tool: ContextExternalTool = await this.contextExternalToolService.findById(contextExternalToolId); + const context = AuthorizationContextBuilder.write([Permission.CONTEXT_TOOL_ADMIN]); await this.toolPermissionHelper.ensureContextPermissions(userId, tool, context); - const promise: Promise = this.contextExternalToolService.deleteContextExternalTool(tool); - - return promise; + await this.contextExternalToolService.deleteContextExternalTool(tool); } - async getContextExternalToolsForContext(userId: EntityId, contextType: ToolContextType, contextId: string) { + public async getContextExternalToolsForContext( + userId: EntityId, + contextType: ToolContextType, + contextId: string + ): Promise { const tools: ContextExternalTool[] = await this.contextExternalToolService.findAllByContext( new ContextRef({ id: contextId, type: contextType }) ); @@ -85,7 +113,7 @@ export class ContextExternalToolUc { } async getContextExternalTool(userId: EntityId, contextToolId: EntityId) { - const tool: ContextExternalTool = await this.contextExternalToolService.getContextExternalToolById(contextToolId); + const tool: ContextExternalTool = await this.contextExternalToolService.findById(contextToolId); const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_ADMIN]); await this.toolPermissionHelper.ensureContextPermissions(userId, tool, context); diff --git a/apps/server/src/modules/tool/context-external-tool/uc/index.ts b/apps/server/src/modules/tool/context-external-tool/uc/index.ts index cd34b162bad..12f2a82a9f1 100644 --- a/apps/server/src/modules/tool/context-external-tool/uc/index.ts +++ b/apps/server/src/modules/tool/context-external-tool/uc/index.ts @@ -1 +1,2 @@ export * from './context-external-tool.uc'; +export * from './tool-reference.uc'; diff --git a/apps/server/src/modules/tool/context-external-tool/uc/tool-reference.uc.spec.ts b/apps/server/src/modules/tool/context-external-tool/uc/tool-reference.uc.spec.ts new file mode 100644 index 00000000000..9b18e7ffd3b --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/uc/tool-reference.uc.spec.ts @@ -0,0 +1,215 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ForbiddenException } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Permission } from '@shared/domain'; +import { contextExternalToolFactory, externalToolFactory } from '@shared/testing'; +import { AuthorizationContextBuilder } from '@modules/authorization'; +import { ToolConfigurationStatus, ToolContextType } from '../../common/enum'; +import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; +import { ExternalTool } from '../../external-tool/domain'; +import { ContextExternalTool, ToolReference } from '../domain'; +import { ContextExternalToolService, ToolReferenceService } from '../service'; +import { ToolReferenceUc } from './tool-reference.uc'; + +describe('ToolReferenceUc', () => { + let module: TestingModule; + let uc: ToolReferenceUc; + + let contextExternalToolService: DeepMocked; + let toolReferenceService: DeepMocked; + let toolPermissionHelper: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + ToolReferenceUc, + { + provide: ContextExternalToolService, + useValue: createMock(), + }, + { + provide: ToolReferenceService, + useValue: createMock(), + }, + { + provide: ToolPermissionHelper, + useValue: createMock(), + }, + ], + }).compile(); + + uc = module.get(ToolReferenceUc); + + contextExternalToolService = module.get(ContextExternalToolService); + toolReferenceService = module.get(ToolReferenceService); + toolPermissionHelper = module.get(ToolPermissionHelper); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('getToolReferencesForContext', () => { + describe('when called with a context type and id', () => { + const setup = () => { + const userId = 'userId'; + + const externalTool: ExternalTool = externalToolFactory.withBase64Logo().buildWithId(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId(); + const toolReference: ToolReference = new ToolReference({ + logoUrl: externalTool.logoUrl, + contextToolId: contextExternalTool.id as string, + displayName: contextExternalTool.displayName as string, + status: ToolConfigurationStatus.LATEST, + openInNewTab: externalTool.openNewTab, + }); + + const contextType: ToolContextType = ToolContextType.COURSE; + const contextId = 'contextId'; + + contextExternalToolService.findAllByContext.mockResolvedValueOnce([contextExternalTool]); + toolPermissionHelper.ensureContextPermissions.mockResolvedValueOnce(); + toolReferenceService.getToolReference.mockResolvedValue(toolReference); + + return { + userId, + contextType, + contextId, + contextExternalTool, + externalTool, + toolReference, + }; + }; + + it('should call toolPermissionHelper.ensureContextPermissions', async () => { + const { userId, contextType, contextId, contextExternalTool } = setup(); + + await uc.getToolReferencesForContext(userId, contextType, contextId); + + expect(toolPermissionHelper.ensureContextPermissions).toHaveBeenCalledWith( + userId, + contextExternalTool, + AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]) + ); + }); + + it('should return a list of tool references', async () => { + const { userId, contextType, contextId, toolReference } = setup(); + + const result: ToolReference[] = await uc.getToolReferencesForContext(userId, contextType, contextId); + + expect(result).toEqual([toolReference]); + }); + }); + + describe('when user does not have permission to a tool', () => { + const setup = () => { + const userId = 'userId'; + + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId(); + + const contextType: ToolContextType = ToolContextType.COURSE; + const contextId = 'contextId'; + + contextExternalToolService.findAllByContext.mockResolvedValueOnce([contextExternalTool]); + toolPermissionHelper.ensureContextPermissions.mockRejectedValueOnce(new ForbiddenException()); + + return { + userId, + contextType, + contextId, + }; + }; + + it('should filter out tool references if a ForbiddenException is thrown', async () => { + const { userId, contextType, contextId } = setup(); + + const result: ToolReference[] = await uc.getToolReferencesForContext(userId, contextType, contextId); + + expect(result).toEqual([]); + }); + }); + }); + + describe('getToolReference', () => { + describe('when called with a context type and id', () => { + const setup = () => { + const userId = 'userId'; + const contextExternalToolId = 'contextExternalToolId'; + + const externalTool: ExternalTool = externalToolFactory.withBase64Logo().buildWithId(); + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId( + undefined, + contextExternalToolId + ); + const toolReference: ToolReference = new ToolReference({ + logoUrl: externalTool.logoUrl, + contextToolId: contextExternalTool.id as string, + displayName: contextExternalTool.displayName as string, + status: ToolConfigurationStatus.LATEST, + openInNewTab: externalTool.openNewTab, + }); + + contextExternalToolService.findById.mockResolvedValueOnce(contextExternalTool); + toolPermissionHelper.ensureContextPermissions.mockResolvedValueOnce(); + toolReferenceService.getToolReference.mockResolvedValue(toolReference); + + return { + userId, + contextExternalTool, + externalTool, + toolReference, + contextExternalToolId, + }; + }; + + it('should call toolPermissionHelper.ensureContextPermissions', async () => { + const { userId, contextExternalToolId, contextExternalTool } = setup(); + + await uc.getToolReference(userId, contextExternalToolId); + + expect(toolPermissionHelper.ensureContextPermissions).toHaveBeenCalledWith( + userId, + contextExternalTool, + AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]) + ); + }); + + it('should return a list of tool references', async () => { + const { userId, contextExternalToolId, toolReference } = setup(); + + const result: ToolReference = await uc.getToolReference(userId, contextExternalToolId); + + expect(result).toEqual(toolReference); + }); + }); + + describe('when user does not have permission to a tool', () => { + const setup = () => { + const userId = 'userId'; + const contextExternalToolId = 'contextExternalToolId'; + + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.buildWithId( + undefined, + contextExternalToolId + ); + const error = new ForbiddenException(); + + contextExternalToolService.findById.mockResolvedValueOnce(contextExternalTool); + toolPermissionHelper.ensureContextPermissions.mockRejectedValueOnce(error); + + return { + userId, + contextExternalToolId, + error, + }; + }; + + it('should filter out tool references if a ForbiddenException is thrown', async () => { + const { userId, contextExternalToolId, error } = setup(); + + await expect(uc.getToolReference(userId, contextExternalToolId)).rejects.toThrow(error); + }); + }); + }); +}); diff --git a/apps/server/src/modules/tool/context-external-tool/uc/tool-reference.uc.ts b/apps/server/src/modules/tool/context-external-tool/uc/tool-reference.uc.ts new file mode 100644 index 00000000000..ac6ccda016f --- /dev/null +++ b/apps/server/src/modules/tool/context-external-tool/uc/tool-reference.uc.ts @@ -0,0 +1,82 @@ +import { Injectable } from '@nestjs/common'; +import { EntityId, Permission } from '@shared/domain'; +import { AuthorizationContext, AuthorizationContextBuilder } from '@modules/authorization'; +import { ToolContextType } from '../../common/enum'; +import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; +import { ContextExternalTool, ContextRef, ToolReference } from '../domain'; +import { ContextExternalToolService, ToolReferenceService } from '../service'; + +@Injectable() +export class ToolReferenceUc { + constructor( + private readonly contextExternalToolService: ContextExternalToolService, + private readonly toolReferenceService: ToolReferenceService, + private readonly toolPermissionHelper: ToolPermissionHelper + ) {} + + async getToolReferencesForContext( + userId: EntityId, + contextType: ToolContextType, + contextId: EntityId + ): Promise { + const contextRef = new ContextRef({ type: contextType, id: contextId }); + + const contextExternalTools: ContextExternalTool[] = await this.contextExternalToolService.findAllByContext( + contextRef + ); + + const toolReferencesPromises: Promise[] = contextExternalTools.map( + async (contextExternalTool: ContextExternalTool) => this.tryBuildToolReference(userId, contextExternalTool) + ); + + const toolReferencesWithNull: (ToolReference | null)[] = await Promise.all(toolReferencesPromises); + const filteredToolReferences: ToolReference[] = toolReferencesWithNull.filter( + (toolReference: ToolReference | null): toolReference is ToolReference => toolReference !== null + ); + + return filteredToolReferences; + } + + private async tryBuildToolReference( + userId: EntityId, + contextExternalTool: ContextExternalTool + ): Promise { + try { + await this.ensureToolPermissions(userId, contextExternalTool); + + const toolReference: ToolReference = await this.toolReferenceService.getToolReference( + contextExternalTool.id as string + ); + + return toolReference; + } catch (e: unknown) { + return null; + } + } + + async getToolReference(userId: EntityId, contextExternalToolId: EntityId): Promise { + const contextExternalTool: ContextExternalTool = await this.contextExternalToolService.findById( + contextExternalToolId + ); + + await this.ensureToolPermissions(userId, contextExternalTool); + + const toolReference: ToolReference = await this.toolReferenceService.getToolReference( + contextExternalTool.id as string + ); + + return toolReference; + } + + private async ensureToolPermissions(userId: EntityId, contextExternalTool: ContextExternalTool): Promise { + const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]); + + const promise: Promise = this.toolPermissionHelper.ensureContextPermissions( + userId, + contextExternalTool, + context + ); + + return promise; + } +} diff --git a/apps/server/src/modules/tool/external-tool/controller/api-test/tool-configuration.api.spec.ts b/apps/server/src/modules/tool/external-tool/controller/api-test/tool-configuration.api.spec.ts index 70ec7d4be67..68543982174 100644 --- a/apps/server/src/modules/tool/external-tool/controller/api-test/tool-configuration.api.spec.ts +++ b/apps/server/src/modules/tool/external-tool/controller/api-test/tool-configuration.api.spec.ts @@ -15,8 +15,8 @@ import { UserAndAccountTestFactory, userFactory, } from '@shared/testing'; -import { ServerTestModule } from '@src/modules/server'; -import { CustomParameterTypeParams } from '@src/modules/tool/common/enum'; +import { ServerTestModule } from '@modules/server'; +import { CustomParameterTypeParams } from '@modules/tool/common/enum'; import { Response } from 'supertest'; import { CustomParameterLocationParams, CustomParameterScopeTypeParams } from '../../../common/enum'; import { ContextExternalToolEntity, ContextExternalToolType } from '../../../context-external-tool/entity'; @@ -33,7 +33,6 @@ describe('ToolConfigurationController (API)', () => { let app: INestApplication; let em: EntityManager; let orm: MikroORM; - let testApiClient: TestApiClient; beforeAll(async () => { @@ -346,22 +345,19 @@ describe('ToolConfigurationController (API)', () => { describe('GET tools/school-external-tools/:schoolExternalToolId/configuration-template', () => { describe('when the user is not authorized', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); - - const user: User = userFactory.buildWithId({ school, roles: [] }); - const account: Account = accountFactory.buildWithId({ userId: user.id }); - - const externalTool: ExternalToolEntity = externalToolEntityFactory.buildWithId(); - - const schoolExternalTool: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ + const school = schoolFactory.build(); + // not on same school like the tool + const { adminAccount, adminUser } = UserAndAccountTestFactory.buildAdmin({}, []); + const externalTool: ExternalToolEntity = externalToolEntityFactory.build(); + const schoolExternalTool: SchoolExternalToolEntity = schoolExternalToolEntityFactory.build({ school, tool: externalTool, }); - await em.persistAndFlush([user, account, school, externalTool, schoolExternalTool]); + await em.persistAndFlush([adminAccount, adminUser, school, externalTool, schoolExternalTool]); em.clear(); - const loggedInClient: TestApiClient = await testApiClient.login(account); + const loggedInClient: TestApiClient = await testApiClient.login(adminAccount); return { loggedInClient, @@ -477,51 +473,43 @@ describe('ToolConfigurationController (API)', () => { describe('GET tools/context-external-tools/:contextExternalToolId/configuration-template', () => { describe('when the user is not authorized', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); - - const course: Course = courseFactory.buildWithId(); - - const user: User = userFactory.buildWithId({ school, roles: [] }); - const account: Account = accountFactory.buildWithId({ userId: user.id }); - - const externalTool: ExternalToolEntity = externalToolEntityFactory.buildWithId(); - - const schoolExternalTool: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ + const school = schoolFactory.build(); + const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({}, [Permission.SCHOOL_TOOL_ADMIN]); + // user is not part of the course + const course = courseFactory.build(); + const externalTool: ExternalToolEntity = externalToolEntityFactory.build(); + const schoolExternalTool: SchoolExternalToolEntity = schoolExternalToolEntityFactory.build({ school, tool: externalTool, }); - const contextExternalTool: ContextExternalToolEntity = contextExternalToolEntityFactory.buildWithId({ + await em.persistAndFlush([course, adminUser, adminAccount, school, externalTool, schoolExternalTool]); + + const contextExternalTool: ContextExternalToolEntity = contextExternalToolEntityFactory.build({ schoolTool: schoolExternalTool, + contextId: course.id, }); - await em.persistAndFlush([ - user, - account, - school, - externalTool, - schoolExternalTool, - contextExternalTool, - course, - ]); + await em.persistAndFlush([contextExternalTool]); em.clear(); - const loggedInClient: TestApiClient = await testApiClient.login(account); + const loggedInClient: TestApiClient = await testApiClient.login(adminAccount); return { loggedInClient, - contextExternalTool, + contextExternalToolId: contextExternalTool.id, }; }; it('should return a forbidden status', async () => { - const { loggedInClient, contextExternalTool } = await setup(); + const { loggedInClient, contextExternalToolId } = await setup(); const response: Response = await loggedInClient.get( - `context-external-tools/${contextExternalTool.id}/configuration-template` + `context-external-tools/${contextExternalToolId}/configuration-template` ); expect(response.status).toEqual(HttpStatus.FORBIDDEN); + // body }); }); @@ -607,36 +595,26 @@ describe('ToolConfigurationController (API)', () => { describe('when tool is hidden', () => { const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); - + const school = schoolFactory.build(); const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({ school }, [ Permission.CONTEXT_TOOL_ADMIN, ]); - - const course: Course = courseFactory.buildWithId({ school, teachers: [teacherUser] }); - - const externalTool: ExternalToolEntity = externalToolEntityFactory.buildWithId({ isHidden: true }); - - const schoolExternalTool: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ + const course = courseFactory.build({ school, teachers: [teacherUser] }); + const externalTool: ExternalToolEntity = externalToolEntityFactory.build({ isHidden: true }); + const schoolExternalTool: SchoolExternalToolEntity = schoolExternalToolEntityFactory.build({ school, tool: externalTool, }); - const contextExternalTool: ContextExternalToolEntity = contextExternalToolEntityFactory.buildWithId({ + await em.persistAndFlush([teacherUser, school, teacherAccount, externalTool, schoolExternalTool, course]); + + const contextExternalTool: ContextExternalToolEntity = contextExternalToolEntityFactory.build({ schoolTool: schoolExternalTool, contextType: ContextExternalToolType.COURSE, contextId: course.id, }); - await em.persistAndFlush([ - teacherUser, - school, - teacherAccount, - externalTool, - schoolExternalTool, - contextExternalTool, - course, - ]); + await em.persistAndFlush([contextExternalTool]); em.clear(); const loggedInClient: TestApiClient = await testApiClient.login(teacherAccount); diff --git a/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts b/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts index 6498f66fc1f..ebff659529d 100644 --- a/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts +++ b/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts @@ -1,41 +1,27 @@ +import { Loaded } from '@mikro-orm/core'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; -import { Course, Permission, SchoolEntity } from '@shared/domain'; +import { Permission } from '@shared/domain'; import { cleanupCollections, - contextExternalToolEntityFactory, - courseFactory, externalToolEntityFactory, externalToolFactory, - schoolExternalToolEntityFactory, - schoolFactory, TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; +import { ServerTestModule } from '@modules/server'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { Response } from 'supertest'; -import { Loaded } from '@mikro-orm/core'; -import { ServerTestModule } from '@src/modules/server'; import { CustomParameterLocationParams, CustomParameterScopeTypeParams, CustomParameterTypeParams, ToolConfigType, - ToolContextType, } from '../../../common/enum'; -import { - ExternalToolCreateParams, - ExternalToolResponse, - ExternalToolSearchListResponse, - ToolConfigurationStatusResponse, - ToolReferenceListResponse, -} from '../dto'; -import { ContextExternalToolContextParams } from '../../../context-external-tool/controller/dto'; import { ExternalToolEntity } from '../../entity'; -import { ContextExternalToolEntity, ContextExternalToolType } from '../../../context-external-tool/entity'; -import { SchoolExternalToolEntity } from '../../../school-external-tool/entity'; +import { ExternalToolCreateParams, ExternalToolResponse, ExternalToolSearchListResponse } from '../dto'; describe('ToolController (API)', () => { let app: INestApplication; @@ -597,126 +583,6 @@ describe('ToolController (API)', () => { }); }); - describe('[GET] tools/external-tools/:contextType/:contextId/references', () => { - describe('when user is not authenticated', () => { - it('should return unauthorized', async () => { - const response: Response = await testApiClient.get(`contextType/${new ObjectId().toHexString()}/references`); - - expect(response.statusCode).toEqual(HttpStatus.UNAUTHORIZED); - }); - }); - - describe('when user has no access to a tool', () => { - const setup = async () => { - const schoolWithoutTool: SchoolEntity = schoolFactory.buildWithId(); - const school: SchoolEntity = schoolFactory.buildWithId(); - const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school: schoolWithoutTool }); - const course: Course = courseFactory.buildWithId({ school, teachers: [adminUser] }); - const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId(); - const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ - school, - tool: externalToolEntity, - }); - const contextExternalToolEntity: ContextExternalToolEntity = contextExternalToolEntityFactory.buildWithId({ - schoolTool: schoolExternalToolEntity, - contextId: course.id, - contextType: ContextExternalToolType.COURSE, - }); - - await em.persistAndFlush([ - school, - adminAccount, - adminUser, - course, - externalToolEntity, - schoolExternalToolEntity, - contextExternalToolEntity, - ]); - em.clear(); - - const params: ContextExternalToolContextParams = { - contextId: course.id, - contextType: ToolContextType.COURSE, - }; - - const loggedInClient: TestApiClient = await testApiClient.login(adminAccount); - - return { loggedInClient, params }; - }; - - it('should filter out the tool', async () => { - const { loggedInClient, params } = await setup(); - - const response: Response = await loggedInClient.get(`${params.contextType}/${params.contextId}/references`); - - expect(response.statusCode).toEqual(HttpStatus.OK); - expect(response.body).toEqual({ data: [] }); - }); - }); - - describe('when user has access for a tool', () => { - const setup = async () => { - const school: SchoolEntity = schoolFactory.buildWithId(); - const { adminUser, adminAccount } = UserAndAccountTestFactory.buildAdmin({ school }, [ - Permission.CONTEXT_TOOL_USER, - ]); - const course: Course = courseFactory.buildWithId({ school, teachers: [adminUser] }); - const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.buildWithId({ logoUrl: undefined }); - const schoolExternalToolEntity: SchoolExternalToolEntity = schoolExternalToolEntityFactory.buildWithId({ - school, - tool: externalToolEntity, - toolVersion: externalToolEntity.version, - }); - const contextExternalToolEntity: ContextExternalToolEntity = contextExternalToolEntityFactory.buildWithId({ - schoolTool: schoolExternalToolEntity, - contextId: course.id, - contextType: ContextExternalToolType.COURSE, - displayName: 'This is a test tool', - toolVersion: schoolExternalToolEntity.toolVersion, - }); - - await em.persistAndFlush([ - school, - adminAccount, - adminUser, - course, - externalToolEntity, - schoolExternalToolEntity, - contextExternalToolEntity, - ]); - em.clear(); - - const params: ContextExternalToolContextParams = { - contextId: course.id, - contextType: ToolContextType.COURSE, - }; - - const loggedInClient: TestApiClient = await testApiClient.login(adminAccount); - - return { loggedInClient, params, contextExternalToolEntity, externalToolEntity }; - }; - - it('should return an ToolReferenceListResponse with data', async () => { - const { loggedInClient, params, contextExternalToolEntity, externalToolEntity } = await setup(); - - const response: Response = await loggedInClient.get(`${params.contextType}/${params.contextId}/references`); - - expect(response.statusCode).toEqual(HttpStatus.OK); - expect(response.body).toEqual({ - data: [ - { - contextToolId: contextExternalToolEntity.id, - displayName: contextExternalToolEntity.displayName as string, - status: ToolConfigurationStatusResponse.LATEST, - logoUrl: externalToolEntity.logoUrl, - openInNewTab: externalToolEntity.openNewTab, - }, - ], - }); - }); - }); - }); - describe('[GET] tools/external-tools/:externalToolId/logo', () => { const setup = async () => { const externalToolEntity: ExternalToolEntity = externalToolEntityFactory.withBase64Logo().buildWithId(); diff --git a/apps/server/src/modules/tool/external-tool/controller/dto/response/index.ts b/apps/server/src/modules/tool/external-tool/controller/dto/response/index.ts index fbae39a8b33..e9e5fafa376 100644 --- a/apps/server/src/modules/tool/external-tool/controller/dto/response/index.ts +++ b/apps/server/src/modules/tool/external-tool/controller/dto/response/index.ts @@ -1,10 +1,7 @@ export * from './config'; export * from './external-tool.response'; -export * from './tool-reference.response'; export * from './custom-parameter.response'; -export * from './tool-reference-list.response'; export * from './external-tool-search-list.response'; -export * from './tool-configuration-status.response'; export * from './context-external-tool-configuration-template.response'; export * from './context-external-tool-configuration-template-list.response'; export * from './school-external-tool-configuration-template.response'; diff --git a/apps/server/src/modules/tool/external-tool/controller/tool-configuration.controller.ts b/apps/server/src/modules/tool/external-tool/controller/tool-configuration.controller.ts index ecdcc32f2a2..589a1e38e7c 100644 --- a/apps/server/src/modules/tool/external-tool/controller/tool-configuration.controller.ts +++ b/apps/server/src/modules/tool/external-tool/controller/tool-configuration.controller.ts @@ -7,8 +7,7 @@ import { ApiTags, ApiUnauthorizedResponse, } from '@nestjs/swagger'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { ExternalTool } from '../domain'; import { ToolConfigurationMapper } from '../mapper/tool-configuration.mapper'; import { ContextExternalToolTemplateInfo, ExternalToolConfigurationUc } from '../uc'; diff --git a/apps/server/src/modules/tool/external-tool/controller/tool.controller.ts b/apps/server/src/modules/tool/external-tool/controller/tool.controller.ts index 3e6ac38fedc..80139a586cb 100644 --- a/apps/server/src/modules/tool/external-tool/controller/tool.controller.ts +++ b/apps/server/src/modules/tool/external-tool/controller/tool.controller.ts @@ -14,27 +14,23 @@ import { ValidationError } from '@shared/common'; import { PaginationParams } from '@shared/controller'; import { IFindOptions, Page } from '@shared/domain'; import { LegacyLogger } from '@src/core/logger'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Response } from 'express'; import { ExternalToolSearchQuery } from '../../common/interface'; -import { ContextExternalToolContextParams } from '../../context-external-tool/controller/dto'; -import { ExternalTool, ToolReference } from '../domain'; +import { ExternalTool } from '../domain'; import { ExternalToolLogo } from '../domain/external-tool-logo'; import { ExternalToolRequestMapper, ExternalToolResponseMapper } from '../mapper'; -import { ExternalToolCreate, ExternalToolUc, ExternalToolUpdate, ToolReferenceUc } from '../uc'; +import { ExternalToolLogoService } from '../service'; +import { ExternalToolCreate, ExternalToolUc, ExternalToolUpdate } from '../uc'; import { ExternalToolCreateParams, + ExternalToolIdParams, ExternalToolResponse, ExternalToolSearchListResponse, ExternalToolSearchParams, ExternalToolUpdateParams, SortExternalToolParams, - ExternalToolIdParams, - ToolReferenceListResponse, - ToolReferenceResponse, } from './dto'; -import { ExternalToolLogoService } from '../service'; @ApiTags('Tool') @Authenticate('jwt') @@ -43,7 +39,6 @@ export class ToolController { constructor( private readonly externalToolUc: ExternalToolUc, private readonly externalToolDOMapper: ExternalToolRequestMapper, - private readonly toolReferenceUc: ToolReferenceUc, private readonly logger: LegacyLogger, private readonly externalToolLogoService: ExternalToolLogoService ) {} @@ -156,32 +151,6 @@ export class ToolController { return promise; } - @Get('/:contextType/:contextId/references') - @ApiOperation({ summary: 'Get ExternalTool References for a given context' }) - @ApiOkResponse({ - description: 'The Tool References has been successfully fetched.', - type: ToolReferenceListResponse, - }) - @ApiForbiddenResponse({ description: 'User is not allowed to access this resource.' }) - @ApiUnauthorizedResponse({ description: 'User is not logged in.' }) - async getToolReferences( - @CurrentUser() currentUser: ICurrentUser, - @Param() params: ContextExternalToolContextParams - ): Promise { - const toolReferences: ToolReference[] = await this.toolReferenceUc.getToolReferences( - currentUser.userId, - params.contextType, - params.contextId, - '/v3/tools/external-tools/{id}/logo' - ); - - const toolReferenceResponses: ToolReferenceResponse[] = - ExternalToolResponseMapper.mapToToolReferenceResponses(toolReferences); - const toolReferenceListResponse = new ToolReferenceListResponse(toolReferenceResponses); - - return toolReferenceListResponse; - } - @Get('/:externalToolId/logo') @ApiOperation({ summary: 'Gets the logo of an external tool.' }) @ApiOkResponse({ diff --git a/apps/server/src/modules/tool/external-tool/domain/index.ts b/apps/server/src/modules/tool/external-tool/domain/index.ts index 9eaf1f03cbb..e5a1dab735d 100644 --- a/apps/server/src/modules/tool/external-tool/domain/index.ts +++ b/apps/server/src/modules/tool/external-tool/domain/index.ts @@ -1,3 +1,2 @@ export * from './external-tool.do'; export * from './config'; -export * from './tool-reference'; diff --git a/apps/server/src/modules/tool/external-tool/mapper/external-tool-response.mapper.ts b/apps/server/src/modules/tool/external-tool/mapper/external-tool-response.mapper.ts index 2885c2ea0c0..b2035e66477 100644 --- a/apps/server/src/modules/tool/external-tool/mapper/external-tool-response.mapper.ts +++ b/apps/server/src/modules/tool/external-tool/mapper/external-tool-response.mapper.ts @@ -8,16 +8,14 @@ import { CustomParameterType, CustomParameterTypeParams, } from '../../common/enum'; -import { statusMapping } from '../../school-external-tool/mapper'; import { BasicToolConfigResponse, CustomParameterResponse, ExternalToolResponse, Lti11ToolConfigResponse, Oauth2ToolConfigResponse, - ToolReferenceResponse, } from '../controller/dto'; -import { BasicToolConfig, ExternalTool, Lti11ToolConfig, Oauth2ToolConfig, ToolReference } from '../domain'; +import { BasicToolConfig, ExternalTool, Lti11ToolConfig, Oauth2ToolConfig } from '../domain'; const scopeMapping: Record = { [CustomParameterScope.GLOBAL]: CustomParameterScopeTypeParams.GLOBAL, @@ -98,24 +96,4 @@ export class ExternalToolResponseMapper { }; }); } - - static mapToToolReferenceResponses(toolReferences: ToolReference[]): ToolReferenceResponse[] { - const toolReferenceResponses: ToolReferenceResponse[] = toolReferences.map((toolReference: ToolReference) => - this.mapToToolReferenceResponse(toolReference) - ); - - return toolReferenceResponses; - } - - private static mapToToolReferenceResponse(toolReference: ToolReference): ToolReferenceResponse { - const response = new ToolReferenceResponse({ - contextToolId: toolReference.contextToolId, - displayName: toolReference.displayName, - logoUrl: toolReference.logoUrl, - openInNewTab: toolReference.openInNewTab, - status: statusMapping[toolReference.status], - }); - - return response; - } } diff --git a/apps/server/src/modules/tool/external-tool/mapper/index.ts b/apps/server/src/modules/tool/external-tool/mapper/index.ts index 92aff5e73c9..4149a17a519 100644 --- a/apps/server/src/modules/tool/external-tool/mapper/index.ts +++ b/apps/server/src/modules/tool/external-tool/mapper/index.ts @@ -1,3 +1,2 @@ -export * from './tool-reference.mapper'; export * from './external-tool-request.mapper'; export * from './external-tool-response.mapper'; 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 57acd50122f..c53154098a5 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,4 +1,4 @@ -import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { HttpService } from '@nestjs/axios'; import { HttpException, HttpStatus } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; @@ -9,8 +9,8 @@ import { IToolFeatures, ToolFeatures } from '../../tool-config'; import { ExternalTool } from '../domain'; import { ExternalToolLogo } from '../domain/external-tool-logo'; import { - ExternalToolLogoFetchFailedLoggableException, ExternalToolLogoFetchedLoggable, + ExternalToolLogoFetchFailedLoggableException, ExternalToolLogoNotFoundLoggableException, ExternalToolLogoSizeExceededLoggableException, ExternalToolLogoWrongFileTypeLoggableException, @@ -329,7 +329,7 @@ describe('ExternalToolLogoService', () => { const setup = () => { const externalTool: ExternalTool = externalToolFactory.withBase64Logo().buildWithId(); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + externalToolService.findById.mockResolvedValue(externalTool); return { externalToolId: externalTool.id as string, @@ -355,7 +355,7 @@ describe('ExternalToolLogoService', () => { const setup = () => { const externalTool: ExternalTool = externalToolFactory.buildWithId({ logo: 'notAValidBase64File' }); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + externalToolService.findById.mockResolvedValue(externalTool); return { externalToolId: externalTool.id as string, @@ -375,7 +375,7 @@ describe('ExternalToolLogoService', () => { const setup = () => { const externalTool: ExternalTool = externalToolFactory.buildWithId(); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + externalToolService.findById.mockResolvedValue(externalTool); return { externalToolId: externalTool.id as string, diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-logo.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-logo.service.ts index f2518e65a3a..b39684fbd1b 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-logo.service.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-logo.service.ts @@ -1,19 +1,19 @@ +import { HttpService } from '@nestjs/axios'; import { HttpException, Inject } from '@nestjs/common'; +import { EntityId } from '@shared/domain'; +import { Logger } from '@src/core/logger'; import { AxiosResponse } from 'axios'; import { lastValueFrom } from 'rxjs'; -import { HttpService } from '@nestjs/axios'; -import { Logger } from '@src/core/logger'; -import { EntityId } from '@shared/domain'; +import { IToolFeatures, ToolFeatures } from '../../tool-config'; import { ExternalTool } from '../domain'; +import { ExternalToolLogo } from '../domain/external-tool-logo'; import { ExternalToolLogoFetchedLoggable, + ExternalToolLogoFetchFailedLoggableException, ExternalToolLogoNotFoundLoggableException, ExternalToolLogoSizeExceededLoggableException, ExternalToolLogoWrongFileTypeLoggableException, - ExternalToolLogoFetchFailedLoggableException, } from '../loggable'; -import { IToolFeatures, ToolFeatures } from '../../tool-config'; -import { ExternalToolLogo } from '../domain/external-tool-logo'; import { ExternalToolService } from './external-tool.service'; const contentTypeDetector: Record = { @@ -95,7 +95,7 @@ export class ExternalToolLogoService { } async getExternalToolBinaryLogo(toolId: EntityId): Promise { - const tool: ExternalTool = await this.externalToolService.findExternalToolById(toolId); + const tool: ExternalTool = await this.externalToolService.findById(toolId); if (!tool.logo) { throw new ExternalToolLogoNotFoundLoggableException(toolId); diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.spec.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.spec.ts index 8f5f1607df6..42efcd6559a 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.spec.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.spec.ts @@ -4,10 +4,10 @@ import { ValidationError } from '@shared/common'; import { externalToolFactory } from '@shared/testing/factory/domainobject/tool/external-tool.factory'; import { IToolFeatures, ToolFeatures } from '../../tool-config'; import { ExternalTool } from '../domain'; +import { ExternalToolLogoService } from './external-tool-logo.service'; import { ExternalToolParameterValidationService } from './external-tool-parameter-validation.service'; import { ExternalToolValidationService } from './external-tool-validation.service'; import { ExternalToolService } from './external-tool.service'; -import { ExternalToolLogoService } from './external-tool-logo.service'; describe('ExternalToolValidationService', () => { let module: TestingModule; @@ -232,7 +232,7 @@ describe('ExternalToolValidationService', () => { .buildWithId(); externalOauthTool.id = 'toolId'; - externalToolService.findExternalToolById.mockResolvedValue(externalOauthTool); + externalToolService.findById.mockResolvedValue(externalOauthTool); return { externalOauthTool, @@ -266,7 +266,7 @@ describe('ExternalToolValidationService', () => { .withOauth2Config({ clientId: 'ClientId', clientSecret: 'secret' }) .buildWithId(); - externalToolService.findExternalToolById.mockResolvedValue(existingExternalOauthTool); + externalToolService.findById.mockResolvedValue(existingExternalOauthTool); const newExternalTool: ExternalTool = externalToolFactory.buildWithId(); @@ -296,7 +296,7 @@ describe('ExternalToolValidationService', () => { .withOauth2Config({ clientId: 'ClientId', clientSecret: 'secret' }) .buildWithId(); - externalToolService.findExternalToolById.mockResolvedValue(externalOauthTool); + externalToolService.findById.mockResolvedValue(externalOauthTool); return { externalOauthTool }; }; @@ -318,7 +318,7 @@ describe('ExternalToolValidationService', () => { const existingExternalOauthToolDOWithDifferentClientId: ExternalTool = externalToolFactory .withOauth2Config({ clientId: 'DifferentClientId', clientSecret: 'secret' }) .buildWithId(); - externalToolService.findExternalToolById.mockResolvedValue(existingExternalOauthToolDOWithDifferentClientId); + externalToolService.findById.mockResolvedValue(existingExternalOauthToolDOWithDifferentClientId); return { externalOauthTool, @@ -344,7 +344,7 @@ describe('ExternalToolValidationService', () => { const externalLtiToolDO: ExternalTool = externalToolFactory.withLti11Config().buildWithId(); externalLtiToolDO.id = 'toolId'; - externalToolService.findExternalToolById.mockResolvedValue(externalLtiToolDO); + externalToolService.findById.mockResolvedValue(externalLtiToolDO); return { externalLtiToolDO, diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.ts index 90b8307dc7e..434e7fac86e 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.ts @@ -2,9 +2,9 @@ import { Inject, Injectable } from '@nestjs/common'; import { ValidationError } from '@shared/common'; import { IToolFeatures, ToolFeatures } from '../../tool-config'; import { ExternalTool } from '../domain'; +import { ExternalToolLogoService } from './external-tool-logo.service'; import { ExternalToolParameterValidationService } from './external-tool-parameter-validation.service'; import { ExternalToolService } from './external-tool.service'; -import { ExternalToolLogoService } from './external-tool-logo.service'; @Injectable() export class ExternalToolValidationService { @@ -32,7 +32,7 @@ export class ExternalToolValidationService { await this.externalToolParameterValidationService.validateCommon(externalTool); - const loadedTool: ExternalTool = await this.externalToolService.findExternalToolById(toolId); + const loadedTool: ExternalTool = await this.externalToolService.findById(toolId); if ( ExternalTool.isOauth2Config(loadedTool.config) && externalTool.config && diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts b/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts index d2913e5401a..4db2a5be0b0 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool.service.spec.ts @@ -308,7 +308,7 @@ describe('ExternalToolService', () => { }); }); - describe('findExternalToolById', () => { + describe('findById', () => { describe('when external tool id is set', () => { const setup = () => { const { externalTool } = createTools(); @@ -320,7 +320,7 @@ describe('ExternalToolService', () => { it('should get domain object', async () => { const { externalTool } = setup(); - const result: ExternalTool = await service.findExternalToolById('toolId'); + const result: ExternalTool = await service.findById('toolId'); expect(result).toEqual(externalTool); }); @@ -340,7 +340,7 @@ describe('ExternalToolService', () => { it('should get domain object and add external oauth2 data', async () => { const { externalTool, oauth2ToolConfig } = setup(); - const result: ExternalTool = await service.findExternalToolById('toolId'); + const result: ExternalTool = await service.findById('toolId'); expect(result).toEqual({ ...externalTool, config: oauth2ToolConfig }); }); @@ -362,7 +362,7 @@ describe('ExternalToolService', () => { it('should throw UnprocessableEntityException ', async () => { const { externalTool } = setup(); - const func = () => service.findExternalToolById('toolId'); + const func = () => service.findById('toolId'); await expect(func()).rejects.toThrow(`Could not resolve oauth2Config of tool ${externalTool.name}.`); }); diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts index 2a53f8aae45..fcc1a7e2d5c 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool.service.ts @@ -75,7 +75,7 @@ export class ExternalToolService { return tools; } - async findExternalToolById(id: EntityId): Promise { + async findById(id: EntityId): Promise { const tool: ExternalTool = await this.externalToolRepo.findById(id); if (ExternalTool.isOauth2Config(tool.config)) { try { diff --git a/apps/server/src/modules/tool/external-tool/uc/external-tool-configuration.uc.spec.ts b/apps/server/src/modules/tool/external-tool/uc/external-tool-configuration.uc.spec.ts index 5490f0c546b..5327c8d4f80 100644 --- a/apps/server/src/modules/tool/external-tool/uc/external-tool-configuration.uc.spec.ts +++ b/apps/server/src/modules/tool/external-tool/uc/external-tool-configuration.uc.spec.ts @@ -10,16 +10,16 @@ import { schoolExternalToolFactory, setupEntities, } from '@shared/testing'; -import { AuthorizationContextBuilder } from '@src/modules/authorization'; +import { AuthorizationContextBuilder } from '@modules/authorization'; import { CustomParameterScope, ToolContextType } from '../../common/enum'; +import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; import { ContextExternalTool } from '../../context-external-tool/domain'; import { ContextExternalToolService } from '../../context-external-tool/service'; import { SchoolExternalTool } from '../../school-external-tool/domain'; import { SchoolExternalToolService } from '../../school-external-tool/service'; import { ExternalTool } from '../domain'; -import { ExternalToolLogoService, ExternalToolService, ExternalToolConfigurationService } from '../service'; +import { ExternalToolConfigurationService, ExternalToolLogoService, ExternalToolService } from '../service'; import { ExternalToolConfigurationUc } from './external-tool-configuration.uc'; -import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; describe('ExternalToolConfigurationUc', () => { let module: TestingModule; @@ -439,8 +439,8 @@ describe('ExternalToolConfigurationUc', () => { schoolExternalToolId ); - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValueOnce(schoolExternalTool); - externalToolService.findExternalToolById.mockResolvedValueOnce(externalTool); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); + externalToolService.findById.mockResolvedValueOnce(externalTool); return { externalTool, @@ -478,7 +478,7 @@ describe('ExternalToolConfigurationUc', () => { schoolExternalToolId ); - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValueOnce(schoolExternalTool); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); toolPermissionHelper.ensureSchoolPermissions.mockImplementation(() => { throw new UnauthorizedException(); }); @@ -512,8 +512,8 @@ describe('ExternalToolConfigurationUc', () => { schoolExternalToolId ); - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValueOnce(schoolExternalTool); - externalToolService.findExternalToolById.mockResolvedValueOnce(externalTool); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); + externalToolService.findById.mockResolvedValueOnce(externalTool); return { schoolExternalToolId, @@ -553,9 +553,9 @@ describe('ExternalToolConfigurationUc', () => { contextExternalToolId ); - contextExternalToolService.getContextExternalToolById.mockResolvedValueOnce(contextExternalTool); - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValueOnce(schoolExternalTool); - externalToolService.findExternalToolById.mockResolvedValueOnce(externalTool); + contextExternalToolService.findById.mockResolvedValueOnce(contextExternalTool); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); + externalToolService.findById.mockResolvedValueOnce(externalTool); return { externalTool, @@ -593,7 +593,7 @@ describe('ExternalToolConfigurationUc', () => { contextExternalToolId ); - contextExternalToolService.getContextExternalToolById.mockResolvedValueOnce(contextExternalTool); + contextExternalToolService.findById.mockResolvedValueOnce(contextExternalTool); toolPermissionHelper.ensureContextPermissions.mockImplementation(() => { throw new UnauthorizedException(); }); @@ -632,9 +632,9 @@ describe('ExternalToolConfigurationUc', () => { contextExternalToolId ); - contextExternalToolService.getContextExternalToolById.mockResolvedValueOnce(contextExternalTool); - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValueOnce(schoolExternalTool); - externalToolService.findExternalToolById.mockResolvedValueOnce(externalTool); + contextExternalToolService.findById.mockResolvedValueOnce(contextExternalTool); + schoolExternalToolService.findById.mockResolvedValueOnce(schoolExternalTool); + externalToolService.findById.mockResolvedValueOnce(externalTool); return { contextExternalToolId, diff --git a/apps/server/src/modules/tool/external-tool/uc/external-tool-configuration.uc.ts b/apps/server/src/modules/tool/external-tool/uc/external-tool-configuration.uc.ts index 9607beb84df..f0c1af811fb 100644 --- a/apps/server/src/modules/tool/external-tool/uc/external-tool-configuration.uc.ts +++ b/apps/server/src/modules/tool/external-tool/uc/external-tool-configuration.uc.ts @@ -2,16 +2,16 @@ import { forwardRef, Inject, Injectable } from '@nestjs/common'; import { NotFoundException } from '@nestjs/common/exceptions/not-found.exception'; import { EntityId, Permission } from '@shared/domain'; import { Page } from '@shared/domain/domainobject/page'; -import { AuthorizationContext, AuthorizationContextBuilder } from '@src/modules/authorization'; +import { AuthorizationContext, AuthorizationContextBuilder } from '@modules/authorization'; import { CustomParameterScope, ToolContextType } from '../../common/enum'; +import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; import { ContextExternalTool } from '../../context-external-tool/domain'; import { ContextExternalToolService } from '../../context-external-tool/service'; import { SchoolExternalTool } from '../../school-external-tool/domain'; import { SchoolExternalToolService } from '../../school-external-tool/service'; import { ExternalTool } from '../domain'; -import { ExternalToolLogoService, ExternalToolService, ExternalToolConfigurationService } from '../service'; +import { ExternalToolConfigurationService, ExternalToolLogoService, ExternalToolService } from '../service'; import { ContextExternalToolTemplateInfo } from './dto'; -import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; @Injectable() export class ExternalToolConfigurationUc { @@ -117,14 +117,12 @@ export class ExternalToolConfigurationUc { userId: EntityId, schoolExternalToolId: EntityId ): Promise { - const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.getSchoolExternalToolById( - schoolExternalToolId - ); + const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById(schoolExternalToolId); const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.SCHOOL_TOOL_ADMIN]); await this.toolPermissionHelper.ensureSchoolPermissions(userId, schoolExternalTool, context); - const externalTool: ExternalTool = await this.externalToolService.findExternalToolById(schoolExternalTool.toolId); + const externalTool: ExternalTool = await this.externalToolService.findById(schoolExternalTool.toolId); if (externalTool.isHidden) { throw new NotFoundException('Could not find the Tool Template'); @@ -139,18 +137,18 @@ export class ExternalToolConfigurationUc { userId: EntityId, contextExternalToolId: EntityId ): Promise { - const contextExternalTool: ContextExternalTool = await this.contextExternalToolService.getContextExternalToolById( + const contextExternalTool: ContextExternalTool = await this.contextExternalToolService.findById( contextExternalToolId ); - const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_ADMIN]); + const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_ADMIN]); await this.toolPermissionHelper.ensureContextPermissions(userId, contextExternalTool, context); - const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.getSchoolExternalToolById( + const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById( contextExternalTool.schoolToolRef.schoolToolId ); - const externalTool: ExternalTool = await this.externalToolService.findExternalToolById(schoolExternalTool.toolId); + const externalTool: ExternalTool = await this.externalToolService.findById(schoolExternalTool.toolId); if (externalTool.isHidden) { throw new NotFoundException('Could not find the Tool Template'); diff --git a/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.spec.ts b/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.spec.ts index d0b02f1e4f3..2d371f47c9e 100644 --- a/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.spec.ts +++ b/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.spec.ts @@ -8,8 +8,8 @@ import { externalToolFactory, oauth2ToolConfigFactory, } from '@shared/testing/factory/domainobject/tool/external-tool.factory'; -import { ICurrentUser } from '@src/modules/authentication'; -import { AuthorizationService } from '@src/modules/authorization'; +import { ICurrentUser } from '@modules/authentication'; +import { AuthorizationService } from '@modules/authorization'; import { ExternalToolSearchQuery } from '../../common/interface'; import { ExternalTool, Oauth2ToolConfig } from '../domain'; import { ExternalToolLogoService, ExternalToolService, ExternalToolValidationService } from '../service'; @@ -301,7 +301,7 @@ describe('ExternalToolUc', () => { it('should fetch a tool', async () => { const { currentUser } = setupAuthorization(); const { externalTool, toolId } = setup(); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + externalToolService.findById.mockResolvedValue(externalTool); const result: ExternalTool = await uc.getExternalTool(currentUser.userId, toolId); @@ -327,7 +327,7 @@ describe('ExternalToolUc', () => { }); externalToolService.updateExternalTool.mockResolvedValue(updatedExternalToolDO); - externalToolService.findExternalToolById.mockResolvedValue(new ExternalTool(externalToolDOtoUpdate)); + externalToolService.findById.mockResolvedValue(new ExternalTool(externalToolDOtoUpdate)); return { externalTool, diff --git a/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.ts b/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.ts index 240977b2b38..2cf49867103 100644 --- a/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.ts +++ b/apps/server/src/modules/tool/external-tool/uc/external-tool.uc.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { EntityId, IFindOptions, Page, Permission, User } from '@shared/domain'; -import { AuthorizationService } from '@src/modules/authorization'; +import { AuthorizationService } from '@modules/authorization'; import { ExternalToolSearchQuery } from '../../common/interface'; import { ExternalTool, ExternalToolConfig } from '../domain'; import { ExternalToolLogoService, ExternalToolService, ExternalToolValidationService } from '../service'; @@ -35,7 +35,7 @@ export class ExternalToolUc { await this.toolValidationService.validateUpdate(toolId, externalTool); - const loaded: ExternalTool = await this.externalToolService.findExternalToolById(toolId); + const loaded: ExternalTool = await this.externalToolService.findById(toolId); const configToUpdate: ExternalToolConfig = { ...loaded.config, ...externalTool.config }; const toUpdate: ExternalTool = new ExternalTool({ ...loaded, @@ -63,7 +63,7 @@ export class ExternalToolUc { async getExternalTool(userId: EntityId, toolId: EntityId): Promise { await this.ensurePermission(userId, Permission.TOOL_ADMIN); - const tool: ExternalTool = await this.externalToolService.findExternalToolById(toolId); + const tool: ExternalTool = await this.externalToolService.findById(toolId); return tool; } diff --git a/apps/server/src/modules/tool/external-tool/uc/index.ts b/apps/server/src/modules/tool/external-tool/uc/index.ts index 46f3a860080..0a61273b29b 100644 --- a/apps/server/src/modules/tool/external-tool/uc/index.ts +++ b/apps/server/src/modules/tool/external-tool/uc/index.ts @@ -1,4 +1,3 @@ export * from './dto'; export * from './external-tool.uc'; -export * from './tool-reference.uc'; export * from './external-tool-configuration.uc'; diff --git a/apps/server/src/modules/tool/external-tool/uc/tool-reference.uc.spec.ts b/apps/server/src/modules/tool/external-tool/uc/tool-reference.uc.spec.ts deleted file mode 100644 index e06c34e5e8b..00000000000 --- a/apps/server/src/modules/tool/external-tool/uc/tool-reference.uc.spec.ts +++ /dev/null @@ -1,234 +0,0 @@ -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { ForbiddenException } from '@nestjs/common'; -import { Test, TestingModule } from '@nestjs/testing'; -import { Permission } from '@shared/domain'; -import { contextExternalToolFactory, externalToolFactory, schoolExternalToolFactory } from '@shared/testing'; -import { AuthorizationContextBuilder } from '@src/modules/authorization'; -import { ToolReferenceUc } from './tool-reference.uc'; -import { ToolConfigurationStatus, ToolContextType } from '../../common/enum'; -import { CommonToolService } from '../../common/service'; -import { ContextExternalTool } from '../../context-external-tool/domain'; -import { ContextExternalToolService } from '../../context-external-tool/service'; -import { SchoolExternalTool } from '../../school-external-tool/domain'; -import { SchoolExternalToolService } from '../../school-external-tool/service'; -import { ExternalTool, ToolReference } from '../domain'; -import { ExternalToolLogoService, ExternalToolService } from '../service'; -import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; - -describe('ToolReferenceUc', () => { - let module: TestingModule; - let uc: ToolReferenceUc; - - let externalToolService: DeepMocked; - let schoolExternalToolService: DeepMocked; - let contextExternalToolService: DeepMocked; - let toolPermissionHelper: DeepMocked; - let commonToolService: DeepMocked; - let logoService: DeepMocked; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - ToolReferenceUc, - { - provide: ExternalToolService, - useValue: createMock(), - }, - { - provide: SchoolExternalToolService, - useValue: createMock(), - }, - { - provide: ContextExternalToolService, - useValue: createMock(), - }, - { - provide: CommonToolService, - useValue: createMock(), - }, - { - provide: ExternalToolLogoService, - useValue: createMock(), - }, - { - provide: ToolPermissionHelper, - useValue: createMock(), - }, - ], - }).compile(); - - uc = module.get(ToolReferenceUc); - - externalToolService = module.get(ExternalToolService); - schoolExternalToolService = module.get(SchoolExternalToolService); - contextExternalToolService = module.get(ContextExternalToolService); - toolPermissionHelper = module.get(ToolPermissionHelper); - commonToolService = module.get(CommonToolService); - logoService = module.get(ExternalToolLogoService); - }); - - afterAll(async () => { - await module.close(); - }); - - describe('getToolReferences', () => { - describe('when called with a context type and id', () => { - const setup = () => { - const userId = 'userId'; - - const externalTool: ExternalTool = externalToolFactory.withBase64Logo().buildWithId(); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ - toolId: externalTool.id, - }); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory - .withSchoolExternalToolRef('schoolToolId', 'schoolId') - .buildWithId(); - - const contextType: ToolContextType = ToolContextType.COURSE; - const contextId = 'contextId'; - - contextExternalToolService.findAllByContext.mockResolvedValueOnce([contextExternalTool]); - toolPermissionHelper.ensureContextPermissions.mockResolvedValueOnce(); - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValueOnce(schoolExternalTool); - externalToolService.findExternalToolById.mockResolvedValueOnce(externalTool); - commonToolService.determineToolConfigurationStatus.mockReturnValueOnce(ToolConfigurationStatus.LATEST); - - return { - userId, - contextType, - contextId, - contextExternalTool, - schoolExternalTool, - externalTool, - externalToolId: externalTool.id as string, - }; - }; - - it('should call toolPermissionHelper.ensureContextPermissions', async () => { - const { userId, contextType, contextId, contextExternalTool } = setup(); - - await uc.getToolReferences(userId, contextType, contextId, '/v3/tools/external-tools/{id}/logo'); - - expect(toolPermissionHelper.ensureContextPermissions).toHaveBeenCalledWith( - userId, - contextExternalTool, - AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]) - ); - }); - - it('should call contextExternalToolService.findAllByContext', async () => { - const { userId, contextType, contextId } = setup(); - - await uc.getToolReferences(userId, contextType, contextId, '/v3/tools/external-tools/{id}/logo'); - - expect(contextExternalToolService.findAllByContext).toHaveBeenCalledWith({ - type: contextType, - id: contextId, - }); - }); - - it('should call schoolExternalToolService.findByExternalToolId', async () => { - const { userId, contextType, contextId, contextExternalTool } = setup(); - - await uc.getToolReferences(userId, contextType, contextId, '/v3/tools/external-tools/{id}/logo'); - - expect(schoolExternalToolService.getSchoolExternalToolById).toHaveBeenCalledWith( - contextExternalTool.schoolToolRef.schoolToolId - ); - }); - - it('should call externalToolService.findById', async () => { - const { userId, contextType, contextId, externalToolId } = setup(); - - await uc.getToolReferences(userId, contextType, contextId, '/v3/tools/external-tools/{id}/logo'); - - expect(externalToolService.findExternalToolById).toHaveBeenCalledWith(externalToolId); - }); - - it('should call commonToolService.determineToolConfigurationStatus', async () => { - const { userId, contextType, contextId, contextExternalTool, schoolExternalTool, externalTool } = setup(); - - await uc.getToolReferences(userId, contextType, contextId, '/v3/tools/external-tools/{id}/logo'); - - expect(commonToolService.determineToolConfigurationStatus).toHaveBeenCalledWith( - externalTool, - schoolExternalTool, - contextExternalTool - ); - }); - - it('should call externalToolLogoService.buildLogoUrl', async () => { - const { userId, contextType, contextId, externalTool } = setup(); - - await uc.getToolReferences(userId, contextType, contextId, '/v3/tools/external-tools/{id}/logo'); - - expect(logoService.buildLogoUrl).toHaveBeenCalledWith('/v3/tools/external-tools/{id}/logo', externalTool); - }); - - it('should return a list of tool references', async () => { - const { userId, contextType, contextId, contextExternalTool, externalTool } = setup(); - - const result: ToolReference[] = await uc.getToolReferences( - userId, - contextType, - contextId, - '/v3/tools/external-tools/{id}/logo' - ); - - expect(result).toEqual([ - { - logoUrl: `${Configuration.get('PUBLIC_BACKEND_URL') as string}/v3/tools/external-tools/${ - externalTool.id as string - }/logo`, - openInNewTab: externalTool.openNewTab, - contextToolId: contextExternalTool.id as string, - displayName: contextExternalTool.displayName as string, - status: ToolConfigurationStatus.LATEST, - }, - ]); - }); - }); - - describe('when user does not have permission to a tool', () => { - const setup = () => { - const userId = 'userId'; - - const externalTool: ExternalTool = externalToolFactory.buildWithId(); - const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ - toolId: externalTool.id, - }); - const contextExternalTool: ContextExternalTool = contextExternalToolFactory - .withSchoolExternalToolRef('schoolToolId', 'schoolId') - .buildWithId(); - - const contextType: ToolContextType = ToolContextType.COURSE; - const contextId = 'contextId'; - - contextExternalToolService.findAllByContext.mockResolvedValueOnce([contextExternalTool]); - toolPermissionHelper.ensureContextPermissions.mockRejectedValueOnce(new ForbiddenException()); - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValueOnce(schoolExternalTool); - externalToolService.findExternalToolById.mockResolvedValueOnce(externalTool); - - return { - userId, - contextType, - contextId, - }; - }; - - it('should filter out tool references if a ForbiddenException is thrown', async () => { - const { userId, contextType, contextId } = setup(); - - const result: ToolReference[] = await uc.getToolReferences( - userId, - contextType, - contextId, - '/v3/tools/external-tools/{id}/logo' - ); - - expect(result).toEqual([]); - }); - }); - }); -}); diff --git a/apps/server/src/modules/tool/external-tool/uc/tool-reference.uc.ts b/apps/server/src/modules/tool/external-tool/uc/tool-reference.uc.ts deleted file mode 100644 index 5ddf0e467c6..00000000000 --- a/apps/server/src/modules/tool/external-tool/uc/tool-reference.uc.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { ForbiddenException, Injectable } from '@nestjs/common'; -import { EntityId, Permission } from '@shared/domain'; -import { AuthorizationContext, AuthorizationContextBuilder } from '@src/modules/authorization'; -import { ExternalTool, ToolReference } from '../domain'; -import { ToolConfigurationStatus, ToolContextType } from '../../common/enum'; -import { CommonToolService } from '../../common/service'; -import { ContextExternalTool, ContextRef } from '../../context-external-tool/domain'; -import { ContextExternalToolService } from '../../context-external-tool/service'; -import { SchoolExternalTool } from '../../school-external-tool/domain'; -import { SchoolExternalToolService } from '../../school-external-tool/service'; -import { ToolReferenceMapper } from '../mapper/tool-reference.mapper'; -import { ExternalToolLogoService, ExternalToolService } from '../service'; -import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; - -@Injectable() -export class ToolReferenceUc { - constructor( - private readonly externalToolService: ExternalToolService, - private readonly schoolExternalToolService: SchoolExternalToolService, - private readonly contextExternalToolService: ContextExternalToolService, - private readonly toolPermissionHelper: ToolPermissionHelper, - private readonly commonToolService: CommonToolService, - private readonly externalToolLogoService: ExternalToolLogoService - ) {} - - async getToolReferences( - userId: EntityId, - contextType: ToolContextType, - contextId: string, - logoUrlTemplate: string - ): Promise { - const contextRef = new ContextRef({ type: contextType, id: contextId }); - - const contextExternalTools: ContextExternalTool[] = await this.contextExternalToolService.findAllByContext( - contextRef - ); - - const toolReferencesPromises: Promise[] = contextExternalTools.map( - (contextExternalTool: ContextExternalTool) => - this.buildToolReference(userId, contextExternalTool, logoUrlTemplate) - ); - - const toolReferencesWithNull: (ToolReference | null)[] = await Promise.all(toolReferencesPromises); - const filteredToolReferences: ToolReference[] = toolReferencesWithNull.filter( - (toolReference: ToolReference | null): toolReference is ToolReference => toolReference !== null - ); - - return filteredToolReferences; - } - - private async buildToolReference( - userId: EntityId, - contextExternalTool: ContextExternalTool, - logoUrlTemplate: string - ): Promise { - try { - await this.ensureToolPermissions(userId, contextExternalTool); - } catch (e: unknown) { - if (e instanceof ForbiddenException) { - return null; - } - } - - const schoolExternalTool: SchoolExternalTool = await this.fetchSchoolExternalTool(contextExternalTool); - const externalTool: ExternalTool = await this.fetchExternalTool(schoolExternalTool); - - const status: ToolConfigurationStatus = this.commonToolService.determineToolConfigurationStatus( - externalTool, - schoolExternalTool, - contextExternalTool - ); - - const toolReference: ToolReference = ToolReferenceMapper.mapToToolReference( - externalTool, - contextExternalTool, - status - ); - toolReference.logoUrl = this.externalToolLogoService.buildLogoUrl(logoUrlTemplate, externalTool); - - return toolReference; - } - - private async ensureToolPermissions(userId: EntityId, contextExternalTool: ContextExternalTool): Promise { - const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]); - - const promise: Promise = this.toolPermissionHelper.ensureContextPermissions( - userId, - contextExternalTool, - context - ); - - return promise; - } - - private async fetchSchoolExternalTool(contextExternalTool: ContextExternalTool): Promise { - return this.schoolExternalToolService.getSchoolExternalToolById(contextExternalTool.schoolToolRef.schoolToolId); - } - - private async fetchExternalTool(schoolExternalTool: SchoolExternalTool): Promise { - return this.externalToolService.findExternalToolById(schoolExternalTool.toolId); - } -} diff --git a/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts b/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts index 89bbe0c2cb7..3512e66038e 100644 --- a/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/controller/api-test/tool-school.api.spec.ts @@ -11,7 +11,10 @@ import { UserAndAccountTestFactory, userFactory, } from '@shared/testing'; -import { ServerTestModule } from '@src/modules/server'; +import { ServerTestModule } from '@modules/server'; +import { ToolConfigurationStatusResponse } from '../../../context-external-tool/controller/dto/tool-configuration-status.response'; +import { ExternalToolEntity } from '../../../external-tool/entity'; +import { SchoolExternalToolEntity } from '../../entity'; import { CustomParameterEntryParam, SchoolExternalToolPostParams, @@ -19,9 +22,6 @@ import { SchoolExternalToolSearchListResponse, SchoolExternalToolSearchParams, } from '../dto'; -import { ToolConfigurationStatusResponse } from '../../../external-tool/controller/dto'; -import { SchoolExternalToolEntity } from '../../entity'; -import { ExternalToolEntity } from '../../../external-tool/entity'; describe('ToolSchoolController (API)', () => { let app: INestApplication; diff --git a/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool.response.ts b/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool.response.ts index 62ad203fb02..32dd35f10bd 100644 --- a/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool.response.ts +++ b/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool.response.ts @@ -1,6 +1,6 @@ import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ToolConfigurationStatusResponse } from '../../../context-external-tool/controller/dto/tool-configuration-status.response'; import { CustomParameterEntryResponse } from './custom-parameter-entry.response'; -import { ToolConfigurationStatusResponse } from '../../../external-tool/controller/dto'; export class SchoolExternalToolResponse { @ApiProperty() diff --git a/apps/server/src/modules/tool/school-external-tool/controller/tool-school.controller.ts b/apps/server/src/modules/tool/school-external-tool/controller/tool-school.controller.ts index c34b669f75c..79d6f789443 100644 --- a/apps/server/src/modules/tool/school-external-tool/controller/tool-school.controller.ts +++ b/apps/server/src/modules/tool/school-external-tool/controller/tool-school.controller.ts @@ -12,9 +12,8 @@ import { } from '@nestjs/swagger'; import { Body, Controller, Delete, Get, Param, Post, Query, Put, HttpCode, HttpStatus } from '@nestjs/common'; import { ValidationError } from '@shared/common'; -import { ICurrentUser } from '@src/modules/authentication'; import { LegacyLogger } from '@src/core/logger'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { SchoolExternalToolRequestMapper, SchoolExternalToolResponseMapper } from '../mapper'; import { ExternalToolSearchListResponse } from '../../external-tool/controller/dto'; import { diff --git a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.spec.ts b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.spec.ts index 916445770e3..ca2296e6df7 100644 --- a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.spec.ts @@ -1,5 +1,5 @@ import { schoolExternalToolFactory } from '@shared/testing/factory'; -import { ToolConfigurationStatusResponse } from '../../external-tool/controller/dto'; +import { ToolConfigurationStatusResponse } from '../../context-external-tool/controller/dto'; import { SchoolExternalToolResponse, SchoolExternalToolSearchListResponse } from '../controller/dto'; import { SchoolExternalTool } from '../domain'; import { SchoolExternalToolResponseMapper } from './school-external-tool-response.mapper'; diff --git a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.ts b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.ts index 10ee706dd81..7388b1a6a41 100644 --- a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.ts +++ b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.ts @@ -1,7 +1,7 @@ import { Injectable } from '@nestjs/common'; import { CustomParameterEntry } from '../../common/domain'; -import { ToolConfigurationStatus } from '../../common/enum'; -import { ToolConfigurationStatusResponse } from '../../external-tool/controller/dto'; +import { ToolStatusResponseMapper } from '../../common/mapper/tool-status-response.mapper'; +import { ToolConfigurationStatusResponse } from '../../context-external-tool/controller/dto'; import { CustomParameterEntryResponse, SchoolExternalToolResponse, @@ -9,12 +9,6 @@ import { } from '../controller/dto'; import { SchoolExternalTool } from '../domain'; -export const statusMapping: Record = { - [ToolConfigurationStatus.LATEST]: ToolConfigurationStatusResponse.LATEST, - [ToolConfigurationStatus.OUTDATED]: ToolConfigurationStatusResponse.OUTDATED, - [ToolConfigurationStatus.UNKNOWN]: ToolConfigurationStatusResponse.UNKNOWN, -}; - @Injectable() export class SchoolExternalToolResponseMapper { mapToSearchListResponse(externalTools: SchoolExternalTool[]): SchoolExternalToolSearchListResponse { @@ -33,7 +27,7 @@ export class SchoolExternalToolResponseMapper { parameters: this.mapToCustomParameterEntryResponse(schoolExternalTool.parameters), toolVersion: schoolExternalTool.toolVersion, status: schoolExternalTool.status - ? statusMapping[schoolExternalTool.status] + ? ToolStatusResponseMapper.mapToResponse(schoolExternalTool.status) : ToolConfigurationStatusResponse.UNKNOWN, }; } diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.spec.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.spec.ts index 7ca001675b0..e43bdeb42e0 100644 --- a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.spec.ts @@ -51,7 +51,7 @@ describe('SchoolExternalToolValidationService', () => { ...externalToolFactory.buildWithId(), ...externalToolDoMock, }); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + externalToolService.findById.mockResolvedValue(externalTool); const schoolExternalToolId = schoolExternalTool.id as string; return { schoolExternalTool, @@ -66,7 +66,7 @@ describe('SchoolExternalToolValidationService', () => { await service.validate(schoolExternalTool); - expect(externalToolService.findExternalToolById).toHaveBeenCalledWith(schoolExternalTool.toolId); + expect(externalToolService.findById).toHaveBeenCalledWith(schoolExternalTool.toolId); }); it('should call commonToolValidationService.checkForDuplicateParameters', async () => { diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.ts index 8cc50097d5f..315d738ca64 100644 --- a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.ts +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool-validation.service.ts @@ -15,9 +15,7 @@ export class SchoolExternalToolValidationService { async validate(schoolExternalTool: SchoolExternalTool): Promise { this.commonToolValidationService.checkForDuplicateParameters(schoolExternalTool); - const loadedExternalTool: ExternalTool = await this.externalToolService.findExternalToolById( - schoolExternalTool.toolId - ); + const loadedExternalTool: ExternalTool = await this.externalToolService.findById(schoolExternalTool.toolId); this.checkVersionMatch(schoolExternalTool.toolVersion, loadedExternalTool.version); diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.spec.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.spec.ts index 7c4031ef0b7..52f9b0a4c02 100644 --- a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.spec.ts @@ -3,11 +3,11 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SchoolExternalToolRepo } from '@shared/repo'; import { externalToolFactory } from '@shared/testing/factory/domainobject/tool/external-tool.factory'; import { schoolExternalToolFactory } from '@shared/testing/factory/domainobject/tool/school-external-tool.factory'; -import { ExternalToolService } from '../../external-tool/service'; -import { SchoolExternalToolService } from './school-external-tool.service'; +import { ToolConfigurationStatus } from '../../common/enum'; import { ExternalTool } from '../../external-tool/domain'; +import { ExternalToolService } from '../../external-tool/service'; import { SchoolExternalTool } from '../domain'; -import { ToolConfigurationStatus } from '../../common/enum'; +import { SchoolExternalToolService } from './school-external-tool.service'; describe('SchoolExternalToolService', () => { let module: TestingModule; @@ -77,7 +77,7 @@ describe('SchoolExternalToolService', () => { await service.findSchoolExternalTools(schoolExternalTool); - expect(externalToolService.findExternalToolById).toHaveBeenCalledWith(schoolExternalTool.toolId); + expect(externalToolService.findById).toHaveBeenCalledWith(schoolExternalTool.toolId); }); describe('when determine status', () => { @@ -86,7 +86,7 @@ describe('SchoolExternalToolService', () => { const { schoolExternalTool, externalTool } = setup(); externalTool.version = 1337; schoolExternalToolRepo.find.mockResolvedValue([schoolExternalTool]); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + externalToolService.findById.mockResolvedValue(externalTool); const schoolExternalToolDOs: SchoolExternalTool[] = await service.findSchoolExternalTools(schoolExternalTool); @@ -100,7 +100,7 @@ describe('SchoolExternalToolService', () => { schoolExternalTool.toolVersion = 1; externalTool.version = 0; schoolExternalToolRepo.find.mockResolvedValue([schoolExternalTool]); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + externalToolService.findById.mockResolvedValue(externalTool); const schoolExternalToolDOs: SchoolExternalTool[] = await service.findSchoolExternalTools(schoolExternalTool); @@ -114,7 +114,7 @@ describe('SchoolExternalToolService', () => { schoolExternalTool.toolVersion = 1; externalTool.version = 1; schoolExternalToolRepo.find.mockResolvedValue([schoolExternalTool]); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + externalToolService.findById.mockResolvedValue(externalTool); const schoolExternalToolDOs: SchoolExternalTool[] = await service.findSchoolExternalTools(schoolExternalTool); @@ -136,12 +136,12 @@ describe('SchoolExternalToolService', () => { }); }); - describe('getSchoolExternalToolById', () => { + describe('findById', () => { describe('when schoolExternalToolId is given', () => { it('should call schoolExternalToolRepo.findById', async () => { const { schoolExternalToolId } = setup(); - await service.getSchoolExternalToolById(schoolExternalToolId); + await service.findById(schoolExternalToolId); expect(schoolExternalToolRepo.findById).toHaveBeenCalledWith(schoolExternalToolId); }); @@ -163,7 +163,7 @@ describe('SchoolExternalToolService', () => { await service.saveSchoolExternalTool(schoolExternalTool); - expect(externalToolService.findExternalToolById).toHaveBeenCalledWith(schoolExternalTool.toolId); + expect(externalToolService.findById).toHaveBeenCalledWith(schoolExternalTool.toolId); }); }); }); diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.ts index 9ee30d70db6..2f011560f6a 100644 --- a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.ts +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.ts @@ -1,11 +1,11 @@ import { Injectable } from '@nestjs/common'; -import { SchoolExternalToolRepo } from '@shared/repo'; import { EntityId } from '@shared/domain'; -import { SchoolExternalToolQuery } from '../uc/dto/school-external-tool.types'; +import { SchoolExternalToolRepo } from '@shared/repo'; +import { ToolConfigurationStatus } from '../../common/enum'; +import { ExternalTool } from '../../external-tool/domain'; import { ExternalToolService } from '../../external-tool/service'; import { SchoolExternalTool } from '../domain'; -import { ExternalTool } from '../../external-tool/domain'; -import { ToolConfigurationStatus } from '../../common/enum'; +import { SchoolExternalToolQuery } from '../uc/dto/school-external-tool.types'; @Injectable() export class SchoolExternalToolService { @@ -14,7 +14,7 @@ export class SchoolExternalToolService { private readonly externalToolService: ExternalToolService ) {} - async getSchoolExternalToolById(schoolExternalToolId: EntityId): Promise { + async findById(schoolExternalToolId: EntityId): Promise { const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolRepo.findById(schoolExternalToolId); return schoolExternalTool; } @@ -38,7 +38,7 @@ export class SchoolExternalToolService { } private async enrichDataFromExternalTool(tool: SchoolExternalTool): Promise { - const externalTool: ExternalTool = await this.externalToolService.findExternalToolById(tool.toolId); + const externalTool: ExternalTool = await this.externalToolService.findById(tool.toolId); const status: ToolConfigurationStatus = this.determineStatus(tool, externalTool); const schoolExternalTool: SchoolExternalTool = new SchoolExternalTool({ ...tool, diff --git a/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.spec.ts b/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.spec.ts index c0daab13cff..377a5d24a38 100644 --- a/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.spec.ts @@ -2,13 +2,13 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { EntityId, Permission, User } from '@shared/domain'; import { schoolExternalToolFactory, setupEntities, userFactory } from '@shared/testing'; -import { AuthorizationContextBuilder } from '@src/modules/authorization'; -import { SchoolExternalToolUc } from './school-external-tool.uc'; -import { SchoolExternalToolService, SchoolExternalToolValidationService } from '../service'; +import { AuthorizationContextBuilder } from '@modules/authorization'; +import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; import { ContextExternalToolService } from '../../context-external-tool/service'; -import { SchoolExternalToolQueryInput } from './dto/school-external-tool.types'; import { SchoolExternalTool } from '../domain'; -import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; +import { SchoolExternalToolService, SchoolExternalToolValidationService } from '../service'; +import { SchoolExternalToolQueryInput } from './dto/school-external-tool.types'; +import { SchoolExternalToolUc } from './school-external-tool.uc'; describe('SchoolExternalToolUc', () => { let module: TestingModule; @@ -259,7 +259,7 @@ describe('SchoolExternalToolUc', () => { const tool: SchoolExternalTool = schoolExternalToolFactory.buildWithId(); const user: User = userFactory.buildWithId(); - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValue(tool); + schoolExternalToolService.findById.mockResolvedValue(tool); return { user, @@ -285,7 +285,7 @@ describe('SchoolExternalToolUc', () => { const setup = () => { const tool: SchoolExternalTool = schoolExternalToolFactory.buildWithId(); const user: User = userFactory.buildWithId(); - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValue(tool); + schoolExternalToolService.findById.mockResolvedValue(tool); return { user, diff --git a/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.ts b/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.ts index 63067c234d7..d7adf3f4937 100644 --- a/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.ts +++ b/apps/server/src/modules/tool/school-external-tool/uc/school-external-tool.uc.ts @@ -1,11 +1,11 @@ import { Injectable } from '@nestjs/common'; import { EntityId, Permission } from '@shared/domain'; -import { AuthorizationContext, AuthorizationContextBuilder } from '@src/modules/authorization'; -import { SchoolExternalToolService, SchoolExternalToolValidationService } from '../service'; +import { AuthorizationContext, AuthorizationContextBuilder } from '@modules/authorization'; +import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; import { ContextExternalToolService } from '../../context-external-tool/service'; -import { SchoolExternalToolDto, SchoolExternalToolQueryInput } from './dto/school-external-tool.types'; import { SchoolExternalTool } from '../domain'; -import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; +import { SchoolExternalToolService, SchoolExternalToolValidationService } from '../service'; +import { SchoolExternalToolDto, SchoolExternalToolQueryInput } from './dto/school-external-tool.types'; @Injectable() export class SchoolExternalToolUc { @@ -57,9 +57,7 @@ export class SchoolExternalToolUc { } async deleteSchoolExternalTool(userId: EntityId, schoolExternalToolId: EntityId): Promise { - const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.getSchoolExternalToolById( - schoolExternalToolId - ); + const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById(schoolExternalToolId); const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.SCHOOL_TOOL_ADMIN]); await this.toolPermissionHelper.ensureSchoolPermissions(userId, schoolExternalTool, context); @@ -71,9 +69,7 @@ export class SchoolExternalToolUc { } async getSchoolExternalTool(userId: EntityId, schoolExternalToolId: EntityId): Promise { - const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.getSchoolExternalToolById( - schoolExternalToolId - ); + const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById(schoolExternalToolId); const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.SCHOOL_TOOL_ADMIN]); await this.toolPermissionHelper.ensureSchoolPermissions(userId, schoolExternalTool, context); diff --git a/apps/server/src/modules/tool/tool-api.module.ts b/apps/server/src/modules/tool/tool-api.module.ts index fe775e01fd3..b8d12a16006 100644 --- a/apps/server/src/modules/tool/tool-api.module.ts +++ b/apps/server/src/modules/tool/tool-api.module.ts @@ -1,14 +1,20 @@ +import { AuthorizationModule } from '@modules/authorization'; +import { LegacySchoolModule } from '@modules/legacy-school'; +import { UserModule } from '@modules/user'; import { Module } from '@nestjs/common'; import { LtiToolRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; -import { AuthorizationModule } from '@src/modules/authorization'; -import { LegacySchoolModule } from '@src/modules/legacy-school'; -import { UserModule } from '@src/modules/user'; +import { BoardModule } from '../board'; +import { LearnroomModule } from '../learnroom'; +import { CommonToolModule } from './common'; +import { ToolPermissionHelper } from './common/uc/tool-permission-helper'; import { ToolContextController } from './context-external-tool/controller'; -import { ContextExternalToolUc } from './context-external-tool/uc'; +import { ToolReferenceController } from './context-external-tool/controller/tool-reference.controller'; +import { ContextExternalToolUc, ToolReferenceUc } from './context-external-tool/uc'; import { ToolConfigurationController, ToolController } from './external-tool/controller'; import { ExternalToolRequestMapper, ExternalToolResponseMapper } from './external-tool/mapper'; -import { ExternalToolConfigurationUc, ExternalToolUc, ToolReferenceUc } from './external-tool/uc'; +import { ExternalToolConfigurationService } from './external-tool/service'; +import { ExternalToolConfigurationUc, ExternalToolUc } from './external-tool/uc'; import { ToolSchoolController } from './school-external-tool/controller'; import { SchoolExternalToolRequestMapper, SchoolExternalToolResponseMapper } from './school-external-tool/mapper'; import { SchoolExternalToolUc } from './school-external-tool/uc'; @@ -16,8 +22,6 @@ import { ToolConfigModule } from './tool-config.module'; import { ToolLaunchController } from './tool-launch/controller/tool-launch.controller'; import { ToolLaunchUc } from './tool-launch/uc'; import { ToolModule } from './tool.module'; -import { ExternalToolConfigurationService } from './external-tool/service'; -import { CommonToolModule } from './common'; @Module({ imports: [ @@ -28,12 +32,15 @@ import { CommonToolModule } from './common'; LoggerModule, LegacySchoolModule, ToolConfigModule, + LearnroomModule, + BoardModule, ], controllers: [ ToolLaunchController, ToolConfigurationController, ToolSchoolController, ToolContextController, + ToolReferenceController, ToolController, ], providers: [ @@ -49,6 +56,7 @@ import { CommonToolModule } from './common'; ContextExternalToolUc, ToolLaunchUc, ToolReferenceUc, + ToolPermissionHelper, ], }) export class ToolApiModule {} diff --git a/apps/server/src/modules/tool/tool-launch/controller/api-test/tool-launch.controller.api.spec.ts b/apps/server/src/modules/tool/tool-launch/controller/api-test/tool-launch.controller.api.spec.ts index 33512062711..04defeeb0dc 100644 --- a/apps/server/src/modules/tool/tool-launch/controller/api-test/tool-launch.controller.api.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/controller/api-test/tool-launch.controller.api.spec.ts @@ -13,7 +13,7 @@ import { TestApiClient, UserAndAccountTestFactory, } from '@shared/testing'; -import { ServerTestModule } from '@src/modules/server'; +import { ServerTestModule } from '@modules/server'; import { Response } from 'supertest'; import { SchoolExternalToolEntity } from '../../../school-external-tool/entity'; import { LaunchRequestMethod } from '../../types'; diff --git a/apps/server/src/modules/tool/tool-launch/controller/tool-launch.controller.ts b/apps/server/src/modules/tool/tool-launch/controller/tool-launch.controller.ts index 957ec92de92..12424b294ed 100644 --- a/apps/server/src/modules/tool/tool-launch/controller/tool-launch.controller.ts +++ b/apps/server/src/modules/tool/tool-launch/controller/tool-launch.controller.ts @@ -7,8 +7,7 @@ import { ApiTags, ApiUnauthorizedResponse, } from '@nestjs/swagger'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { ToolLaunchUc } from '../uc'; import { ToolLaunchParams, ToolLaunchRequestResponse } from './dto'; import { ToolLaunchMapper } from '../mapper'; 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 7dba13fd2f5..7ee237b3e93 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 @@ -12,8 +12,8 @@ import { schoolExternalToolFactory, setupEntities, } from '@shared/testing'; -import { CourseService } from '@src/modules/learnroom/service'; -import { LegacySchoolService } from '@src/modules/legacy-school'; +import { CourseService } from '@modules/learnroom/service'; +import { LegacySchoolService } from '@modules/legacy-school'; import { CustomParameterEntry } from '../../../common/domain'; import { CustomParameterLocation, @@ -133,18 +133,6 @@ describe('AbstractLaunchStrategy', () => { name: 'autoSchoolIdParam', type: CustomParameterType.AUTO_SCHOOLID, }); - const autoCourseIdCustomParameter = customParameterFactory.build({ - scope: CustomParameterScope.GLOBAL, - location: CustomParameterLocation.BODY, - name: 'autoCourseIdParam', - type: CustomParameterType.AUTO_CONTEXTID, - }); - const autoCourseNameCustomParameter = customParameterFactory.build({ - scope: CustomParameterScope.GLOBAL, - location: CustomParameterLocation.BODY, - name: 'autoCourseNameParam', - type: CustomParameterType.AUTO_CONTEXTNAME, - }); const autoSchoolNumberCustomParameter = customParameterFactory.build({ scope: CustomParameterScope.GLOBAL, location: CustomParameterLocation.BODY, @@ -158,8 +146,6 @@ describe('AbstractLaunchStrategy', () => { schoolCustomParameter, contextCustomParameter, autoSchoolIdCustomParameter, - autoCourseIdCustomParameter, - autoCourseNameCustomParameter, autoSchoolNumberCustomParameter, ], }); @@ -191,15 +177,7 @@ describe('AbstractLaunchStrategy', () => { schoolId ); - const course: Course = courseFactory.buildWithId( - { - name: 'testName', - }, - contextExternalTool.contextRef.id - ); - schoolService.getSchoolById.mockResolvedValue(school); - courseService.findById.mockResolvedValue(course); const sortFn = (a: PropertyData, b: PropertyData) => { if (a.name < b.name) { @@ -215,15 +193,12 @@ describe('AbstractLaunchStrategy', () => { globalCustomParameter, schoolCustomParameter, autoSchoolIdCustomParameter, - autoCourseIdCustomParameter, - autoCourseNameCustomParameter, autoSchoolNumberCustomParameter, schoolParameterEntry, contextParameterEntry, externalTool, schoolExternalTool, contextExternalTool, - course, school, sortFn, }; @@ -235,14 +210,11 @@ describe('AbstractLaunchStrategy', () => { schoolCustomParameter, contextParameterEntry, autoSchoolIdCustomParameter, - autoCourseIdCustomParameter, - autoCourseNameCustomParameter, autoSchoolNumberCustomParameter, schoolParameterEntry, externalTool, schoolExternalTool, contextExternalTool, - course, school, sortFn, } = setup(); @@ -280,18 +252,131 @@ describe('AbstractLaunchStrategy', () => { location: PropertyLocation.BODY, }, { - name: autoCourseIdCustomParameter.name, - value: course.id, + name: autoSchoolNumberCustomParameter.name, + value: school.officialSchoolNumber as string, location: PropertyLocation.BODY, }, + { + name: concreteConfigParameter.name, + value: concreteConfigParameter.value, + location: concreteConfigParameter.location, + }, + ].sort(sortFn), + }); + }); + }); + + describe('when launching with context name parameter for the context "course"', () => { + const setup = () => { + const autoCourseNameCustomParameter = customParameterFactory.build({ + scope: CustomParameterScope.GLOBAL, + location: CustomParameterLocation.BODY, + name: 'autoCourseNameParam', + type: CustomParameterType.AUTO_CONTEXTNAME, + }); + + const externalTool: ExternalTool = externalToolFactory.build({ + parameters: [autoCourseNameCustomParameter], + }); + + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build({ + contextRef: { + type: ToolContextType.COURSE, + }, + }); + + const course: Course = courseFactory.buildWithId( + { + name: 'testName', + }, + contextExternalTool.contextRef.id + ); + + courseService.findById.mockResolvedValue(course); + + return { + autoCourseNameCustomParameter, + externalTool, + schoolExternalTool, + contextExternalTool, + course, + }; + }; + + it('should return ToolLaunchData with the course name as parameter value', async () => { + const { externalTool, schoolExternalTool, contextExternalTool, autoCourseNameCustomParameter, course } = + setup(); + + const result: ToolLaunchData = await launchStrategy.createLaunchData('userId', { + externalTool, + schoolExternalTool, + contextExternalTool, + }); + + expect(result).toEqual({ + baseUrl: externalTool.config.baseUrl, + type: ToolLaunchDataType.BASIC, + openNewTab: false, + properties: [ { name: autoCourseNameCustomParameter.name, value: course.name, location: PropertyLocation.BODY, }, { - name: autoSchoolNumberCustomParameter.name, - value: school.officialSchoolNumber as string, + name: concreteConfigParameter.name, + value: concreteConfigParameter.value, + location: concreteConfigParameter.location, + }, + ], + }); + }); + }); + + describe('when launching with context id parameter', () => { + const setup = () => { + const autoContextIdCustomParameter = customParameterFactory.build({ + scope: CustomParameterScope.GLOBAL, + location: CustomParameterLocation.BODY, + name: 'autoContextIdParam', + type: CustomParameterType.AUTO_CONTEXTID, + }); + + const externalTool: ExternalTool = externalToolFactory.build({ + parameters: [autoContextIdCustomParameter], + }); + + const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build(); + + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build(); + + return { + autoContextIdCustomParameter, + externalTool, + schoolExternalTool, + contextExternalTool, + }; + }; + + it('should return ToolLaunchData with the context id as parameter value', async () => { + const { externalTool, schoolExternalTool, contextExternalTool, autoContextIdCustomParameter } = setup(); + + const result: ToolLaunchData = await launchStrategy.createLaunchData('userId', { + externalTool, + schoolExternalTool, + contextExternalTool, + }); + + expect(result).toEqual({ + baseUrl: externalTool.config.baseUrl, + type: ToolLaunchDataType.BASIC, + openNewTab: false, + properties: [ + { + name: autoContextIdCustomParameter.name, + value: contextExternalTool.contextRef.id, location: PropertyLocation.BODY, }, { @@ -299,7 +384,7 @@ describe('AbstractLaunchStrategy', () => { value: concreteConfigParameter.value, location: concreteConfigParameter.location, }, - ].sort(sortFn), + ], }); }); }); 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 9004e461ae2..63ba0680734 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 { CourseService } from '@src/modules/learnroom/service'; -import { LegacySchoolService } from '@src/modules/legacy-school'; +import { CourseService } from '@modules/learnroom/service'; +import { LegacySchoolService } from '@modules/legacy-school'; import { URLSearchParams } from 'url'; import { CustomParameter, CustomParameterEntry } from '../../../common/domain'; import { @@ -215,15 +215,18 @@ export abstract class AbstractLaunchStrategy implements IToolLaunchStrategy { return contextExternalTool.contextRef.id; } case CustomParameterType.AUTO_CONTEXTNAME: { - if (contextExternalTool.contextRef.type === ToolContextType.COURSE) { - const course: Course = await this.courseService.findById(contextExternalTool.contextRef.id); - - return course.name; + switch (contextExternalTool.contextRef.type) { + case ToolContextType.COURSE: { + const course: Course = await this.courseService.findById(contextExternalTool.contextRef.id); + + return course.name; + } + default: { + throw new ParameterTypeNotImplementedLoggableException( + `${customParameter.type}/${contextExternalTool.contextRef.type as string}` + ); + } } - - throw new ParameterTypeNotImplementedLoggableException( - `${customParameter.type}/${contextExternalTool.contextRef.type as string}` - ); } case CustomParameterType.AUTO_SCHOOLNUMBER: { const school: LegacySchoolDo = await this.schoolService.getSchoolById(schoolExternalTool.schoolId); 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 a18a760b0b3..3bb95b97755 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 @@ -1,8 +1,8 @@ import { createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { contextExternalToolFactory, externalToolFactory, schoolExternalToolFactory } from '@shared/testing'; -import { CourseService } from '@src/modules/learnroom/service'; -import { LegacySchoolService } from '@src/modules/legacy-school'; +import { CourseService } from '@modules/learnroom/service'; +import { LegacySchoolService } from '@modules/legacy-school'; import { ContextExternalTool } from '../../../context-external-tool/domain'; import { ExternalTool } from '../../../external-tool/domain'; import { SchoolExternalTool } from '../../../school-external-tool/domain'; 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 a0db37651be..5113ff3cc76 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 @@ -9,12 +9,12 @@ import { userDoFactory, } from '@shared/testing'; import { pseudonymFactory } from '@shared/testing/factory/domainobject/pseudonym.factory'; -import { PseudonymService } from '@src/modules/pseudonym/service'; -import { LegacySchoolService } from '@src/modules/legacy-school'; -import { UserService } from '@src/modules/user'; +import { CourseService } from '@modules/learnroom/service'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { PseudonymService } from '@modules/pseudonym/service'; +import { UserService } from '@modules/user'; import { ObjectId } from 'bson'; import { Authorization } from 'oauth-1.0a'; -import { CourseService } from '@src/modules/learnroom/service'; import { LtiMessageType, LtiPrivacyPermission, LtiRole, ToolContextType } from '../../../common/enum'; import { ContextExternalTool } from '../../../context-external-tool/domain'; import { ExternalTool } from '../../../external-tool/domain'; 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 8c957ca9421..09d04e388f3 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 @@ -1,15 +1,15 @@ import { Injectable, InternalServerErrorException, UnprocessableEntityException } from '@nestjs/common'; import { EntityId, LtiPrivacyPermission, Pseudonym, RoleName, UserDO } from '@shared/domain'; import { RoleReference } from '@shared/domain/domainobject'; -import { CourseService } from '@src/modules/learnroom/service'; -import { LegacySchoolService } from '@src/modules/legacy-school'; -import { PseudonymService } from '@src/modules/pseudonym/service'; -import { UserService } from '@src/modules/user'; +import { CourseService } from '@modules/learnroom/service'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { PseudonymService } from '@modules/pseudonym/service'; +import { UserService } from '@modules/user'; import { Authorization } from 'oauth-1.0a'; import { LtiRole } from '../../../common/enum'; import { ExternalTool } from '../../../external-tool/domain'; import { LtiRoleMapper } from '../../mapper'; -import { LaunchRequestMethod, PropertyData, PropertyLocation, AuthenticationValues } from '../../types'; +import { AuthenticationValues, LaunchRequestMethod, PropertyData, PropertyLocation } from '../../types'; import { Lti11EncryptionService } from '../lti11-encryption.service'; import { AbstractLaunchStrategy } from './abstract-launch.strategy'; import { IToolLaunchParams } from './tool-launch-params.interface'; 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 f1e3388ed15..bd97fafde71 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 @@ -1,8 +1,8 @@ import { createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { contextExternalToolFactory, externalToolFactory, schoolExternalToolFactory } from '@shared/testing'; -import { CourseService } from '@src/modules/learnroom/service'; -import { LegacySchoolService } from '@src/modules/legacy-school'; +import { CourseService } from '@modules/learnroom/service'; +import { LegacySchoolService } from '@modules/legacy-school'; import { ContextExternalTool } from '../../../context-external-tool/domain'; import { ExternalTool } from '../../../external-tool/domain'; import { SchoolExternalTool } from '../../../school-external-tool/domain'; diff --git a/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts b/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts index 02bb484093f..3330b0c9f0e 100644 --- a/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.spec.ts @@ -7,7 +7,14 @@ import { externalToolFactory, schoolExternalToolFactory, } from '@shared/testing'; +import { ToolConfigType, ToolConfigurationStatus } from '../../common/enum'; +import { CommonToolService } from '../../common/service'; import { ContextExternalTool } from '../../context-external-tool/domain'; +import { BasicToolConfig, ExternalTool } from '../../external-tool/domain'; +import { ExternalToolService } from '../../external-tool/service'; +import { SchoolExternalTool } from '../../school-external-tool/domain'; +import { SchoolExternalToolService } from '../../school-external-tool/service'; +import { ToolStatusOutdatedLoggableException } from '../error'; import { LaunchRequestMethod, ToolLaunchData, ToolLaunchDataType, ToolLaunchRequest } from '../types'; import { BasicToolLaunchStrategy, @@ -16,13 +23,6 @@ import { OAuth2ToolLaunchStrategy, } from './strategy'; import { ToolLaunchService } from './tool-launch.service'; -import { ToolStatusOutdatedLoggableException } from '../error'; -import { SchoolExternalToolService } from '../../school-external-tool/service'; -import { ExternalToolService } from '../../external-tool/service'; -import { CommonToolService } from '../../common/service'; -import { SchoolExternalTool } from '../../school-external-tool/domain'; -import { BasicToolConfig, ExternalTool } from '../../external-tool/domain'; -import { ToolConfigType, ToolConfigurationStatus } from '../../common/enum'; describe('ToolLaunchService', () => { let module: TestingModule; @@ -104,8 +104,8 @@ describe('ToolLaunchService', () => { contextExternalTool, }; - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValue(schoolExternalTool); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + schoolExternalToolService.findById.mockResolvedValue(schoolExternalTool); + externalToolService.findById.mockResolvedValue(externalTool); basicToolLaunchStrategy.createLaunchData.mockResolvedValue(launchDataDO); commonToolService.determineToolConfigurationStatus.mockReturnValueOnce(ToolConfigurationStatus.LATEST); @@ -136,9 +136,7 @@ describe('ToolLaunchService', () => { await service.getLaunchData('userId', launchParams.contextExternalTool); - expect(schoolExternalToolService.getSchoolExternalToolById).toHaveBeenCalledWith( - launchParams.schoolExternalTool.id - ); + expect(schoolExternalToolService.findById).toHaveBeenCalledWith(launchParams.schoolExternalTool.id); }); it('should call findExternalToolById', async () => { @@ -146,7 +144,7 @@ describe('ToolLaunchService', () => { await service.getLaunchData('userId', launchParams.contextExternalTool); - expect(externalToolService.findExternalToolById).toHaveBeenCalledWith(launchParams.schoolExternalTool.toolId); + expect(externalToolService.findById).toHaveBeenCalledWith(launchParams.schoolExternalTool.toolId); }); }); @@ -165,8 +163,8 @@ describe('ToolLaunchService', () => { contextExternalTool, }; - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValue(schoolExternalTool); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + schoolExternalToolService.findById.mockResolvedValue(schoolExternalTool); + externalToolService.findById.mockResolvedValue(externalTool); commonToolService.determineToolConfigurationStatus.mockReturnValueOnce(ToolConfigurationStatus.LATEST); return { @@ -209,8 +207,8 @@ describe('ToolLaunchService', () => { const userId = 'userId'; - schoolExternalToolService.getSchoolExternalToolById.mockResolvedValue(schoolExternalTool); - externalToolService.findExternalToolById.mockResolvedValue(externalTool); + schoolExternalToolService.findById.mockResolvedValue(schoolExternalTool); + externalToolService.findById.mockResolvedValue(externalTool); basicToolLaunchStrategy.createLaunchData.mockResolvedValue(launchDataDO); commonToolService.determineToolConfigurationStatus.mockReturnValueOnce(ToolConfigurationStatus.OUTDATED); diff --git a/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.ts b/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.ts index 3321e782f09..46d2efdeb70 100644 --- a/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.ts +++ b/apps/server/src/modules/tool/tool-launch/service/tool-launch.service.ts @@ -1,6 +1,13 @@ import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { EntityId } from '@shared/domain'; +import { ToolConfigType, ToolConfigurationStatus } from '../../common/enum'; import { CommonToolService } from '../../common/service'; +import { ContextExternalTool } from '../../context-external-tool/domain'; +import { ExternalTool } from '../../external-tool/domain'; +import { ExternalToolService } from '../../external-tool/service'; +import { SchoolExternalTool } from '../../school-external-tool/domain'; +import { SchoolExternalToolService } from '../../school-external-tool/service'; +import { ToolStatusOutdatedLoggableException } from '../error'; import { ToolLaunchMapper } from '../mapper'; import { ToolLaunchData, ToolLaunchRequest } from '../types'; import { @@ -9,13 +16,6 @@ import { Lti11ToolLaunchStrategy, OAuth2ToolLaunchStrategy, } from './strategy'; -import { ToolStatusOutdatedLoggableException } from '../error'; -import { SchoolExternalToolService } from '../../school-external-tool/service'; -import { ExternalToolService } from '../../external-tool/service'; -import { ToolConfigType, ToolConfigurationStatus } from '../../common/enum'; -import { ContextExternalTool } from '../../context-external-tool/domain'; -import { ExternalTool } from '../../external-tool/domain'; -import { SchoolExternalTool } from '../../school-external-tool/domain'; @Injectable() export class ToolLaunchService { @@ -73,11 +73,9 @@ export class ToolLaunchService { private async loadToolHierarchy( schoolExternalToolId: string ): Promise<{ schoolExternalTool: SchoolExternalTool; externalTool: ExternalTool }> { - const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.getSchoolExternalToolById( - schoolExternalToolId - ); + const schoolExternalTool: SchoolExternalTool = await this.schoolExternalToolService.findById(schoolExternalToolId); - const externalTool: ExternalTool = await this.externalToolService.findExternalToolById(schoolExternalTool.toolId); + const externalTool: ExternalTool = await this.externalToolService.findById(schoolExternalTool.toolId); return { schoolExternalTool, diff --git a/apps/server/src/modules/tool/tool-launch/tool-launch.module.ts b/apps/server/src/modules/tool/tool-launch/tool-launch.module.ts index 6799a50bca2..4ae6a3a38a5 100644 --- a/apps/server/src/modules/tool/tool-launch/tool-launch.module.ts +++ b/apps/server/src/modules/tool/tool-launch/tool-launch.module.ts @@ -1,8 +1,8 @@ -import { Module } from '@nestjs/common'; -import { LearnroomModule } from '@src/modules/learnroom'; -import { LegacySchoolModule } from '@src/modules/legacy-school'; -import { PseudonymModule } from '@src/modules/pseudonym'; -import { UserModule } from '@src/modules/user'; +import { Module, forwardRef } from '@nestjs/common'; +import { LearnroomModule } from '@modules/learnroom'; +import { LegacySchoolModule } from '@modules/legacy-school'; +import { PseudonymModule } from '@modules/pseudonym'; +import { UserModule } from '@modules/user'; import { CommonToolModule } from '../common'; import { ContextExternalToolModule } from '../context-external-tool'; import { ExternalToolModule } from '../external-tool'; @@ -18,7 +18,7 @@ import { BasicToolLaunchStrategy, Lti11ToolLaunchStrategy, OAuth2ToolLaunchStrat ContextExternalToolModule, LegacySchoolModule, UserModule, - PseudonymModule, + forwardRef(() => PseudonymModule), // i do not like this solution, the root problem is on other place but not detectable for me LearnroomModule, ], providers: [ diff --git a/apps/server/src/modules/tool/tool-launch/uc/tool-launch.uc.spec.ts b/apps/server/src/modules/tool/tool-launch/uc/tool-launch.uc.spec.ts index 62424d8b8aa..e9b7311e06a 100644 --- a/apps/server/src/modules/tool/tool-launch/uc/tool-launch.uc.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/uc/tool-launch.uc.spec.ts @@ -2,12 +2,12 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { contextExternalToolFactory } from '@shared/testing'; import { ObjectId } from 'bson'; +import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; +import { ContextExternalTool } from '../../context-external-tool/domain'; +import { ContextExternalToolService } from '../../context-external-tool/service'; import { ToolLaunchService } from '../service'; import { ToolLaunchData, ToolLaunchDataType, ToolLaunchRequest } from '../types'; import { ToolLaunchUc } from './tool-launch.uc'; -import { ContextExternalToolService } from '../../context-external-tool/service'; -import { ContextExternalTool } from '../../context-external-tool/domain'; -import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; describe('ToolLaunchUc', () => { let module: TestingModule; @@ -66,7 +66,7 @@ describe('ToolLaunchUc', () => { const userId: string = new ObjectId().toHexString(); toolPermissionHelper.ensureContextPermissions.mockResolvedValueOnce(); - contextExternalToolService.getContextExternalToolById.mockResolvedValueOnce(contextExternalTool); + contextExternalToolService.findById.mockResolvedValueOnce(contextExternalTool); toolLaunchService.getLaunchData.mockResolvedValueOnce(toolLaunchData); return { @@ -82,7 +82,7 @@ describe('ToolLaunchUc', () => { await uc.getToolLaunchRequest(userId, contextExternalToolId); - expect(contextExternalToolService.getContextExternalToolById).toHaveBeenCalledWith(contextExternalToolId); + expect(contextExternalToolService.findById).toHaveBeenCalledWith(contextExternalToolId); }); it('should call service to get data', async () => { diff --git a/apps/server/src/modules/tool/tool-launch/uc/tool-launch.uc.ts b/apps/server/src/modules/tool/tool-launch/uc/tool-launch.uc.ts index c397ae1d1af..272d8dfd376 100644 --- a/apps/server/src/modules/tool/tool-launch/uc/tool-launch.uc.ts +++ b/apps/server/src/modules/tool/tool-launch/uc/tool-launch.uc.ts @@ -1,11 +1,11 @@ import { Injectable } from '@nestjs/common'; import { EntityId, Permission } from '@shared/domain'; -import { AuthorizationContext, AuthorizationContextBuilder } from '@src/modules/authorization'; +import { AuthorizationContext, AuthorizationContextBuilder } from '@modules/authorization'; +import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; +import { ContextExternalTool } from '../../context-external-tool/domain'; +import { ContextExternalToolService } from '../../context-external-tool/service'; import { ToolLaunchService } from '../service'; import { ToolLaunchData, ToolLaunchRequest } from '../types'; -import { ContextExternalToolService } from '../../context-external-tool/service'; -import { ContextExternalTool } from '../../context-external-tool/domain'; -import { ToolPermissionHelper } from '../../common/uc/tool-permission-helper'; @Injectable() export class ToolLaunchUc { @@ -16,7 +16,7 @@ export class ToolLaunchUc { ) {} async getToolLaunchRequest(userId: EntityId, contextExternalToolId: EntityId): Promise { - const contextExternalTool: ContextExternalTool = await this.contextExternalToolService.getContextExternalToolById( + const contextExternalTool: ContextExternalTool = await this.contextExternalToolService.findById( contextExternalToolId ); const context: AuthorizationContext = AuthorizationContextBuilder.read([Permission.CONTEXT_TOOL_USER]); diff --git a/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts b/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts index 0315d40ee26..29b7796b40d 100644 --- a/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts +++ b/apps/server/src/modules/user-import/controller/api-test/import-user.api.spec.ts @@ -14,7 +14,7 @@ import { SystemEntity, User, } from '@shared/domain'; -import { ICurrentUser } from '@src/modules/authentication'; +import { ICurrentUser } from '@modules/authentication'; import { cleanupCollections, importUserFactory, @@ -24,8 +24,8 @@ import { systemFactory, userFactory, } from '@shared/testing'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; import { FilterImportUserParams, FilterMatchType, @@ -41,7 +41,7 @@ import { UserMatchListResponse, UserMatchResponse, UserRole, -} from '@src/modules/user-import/controller/dto'; +} from '@modules/user-import/controller/dto'; import { Request } from 'express'; import request from 'supertest'; 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 91b1ebc6ec7..c93c7bc58a7 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 @@ -1,8 +1,8 @@ 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/legacy-school'; +import { AccountService } from '@modules/account/services/account.service'; +import { AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school'; import { LoggerModule } from '@src/core/logger'; import { ConfigModule } from '@nestjs/config'; import { UserImportUc } from '../uc/user-import.uc'; diff --git a/apps/server/src/modules/user-import/controller/import-user.controller.ts b/apps/server/src/modules/user-import/controller/import-user.controller.ts index 6b87a9f66d2..c4368b5ea3f 100644 --- a/apps/server/src/modules/user-import/controller/import-user.controller.ts +++ b/apps/server/src/modules/user-import/controller/import-user.controller.ts @@ -1,11 +1,8 @@ import { Body, Controller, Delete, Get, Param, Patch, Post, Query } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; - import { PaginationParams } from '@shared/controller'; import { IFindOptions, ImportUser, User } from '@shared/domain'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; -import { ICurrentUser } from '@src/modules/authentication'; - +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { ImportUserMapper } from '../mapper/import-user.mapper'; import { UserMatchMapper } from '../mapper/user-match.mapper'; import { UserImportUc } from '../uc/user-import.uc'; 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 8dcccaf84e2..3c3baa56475 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 @@ -21,9 +21,9 @@ 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 '@src/modules/legacy-school'; +import { AccountService } from '@modules/account/services/account.service'; +import { AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school'; import { LdapAlreadyPersistedException, MigrationAlreadyActivatedException, 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 ecef86207a0..2dda936b1fb 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 @@ -19,10 +19,10 @@ import { } from '@shared/domain'; 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/legacy-school'; +import { AccountService } from '@modules/account/services/account.service'; +import { AccountDto } from '@modules/account/services/dto/account.dto'; +import { AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school'; import { AccountSaveDto } from '../../account/services/dto'; import { MigrationMayBeCompleted, 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 6e881ff5b2f..2cf4d94704e 100644 --- a/apps/server/src/modules/user-import/user-import.module.ts +++ b/apps/server/src/modules/user-import/user-import.module.ts @@ -1,7 +1,7 @@ import { Module } from '@nestjs/common'; import { ImportUserRepo, LegacySchoolRepo, SystemRepo, UserRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; -import { LegacySchoolModule } from '@src/modules/legacy-school'; +import { LegacySchoolModule } from '@modules/legacy-school'; import { AccountModule } from '../account'; import { AuthorizationModule } from '../authorization'; import { ImportUserController } from './controller/import-user.controller'; 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 486c814ccd7..b2de2ac9fc0 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 @@ -15,13 +15,13 @@ import { userLoginMigrationFactory, } from '@shared/testing'; import { JwtTestFactory } from '@shared/testing/factory/jwt.test.factory'; -import { OauthTokenResponse } from '@src/modules/oauth/service/dto'; -import { ServerTestModule } from '@src/modules/server'; +import { OauthTokenResponse } from '@modules/oauth/service/dto'; +import { ServerTestModule } from '@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 { SanisResponse, SanisRole } from '@modules/provisioning/strategy/sanis/response'; import { UserLoginMigrationResponse } from '../dto'; import { Oauth2MigrationParams } from '../dto/oauth2-migration.params'; diff --git a/apps/server/src/modules/user-login-migration/controller/user-login-migration.controller.ts b/apps/server/src/modules/user-login-migration/controller/user-login-migration.controller.ts index 19098167d96..3e788a54725 100644 --- a/apps/server/src/modules/user-login-migration/controller/user-login-migration.controller.ts +++ b/apps/server/src/modules/user-login-migration/controller/user-login-migration.controller.ts @@ -11,8 +11,7 @@ import { ApiUnprocessableEntityResponse, } from '@nestjs/swagger'; import { Page, UserLoginMigrationDO } from '@shared/domain'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser, JWT } from '@src/modules/authentication/decorator/auth.decorator'; +import { Authenticate, CurrentUser, ICurrentUser, JWT } from '@modules/authentication'; import { SchoolNumberMissingLoggableException, UserLoginMigrationAlreadyClosedLoggableException, diff --git a/apps/server/src/modules/user-login-migration/error/oauth-migration.error.ts b/apps/server/src/modules/user-login-migration/error/oauth-migration.error.ts index 23d6b58c0fe..c21185f1c93 100644 --- a/apps/server/src/modules/user-login-migration/error/oauth-migration.error.ts +++ b/apps/server/src/modules/user-login-migration/error/oauth-migration.error.ts @@ -1,4 +1,4 @@ -import { OAuthSSOError } from '@src/modules/oauth/error/oauth-sso.error'; +import { OAuthSSOError } from '@modules/oauth/loggable'; export class OAuthMigrationError extends OAuthSSOError { readonly message: string; 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 c61382b9f63..7afc017cf98 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 @@ -3,8 +3,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { LegacySchoolDo, UserDO, UserLoginMigrationDO } from '@shared/domain'; import { UserLoginMigrationRepo } from '@shared/repo'; import { legacySchoolDoFactory, userDoFactory } from '@shared/testing'; -import { LegacySchoolService } from '@src/modules/legacy-school'; -import { UserService } from '@src/modules/user'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; import { MigrationCheckService } from './migration-check.service'; describe('MigrationCheckService', () => { 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 7a0596e3599..70d0ab94066 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,8 +1,8 @@ import { Injectable } from '@nestjs/common'; import { EntityId, LegacySchoolDo, UserDO, UserLoginMigrationDO } from '@shared/domain'; import { UserLoginMigrationRepo } from '@shared/repo'; -import { LegacySchoolService } from '@src/modules/legacy-school'; -import { UserService } from '@src/modules/user'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; @Injectable() export class MigrationCheckService { 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 19841ffb6de..8addd2afae6 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 @@ -7,10 +7,10 @@ import { LegacySchoolDo, Page, UserDO, UserLoginMigrationDO } from '@shared/doma import { UserLoginMigrationRepo } from '@shared/repo/userloginmigration/user-login-migration.repo'; import { legacySchoolDoFactory, setupEntities, userDoFactory, userLoginMigrationDOFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { ICurrentUser } from '@src/modules/authentication'; -import { LegacySchoolService } from '@src/modules/legacy-school'; -import { UserService } from '@src/modules/user'; -import { OAuthMigrationError } from '@src/modules/user-login-migration/error/oauth-migration.error'; +import { ICurrentUser } from '@modules/authentication'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; +import { OAuthMigrationError } from '@modules/user-login-migration/error/oauth-migration.error'; import { SchoolMigrationService } from './school-migration.service'; describe('SchoolMigrationService', () => { 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 04f3e44b265..147d9ec112b 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 @@ -3,8 +3,8 @@ import { ValidationError } from '@shared/common'; import { Page, LegacySchoolDo, UserDO, UserLoginMigrationDO } from '@shared/domain'; import { UserLoginMigrationRepo } from '@shared/repo'; import { LegacyLogger } from '@src/core/logger'; -import { LegacySchoolService } from '@src/modules/legacy-school'; -import { UserService } from '@src/modules/user'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; import { performance } from 'perf_hooks'; import { OAuthMigrationError } from '../error'; 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 0b677fd8cf6..36f2176e00f 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/legacy-school'; +import { LegacySchoolService } from '@modules/legacy-school'; import { setupEntities, userLoginMigrationDOFactory } from '@shared/testing'; import { UserLoginMigrationRevertService } from './user-login-migration-revert.service'; import { UserLoginMigrationService } from './user-login-migration.service'; 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 88397e0b179..6d6512bc700 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,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { SchoolFeatures, UserLoginMigrationDO } from '@shared/domain'; -import { LegacySchoolService } from '@src/modules/legacy-school'; +import { LegacySchoolService } from '@modules/legacy-school'; import { UserLoginMigrationService } from './user-login-migration.service'; @Injectable() 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 0b755b68415..01e12e0df19 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 @@ -6,10 +6,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { EntityId, LegacySchoolDo, SchoolFeatures, UserDO, UserLoginMigrationDO } from '@shared/domain'; import { UserLoginMigrationRepo } from '@shared/repo'; import { legacySchoolDoFactory, userDoFactory, userLoginMigrationDOFactory } from '@shared/testing'; -import { LegacySchoolService } from '@src/modules/legacy-school'; -import { SystemService } from '@src/modules/system'; -import { SystemDto } from '@src/modules/system/service'; -import { UserService } from '@src/modules/user'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { SystemService } from '@modules/system'; +import { SystemDto } from '@modules/system/service'; +import { UserService } from '@modules/user'; import { UserLoginMigrationNotFoundLoggableException } from '../error'; import { SchoolMigrationService } from './school-migration.service'; import { UserLoginMigrationService } from './user-login-migration.service'; 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 9f3d6c59e84..534bb71e104 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 @@ -2,9 +2,9 @@ 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 { UserLoginMigrationRepo } from '@shared/repo'; -import { LegacySchoolService } from '@src/modules/legacy-school'; -import { SystemDto, SystemService } from '@src/modules/system'; -import { UserService } from '@src/modules/user'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { SystemDto, SystemService } from '@modules/system'; +import { UserService } from '@modules/user'; import { UserLoginMigrationNotFoundLoggableException } from '../error'; import { SchoolMigrationService } from './school-migration.service'; 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 24cb59030a5..c098066664d 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 @@ -7,13 +7,13 @@ import { Test, TestingModule } from '@nestjs/testing'; import { LegacySchoolDo, RoleName, UserDO } from '@shared/domain'; import { legacySchoolDoFactory, 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/legacy-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'; -import { UserService } from '@src/modules/user'; +import { AccountService } from '@modules/account/services/account.service'; +import { AccountDto, AccountSaveDto } from '@modules/account/services/dto'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { SystemService } from '@modules/system'; +import { OauthConfigDto } from '@modules/system/service/dto/oauth-config.dto'; +import { SystemDto } from '@modules/system/service/dto/system.dto'; +import { UserService } from '@modules/user'; import { PageTypes } from '../interface/page-types.enum'; import { PageContentDto } from './dto'; import { UserMigrationService } from './user-migration.service'; 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 a2e999994f5..c9a6e8648c8 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 @@ -3,11 +3,11 @@ import { BadRequestException, Injectable, NotFoundException, UnprocessableEntity import { LegacySchoolDo } from '@shared/domain'; 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/legacy-school'; -import { SystemDto, SystemService } from '@src/modules/system/service'; -import { UserService } from '@src/modules/user'; +import { AccountService } from '@modules/account/services/account.service'; +import { AccountDto } from '@modules/account/services/dto'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { SystemDto, SystemService } from '@modules/system/service'; +import { UserService } from '@modules/user'; import { EntityId } from '@src/shared/domain/types'; import { PageTypes } from '../interface/page-types.enum'; import { MigrationDto } from './dto/migration.dto'; diff --git a/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.spec.ts b/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.spec.ts index 6b796e69bba..b14ab751d40 100644 --- a/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.spec.ts +++ b/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.spec.ts @@ -2,7 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission, UserLoginMigrationDO } from '@shared/domain'; import { setupEntities, userFactory, userLoginMigrationDOFactory } from '@shared/testing'; -import { Action, AuthorizationService } from '@src/modules/authorization'; +import { Action, AuthorizationService } from '@modules/authorization'; import { UserLoginMigrationNotFoundLoggableException } from '../error'; import { SchoolMigrationService, UserLoginMigrationRevertService, UserLoginMigrationService } from '../service'; import { CloseUserLoginMigrationUc } from './close-user-login-migration.uc'; diff --git a/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.ts b/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.ts index 13058e64c49..65bdad24782 100644 --- a/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.ts +++ b/apps/server/src/modules/user-login-migration/uc/close-user-login-migration.uc.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator'; import { EntityId, Permission, User, UserLoginMigrationDO } from '@shared/domain'; -import { Action, AuthorizationService } from '@src/modules/authorization'; +import { Action, AuthorizationService } from '@modules/authorization'; import { UserLoginMigrationGracePeriodExpiredLoggableException, UserLoginMigrationNotFoundLoggableException, 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 c6e53ab483d..dd4ac4f835b 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 @@ -4,8 +4,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Permission, LegacySchoolDo, User, UserLoginMigrationDO } from '@shared/domain'; import { legacySchoolDoFactory, setupEntities, userFactory, userLoginMigrationDOFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/legacy-school'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school'; import { UserLoginMigrationGracePeriodExpiredLoggableException, UserLoginMigrationNotFoundLoggableException, 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 42412e50fe3..997276c3661 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 { Logger } from '@src/core/logger'; -import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/legacy-school'; +import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school'; import { UserLoginMigrationGracePeriodExpiredLoggableException, UserLoginMigrationNotFoundLoggableException, 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 e4c7510e684..3f0c03c07ff 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 @@ -4,8 +4,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Permission, LegacySchoolDo, User, UserLoginMigrationDO } from '@shared/domain'; import { legacySchoolDoFactory, setupEntities, userFactory, userLoginMigrationDOFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/legacy-school'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school'; import { SchoolNumberMissingLoggableException, UserLoginMigrationAlreadyClosedLoggableException } from '../error'; import { UserLoginMigrationService } from '../service'; import { StartUserLoginMigrationUc } from './start-user-login-migration.uc'; 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 9e670e7fdf3..3dd84cc6ef5 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 { Logger } from '@src/core/logger'; -import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/legacy-school'; +import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school'; import { SchoolNumberMissingLoggableException, UserLoginMigrationAlreadyClosedLoggableException } from '../error'; import { UserLoginMigrationStartLoggable } from '../loggable'; import { UserLoginMigrationService } from '../service'; 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 cd97ad1e4ef..b0ff1f67e54 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 @@ -4,8 +4,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { Permission, LegacySchoolDo, User, UserLoginMigrationDO } from '@shared/domain'; import { legacySchoolDoFactory, setupEntities, userFactory, userLoginMigrationDOFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; -import { AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/legacy-school'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school'; import { UserLoginMigrationAlreadyClosedLoggableException, UserLoginMigrationGracePeriodExpiredLoggableException, 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 d07578e7b23..45de7b6e1e3 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 { Logger } from '@src/core/logger'; -import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/legacy-school'; +import { AuthorizationContext, AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school'; import { UserLoginMigrationAlreadyClosedLoggableException, UserLoginMigrationGracePeriodExpiredLoggableException, 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 e9fc294f2ce..f5f710ae990 100644 --- a/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.spec.ts +++ b/apps/server/src/modules/user-login-migration/uc/user-login-migration.uc.spec.ts @@ -12,13 +12,13 @@ import { userLoginMigrationDOFactory, } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { AuthenticationService } from '@src/modules/authentication/services/authentication.service'; -import { Action, AuthorizationService } from '@src/modules/authorization'; -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/legacy-school'; +import { AuthenticationService } from '@modules/authentication/services/authentication.service'; +import { Action, AuthorizationService } from '@modules/authorization'; +import { OAuthTokenDto } from '@modules/oauth'; +import { OAuthService } from '@modules/oauth/service/oauth.service'; +import { ProvisioningService } from '@modules/provisioning'; +import { ExternalSchoolDto, ExternalUserDto, OauthDataDto, ProvisioningSystemDto } from '@modules/provisioning/dto'; +import { LegacySchoolService } from '@modules/legacy-school'; import { Oauth2MigrationParams } from '../controller/dto/oauth2-migration.params'; import { OAuthMigrationError, SchoolMigrationError, UserLoginMigrationError } from '../error'; import { PageTypes } from '../interface/page-types.enum'; 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..a637afe01f6 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 @@ -2,12 +2,12 @@ import { ForbiddenException, Injectable } from '@nestjs/common'; import { NotFoundLoggableException } from '@shared/common/loggable-exception'; import { EntityId, Page, Permission, LegacySchoolDo, 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'; -import { OAuthTokenDto } from '@src/modules/oauth'; -import { OAuthService } from '@src/modules/oauth/service/oauth.service'; -import { ProvisioningService } from '@src/modules/provisioning'; -import { OauthDataDto } from '@src/modules/provisioning/dto'; +import { AuthenticationService } from '@modules/authentication/services/authentication.service'; +import { Action, AuthorizationService } from '@modules/authorization'; +import { OAuthTokenDto } from '@modules/oauth'; +import { OAuthService } from '@modules/oauth/service/oauth.service'; +import { ProvisioningService } from '@modules/provisioning'; +import { OauthDataDto } from '@modules/provisioning/dto'; import { OAuthMigrationError, SchoolMigrationError, UserLoginMigrationError } from '../error'; import { PageTypes } from '../interface/page-types.enum'; import { SchoolMigrationService, UserLoginMigrationService, UserMigrationService } from '../service'; diff --git a/apps/server/src/modules/user-login-migration/user-login-migration-api.module.ts b/apps/server/src/modules/user-login-migration/user-login-migration-api.module.ts index 4556103658d..b30a3f40f4d 100644 --- a/apps/server/src/modules/user-login-migration/user-login-migration-api.module.ts +++ b/apps/server/src/modules/user-login-migration/user-login-migration-api.module.ts @@ -1,10 +1,10 @@ import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; -import { AuthenticationModule } from '@src/modules/authentication/authentication.module'; -import { AuthorizationModule } from '@src/modules/authorization'; -import { OauthModule } from '@src/modules/oauth'; -import { ProvisioningModule } from '@src/modules/provisioning'; -import { LegacySchoolModule } from '@src/modules/legacy-school'; +import { AuthenticationModule } from '@modules/authentication/authentication.module'; +import { AuthorizationModule } from '@modules/authorization'; +import { OauthModule } from '@modules/oauth'; +import { ProvisioningModule } from '@modules/provisioning'; +import { LegacySchoolModule } from '@modules/legacy-school'; import { UserLoginMigrationController } from './controller/user-login-migration.controller'; import { UserMigrationController } from './controller/user-migration.controller'; import { PageContentMapper } from './mapper'; diff --git a/apps/server/src/modules/user-login-migration/user-login-migration.module.ts b/apps/server/src/modules/user-login-migration/user-login-migration.module.ts index 40e965a4df3..705e5cb4094 100644 --- a/apps/server/src/modules/user-login-migration/user-login-migration.module.ts +++ b/apps/server/src/modules/user-login-migration/user-login-migration.module.ts @@ -1,10 +1,10 @@ import { Module } from '@nestjs/common'; import { UserLoginMigrationRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; -import { AccountModule } from '@src/modules/account'; -import { LegacySchoolModule } from '@src/modules/legacy-school'; -import { SystemModule } from '@src/modules/system'; -import { UserModule } from '@src/modules/user'; +import { AccountModule } from '@modules/account'; +import { LegacySchoolModule } from '@modules/legacy-school'; +import { SystemModule } from '@modules/system'; +import { UserModule } from '@modules/user'; import { MigrationCheckService, SchoolMigrationService, diff --git a/apps/server/src/modules/user/controller/api-test/user-language.api.spec.ts b/apps/server/src/modules/user/controller/api-test/user-language.api.spec.ts index f7929db5d37..28bc7c5d14e 100644 --- a/apps/server/src/modules/user/controller/api-test/user-language.api.spec.ts +++ b/apps/server/src/modules/user/controller/api-test/user-language.api.spec.ts @@ -5,9 +5,9 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ApiValidationError } from '@shared/common'; import { LanguageType, User } from '@shared/domain'; import { cleanupCollections, mapUserToCurrentUser, roleFactory, userFactory } from '@shared/testing'; -import { ICurrentUser } from '@src/modules/authentication'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@src/modules/server/server.module'; +import { ICurrentUser } from '@modules/authentication'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; import { Request } from 'express'; import request from 'supertest'; diff --git a/apps/server/src/modules/user/controller/api-test/user-me.api.spec.ts b/apps/server/src/modules/user/controller/api-test/user-me.api.spec.ts index 4fbf963266a..370de7e4fa6 100644 --- a/apps/server/src/modules/user/controller/api-test/user-me.api.spec.ts +++ b/apps/server/src/modules/user/controller/api-test/user-me.api.spec.ts @@ -7,11 +7,11 @@ import request from 'supertest'; import { ApiValidationError } from '@shared/common'; import { LanguageType } from '@shared/domain'; -import { ICurrentUser } from '@src/modules/authentication'; +import { ICurrentUser } from '@modules/authentication'; import { cleanupCollections, mapUserToCurrentUser, roleFactory, userFactory } from '@shared/testing'; -import { JwtAuthGuard } from '@src/modules/authentication/guard/jwt-auth.guard'; -import { ServerTestModule } from '@src/modules/server/server.module'; -import { ResolvedUserResponse } from '@src/modules/user/controller/dto'; +import { JwtAuthGuard } from '@modules/authentication/guard/jwt-auth.guard'; +import { ServerTestModule } from '@modules/server/server.module'; +import { ResolvedUserResponse } from '@modules/user/controller/dto'; const baseRouteName = '/user/me'; diff --git a/apps/server/src/modules/user/controller/dto/resolved-user.response.ts b/apps/server/src/modules/user/controller/dto/resolved-user.response.ts index 31347ad0054..e61114be6bf 100644 --- a/apps/server/src/modules/user/controller/dto/resolved-user.response.ts +++ b/apps/server/src/modules/user/controller/dto/resolved-user.response.ts @@ -1,9 +1,12 @@ import { ApiProperty } from '@nestjs/swagger'; -import { IResolvedUser, IRole } from '@src/modules/authentication/interface/user'; -export type Role = IRole; +export type Role = { + name: string; -export class ResolvedUserResponse implements IResolvedUser { + id: string; +}; + +export class ResolvedUserResponse { @ApiProperty() firstName!: string; diff --git a/apps/server/src/modules/user/controller/user.controller.ts b/apps/server/src/modules/user/controller/user.controller.ts index 1cf26a90465..16729c2667b 100644 --- a/apps/server/src/modules/user/controller/user.controller.ts +++ b/apps/server/src/modules/user/controller/user.controller.ts @@ -1,7 +1,6 @@ import { Body, Controller, Get, Patch } from '@nestjs/common'; import { ApiTags } from '@nestjs/swagger'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { ResolvedUserMapper } from '../mapper'; import { UserUc } from '../uc'; import { ChangeLanguageParams, ResolvedUserResponse, SuccessfulResponse } from './dto'; diff --git a/apps/server/src/modules/user/mapper/user.mapper.spec.ts b/apps/server/src/modules/user/mapper/user.mapper.spec.ts index fd97d8e9c9b..dbb5d475f05 100644 --- a/apps/server/src/modules/user/mapper/user.mapper.spec.ts +++ b/apps/server/src/modules/user/mapper/user.mapper.spec.ts @@ -1,7 +1,7 @@ import { User } from '@shared/domain'; import { roleFactory, setupEntities, userFactory } from '@shared/testing'; -import { UserMapper } from '@src/modules/user/mapper/user.mapper'; -import { UserDto } from '@src/modules/user/uc/dto/user.dto'; +import { UserMapper } from '@modules/user/mapper/user.mapper'; +import { UserDto } from '@modules/user/uc/dto/user.dto'; describe('UserMapper', () => { let userEntity: User; diff --git a/apps/server/src/modules/user/mapper/user.mapper.ts b/apps/server/src/modules/user/mapper/user.mapper.ts index 72dd3e626d7..2238f0307b7 100644 --- a/apps/server/src/modules/user/mapper/user.mapper.ts +++ b/apps/server/src/modules/user/mapper/user.mapper.ts @@ -1,5 +1,5 @@ import { Role, User } from '@shared/domain'; -import { UserDto } from '@src/modules/user/uc/dto/user.dto'; +import { UserDto } from '@modules/user/uc/dto/user.dto'; export class UserMapper { static mapFromEntityToDto(entity: User): UserDto { diff --git a/apps/server/src/modules/user/service/user.service.spec.ts b/apps/server/src/modules/user/service/user.service.spec.ts index ea29891cee7..1b208764c8f 100644 --- a/apps/server/src/modules/user/service/user.service.spec.ts +++ b/apps/server/src/modules/user/service/user.service.spec.ts @@ -2,17 +2,17 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { EntityManager } from '@mikro-orm/core'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { IFindOptions, LanguageType, Permission, Role, RoleName, SortOrder, User } from '@shared/domain'; +import { EntityId, IFindOptions, LanguageType, Permission, Role, RoleName, SortOrder, User } from '@shared/domain'; import { UserDO } from '@shared/domain/domainobject/user.do'; import { UserRepo } from '@shared/repo'; import { UserDORepo } from '@shared/repo/user/user-do.repo'; import { roleFactory, setupEntities, userDoFactory, userFactory } from '@shared/testing'; -import { AccountService } from '@src/modules/account/services/account.service'; -import { AccountDto } from '@src/modules/account/services/dto'; -import { ICurrentUser } from '@src/modules/authentication'; -import { RoleService } from '@src/modules/role/service/role.service'; -import { UserService } from '@src/modules/user/service/user.service'; -import { UserDto } from '@src/modules/user/uc/dto/user.dto'; +import { AccountService } from '@modules/account/services/account.service'; +import { AccountDto } from '@modules/account/services/dto'; +import { ICurrentUser } from '@modules/authentication'; +import { RoleService } from '@modules/role/service/role.service'; +import { UserService } from '@modules/user/service/user.service'; +import { UserDto } from '@modules/user/uc/dto/user.dto'; import { UserQuery } from './user-query.type'; describe('UserService', () => { @@ -329,4 +329,50 @@ describe('UserService', () => { expect(userDORepo.saveAll).toHaveBeenCalledWith(users); }); }); + + describe('deleteUser', () => { + describe('when user is missing', () => { + const setup = () => { + const user: UserDO = userDoFactory.build({ id: undefined }); + const userId: EntityId = user.id as EntityId; + + userRepo.deleteUser.mockResolvedValue(0); + + return { + userId, + }; + }; + + it('should return 0', async () => { + const { userId } = setup(); + + const result = await service.deleteUser(userId); + + expect(result).toEqual(0); + }); + }); + + describe('when deleting by userId', () => { + const setup = () => { + const user1: User = userFactory.asStudent().buildWithId(); + userFactory.asStudent().buildWithId(); + + userRepo.findById.mockResolvedValue(user1); + userRepo.deleteUser.mockResolvedValue(1); + + return { + user1, + }; + }; + + it('should delete user by userId', async () => { + const { user1 } = setup(); + + const result = await service.deleteUser(user1.id); + + expect(userRepo.deleteUser).toHaveBeenCalledWith(user1.id); + expect(result).toEqual(1); + }); + }); + }); }); diff --git a/apps/server/src/modules/user/service/user.service.ts b/apps/server/src/modules/user/service/user.service.ts index 8f7bf0beeba..cc15404fc63 100644 --- a/apps/server/src/modules/user/service/user.service.ts +++ b/apps/server/src/modules/user/service/user.service.ts @@ -1,17 +1,16 @@ -import { BadRequestException, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { EntityId, IFindOptions, LanguageType, User } from '@shared/domain'; -import { RoleReference } from '@shared/domain/domainobject'; -import { Page } from '@shared/domain/domainobject/page'; -import { UserDO } from '@shared/domain/domainobject/user.do'; +import { RoleReference, Page, UserDO } from '@shared/domain/domainobject'; import { UserRepo } from '@shared/repo'; import { UserDORepo } from '@shared/repo/user/user-do.repo'; -import { AccountService } from '@src/modules/account/services/account.service'; -import { AccountDto } from '@src/modules/account/services/dto'; -import { ICurrentUser } from '@src/modules/authentication'; -import { CurrentUserMapper } from '@src/modules/authentication/mapper'; -import { RoleDto } from '@src/modules/role/service/dto/role.dto'; -import { RoleService } from '@src/modules/role/service/role.service'; +import { AccountService } from '@modules/account'; +import { AccountDto } from '@modules/account/services/dto'; +import { ICurrentUser } from '@modules/authentication'; +// invalid import +import { CurrentUserMapper } from '@modules/authentication/mapper'; +import { RoleDto } from '@modules/role/service/dto/role.dto'; +import { RoleService } from '@modules/role/service/role.service'; +import { BadRequestException, Injectable } from '@nestjs/common'; import { IUserConfig } from '../interfaces'; import { UserMapper } from '../mapper/user.mapper'; import { UserDto } from '../uc/dto/user.dto'; @@ -40,6 +39,7 @@ export class UserService { async getUser(id: string): Promise { const userEntity = await this.userRepo.findById(id, true); const userDto = UserMapper.mapFromEntityToDto(userEntity); + return userDto; } @@ -48,36 +48,43 @@ export class UserService { const account: AccountDto = await this.accountService.findByUserIdOrFail(userId); const resolvedUser: ICurrentUser = CurrentUserMapper.userToICurrentUser(account.id, user, account.systemId); + return resolvedUser; } async findById(id: string): Promise { const userDO = await this.userDORepo.findById(id, true); + return userDO; } async save(user: UserDO): Promise { const savedUser: Promise = this.userDORepo.save(user); + return savedUser; } async saveAll(users: UserDO[]): Promise { const savedUsers: Promise = this.userDORepo.saveAll(users); + return savedUsers; } async findUsers(query: UserQuery, options?: IFindOptions): Promise> { const users: Page = await this.userDORepo.find(query, options); + return users; } async findByExternalId(externalId: string, systemId: EntityId): Promise { const user: Promise = this.userDORepo.findByExternalId(externalId, systemId); + return user; } async findByEmail(email: string): Promise { const user: Promise = this.userRepo.findByEmail(email); + return user; } @@ -107,4 +114,10 @@ export class UserService { throw new BadRequestException('Language is not activated.'); } } + + async deleteUser(userId: EntityId): Promise { + const deletedUserNumber: Promise = this.userRepo.deleteUser(userId); + + return deletedUserNumber; + } } diff --git a/apps/server/src/modules/user/uc/user.uc.spec.ts b/apps/server/src/modules/user/uc/user.uc.spec.ts index 2988ff38671..3781a8c914a 100644 --- a/apps/server/src/modules/user/uc/user.uc.spec.ts +++ b/apps/server/src/modules/user/uc/user.uc.spec.ts @@ -5,7 +5,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { LanguageType, Permission, User } from '@shared/domain'; import { UserRepo } from '@shared/repo'; import { roleFactory, setupEntities, userFactory } from '@shared/testing'; -import { UserService } from '@src/modules/user/service/user.service'; +import { UserService } from '@modules/user/service/user.service'; import { UserUc } from './user.uc'; describe('UserUc', () => { diff --git a/apps/server/src/modules/user/user.module.ts b/apps/server/src/modules/user/user.module.ts index 8b462c8ca20..d58c24546c6 100644 --- a/apps/server/src/modules/user/user.module.ts +++ b/apps/server/src/modules/user/user.module.ts @@ -2,9 +2,9 @@ import { Module } from '@nestjs/common'; import { UserRepo } from '@shared/repo'; import { UserDORepo } from '@shared/repo/user/user-do.repo'; import { LoggerModule } from '@src/core/logger'; -import { AccountModule } from '@src/modules/account'; -import { RoleModule } from '@src/modules/role/role.module'; -import { LegacySchoolModule } from '@src/modules/legacy-school'; +import { AccountModule } from '@modules/account'; +import { RoleModule } from '@modules/role/role.module'; +import { LegacySchoolModule } from '@modules/legacy-school'; import { UserService } from './service/user.service'; @Module({ diff --git a/apps/server/src/modules/video-conference/controller/api-test/video-conference.api.spec.ts b/apps/server/src/modules/video-conference/controller/api-test/video-conference.api.spec.ts index fad7295bee3..d8b81c0c698 100644 --- a/apps/server/src/modules/video-conference/controller/api-test/video-conference.api.spec.ts +++ b/apps/server/src/modules/video-conference/controller/api-test/video-conference.api.spec.ts @@ -25,7 +25,7 @@ import { userFactory, } from '@shared/testing'; import { videoConferenceFactory } from '@shared/testing/factory/video-conference.factory'; -import { ServerTestModule } from '@src/modules/server'; +import { ServerTestModule } from '@modules/server'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { Response } from 'supertest'; diff --git a/apps/server/src/modules/video-conference/controller/video-conference-deprecated.controller.spec.ts b/apps/server/src/modules/video-conference/controller/video-conference-deprecated.controller.spec.ts index 84e78921410..6d78b513d75 100644 --- a/apps/server/src/modules/video-conference/controller/video-conference-deprecated.controller.spec.ts +++ b/apps/server/src/modules/video-conference/controller/video-conference-deprecated.controller.spec.ts @@ -2,7 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { VideoConferenceScope } from '@shared/domain/interface'; -import { ICurrentUser } from '@src/modules/authentication'; +import { ICurrentUser } from '@modules/authentication'; import { BBBBaseResponse, BBBCreateResponse } from '../bbb'; import { defaultVideoConferenceOptions } from '../interface'; import { VideoConferenceDeprecatedUc } from '../uc'; diff --git a/apps/server/src/modules/video-conference/controller/video-conference-deprecated.controller.ts b/apps/server/src/modules/video-conference/controller/video-conference-deprecated.controller.ts index 79c22cc21e1..8dbec66972e 100644 --- a/apps/server/src/modules/video-conference/controller/video-conference-deprecated.controller.ts +++ b/apps/server/src/modules/video-conference/controller/video-conference-deprecated.controller.ts @@ -11,8 +11,7 @@ import { } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; import { VideoConferenceScope } from '@shared/domain/interface'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; -import { ICurrentUser } from '@src/modules/authentication/interface'; +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { BBBBaseResponse } from '../bbb'; import { defaultVideoConferenceOptions } from '../interface'; import { VideoConferenceResponseDeprecatedMapper } from '../mapper/vc-deprecated-response.mapper'; diff --git a/apps/server/src/modules/video-conference/controller/video-conference.controller.ts b/apps/server/src/modules/video-conference/controller/video-conference.controller.ts index 884be5512cb..442e9132758 100644 --- a/apps/server/src/modules/video-conference/controller/video-conference.controller.ts +++ b/apps/server/src/modules/video-conference/controller/video-conference.controller.ts @@ -1,7 +1,6 @@ import { Body, Controller, Get, HttpStatus, Param, Put, Req } from '@nestjs/common'; import { ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger'; -import { ICurrentUser } from '@src/modules/authentication'; -import { Authenticate, CurrentUser } from '@src/modules/authentication/decorator/auth.decorator'; +import { Authenticate, CurrentUser, ICurrentUser } from '@modules/authentication'; import { Request } from 'express'; import { InvalidOriginForLogoutUrlLoggableException } from '../error'; import { VideoConferenceOptions } from '../interface'; diff --git a/apps/server/src/modules/video-conference/mapper/video-conference.mapper.ts b/apps/server/src/modules/video-conference/mapper/video-conference.mapper.ts index 727cfdfce7d..1f51a8cb3ff 100644 --- a/apps/server/src/modules/video-conference/mapper/video-conference.mapper.ts +++ b/apps/server/src/modules/video-conference/mapper/video-conference.mapper.ts @@ -1,5 +1,4 @@ -import { Permission, VideoConferenceScope } from '@shared/domain'; -import { AuthorizableReferenceType } from '@src/modules/authorization'; +import { Permission } from '@shared/domain'; import { BBBRole } from '../bbb'; import { VideoConferenceCreateParams, @@ -16,11 +15,6 @@ export const PermissionMapping = { [BBBRole.VIEWER]: Permission.JOIN_MEETING, }; -export const PermissionScopeMapping = { - [VideoConferenceScope.COURSE]: AuthorizableReferenceType.Course, - [VideoConferenceScope.EVENT]: AuthorizableReferenceType.Team, -}; - const stateMapping = { [VideoConferenceState.NOT_STARTED]: VideoConferenceStateResponse.NOT_STARTED, [VideoConferenceState.RUNNING]: VideoConferenceStateResponse.RUNNING, 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 b0c28b6a3c9..c7a1ed30668 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 @@ -14,20 +14,16 @@ import { } from '@shared/domain'; import { CalendarEventDto, CalendarService } from '@shared/infra/calendar'; import { TeamsRepo, VideoConferenceRepo } from '@shared/repo'; -import { - AuthorizableReferenceType, - AuthorizationContextBuilder, - AuthorizationService, -} from '@src/modules/authorization'; -import { LegacySchoolService } from '@src/modules/legacy-school'; -import { UserService } from '@src/modules/user'; -import { courseFactory, roleFactory, setupEntities, userDoFactory } from '@shared/testing'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; +import { courseFactory, roleFactory, setupEntities, userDoFactory, userFactory } from '@shared/testing'; import { videoConferenceDOFactory } from '@shared/testing/factory/video-conference.do.factory'; import { ObjectId } from 'bson'; import { teamFactory } from '@shared/testing/factory/team.factory'; import { NotFoundException } from '@nestjs/common/exceptions/not-found.exception'; import { teamUserFactory } from '@shared/testing/factory/teamuser.factory'; -import { CourseService } from '@src/modules/learnroom/service'; +import { CourseService } from '@modules/learnroom/service'; import { VideoConferenceService } from './video-conference.service'; import { ErrorStatus } from '../error'; import { BBBRole } from '../bbb'; @@ -332,64 +328,160 @@ describe('VideoConferenceService', () => { }); describe('checkPermission', () => { - const setup = () => { - const userId = 'user-id'; - const conferenceScope = VideoConferenceScope.COURSE; - const entityId = 'entity-id'; + describe('when user has START_MEETING permission and is in course scope', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const entity = courseFactory.buildWithId(); + const conferenceScope = VideoConferenceScope.COURSE; - return { - userId, - conferenceScope, - entityId, + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + authorizationService.hasPermission.mockReturnValueOnce(true).mockReturnValueOnce(false); + courseService.findById.mockResolvedValueOnce(entity); + + return { + user, + userId: user.id, + entity, + entityId: entity.id, + conferenceScope, + }; }; - }; - describe('when user has START_MEETING permission', () => { + it('should call the correct authorization order', async () => { + const { user, entity, userId, conferenceScope, entityId } = setup(); + + await service.determineBbbRole(userId, entityId, conferenceScope); + + expect(authorizationService.hasPermission).toHaveBeenCalledWith( + user, + entity, + AuthorizationContextBuilder.read([Permission.START_MEETING]) + ); + }); + it('should return BBBRole.MODERATOR', async () => { const { userId, conferenceScope, entityId } = setup(); - authorizationService.hasPermissionByReferences.mockResolvedValueOnce(true); - authorizationService.hasPermissionByReferences.mockResolvedValueOnce(false); - - const result: BBBRole = await service.determineBbbRole(userId, entityId, conferenceScope); + const result = await service.determineBbbRole(userId, entityId, conferenceScope); expect(result).toBe(BBBRole.MODERATOR); - expect(authorizationService.hasPermissionByReferences).toHaveBeenCalledWith( - userId, - AuthorizableReferenceType.Course, - entityId, + }); + }); + + // can be removed when team / course / user is passed from UC + // missing when course / team loading throw an error, but also not nessasary if it is passed to UC. + describe('when user has START_MEETING permission and is in team(event) scope', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const entity = teamFactory.buildWithId(); + const conferenceScope = VideoConferenceScope.EVENT; + + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + authorizationService.hasPermission.mockReturnValueOnce(true).mockReturnValueOnce(false); + teamsRepo.findById.mockResolvedValueOnce(entity); + + return { + user, + userId: user.id, + entity, + entityId: entity.id, + conferenceScope, + }; + }; + + it('should call the correct authorization order', async () => { + const { user, entity, userId, conferenceScope, entityId } = setup(); + + await service.determineBbbRole(userId, entityId, conferenceScope); + + expect(authorizationService.hasPermission).toHaveBeenCalledWith( + user, + entity, AuthorizationContextBuilder.read([Permission.START_MEETING]) ); }); + + it('should return BBBRole.MODERATOR', async () => { + const { userId, conferenceScope, entityId } = setup(); + + const result = await service.determineBbbRole(userId, entityId, conferenceScope); + + expect(result).toBe(BBBRole.MODERATOR); + }); }); describe('when user has JOIN_MEETING permission', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const entity = courseFactory.buildWithId(); + const conferenceScope = VideoConferenceScope.COURSE; + + authorizationService.hasPermission.mockReturnValueOnce(false).mockReturnValueOnce(true); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + courseService.findById.mockResolvedValueOnce(entity); + + return { + user, + userId: user.id, + entity, + entityId: entity.id, + conferenceScope, + }; + }; + + it('should call the correct authorization order', async () => { + const { user, entity, userId, conferenceScope, entityId } = setup(); + + await service.determineBbbRole(userId, entityId, conferenceScope); + + expect(authorizationService.hasPermission).toHaveBeenNthCalledWith( + 1, + user, + entity, + AuthorizationContextBuilder.read([Permission.START_MEETING]) + ); + expect(authorizationService.hasPermission).toHaveBeenNthCalledWith( + 2, + user, + entity, + AuthorizationContextBuilder.read([Permission.JOIN_MEETING]) + ); + }); + it('should return BBBRole.VIEWER', async () => { const { userId, conferenceScope, entityId } = setup(); - authorizationService.hasPermissionByReferences.mockResolvedValueOnce(false); - authorizationService.hasPermissionByReferences.mockResolvedValueOnce(true); - const result: BBBRole = await service.determineBbbRole(userId, entityId, conferenceScope); + const result = await service.determineBbbRole(userId, entityId, conferenceScope); expect(result).toBe(BBBRole.VIEWER); - expect(authorizationService.hasPermissionByReferences).toHaveBeenCalledWith( - userId, - AuthorizableReferenceType.Course, - entityId, - AuthorizationContextBuilder.read([Permission.JOIN_MEETING]) - ); }); }); describe('when user has neither START_MEETING nor JOIN_MEETING permission', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const entity = courseFactory.buildWithId(); + const conferenceScope = VideoConferenceScope.COURSE; + + authorizationService.hasPermission.mockReturnValueOnce(false).mockReturnValueOnce(false); + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + courseService.findById.mockResolvedValueOnce(entity); + + return { + user, + userId: user.id, + entity, + entityId: entity.id, + conferenceScope, + }; + }; + it('should throw a ForbiddenException', async () => { const { userId, conferenceScope, entityId } = setup(); - authorizationService.hasPermissionByReferences.mockResolvedValueOnce(false); - authorizationService.hasPermissionByReferences.mockResolvedValueOnce(false); - const func = () => service.determineBbbRole(userId, entityId, conferenceScope); + const callDetermineBbbRole = () => service.determineBbbRole(userId, entityId, conferenceScope); - await expect(func).rejects.toThrow(new ForbiddenException(ErrorStatus.INSUFFICIENT_PERMISSION)); + await expect(callDetermineBbbRole).rejects.toThrow(new ForbiddenException(ErrorStatus.INSUFFICIENT_PERMISSION)); }); }); }); 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 0201b150f33..22e7a7462f1 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 @@ -8,6 +8,7 @@ import { SchoolFeatures, TeamEntity, TeamUserEntity, + User, UserDO, VideoConferenceDO, VideoConferenceOptionsDO, @@ -15,19 +16,13 @@ import { } from '@shared/domain'; import { CalendarEventDto, CalendarService } from '@shared/infra/calendar'; import { TeamsRepo, VideoConferenceRepo } from '@shared/repo'; -import { - Action, - AuthorizableReferenceType, - AuthorizationContextBuilder, - AuthorizationService, -} from '@src/modules/authorization'; -import { CourseService } from '@src/modules/learnroom/service'; -import { LegacySchoolService } from '@src/modules/legacy-school'; -import { UserService } from '@src/modules/user'; +import { AuthorizationContextBuilder, AuthorizationService } from '@modules/authorization'; +import { CourseService } from '@modules/learnroom'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; import { BBBRole } from '../bbb'; import { ErrorStatus } from '../error'; import { IVideoConferenceSettings, VideoConferenceOptions, VideoConferenceSettings } from '../interface'; -import { PermissionScopeMapping } from '../mapper/video-conference.mapper'; import { IScopeInfo, VideoConferenceState } from '../uc/dto'; @Injectable() @@ -97,39 +92,59 @@ export class VideoConferenceService { return isExpert; } - async determineBbbRole(userId: EntityId, scopeId: EntityId, scope: VideoConferenceScope): Promise { - const permissionMap: Map> = this.hasPermissions( - userId, - PermissionScopeMapping[scope], - scopeId, - [Permission.START_MEETING, Permission.JOIN_MEETING], - Action.read - ); - - if (await permissionMap.get(Permission.START_MEETING)) { - return BBBRole.MODERATOR; - } - if (await permissionMap.get(Permission.JOIN_MEETING)) { - return BBBRole.VIEWER; + // should be public to expose ressources to UC for passing it to authrisation and improve performance + private async loadScopeRessources( + scopeId: EntityId, + scope: VideoConferenceScope + ): Promise { + let scopeRessource: Course | TeamEntity | null = null; + + if (scope === VideoConferenceScope.COURSE) { + scopeRessource = await this.courseService.findById(scopeId); + } else if (scope === VideoConferenceScope.EVENT) { + scopeRessource = await this.teamsRepo.findById(scopeId); + } else { + // Need to be solve the null with throw by it self. } - throw new ForbiddenException(ErrorStatus.INSUFFICIENT_PERMISSION); + + return scopeRessource; + } + + private isNullOrUndefined(value: unknown): value is null { + return !value; } - private hasPermissions( - userId: EntityId, - entityName: AuthorizableReferenceType, - entityId: EntityId, - permissions: Permission[], - action: Action - ): Map> { - const returnMap: Map> = new Map(); - permissions.forEach((perm) => { - const context = - action === Action.read ? AuthorizationContextBuilder.read([perm]) : AuthorizationContextBuilder.write([perm]); - const ret = this.authorizationService.hasPermissionByReferences(userId, entityName, entityId, context); - returnMap.set(perm, ret); - }); - return returnMap; + private hasStartMeetingAndCanRead(authorizableUser: User, entity: Course | TeamEntity): boolean { + const context = AuthorizationContextBuilder.read([Permission.START_MEETING]); + const hasPermission = this.authorizationService.hasPermission(authorizableUser, entity, context); + + return hasPermission; + } + + private hasJoinMeetingAndCanRead(authorizableUser: User, entity: Course | TeamEntity): boolean { + const context = AuthorizationContextBuilder.read([Permission.JOIN_MEETING]); + const hasPermission = this.authorizationService.hasPermission(authorizableUser, entity, context); + + return hasPermission; + } + + async determineBbbRole(userId: EntityId, scopeId: EntityId, scope: VideoConferenceScope): Promise { + // ressource loading need to be move to uc + const [authorizableUser, scopeRessource]: [User, TeamEntity | Course | null] = await Promise.all([ + this.authorizationService.getUserWithPermissions(userId), + this.loadScopeRessources(scopeId, scope), + ]); + + if (!this.isNullOrUndefined(scopeRessource)) { + if (this.hasStartMeetingAndCanRead(authorizableUser, scopeRessource)) { + return BBBRole.MODERATOR; + } + if (this.hasJoinMeetingAndCanRead(authorizableUser, scopeRessource)) { + return BBBRole.VIEWER; + } + } + + throw new ForbiddenException(ErrorStatus.INSUFFICIENT_PERMISSION); } async throwOnFeaturesDisabled(schoolId: EntityId): Promise { diff --git a/apps/server/src/modules/video-conference/uc/video-conference-create.uc.spec.ts b/apps/server/src/modules/video-conference/uc/video-conference-create.uc.spec.ts index 7570d6d38ff..4c38bf18467 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-create.uc.spec.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-create.uc.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { UserService } from '@src/modules/user'; +import { UserService } from '@modules/user'; import { userDoFactory } from '@shared/testing'; import { UserDO, VideoConferenceScope } from '@shared/domain'; import { ObjectId } from 'bson'; diff --git a/apps/server/src/modules/video-conference/uc/video-conference-create.uc.ts b/apps/server/src/modules/video-conference/uc/video-conference-create.uc.ts index e7a28c9a7ca..940113276a2 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-create.uc.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-create.uc.ts @@ -1,6 +1,6 @@ import { ForbiddenException, Injectable } from '@nestjs/common'; import { EntityId, UserDO } from '@shared/domain'; -import { UserService } from '@src/modules/user'; +import { UserService } from '@modules/user'; import { BBBBaseMeetingConfig, BBBCreateConfigBuilder, @@ -38,6 +38,12 @@ export class VideoConferenceCreateUc { } private async create(currentUserId: EntityId, scope: ScopeRef, options: VideoConferenceOptions): Promise { + /* need to be replace with + const [authorizableUser, scopeRessource]: [User, TeamEntity | Course] = await Promise.all([ + this.authorizationService.getUserWithPermissions(userId), + this.videoConferenceService.loadScopeRessources(scopeId, scope), + ]); + */ const user: UserDO = await this.userService.findById(currentUserId); await this.verifyFeaturesEnabled(user.schoolId); 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 3680c4519da..4d15397548b 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,9 +19,11 @@ 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 { ICurrentUser } from '@src/modules/authentication'; -import { CourseService } from '@src/modules/learnroom/service'; +import { AuthorizationReferenceService } from '@modules/authorization/domain'; +import { ICurrentUser } from '@modules/authentication'; +import { CourseService } from '@modules/learnroom/service'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { UserService } from '@modules/user'; import { IScopeInfo, VideoConference, VideoConferenceJoin, VideoConferenceState } from './dto'; import { VideoConferenceDeprecatedUc } from './video-conference-deprecated.uc'; import { @@ -63,7 +65,7 @@ describe('VideoConferenceUc', () => { let useCase: VideoConferenceDeprecatedUcSpec; let bbbService: DeepMocked; - let authorizationService: DeepMocked; + let authorizationService: DeepMocked; let videoConferenceRepo: DeepMocked; let teamsRepo: DeepMocked; let courseService: DeepMocked; @@ -118,8 +120,8 @@ describe('VideoConferenceUc', () => { useValue: createMock(), }, { - provide: AuthorizationService, - useValue: createMock(), + provide: AuthorizationReferenceService, + useValue: createMock(), }, { provide: VideoConferenceRepo, @@ -149,7 +151,7 @@ describe('VideoConferenceUc', () => { }).compile(); useCase = module.get(VideoConferenceDeprecatedUcSpec); schoolService = module.get(LegacySchoolService); - authorizationService = module.get(AuthorizationService); + authorizationService = module.get(AuthorizationReferenceService); courseService = module.get(CourseService); calendarService = module.get(CalendarService); videoConferenceRepo = module.get(VideoConferenceRepo); 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 7bce5b2f5a4..41e011c4acd 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 @@ -17,16 +17,12 @@ import { CalendarService } from '@shared/infra/calendar'; import { CalendarEventDto } from '@shared/infra/calendar/dto/calendar-event.dto'; import { TeamsRepo } from '@shared/repo'; import { VideoConferenceRepo } from '@shared/repo/videoconference/video-conference.repo'; -import { ICurrentUser } from '@src/modules/authentication'; -import { - Action, - AuthorizableReferenceType, - AuthorizationContextBuilder, - AuthorizationService, -} from '@src/modules/authorization'; -import { CourseService } from '@src/modules/learnroom/service'; -import { LegacySchoolService } from '@src/modules/legacy-school'; -import { UserService } from '@src/modules/user'; +import { ICurrentUser } from '@modules/authentication'; +import { Action, AuthorizationContextBuilder } from '@modules/authorization'; +import { AuthorizationReferenceService, AuthorizableReferenceType } from '@modules/authorization/domain'; +import { LegacySchoolService } from '@modules/legacy-school'; +import { CourseService } from '@modules/learnroom'; +import { UserService } from '@modules/user'; import { BBBBaseMeetingConfig, BBBBaseResponse, @@ -62,7 +58,7 @@ export class VideoConferenceDeprecatedUc { constructor( private readonly bbbService: BBBService, - private readonly authorizationService: AuthorizationService, + private readonly authorizationReferenceService: AuthorizationReferenceService, private readonly videoConferenceRepo: VideoConferenceRepo, private readonly teamsRepo: TeamsRepo, private readonly courseService: CourseService, @@ -413,7 +409,7 @@ export class VideoConferenceDeprecatedUc { permissions.forEach((perm) => { const context = action === Action.read ? AuthorizationContextBuilder.read([perm]) : AuthorizationContextBuilder.write([perm]); - const ret = this.authorizationService.hasPermissionByReferences(userId, entityName, entityId, context); + const ret = this.authorizationReferenceService.hasPermissionByReferences(userId, entityName, entityId, context); returnMap.set(perm, ret); }); return returnMap; diff --git a/apps/server/src/modules/video-conference/uc/video-conference-end.uc.spec.ts b/apps/server/src/modules/video-conference/uc/video-conference-end.uc.spec.ts index 2b5744b4d79..d1dbfc18b9d 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-end.uc.spec.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-end.uc.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { UserService } from '@src/modules/user'; +import { UserService } from '@modules/user'; import { userDoFactory } from '@shared/testing'; import { UserDO, VideoConferenceScope } from '@shared/domain'; import { ObjectId } from 'bson'; diff --git a/apps/server/src/modules/video-conference/uc/video-conference-end.uc.ts b/apps/server/src/modules/video-conference/uc/video-conference-end.uc.ts index a9799f67a89..6cb70edc176 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-end.uc.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-end.uc.ts @@ -1,7 +1,7 @@ import { ForbiddenException, Injectable } from '@nestjs/common'; import { EntityId, UserDO } from '@shared/domain'; -import { ErrorStatus } from '@src/modules/video-conference/error/error-status.enum'; -import { UserService } from '@src/modules/user'; +import { ErrorStatus } from '@modules/video-conference/error/error-status.enum'; +import { UserService } from '@modules/user'; import { BBBBaseMeetingConfig, BBBBaseResponse, BBBResponse, BBBRole, BBBService } from '../bbb'; import { IScopeInfo, ScopeRef, VideoConference, VideoConferenceState } from './dto'; import { VideoConferenceService } from '../service'; @@ -16,6 +16,12 @@ export class VideoConferenceEndUc { ) {} async end(currentUserId: EntityId, scope: ScopeRef): Promise> { + /* need to be replace with + const [authorizableUser, scopeRessource]: [User, TeamEntity | Course] = await Promise.all([ + this.authorizationService.getUserWithPermissions(userId), + this.videoConferenceService.loadScopeRessources(scopeId, scope), + ]); + */ const user: UserDO = await this.userService.findById(currentUserId); const userId: string = user.id as string; diff --git a/apps/server/src/modules/video-conference/uc/video-conference-info.uc.spec.ts b/apps/server/src/modules/video-conference/uc/video-conference-info.uc.spec.ts index 1d044c7b9db..ebc8daa4084 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-info.uc.spec.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-info.uc.spec.ts @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { UserService } from '@src/modules/user'; +import { UserService } from '@modules/user'; import { userDoFactory } from '@shared/testing'; import { Permission, UserDO, VideoConferenceDO, VideoConferenceScope } from '@shared/domain'; import { ObjectId } from 'bson'; diff --git a/apps/server/src/modules/video-conference/uc/video-conference-info.uc.ts b/apps/server/src/modules/video-conference/uc/video-conference-info.uc.ts index 91ebb23ea2b..1c3736d1d01 100644 --- a/apps/server/src/modules/video-conference/uc/video-conference-info.uc.ts +++ b/apps/server/src/modules/video-conference/uc/video-conference-info.uc.ts @@ -1,7 +1,7 @@ import { ForbiddenException, Injectable } from '@nestjs/common'; import { EntityId, UserDO, VideoConferenceDO, VideoConferenceOptionsDO } from '@shared/domain'; -import { ErrorStatus } from '@src/modules/video-conference/error/error-status.enum'; -import { UserService } from '@src/modules/user'; +import { ErrorStatus } from '@modules/video-conference/error/error-status.enum'; +import { UserService } from '@modules/user'; import { BBBBaseMeetingConfig, BBBMeetingInfoResponse, BBBResponse, BBBRole, BBBService } from '../bbb'; import { IScopeInfo, ScopeRef, VideoConferenceInfo, VideoConferenceState } from './dto'; import { VideoConferenceService } from '../service'; @@ -17,6 +17,12 @@ export class VideoConferenceInfoUc { ) {} async getMeetingInfo(currentUserId: EntityId, scope: ScopeRef): Promise { + /* need to be replace with + const [authorizableUser, scopeRessource]: [User, TeamEntity | Course] = await Promise.all([ + this.authorizationService.getUserWithPermissions(userId), + this.videoConferenceService.loadScopeRessources(scopeId, scope), + ]); + */ const user: UserDO = await this.userService.findById(currentUserId); await this.videoConferenceService.throwOnFeaturesDisabled(user.schoolId); 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 0f46fcde450..525a43b4c69 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 @@ -1,6 +1,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { UserService } from '@src/modules/user'; +import { UserService } from '@modules/user'; import { userDoFactory } from '@shared/testing'; import { Permission, UserDO, VideoConferenceDO, VideoConferenceScope } from '@shared/domain'; import { ObjectId } from 'bson'; 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 5728a4b0da2..2015c20d381 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 @@ -1,7 +1,7 @@ import { ForbiddenException, Injectable } from '@nestjs/common'; import { EntityId, UserDO, VideoConferenceDO } from '@shared/domain'; -import { ErrorStatus } from '@src/modules/video-conference/error/error-status.enum'; -import { UserService } from '@src/modules/user'; +import { ErrorStatus } from '@modules/video-conference/error/error-status.enum'; +import { UserService } from '@modules/user'; import { BBBJoinConfigBuilder, BBBRole, BBBService } from '../bbb'; import { ScopeRef, VideoConferenceJoin, VideoConferenceState } from './dto'; import { VideoConferenceService } from '../service'; diff --git a/apps/server/src/modules/video-conference/video-conference-api.module.ts b/apps/server/src/modules/video-conference/video-conference-api.module.ts index 4c2448d55d2..d4594a10a34 100644 --- a/apps/server/src/modules/video-conference/video-conference-api.module.ts +++ b/apps/server/src/modules/video-conference/video-conference-api.module.ts @@ -1,6 +1,6 @@ import { Module } from '@nestjs/common'; -import { UserModule } from '@src/modules/user'; -import { AuthorizationModule } from '@src/modules/authorization'; +import { UserModule } from '@modules/user'; +import { AuthorizationModule } from '@modules/authorization'; import { VideoConferenceController } from './controller'; import { VideoConferenceCreateUc, VideoConferenceJoinUc, VideoConferenceEndUc, VideoConferenceInfoUc } from './uc'; import { VideoConferenceModule } from './video-conference.module'; diff --git a/apps/server/src/modules/video-conference/video-conference.module.ts b/apps/server/src/modules/video-conference/video-conference.module.ts index 70a999437c0..c9708b16dc9 100644 --- a/apps/server/src/modules/video-conference/video-conference.module.ts +++ b/apps/server/src/modules/video-conference/video-conference.module.ts @@ -2,12 +2,13 @@ import { Module } from '@nestjs/common'; import { HttpModule } from '@nestjs/axios'; import { CalendarModule } from '@shared/infra/calendar'; import { VideoConferenceRepo } from '@shared/repo/videoconference/video-conference.repo'; -import { AuthorizationModule } from '@src/modules/authorization'; +import { AuthorizationModule } from '@modules/authorization'; +import { AuthorizationReferenceModule } from '@modules/authorization/authorization-reference.module'; import { TeamsRepo } from '@shared/repo'; -import { LegacySchoolModule } from '@src/modules/legacy-school'; +import { LegacySchoolModule } from '@modules/legacy-school'; import { LoggerModule } from '@src/core/logger'; import { ConverterUtil } from '@shared/common'; -import { UserModule } from '@src/modules/user'; +import { UserModule } from '@modules/user'; import { BBBService, BbbSettings } from './bbb'; import { VideoConferenceService } from './service'; import { VideoConferenceDeprecatedUc } from './uc'; @@ -19,6 +20,7 @@ import { LearnroomModule } from '../learnroom'; @Module({ imports: [ AuthorizationModule, + AuthorizationReferenceModule, // can be removed wenn video-conference-deprecated is removed CalendarModule, HttpModule, LegacySchoolModule, diff --git a/apps/server/src/shared/common/interceptor/request-logging.interceptor.ts b/apps/server/src/shared/common/interceptor/request-logging.interceptor.ts index bfcb738847a..838ae020039 100644 --- a/apps/server/src/shared/common/interceptor/request-logging.interceptor.ts +++ b/apps/server/src/shared/common/interceptor/request-logging.interceptor.ts @@ -3,7 +3,7 @@ import { LegacyLogger, RequestLoggingBody } from '@src/core/logger'; import { Request } from 'express'; import { Observable, throwError } from 'rxjs'; import { catchError, tap } from 'rxjs/operators'; -import { ICurrentUser } from '@src/modules/authentication/interface/user'; +import { ICurrentUser } from '@modules/authentication/interface/user'; @Injectable() export class RequestLoggingInterceptor implements NestInterceptor { diff --git a/apps/server/src/shared/controller/swagger.spec.ts b/apps/server/src/shared/controller/swagger.spec.ts index 74236b00811..413a3fa3a9c 100644 --- a/apps/server/src/shared/controller/swagger.spec.ts +++ b/apps/server/src/shared/controller/swagger.spec.ts @@ -1,6 +1,6 @@ import { INestApplication } from '@nestjs/common'; import { NestFactory } from '@nestjs/core'; -import { ServerTestModule } from '@src/modules/server'; +import { ServerTestModule } from '@modules/server'; import request from 'supertest'; import { enableOpenApiDocs } from './swagger'; diff --git a/apps/server/src/shared/domain/domainobject/board/card.do.ts b/apps/server/src/shared/domain/domainobject/board/card.do.ts index 652d30ff027..62931e418dd 100644 --- a/apps/server/src/shared/domain/domainobject/board/card.do.ts +++ b/apps/server/src/shared/domain/domainobject/board/card.do.ts @@ -1,6 +1,7 @@ import { BoardComposite, BoardCompositeProps } from './board-composite.do'; import { ExternalToolElement } from './external-tool-element.do'; import { FileElement } from './file-element.do'; +import { LinkElement } from './link-element.do'; import { RichTextElement } from './rich-text-element.do'; import { SubmissionContainerElement } from './submission-container-element.do'; import type { AnyBoardDo, BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; @@ -25,6 +26,7 @@ export class Card extends BoardComposite { isAllowedAsChild(domainObject: AnyBoardDo): boolean { const allowed = domainObject instanceof FileElement || + domainObject instanceof LinkElement || domainObject instanceof RichTextElement || domainObject instanceof SubmissionContainerElement || domainObject instanceof ExternalToolElement; diff --git a/apps/server/src/shared/domain/domainobject/board/content-element.factory.ts b/apps/server/src/shared/domain/domainobject/board/content-element.factory.ts index fb476d2dbd0..8c34ca54b56 100644 --- a/apps/server/src/shared/domain/domainobject/board/content-element.factory.ts +++ b/apps/server/src/shared/domain/domainobject/board/content-element.factory.ts @@ -3,6 +3,7 @@ import { InputFormat } from '@shared/domain/types'; import { ObjectId } from 'bson'; import { ExternalToolElement } from './external-tool-element.do'; import { FileElement } from './file-element.do'; +import { LinkElement } from './link-element.do'; import { RichTextElement } from './rich-text-element.do'; import { SubmissionContainerElement } from './submission-container-element.do'; import { AnyContentElementDo, ContentElementType } from './types'; @@ -16,6 +17,9 @@ export class ContentElementFactory { case ContentElementType.FILE: element = this.buildFile(); break; + case ContentElementType.LINK: + element = this.buildLink(); + break; case ContentElementType.RICH_TEXT: element = this.buildRichText(); break; @@ -49,6 +53,18 @@ export class ContentElementFactory { return element; } + private buildLink() { + const element = new LinkElement({ + id: new ObjectId().toHexString(), + url: '', + title: '', + createdAt: new Date(), + updatedAt: new Date(), + }); + + return element; + } + private buildRichText() { const element = new RichTextElement({ id: new ObjectId().toHexString(), @@ -65,6 +81,7 @@ export class ContentElementFactory { private buildSubmissionContainer() { const element = new SubmissionContainerElement({ id: new ObjectId().toHexString(), + dueDate: null, children: [], createdAt: new Date(), updatedAt: new Date(), diff --git a/apps/server/src/shared/domain/domainobject/board/index.ts b/apps/server/src/shared/domain/domainobject/board/index.ts index 86f4d2639c3..9701ba40099 100644 --- a/apps/server/src/shared/domain/domainobject/board/index.ts +++ b/apps/server/src/shared/domain/domainobject/board/index.ts @@ -3,10 +3,11 @@ export * from './card.do'; export * from './column-board.do'; export * from './column.do'; export * from './content-element.factory'; +export * from './external-tool-element.do'; export * from './file-element.do'; +export * from './link-element.do'; export * from './rich-text-element.do'; export * from './submission-container-element.do'; export * from './submission-item.do'; export * from './submission-item.factory'; -export * from './external-tool-element.do'; export * from './types'; diff --git a/apps/server/src/shared/domain/domainobject/board/link-element.do.spec.ts b/apps/server/src/shared/domain/domainobject/board/link-element.do.spec.ts new file mode 100644 index 00000000000..4a044e9be58 --- /dev/null +++ b/apps/server/src/shared/domain/domainobject/board/link-element.do.spec.ts @@ -0,0 +1,37 @@ +import { createMock } from '@golevelup/ts-jest'; +import { linkElementFactory } from '@shared/testing'; +import { LinkElement } from './link-element.do'; +import { BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; + +describe(LinkElement.name, () => { + describe('when trying to add a child to a link element', () => { + it('should throw an error ', () => { + const linkElement = linkElementFactory.build(); + const linkElementFactoryChild = linkElementFactory.build(); + + expect(() => linkElement.addChild(linkElementFactoryChild)).toThrow(); + }); + }); + + describe('accept', () => { + it('should call the right visitor method', () => { + const visitor = createMock(); + const linkElement = linkElementFactory.build(); + + linkElement.accept(visitor); + + expect(visitor.visitLinkElement).toHaveBeenCalledWith(linkElement); + }); + }); + + describe('acceptAsync', () => { + it('should call the right async visitor method', async () => { + const visitor = createMock(); + const linkElement = linkElementFactory.build(); + + await linkElement.acceptAsync(visitor); + + expect(visitor.visitLinkElementAsync).toHaveBeenCalledWith(linkElement); + }); + }); +}); diff --git a/apps/server/src/shared/domain/domainobject/board/link-element.do.ts b/apps/server/src/shared/domain/domainobject/board/link-element.do.ts new file mode 100644 index 00000000000..7b38cbd938e --- /dev/null +++ b/apps/server/src/shared/domain/domainobject/board/link-element.do.ts @@ -0,0 +1,59 @@ +import { BoardComposite, BoardCompositeProps } from './board-composite.do'; +import type { BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; + +export class LinkElement extends BoardComposite { + get url(): string { + return this.props.url ?? ''; + } + + set url(value: string) { + this.props.url = value; + } + + get title(): string { + return this.props.title ?? ''; + } + + set title(value: string) { + this.props.title = value; + } + + get description(): string { + return this.props.description ?? ''; + } + + set description(value: string) { + this.props.description = value ?? ''; + } + + get imageUrl(): string { + return this.props.imageUrl ?? ''; + } + + set imageUrl(value: string) { + this.props.imageUrl = value; + } + + isAllowedAsChild(): boolean { + return false; + } + + accept(visitor: BoardCompositeVisitor): void { + visitor.visitLinkElement(this); + } + + async acceptAsync(visitor: BoardCompositeVisitorAsync): Promise { + await visitor.visitLinkElementAsync(this); + } +} + +export interface LinkElementProps extends BoardCompositeProps { + url: string; + title: string; + description?: string; + imageUrl?: string; +} + +export function isLinkElement(reference: unknown): reference is LinkElement { + return reference instanceof LinkElement; +} diff --git a/apps/server/src/shared/domain/domainobject/board/submission-container-element.do.ts b/apps/server/src/shared/domain/domainobject/board/submission-container-element.do.ts index 09756153a90..0980eb7a569 100644 --- a/apps/server/src/shared/domain/domainobject/board/submission-container-element.do.ts +++ b/apps/server/src/shared/domain/domainobject/board/submission-container-element.do.ts @@ -3,11 +3,11 @@ import { SubmissionItem } from './submission-item.do'; import type { AnyBoardDo, BoardCompositeVisitor, BoardCompositeVisitorAsync } from './types'; export class SubmissionContainerElement extends BoardComposite { - get dueDate(): Date | undefined { + get dueDate(): Date | null { return this.props.dueDate; } - set dueDate(value: Date | undefined) { + set dueDate(value: Date | null) { this.props.dueDate = value; } @@ -26,7 +26,7 @@ export class SubmissionContainerElement extends BoardComposite { const result = + element instanceof ExternalToolElement || element instanceof FileElement || + element instanceof LinkElement || element instanceof RichTextElement || - element instanceof SubmissionContainerElement || - element instanceof ExternalToolElement; + element instanceof SubmissionContainerElement; return result; }; diff --git a/apps/server/src/shared/domain/domainobject/board/types/board-composite-visitor.ts b/apps/server/src/shared/domain/domainobject/board/types/board-composite-visitor.ts index 38e16fc8e5f..3fbd4abdd96 100644 --- a/apps/server/src/shared/domain/domainobject/board/types/board-composite-visitor.ts +++ b/apps/server/src/shared/domain/domainobject/board/types/board-composite-visitor.ts @@ -1,17 +1,19 @@ import type { Card } from '../card.do'; import type { ColumnBoard } from '../column-board.do'; import type { Column } from '../column.do'; -import { ExternalToolElement } from '../external-tool-element.do'; +import type { ExternalToolElement } from '../external-tool-element.do'; import type { FileElement } from '../file-element.do'; -import { RichTextElement } from '../rich-text-element.do'; -import { SubmissionContainerElement } from '../submission-container-element.do'; -import { SubmissionItem } from '../submission-item.do'; +import type { LinkElement } from '../link-element.do'; +import type { RichTextElement } from '../rich-text-element.do'; +import type { SubmissionContainerElement } from '../submission-container-element.do'; +import type { SubmissionItem } from '../submission-item.do'; export interface BoardCompositeVisitor { visitColumnBoard(columnBoard: ColumnBoard): void; visitColumn(column: Column): void; visitCard(card: Card): void; visitFileElement(fileElement: FileElement): void; + visitLinkElement(linkElement: LinkElement): void; visitRichTextElement(richTextElement: RichTextElement): void; visitSubmissionContainerElement(submissionContainerElement: SubmissionContainerElement): void; visitSubmissionItem(submissionItem: SubmissionItem): void; @@ -23,6 +25,7 @@ export interface BoardCompositeVisitorAsync { visitColumnAsync(column: Column): Promise; visitCardAsync(card: Card): Promise; visitFileElementAsync(fileElement: FileElement): Promise; + visitLinkElementAsync(linkElement: LinkElement): Promise; visitRichTextElementAsync(richTextElement: RichTextElement): Promise; visitSubmissionContainerElementAsync(submissionContainerElement: SubmissionContainerElement): Promise; visitSubmissionItemAsync(submissionItem: SubmissionItem): Promise; diff --git a/apps/server/src/shared/domain/domainobject/board/types/content-elements.enum.ts b/apps/server/src/shared/domain/domainobject/board/types/content-elements.enum.ts index 4c6ce7269bd..b8d4e166e25 100644 --- a/apps/server/src/shared/domain/domainobject/board/types/content-elements.enum.ts +++ b/apps/server/src/shared/domain/domainobject/board/types/content-elements.enum.ts @@ -1,5 +1,6 @@ export enum ContentElementType { FILE = 'file', + LINK = 'link', RICH_TEXT = 'richText', SUBMISSION_CONTAINER = 'submissionContainer', EXTERNAL_TOOL = 'externalTool', diff --git a/apps/server/src/shared/domain/entity/all-entities.ts b/apps/server/src/shared/domain/entity/all-entities.ts index 406fda13bf4..9dc33c55b78 100644 --- a/apps/server/src/shared/domain/entity/all-entities.ts +++ b/apps/server/src/shared/domain/entity/all-entities.ts @@ -1,10 +1,10 @@ -import { ClassEntity } from '@src/modules/class/entity'; -import { GroupEntity } from '@src/modules/group/entity'; -import { ExternalToolPseudonymEntity, PseudonymEntity } from '@src/modules/pseudonym/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 '@modules/class/entity'; +import { GroupEntity } from '@modules/group/entity'; +import { ExternalToolPseudonymEntity, PseudonymEntity } from '@modules/pseudonym/entity'; +import { ShareToken } from '@modules/sharing/entity/share-token.entity'; +import { ContextExternalToolEntity } from '@modules/tool/context-external-tool/entity'; +import { ExternalToolEntity } from '@modules/tool/external-tool/entity'; +import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; import { Account } from './account.entity'; import { BoardNode, @@ -13,6 +13,7 @@ import { ColumnNode, ExternalToolElementNodeEntity, FileElementNode, + LinkElementNode, RichTextElementNode, SubmissionContainerElementNode, SubmissionItemNode, @@ -58,6 +59,7 @@ export const ALL_ENTITIES = [ ColumnNode, ClassEntity, FileElementNode, + LinkElementNode, RichTextElementNode, SubmissionContainerElementNode, SubmissionItemNode, diff --git a/apps/server/src/shared/domain/entity/boardnode/external-tool-element-node.entity.ts b/apps/server/src/shared/domain/entity/boardnode/external-tool-element-node.entity.ts index 68df0b696b5..ffe2ef83bec 100644 --- a/apps/server/src/shared/domain/entity/boardnode/external-tool-element-node.entity.ts +++ b/apps/server/src/shared/domain/entity/boardnode/external-tool-element-node.entity.ts @@ -1,6 +1,6 @@ import { Entity, ManyToOne } from '@mikro-orm/core'; import { AnyBoardDo } from '@shared/domain/domainobject'; -import { ContextExternalToolEntity } from '@src/modules/tool/context-external-tool/entity/context-external-tool.entity'; +import { ContextExternalToolEntity } from '@modules/tool/context-external-tool/entity/context-external-tool.entity'; import { BoardNode, BoardNodeProps } from './boardnode.entity'; import { BoardDoBuilder, BoardNodeType } from './types'; diff --git a/apps/server/src/shared/domain/entity/boardnode/index.ts b/apps/server/src/shared/domain/entity/boardnode/index.ts index cd7cc9d65be..a3a56e6dfe0 100644 --- a/apps/server/src/shared/domain/entity/boardnode/index.ts +++ b/apps/server/src/shared/domain/entity/boardnode/index.ts @@ -2,9 +2,10 @@ export * from './boardnode.entity'; export * from './card-node.entity'; export * from './column-board-node.entity'; export * from './column-node.entity'; +export * from './external-tool-element-node.entity'; export * from './file-element-node.entity'; +export * from './link-element-node.entity'; export * from './rich-text-element-node.entity'; export * from './submission-container-element-node.entity'; export * from './submission-item-node.entity'; -export * from './external-tool-element-node.entity'; export * from './types'; diff --git a/apps/server/src/shared/domain/entity/boardnode/link-element-node.entity.spec.ts b/apps/server/src/shared/domain/entity/boardnode/link-element-node.entity.spec.ts new file mode 100644 index 00000000000..1093e57922e --- /dev/null +++ b/apps/server/src/shared/domain/entity/boardnode/link-element-node.entity.spec.ts @@ -0,0 +1,54 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { linkElementFactory } from '@shared/testing'; +import { LinkElementNode } from './link-element-node.entity'; +import { BoardDoBuilder, BoardNodeType } from './types'; + +describe(LinkElementNode.name, () => { + describe('when trying to create a link element', () => { + const setup = () => { + const elementProps = { url: 'https://www.any-fake.url/that-is-linked.html', title: 'A Great WebPage' }; + const builder: DeepMocked = createMock(); + + return { elementProps, builder }; + }; + + it('should create a LinkElementNode', () => { + const { elementProps } = setup(); + + const element = new LinkElementNode(elementProps); + + expect(element.type).toEqual(BoardNodeType.LINK_ELEMENT); + }); + }); + + describe('useDoBuilder()', () => { + const setup = () => { + const element = new LinkElementNode({ + url: 'https://www.any-fake.url/that-is-linked.html', + title: 'A Great WebPage', + }); + const builder: DeepMocked = createMock(); + const elementDo = linkElementFactory.build(); + + builder.buildLinkElement.mockReturnValue(elementDo); + + return { element, builder, elementDo }; + }; + + it('should call the specific builder method', () => { + const { element, builder } = setup(); + + element.useDoBuilder(builder); + + expect(builder.buildLinkElement).toHaveBeenCalledWith(element); + }); + + it('should return RichTextElementDo', () => { + const { element, builder, elementDo } = setup(); + + const result = element.useDoBuilder(builder); + + expect(result).toEqual(elementDo); + }); + }); +}); diff --git a/apps/server/src/shared/domain/entity/boardnode/link-element-node.entity.ts b/apps/server/src/shared/domain/entity/boardnode/link-element-node.entity.ts new file mode 100644 index 00000000000..0102821d97b --- /dev/null +++ b/apps/server/src/shared/domain/entity/boardnode/link-element-node.entity.ts @@ -0,0 +1,36 @@ +import { Entity, Property } from '@mikro-orm/core'; +import { AnyBoardDo } from '../../domainobject'; +import { BoardNode, BoardNodeProps } from './boardnode.entity'; +import { BoardDoBuilder, BoardNodeType } from './types'; + +@Entity({ discriminatorValue: BoardNodeType.LINK_ELEMENT }) +export class LinkElementNode extends BoardNode { + @Property() + url: string; + + @Property() + title: string; + + @Property() + imageUrl?: string; + + constructor(props: LinkElementNodeProps) { + super(props); + this.type = BoardNodeType.LINK_ELEMENT; + this.url = props.url; + this.title = props.title; + this.imageUrl = props.imageUrl; + } + + useDoBuilder(builder: BoardDoBuilder): AnyBoardDo { + const domainObject = builder.buildLinkElement(this); + + return domainObject; + } +} + +export interface LinkElementNodeProps extends BoardNodeProps { + url: string; + title: string; + imageUrl?: string; +} diff --git a/apps/server/src/shared/domain/entity/boardnode/submission-container-element-node.entity.ts b/apps/server/src/shared/domain/entity/boardnode/submission-container-element-node.entity.ts index 7fe7c135f29..0dc60b67539 100644 --- a/apps/server/src/shared/domain/entity/boardnode/submission-container-element-node.entity.ts +++ b/apps/server/src/shared/domain/entity/boardnode/submission-container-element-node.entity.ts @@ -6,7 +6,7 @@ import { BoardDoBuilder, BoardNodeType } from './types'; @Entity({ discriminatorValue: BoardNodeType.SUBMISSION_CONTAINER_ELEMENT }) export class SubmissionContainerElementNode extends BoardNode { @Property({ nullable: true }) - dueDate?: Date; + dueDate: Date | null; constructor(props: SubmissionContainerNodeProps) { super(props); @@ -22,5 +22,5 @@ export class SubmissionContainerElementNode extends BoardNode { } export interface SubmissionContainerNodeProps extends BoardNodeProps { - dueDate?: Date; + dueDate: Date | null; } diff --git a/apps/server/src/shared/domain/entity/boardnode/types/board-do.builder.ts b/apps/server/src/shared/domain/entity/boardnode/types/board-do.builder.ts index a5c2a8b2e16..1b759a41180 100644 --- a/apps/server/src/shared/domain/entity/boardnode/types/board-do.builder.ts +++ b/apps/server/src/shared/domain/entity/boardnode/types/board-do.builder.ts @@ -1,18 +1,20 @@ -import { SubmissionItem } from '@shared/domain/domainobject/board/submission-item.do'; import type { Card, Column, ColumnBoard, ExternalToolElement, FileElement, + LinkElement, RichTextElement, SubmissionContainerElement, + SubmissionItem, } from '../../../domainobject'; import type { CardNode } from '../card-node.entity'; import type { ColumnBoardNode } from '../column-board-node.entity'; import type { ColumnNode } from '../column-node.entity'; import type { ExternalToolElementNodeEntity } from '../external-tool-element-node.entity'; import type { FileElementNode } from '../file-element-node.entity'; +import type { LinkElementNode } from '../link-element-node.entity'; import type { RichTextElementNode } from '../rich-text-element-node.entity'; import type { SubmissionContainerElementNode } from '../submission-container-element-node.entity'; import type { SubmissionItemNode } from '../submission-item-node.entity'; @@ -22,6 +24,7 @@ export interface BoardDoBuilder { buildColumn(boardNode: ColumnNode): Column; buildCard(boardNode: CardNode): Card; buildFileElement(boardNode: FileElementNode): FileElement; + buildLinkElement(boardNode: LinkElementNode): LinkElement; buildRichTextElement(boardNode: RichTextElementNode): RichTextElement; buildSubmissionContainerElement(boardNode: SubmissionContainerElementNode): SubmissionContainerElement; buildSubmissionItem(boardNode: SubmissionItemNode): SubmissionItem; diff --git a/apps/server/src/shared/domain/entity/boardnode/types/board-node-type.ts b/apps/server/src/shared/domain/entity/boardnode/types/board-node-type.ts index a1b44207907..0b25a81b053 100644 --- a/apps/server/src/shared/domain/entity/boardnode/types/board-node-type.ts +++ b/apps/server/src/shared/domain/entity/boardnode/types/board-node-type.ts @@ -3,6 +3,7 @@ export enum BoardNodeType { COLUMN = 'column', CARD = 'card', FILE_ELEMENT = 'file-element', + LINK_ELEMENT = 'link-element', RICH_TEXT_ELEMENT = 'rich-text-element', SUBMISSION_CONTAINER_ELEMENT = 'submission-container-element', SUBMISSION_ITEM = 'submission-item', diff --git a/apps/server/src/shared/domain/entity/course.entity.ts b/apps/server/src/shared/domain/entity/course.entity.ts index 1aea75aa3c0..81fdcb741d9 100644 --- a/apps/server/src/shared/domain/entity/course.entity.ts +++ b/apps/server/src/shared/domain/entity/course.entity.ts @@ -1,6 +1,8 @@ import { Collection, Entity, Enum, Index, ManyToMany, ManyToOne, OneToMany, Property, Unique } from '@mikro-orm/core'; import { InternalServerErrorException } from '@nestjs/common/exceptions/internal-server-error.exception'; import { IEntityWithSchool, ILearnroom } from '@shared/domain/interface'; +import { ClassEntity } from '@modules/class/entity/class.entity'; +import { GroupEntity } from '@modules/group/entity/group.entity'; import { EntityId, LearnroomMetadata, LearnroomTypes } from '../types'; import { BaseEntityWithTimestamps } from './base.entity'; import { CourseGroup } from './coursegroup.entity'; @@ -22,6 +24,8 @@ export interface ICourseProperties { untilDate?: Date; copyingSince?: Date; features?: CourseFeatures[]; + classes?: ClassEntity[]; + groups?: GroupEntity[]; } // that is really really shit default handling :D constructor, getter, js default, em default...what the hell @@ -95,6 +99,12 @@ export class Course @Enum({ nullable: true, array: true }) features?: CourseFeatures[]; + @ManyToMany(() => ClassEntity, undefined, { fieldName: 'classIds' }) + classes = new Collection(this); + + @ManyToMany(() => GroupEntity, undefined, { fieldName: 'groupIds' }) + groups = new Collection(this); + constructor(props: ICourseProperties) { super(); if (props.name) this.name = props.name; @@ -108,6 +118,8 @@ export class Course if (props.startDate) this.startDate = props.startDate; if (props.copyingSince) this.copyingSince = props.copyingSince; if (props.features) this.features = props.features; + this.classes.set(props.classes || []); + this.groups.set(props.groups || []); } public getStudentIds(): EntityId[] { diff --git a/apps/server/src/shared/domain/entity/lesson.entity.ts b/apps/server/src/shared/domain/entity/lesson.entity.ts index d83cd2f182a..a47aab2a8b3 100644 --- a/apps/server/src/shared/domain/entity/lesson.entity.ts +++ b/apps/server/src/shared/domain/entity/lesson.entity.ts @@ -73,7 +73,7 @@ export type IComponentProperties = { | { component: ComponentType.ETHERPAD; content: IComponentEtherpadProperties } | { component: ComponentType.GEOGEBRA; content: IComponentGeogebraProperties } | { component: ComponentType.INTERNAL; content: IComponentInternalProperties } - | { component: ComponentType.LERNSTORE; content: IComponentLernstoreProperties } + | { component: ComponentType.LERNSTORE; content?: IComponentLernstoreProperties } | { component: ComponentType.NEXBOARD; content: IComponentNexboardProperties } ); diff --git a/apps/server/src/shared/domain/entity/system.entity.ts b/apps/server/src/shared/domain/entity/system.entity.ts index 8f1b5821fbd..0633e515589 100644 --- a/apps/server/src/shared/domain/entity/system.entity.ts +++ b/apps/server/src/shared/domain/entity/system.entity.ts @@ -62,8 +62,8 @@ export class OauthConfig { @Property() provider: string; - @Property() - logoutEndpoint: string; + @Property({ nullable: true }) + logoutEndpoint?: string; @Property() issuer: string; diff --git a/apps/server/src/shared/domain/interface/permission.enum.ts b/apps/server/src/shared/domain/interface/permission.enum.ts index 3d00ef24be2..4512b95de7f 100644 --- a/apps/server/src/shared/domain/interface/permission.enum.ts +++ b/apps/server/src/shared/domain/interface/permission.enum.ts @@ -54,6 +54,8 @@ export enum Permission { FILE_MOVE = 'FILE_MOVE', FOLDER_CREATE = 'FOLDER_CREATE', FOLDER_DELETE = 'FOLDER_DELETE', + GROUP_LIST = 'GROUP_LIST', + GROUP_VIEW = 'GROUP_VIEW', HELPDESK_CREATE = 'HELPDESK_CREATE', HELPDESK_EDIT = 'HELPDESK_EDIT', HELPDESK_VIEW = 'HELPDESK_VIEW', diff --git a/apps/server/src/shared/domain/rules/index.ts b/apps/server/src/shared/domain/rules/index.ts index 888b2ee8501..e69de29bb2d 100644 --- a/apps/server/src/shared/domain/rules/index.ts +++ b/apps/server/src/shared/domain/rules/index.ts @@ -1,39 +0,0 @@ -import { BoardDoRule } from './board-do.rule'; -import { ContextExternalToolRule } from './context-external-tool.rule'; -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 { SubmissionRule } from './submission.rule'; -import { TaskRule } from './task.rule'; -import { TeamRule } from './team.rule'; -import { UserLoginMigrationRule } from './user-login-migration.rule'; -import { UserRule } from './user.rule'; - -export * from './board-do.rule'; -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 './submission.rule'; -export * from './task.rule'; -export * from './team.rule'; -export * from './user.rule'; -export * from './context-external-tool.rule'; - -export const ALL_RULES = [ - LessonRule, - CourseRule, - CourseGroupRule, - LegacySchoolRule, - SubmissionRule, - TaskRule, - TeamRule, - UserRule, - SchoolExternalToolRule, - BoardDoRule, - ContextExternalToolRule, - UserLoginMigrationRule, -]; diff --git a/apps/server/src/shared/infra/antivirus/antivirus.module.spec.ts b/apps/server/src/shared/infra/antivirus/antivirus.module.spec.ts index 594e283861d..ea7e85b109f 100644 --- a/apps/server/src/shared/infra/antivirus/antivirus.module.spec.ts +++ b/apps/server/src/shared/infra/antivirus/antivirus.module.spec.ts @@ -11,6 +11,8 @@ describe('AntivirusModule', () => { filesServiceBaseUrl: 'http://localhost', exchange: 'exchange', routingKey: 'routingKey', + hostname: 'localhost', + port: 3311, }; beforeAll(async () => { diff --git a/apps/server/src/shared/infra/antivirus/antivirus.module.ts b/apps/server/src/shared/infra/antivirus/antivirus.module.ts index c9078925532..197e263b97d 100644 --- a/apps/server/src/shared/infra/antivirus/antivirus.module.ts +++ b/apps/server/src/shared/infra/antivirus/antivirus.module.ts @@ -1,12 +1,7 @@ -import { Module, DynamicModule } from '@nestjs/common'; +import { DynamicModule, Module } from '@nestjs/common'; +import NodeClam from 'clamscan'; import { AntivirusService } from './antivirus.service'; - -interface AntivirusModuleOptions { - enabled: boolean; - filesServiceBaseUrl: string; - exchange: string; - routingKey: string; -} +import { AntivirusModuleOptions } from './interfaces'; @Module({}) export class AntivirusModule { @@ -24,7 +19,24 @@ export class AntivirusModule { routingKey: options.routingKey, }, }, + { + provide: NodeClam, + useFactory: () => { + const isLocalhost = options.hostname === 'localhost'; + + return new NodeClam().init({ + debugMode: isLocalhost, + clamdscan: { + host: options.hostname, + port: options.port, + bypassTest: isLocalhost, + localFallback: false, + }, + }); + }, + }, ], + exports: [AntivirusService], }; } diff --git a/apps/server/src/shared/infra/antivirus/antivirus.service.spec.ts b/apps/server/src/shared/infra/antivirus/antivirus.service.spec.ts index 6a02415a82b..247b04c48a0 100644 --- a/apps/server/src/shared/infra/antivirus/antivirus.service.spec.ts +++ b/apps/server/src/shared/infra/antivirus/antivirus.service.spec.ts @@ -2,6 +2,8 @@ import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { InternalServerErrorException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; +import NodeClam from 'clamscan'; +import { Readable } from 'stream'; import { v4 as uuid } from 'uuid'; import { AntivirusService } from './antivirus.service'; @@ -9,6 +11,7 @@ describe('AntivirusService', () => { let module: TestingModule; let service: AntivirusService; let amqpConnection: DeepMocked; + let clamavConnection: DeepMocked; const antivirusServiceOptions = { enabled: true, @@ -22,12 +25,14 @@ describe('AntivirusService', () => { providers: [ AntivirusService, { provide: AmqpConnection, useValue: createMock() }, + { provide: NodeClam, useValue: createMock() }, { provide: 'ANTIVIRUS_SERVICE_OPTIONS', useValue: antivirusServiceOptions }, ], }).compile(); service = module.get(AntivirusService); amqpConnection = module.get(AmqpConnection); + clamavConnection = module.get(NodeClam); }); afterAll(async () => { @@ -82,4 +87,104 @@ describe('AntivirusService', () => { }); }); }); + + describe('checkStream', () => { + describe('when service can handle passing parameters', () => { + const setup = () => { + const readable = Readable.from('abc'); + + return { readable }; + }; + + it('should call scanStream', async () => { + const { readable } = setup(); + + await service.checkStream(readable); + + expect(clamavConnection.scanStream).toHaveBeenCalledWith(readable); + }); + }); + + describe('when file infected', () => { + const setup = () => { + const readable = Readable.from('abc'); + // @ts-expect-error unknown types + clamavConnection.scanStream.mockResolvedValueOnce({ isInfected: true, viruses: ['test'] }); + + const expectedResult = { + virus_detected: true, + virus_signature: 'test', + }; + return { readable, expectedResult }; + }; + + it('should return scan result', async () => { + const { readable, expectedResult } = setup(); + + const result = await service.checkStream(readable); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when file not scanned', () => { + const setup = () => { + const readable = Readable.from('abc'); + // @ts-expect-error unknown types + clamavConnection.scanStream.mockResolvedValueOnce({ isInfected: null }); + + const expectedResult = { + virus_detected: undefined, + virus_signature: undefined, + error: '', + }; + return { readable, expectedResult }; + }; + + it('should return scan result', async () => { + const { readable, expectedResult } = setup(); + + const result = await service.checkStream(readable); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when file is good', () => { + const setup = () => { + const readable = Readable.from('abc'); + // @ts-expect-error unknown types + clamavConnection.scanStream.mockResolvedValueOnce({ isInfected: false }); + + const expectedResult = { + virus_detected: false, + }; + return { readable, expectedResult }; + }; + + it('should return scan result', async () => { + const { readable, expectedResult } = setup(); + + const result = await service.checkStream(readable); + + expect(result).toEqual(expectedResult); + }); + }); + + describe('when clamavConnection.scanStream throw an error', () => { + const setup = () => { + const readable = Readable.from('abc'); + // @ts-expect-error unknown types + clamavConnection.scanStream.mockRejectedValueOnce(new Error('fail')); + + return { readable }; + }; + + it('should throw with InternalServerErrorException by error', async () => { + const { readable } = setup(); + + await expect(() => service.checkStream(readable)).rejects.toThrowError(InternalServerErrorException); + }); + }); + }); }); diff --git a/apps/server/src/shared/infra/antivirus/antivirus.service.ts b/apps/server/src/shared/infra/antivirus/antivirus.service.ts index c3c38494853..15fbcbf4c1e 100644 --- a/apps/server/src/shared/infra/antivirus/antivirus.service.ts +++ b/apps/server/src/shared/infra/antivirus/antivirus.service.ts @@ -1,22 +1,46 @@ import { AmqpConnection } from '@golevelup/nestjs-rabbitmq'; import { Inject, Injectable, InternalServerErrorException } from '@nestjs/common'; import { ErrorUtils } from '@src/core/error/utils'; -import { API_VERSION_PATH, FilesStorageInternalActions } from '@src/modules/files-storage/files-storage.const'; - -interface AntivirusServiceOptions { - enabled: boolean; - filesServiceBaseUrl: string; - exchange: string; - routingKey: string; -} +import { API_VERSION_PATH, FilesStorageInternalActions } from '@modules/files-storage/files-storage.const'; +import NodeClam from 'clamscan'; +import { Readable } from 'stream'; +import { AntivirusServiceOptions, ScanResult } from './interfaces'; @Injectable() export class AntivirusService { constructor( private readonly amqpConnection: AmqpConnection, - @Inject('ANTIVIRUS_SERVICE_OPTIONS') private readonly options: AntivirusServiceOptions + @Inject('ANTIVIRUS_SERVICE_OPTIONS') private readonly options: AntivirusServiceOptions, + private readonly clamConnection: NodeClam ) {} + public async checkStream(stream: Readable) { + const scanResult: ScanResult = { + virus_detected: undefined, + virus_signature: undefined, + error: undefined, + }; + try { + const { isInfected, viruses } = await this.clamConnection.scanStream(stream); + if (isInfected === true) { + scanResult.virus_detected = true; + scanResult.virus_signature = viruses.join(','); + } else if (isInfected === null) { + scanResult.virus_detected = undefined; + scanResult.error = ''; + } else { + scanResult.virus_detected = false; + } + } catch (err) { + throw new InternalServerErrorException( + null, + ErrorUtils.createHttpExceptionOptions(err, 'AntivirusService:checkStream') + ); + } + + return scanResult; + } + public async send(requestToken: string | undefined): Promise { try { if (this.options.enabled && requestToken) { diff --git a/apps/server/src/shared/infra/antivirus/index.ts b/apps/server/src/shared/infra/antivirus/index.ts new file mode 100644 index 00000000000..2816f95ee57 --- /dev/null +++ b/apps/server/src/shared/infra/antivirus/index.ts @@ -0,0 +1,3 @@ +export * from './interfaces'; +export * from './antivirus.module'; +export * from './antivirus.service'; diff --git a/apps/server/src/shared/infra/antivirus/interfaces/antivirus.ts b/apps/server/src/shared/infra/antivirus/interfaces/antivirus.ts new file mode 100644 index 00000000000..0d648afacce --- /dev/null +++ b/apps/server/src/shared/infra/antivirus/interfaces/antivirus.ts @@ -0,0 +1,21 @@ +export interface AntivirusModuleOptions { + enabled: boolean; + filesServiceBaseUrl: string; + exchange: string; + routingKey: string; + hostname: string; + port: number; +} + +export interface AntivirusServiceOptions { + enabled: boolean; + filesServiceBaseUrl: string; + exchange: string; + routingKey: string; +} + +export interface ScanResult { + virus_detected?: boolean; + virus_signature?: string; + error?: string; +} diff --git a/apps/server/src/shared/infra/antivirus/interfaces/index.ts b/apps/server/src/shared/infra/antivirus/interfaces/index.ts new file mode 100644 index 00000000000..6c4771f9cd5 --- /dev/null +++ b/apps/server/src/shared/infra/antivirus/interfaces/index.ts @@ -0,0 +1 @@ +export * from './antivirus'; diff --git a/apps/server/src/shared/infra/collaborative-storage/collaborative-storage-adapter.module.ts b/apps/server/src/shared/infra/collaborative-storage/collaborative-storage-adapter.module.ts index 0580777a666..84e4f4596d6 100644 --- a/apps/server/src/shared/infra/collaborative-storage/collaborative-storage-adapter.module.ts +++ b/apps/server/src/shared/infra/collaborative-storage/collaborative-storage-adapter.module.ts @@ -6,9 +6,9 @@ import { NextcloudClient } from '@shared/infra/collaborative-storage/strategy/ne import { NextcloudStrategy } from '@shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy'; import { LtiToolRepo } from '@shared/repo/ltitool/'; import { LoggerModule } from '@src/core/logger'; -import { ToolModule } from '@src/modules/tool'; -import { PseudonymModule } from '@src/modules/pseudonym'; -import { UserModule } from '@src/modules/user'; +import { ToolModule } from '@modules/tool'; +import { PseudonymModule } from '@modules/pseudonym'; +import { UserModule } from '@modules/user'; import { CollaborativeStorageAdapter } from './collaborative-storage.adapter'; const storageStrategy: Provider = { diff --git a/apps/server/src/shared/infra/collaborative-storage/collaborative-storage.adapter.spec.ts b/apps/server/src/shared/infra/collaborative-storage/collaborative-storage.adapter.spec.ts index db2a05e26b5..c33f9be282a 100644 --- a/apps/server/src/shared/infra/collaborative-storage/collaborative-storage.adapter.spec.ts +++ b/apps/server/src/shared/infra/collaborative-storage/collaborative-storage.adapter.spec.ts @@ -6,7 +6,7 @@ import { CollaborativeStorageAdapter } from '@shared/infra/collaborative-storage import { CollaborativeStorageAdapterMapper } from '@shared/infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper'; import { ICollaborativeStorageStrategy } from '@shared/infra/collaborative-storage/strategy/base.interface.strategy'; import { LegacyLogger } from '@src/core/logger'; -import { TeamDto } from '@src/modules/collaborative-storage/services/dto/team.dto'; +import { TeamDto } from '@modules/collaborative-storage/services/dto/team.dto'; class TestStrategy implements ICollaborativeStorageStrategy { baseURL: string; diff --git a/apps/server/src/shared/infra/collaborative-storage/collaborative-storage.adapter.ts b/apps/server/src/shared/infra/collaborative-storage/collaborative-storage.adapter.ts index c6ef05c1e5b..9edcafbdc12 100644 --- a/apps/server/src/shared/infra/collaborative-storage/collaborative-storage.adapter.ts +++ b/apps/server/src/shared/infra/collaborative-storage/collaborative-storage.adapter.ts @@ -1,10 +1,10 @@ -import { TeamPermissionsDto } from '@src/modules/collaborative-storage/services/dto/team-permissions.dto'; -import { TeamDto } from '@src/modules/collaborative-storage/services/dto/team.dto'; +import { TeamPermissionsDto } from '@modules/collaborative-storage/services/dto/team-permissions.dto'; +import { TeamDto } from '@modules/collaborative-storage/services/dto/team.dto'; import { ICollaborativeStorageStrategy } from '@shared/infra/collaborative-storage/strategy/base.interface.strategy'; import { Inject, Injectable } from '@nestjs/common'; import { CollaborativeStorageAdapterMapper } from '@shared/infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper'; import { LegacyLogger } from '@src/core/logger'; -import { RoleDto } from '@src/modules/role/service/dto/role.dto'; +import { RoleDto } from '@modules/role/service/dto/role.dto'; /** * Provides an Adapter to an external collaborative storage. diff --git a/apps/server/src/shared/infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper.ts b/apps/server/src/shared/infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper.ts index e3595440f9f..a04ace04490 100644 --- a/apps/server/src/shared/infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper.ts +++ b/apps/server/src/shared/infra/collaborative-storage/mapper/collaborative-storage-adapter.mapper.ts @@ -1,7 +1,7 @@ -import { TeamPermissionsDto } from '@src/modules/collaborative-storage/services/dto/team-permissions.dto'; -import { TeamDto } from '@src/modules/collaborative-storage/services/dto/team.dto'; +import { TeamPermissionsDto } from '@modules/collaborative-storage/services/dto/team-permissions.dto'; +import { TeamDto } from '@modules/collaborative-storage/services/dto/team.dto'; import { Injectable } from '@nestjs/common'; -import { RoleDto } from '@src/modules/role/service/dto/role.dto'; +import { RoleDto } from '@modules/role/service/dto/role.dto'; import { TeamRolePermissionsDto } from '../dto/team-role-permissions.dto'; @Injectable() diff --git a/apps/server/src/shared/infra/collaborative-storage/strategy/base.interface.strategy.ts b/apps/server/src/shared/infra/collaborative-storage/strategy/base.interface.strategy.ts index 51d741105f0..f960452df5f 100644 --- a/apps/server/src/shared/infra/collaborative-storage/strategy/base.interface.strategy.ts +++ b/apps/server/src/shared/infra/collaborative-storage/strategy/base.interface.strategy.ts @@ -1,4 +1,4 @@ -import { TeamDto } from '@src/modules/collaborative-storage/services/dto/team.dto'; +import { TeamDto } from '@modules/collaborative-storage/services/dto/team.dto'; import { TeamRolePermissionsDto } from '../dto/team-role-permissions.dto'; /** 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 bc4b3878ea5..7684b14dbb0 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 @@ -9,10 +9,10 @@ import { NextcloudStrategy } from '@shared/infra/collaborative-storage/strategy/ 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 { PseudonymService } from '@src/modules/pseudonym'; -import { ExternalToolService } from '@src/modules/tool/external-tool/service'; -import { UserService } from '@src/modules/user'; +import { TeamDto, TeamUserDto } from '@modules/collaborative-storage/services/dto/team.dto'; +import { PseudonymService } from '@modules/pseudonym'; +import { ExternalToolService } from '@modules/tool/external-tool/service'; +import { UserService } from '@modules/user'; class NextcloudStrategySpec extends NextcloudStrategy { static specGenerateGroupId(dto: TeamRolePermissionsDto): string { diff --git a/apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.ts b/apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.ts index 1292bff3a42..6b75d6ec76f 100644 --- a/apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.ts +++ b/apps/server/src/shared/infra/collaborative-storage/strategy/nextcloud/nextcloud.strategy.ts @@ -3,11 +3,11 @@ import { Pseudonym, UserDO } from '@shared/domain/'; import { LtiToolDO } from '@shared/domain/domainobject/ltitool.do'; import { LtiToolRepo } from '@shared/repo/ltitool/'; import { LegacyLogger } from '@src/core/logger'; -import { TeamDto, TeamUserDto } from '@src/modules/collaborative-storage'; -import { PseudonymService } from '@src/modules/pseudonym'; -import { UserService } from '@src/modules/user'; -import { ExternalToolService } from '@src/modules/tool/external-tool/service'; -import { ExternalTool } from '@src/modules/tool/external-tool/domain'; +import { TeamDto, TeamUserDto } from '@modules/collaborative-storage'; +import { PseudonymService } from '@modules/pseudonym'; +import { UserService } from '@modules/user'; +import { ExternalToolService } from '@modules/tool/external-tool/service'; +import { ExternalTool } from '@modules/tool/external-tool/domain'; import { TeamRolePermissionsDto } from '../../dto/team-role-permissions.dto'; import { ICollaborativeStorageStrategy } from '../base.interface.strategy'; import { NextcloudClient } from './nextcloud.client'; diff --git a/apps/server/src/shared/infra/identity-management/identity-management-oauth.service.ts b/apps/server/src/shared/infra/identity-management/identity-management-oauth.service.ts index 5003da4c62e..2a486e87c32 100644 --- a/apps/server/src/shared/infra/identity-management/identity-management-oauth.service.ts +++ b/apps/server/src/shared/infra/identity-management/identity-management-oauth.service.ts @@ -1,4 +1,4 @@ -import { OauthConfigDto } from '@src/modules/system/service/dto'; +import { OauthConfigDto } from '@modules/system/service/dto'; export abstract class IdentityManagementOauthService { /** diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/console/keycloak-configuration.console.ts b/apps/server/src/shared/infra/identity-management/keycloak-configuration/console/keycloak-configuration.console.ts index 57dfd7cdde0..1d597e7020a 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/console/keycloak-configuration.console.ts +++ b/apps/server/src/shared/infra/identity-management/keycloak-configuration/console/keycloak-configuration.console.ts @@ -15,6 +15,10 @@ interface IMigrationOptions { query?: string; verbose?: boolean; } + +interface ICleanOptions { + pageSize?: number; +} @Console({ command: 'idm', description: 'Prefixes all Identity Management (IDM) related console commands.' }) export class KeycloakConsole { constructor( @@ -53,21 +57,29 @@ export class KeycloakConsole { } /** - * For local development. Cleans user from IDM + * Cleans users from IDM * * @param options */ @Command({ command: 'clean', description: 'Remove all users from the IDM.', - options: KeycloakConsole.retryFlags, + options: [ + ...KeycloakConsole.retryFlags, + { + flags: '- mps, --maxPageSize ', + description: 'Maximum users to delete per Keycloak API session. Default 100.', + required: false, + defaultValue: 100, + }, + ], }) - async clean(options: IRetryOptions): Promise { + async clean(options: IRetryOptions & ICleanOptions): Promise { await this.repeatCommand( 'clean', async () => { - const count = await this.keycloakConfigurationUc.clean(); - this.console.info(`Cleaned ${count} users into IDM`); + const count = await this.keycloakConfigurationUc.clean(options.pageSize ? Number(options.pageSize) : 100); + this.console.info(`Cleaned ${count} users in IDM`); return count; }, options.retryCount, diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/controller/keycloak-configuration.controller.spec.ts b/apps/server/src/shared/infra/identity-management/keycloak-configuration/controller/keycloak-configuration.controller.spec.ts index d1155cc3f88..734e8f628b2 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/controller/keycloak-configuration.controller.spec.ts +++ b/apps/server/src/shared/infra/identity-management/keycloak-configuration/controller/keycloak-configuration.controller.spec.ts @@ -3,7 +3,7 @@ import { ServiceUnavailableException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { LegacyLogger } from '@src/core/logger'; -import { NodeEnvType } from '@src/modules/server/server.config'; +import { NodeEnvType } from '@modules/server/server.config'; import { KeycloakConfigurationUc } from '../uc/keycloak-configuration.uc'; import { KeycloakManagementController } from './keycloak-configuration.controller'; diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/keycloak-configuration.module.ts b/apps/server/src/shared/infra/identity-management/keycloak-configuration/keycloak-configuration.module.ts index 8c51103495d..2012dad00a5 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/keycloak-configuration.module.ts +++ b/apps/server/src/shared/infra/identity-management/keycloak-configuration/keycloak-configuration.module.ts @@ -2,7 +2,8 @@ import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; import { EncryptionModule } from '@shared/infra/encryption'; import { ConsoleWriterModule } from '@shared/infra/console'; -import { AccountModule, SystemModule } from '@src/modules'; +import { AccountModule } from '@modules/account'; +import { SystemModule } from '@modules/system'; import { KeycloakAdministrationModule } from '../keycloak-administration/keycloak-administration.module'; import { KeycloakConsole } from './console/keycloak-configuration.console'; import { KeycloakConfigurationInputFiles } from './interface/keycloak-configuration-input-files.interface'; diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.spec.ts b/apps/server/src/shared/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.spec.ts index 2b565bc77e8..b28d74ca3d5 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.spec.ts +++ b/apps/server/src/shared/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.spec.ts @@ -3,7 +3,7 @@ import IdentityProviderRepresentation from '@keycloak/keycloak-admin-client/lib/ import { ObjectId } from '@mikro-orm/mongodb'; import { Test, TestingModule } from '@nestjs/testing'; import { DefaultEncryptionService, SymetricKeyEncryptionService } from '@shared/infra/encryption'; -import { OidcConfigDto } from '@src/modules/system/service'; +import { OidcConfigDto } from '@modules/system/service'; import { OidcIdentityProviderMapper } from './identity-provider.mapper'; describe('OidcIdentityProviderMapper', () => { diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.ts b/apps/server/src/shared/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.ts index c7f6ec65af7..75737263cac 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.ts +++ b/apps/server/src/shared/infra/identity-management/keycloak-configuration/mapper/identity-provider.mapper.ts @@ -1,7 +1,7 @@ import IdentityProviderRepresentation from '@keycloak/keycloak-admin-client/lib/defs/identityProviderRepresentation'; import { Inject } from '@nestjs/common'; import { DefaultEncryptionService, IEncryptionService } from '@shared/infra/encryption'; -import { OidcConfigDto } from '@src/modules/system/service'; +import { OidcConfigDto } from '@modules/system/service'; export class OidcIdentityProviderMapper { constructor(@Inject(DefaultEncryptionService) private readonly defaultEncryptionService: IEncryptionService) {} diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.integration.spec.ts b/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.integration.spec.ts index a18a07e9fe7..ad5af6a1d75 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.integration.spec.ts +++ b/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.integration.spec.ts @@ -8,7 +8,7 @@ import { MongoMemoryDatabaseModule } from '@shared/infra/database'; import { SystemRepo } from '@shared/repo/system/system.repo'; import { systemFactory } from '@shared/testing/factory'; import { LoggerModule } from '@src/core/logger'; -import { SystemService } from '@src/modules/system/service/system.service'; +import { SystemService } from '@modules/system/service/system.service'; import { v1 } from 'uuid'; import { KeycloakAdministrationService } from '../../keycloak-administration/service/keycloak-administration.service'; import { KeycloakConfigurationModule } from '../keycloak-configuration.module'; diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts b/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts index 012d63d18c2..1388392995e 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts +++ b/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.spec.ts @@ -12,8 +12,8 @@ import { Test, TestingModule } from '@nestjs/testing'; import { SystemEntity, SystemTypeEnum } from '@shared/domain'; import { SymetricKeyEncryptionService } from '@shared/infra/encryption'; import { systemFactory } from '@shared/testing'; -import { SystemOidcMapper } from '@src/modules/system/mapper/system-oidc.mapper'; -import { SystemOidcService } from '@src/modules/system/service/system-oidc.service'; +import { SystemOidcMapper } from '@modules/system/mapper/system-oidc.mapper'; +import { SystemOidcService } from '@modules/system/service/system-oidc.service'; import { AxiosResponse } from 'axios'; import { of } from 'rxjs'; import { v1 } from 'uuid'; diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.ts b/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.ts index 02f27db9eb2..ae7f2631bce 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.ts +++ b/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-configuration.service.ts @@ -6,9 +6,9 @@ import IdentityProviderRepresentation from '@keycloak/keycloak-admin-client/lib/ import ProtocolMapperRepresentation from '@keycloak/keycloak-admin-client/lib/defs/protocolMapperRepresentation'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { IServerConfig } from '@src/modules/server/server.config'; -import { OidcConfigDto } from '@src/modules/system/service'; -import { SystemOidcService } from '@src/modules/system/service/system-oidc.service'; +import { IServerConfig } from '@modules/server/server.config'; +import { OidcConfigDto } from '@modules/system/service'; +import { SystemOidcService } from '@modules/system/service/system-oidc.service'; import { KeycloakAdministrationService } from '../../keycloak-administration/service/keycloak-administration.service'; import { OidcIdentityProviderMapper } from '../mapper/identity-provider.mapper'; diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.spec.ts b/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.spec.ts index c8a1011e2c1..e02ade6a26b 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.spec.ts +++ b/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.spec.ts @@ -1,8 +1,8 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { LegacyLogger } from '@src/core/logger'; -import { AccountService } from '@src/modules/account/services/account.service'; -import { AccountDto } from '@src/modules/account/services/dto/account.dto'; +import { AccountService } from '@modules/account/services/account.service'; +import { AccountDto } from '@modules/account/services/dto/account.dto'; import KeycloakAdminClient from '@keycloak/keycloak-admin-client-cjs/keycloak-admin-client-cjs-index'; import { Users } from '@keycloak/keycloak-admin-client/lib/resources/users'; import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation'; diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.ts b/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.ts index b64f69d5043..ce87478f637 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.ts +++ b/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-migration.service.ts @@ -1,8 +1,8 @@ import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation'; import { Injectable } from '@nestjs/common'; import { LegacyLogger } from '@src/core/logger'; -import { AccountService } from '@src/modules/account/services/account.service'; -import { AccountDto } from '@src/modules/account/services/dto'; +import { AccountService } from '@modules/account/services/account.service'; +import { AccountDto } from '@modules/account/services/dto'; import { KeycloakAdministrationService } from '../../keycloak-administration/service/keycloak-administration.service'; @Injectable() diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.integration.spec.ts b/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.integration.spec.ts new file mode 100644 index 00000000000..87f28a28d76 --- /dev/null +++ b/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.integration.spec.ts @@ -0,0 +1,91 @@ +import KeycloakAdminClient from '@keycloak/keycloak-admin-client'; +import { faker } from '@faker-js/faker'; +import { ConfigModule } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { MongoMemoryDatabaseModule } from '@shared/infra/database'; +import { LoggerModule } from '@src/core/logger'; +import { v1 } from 'uuid'; +import { KeycloakAdministrationService } from '../../keycloak-administration/service/keycloak-administration.service'; +import { KeycloakConfigurationModule } from '../keycloak-configuration.module'; +import { KeycloakSeedService } from './keycloak-seed.service'; + +describe('KeycloakSeedService Integration', () => { + let module: TestingModule; + let keycloak: KeycloakAdminClient; + let keycloakSeedService: KeycloakSeedService; + let keycloakAdministrationService: KeycloakAdministrationService; + let isKeycloakAvailable = false; + const numberOfIdmUsers = 1009; + + const testRealm = `test-realm-${v1().toString()}`; + + const createIdmUser = async (index: number): Promise => { + const firstName = faker.person.firstName(); + const lastName = faker.person.lastName(); + await keycloak.users.create({ + username: `${index}.${lastName}@sp-sh.de`, + firstName, + lastName, + email: `${index}.${lastName}@sp-sh.de`, + }); + }; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + KeycloakConfigurationModule, + LoggerModule, + MongoMemoryDatabaseModule.forRoot(), + ConfigModule.forRoot({ + isGlobal: true, + ignoreEnvFile: true, + ignoreEnvVars: true, + validate: () => { + return { + FEATURE_IDENTITY_MANAGEMENT_STORE_ENABLED: true, + }; + }, + }), + ], + providers: [], + }).compile(); + keycloakAdministrationService = module.get(KeycloakAdministrationService); + isKeycloakAvailable = await keycloakAdministrationService.testKcConnection(); + if (isKeycloakAvailable) { + keycloak = await keycloakAdministrationService.callKcAdminClient(); + } + keycloakSeedService = module.get(KeycloakSeedService); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(async () => { + if (isKeycloakAvailable) { + await keycloak.realms.del({ realm: testRealm }); + } + }); + + // Execute this test for a test run against a running Keycloak instance + describe('clean', () => { + describe('Given all users are able to delete', () => { + const setup = async () => { + await keycloak.realms.create({ realm: testRealm, enabled: true }); + keycloak.setConfig({ realmName: testRealm }); + let i = 1; + for (i = 1; i <= numberOfIdmUsers; i += 1) { + // eslint-disable-next-line no-await-in-loop + await createIdmUser(i); + } + }; + + it('should delete all users in the IDM', async () => { + if (!isKeycloakAvailable) return; + await setup(); + const deletedUsers = await keycloakSeedService.clean(500); + expect(deletedUsers).toBe(numberOfIdmUsers); + }, 60000); + }); + }); +}); diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.spec.ts b/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.spec.ts index 8d411a5356a..19a3b28326e 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.spec.ts +++ b/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.spec.ts @@ -1,4 +1,5 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { LegacyLogger } from '@src/core/logger'; import KeycloakAdminClient from '@keycloak/keycloak-admin-client-cjs/keycloak-admin-client-cjs-index'; import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation'; import { AuthenticationManagement } from '@keycloak/keycloak-admin-client/lib/resources/authenticationManagement'; @@ -40,8 +41,12 @@ jest.mock('node:fs/promises', () => { describe('KeycloakSeedService', () => { let module: TestingModule; let serviceUnderTest: KeycloakSeedService; + let logger: DeepMocked; let settings: IKeycloakSettings; + let infoLogSpy: jest.SpyInstance; + let errorLogSpy: jest.SpyInstance; + let kcAdminClient: DeepMocked; const kcApiUsersMock = createMock(); const kcApiAuthenticationManagementMock = createMock(); @@ -142,6 +147,10 @@ describe('KeycloakSeedService', () => { }, }, }, + { + provide: LegacyLogger, + useValue: createMock(), + }, ], }).compile(); serviceUnderTest = module.get(KeycloakSeedService); @@ -177,16 +186,33 @@ describe('KeycloakSeedService', () => { ]; kcApiUsersMock.create.mockResolvedValue({ id: '' }); kcApiUsersMock.del.mockImplementation(async (): Promise => Promise.resolve()); - kcApiUsersMock.find.mockImplementation(async (arg): Promise => { - if (arg?.username) { + kcApiUsersMock.find + .mockImplementationOnce(async (arg): Promise => { + if (arg?.username) { + return Promise.resolve([]); + } + const userArray = [adminUser, ...users]; + return Promise.resolve(userArray); + }) + .mockImplementationOnce(async (arg): Promise => { + if (arg?.username) { + return Promise.resolve([]); + } return Promise.resolve([]); - } - const userArray = [adminUser, ...users]; - return Promise.resolve(userArray); - }); + }) + .mockImplementationOnce(async (): Promise => { + const userArray = [adminUser, ...users]; + return Promise.resolve(userArray); + }) + .mockImplementation(async (): Promise => Promise.resolve([])); + logger = module.get(LegacyLogger); + infoLogSpy = jest.spyOn(logger, 'log'); + errorLogSpy = jest.spyOn(logger, 'error'); }); beforeEach(() => { + infoLogSpy.mockReset(); + errorLogSpy.mockReset(); kcApiUsersMock.create.mockClear(); kcApiUsersMock.del.mockClear(); kcApiUsersMock.find.mockClear(); @@ -208,7 +234,6 @@ describe('KeycloakSeedService', () => { it('should clean all users, but the admin', async () => { const deleteSpy = jest.spyOn(kcApiUsersMock, 'del'); await serviceUnderTest.clean(); - users.forEach((user) => { expect(deleteSpy).toHaveBeenCalledWith(expect.objectContaining({ id: user.id })); }); diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.ts b/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.ts index 078601d3248..eaf08cebe27 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.ts +++ b/apps/server/src/shared/infra/identity-management/keycloak-configuration/service/keycloak-seed.service.ts @@ -1,5 +1,6 @@ import UserRepresentation from '@keycloak/keycloak-admin-client/lib/defs/userRepresentation'; import { Inject } from '@nestjs/common'; +import { LegacyLogger } from '@src/core/logger'; import fs from 'node:fs/promises'; import { IJsonAccount } from '../interface/json-account.interface'; import { IJsonUser } from '../interface/json-user.interface'; @@ -12,6 +13,7 @@ import { export class KeycloakSeedService { constructor( private readonly kcAdmin: KeycloakAdministrationService, + private readonly logger: LegacyLogger, @Inject(KeycloakConfigurationInputFiles) private readonly inputFiles: IKeycloakConfigurationInputFiles ) {} @@ -30,23 +32,29 @@ export class KeycloakSeedService { return userCount; } - public async clean(): Promise { - let kc = await this.kcAdmin.callKcAdminClient(); + public async clean(pageSize = 100): Promise { + let foundUsers = 1; + let deletedUsers = 0; const adminUser = this.kcAdmin.getAdminUser(); - const users = (await kc.users.find()).filter((user) => user.username !== adminUser); - - // eslint-disable-next-line no-restricted-syntax - for (const user of users) { - // needs to be called once per minute. To be save we call it in the loop. Ineffcient but ok, since only used to locally revert seeding + let kc = await this.kcAdmin.callKcAdminClient(); + this.logger.log(`Starting to delete users...`); + while (foundUsers > 0) { // eslint-disable-next-line no-await-in-loop kc = await this.kcAdmin.callKcAdminClient(); // eslint-disable-next-line no-await-in-loop - await kc.users.del({ - // can not be undefined, see filter above - id: user.id ?? '', - }); + const users = (await kc.users.find({ max: pageSize })).filter((user) => user.username !== adminUser); + foundUsers = users.length; + this.logger.log(`Amount of found Users: ${foundUsers}`); + for (const user of users) { + // eslint-disable-next-line no-await-in-loop + await kc.users.del({ + id: user.id ?? '', + }); + } + deletedUsers += foundUsers; + this.logger.log(`...deleted ${deletedUsers} users so far.`); } - return users.length; + return deletedUsers; } private async createOrUpdateIdmAccount(account: IJsonAccount, user: IJsonUser): Promise { diff --git a/apps/server/src/shared/infra/identity-management/keycloak-configuration/uc/keycloak-configuration.uc.ts b/apps/server/src/shared/infra/identity-management/keycloak-configuration/uc/keycloak-configuration.uc.ts index 0da4a04df4d..eabe596055e 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak-configuration/uc/keycloak-configuration.uc.ts +++ b/apps/server/src/shared/infra/identity-management/keycloak-configuration/uc/keycloak-configuration.uc.ts @@ -17,8 +17,8 @@ export class KeycloakConfigurationUc { return this.kcAdmin.testKcConnection(); } - public async clean(): Promise { - return this.keycloakSeedService.clean(); + public async clean(pageSize?: number): Promise { + return this.keycloakSeedService.clean(pageSize); } public async seed(): Promise { diff --git a/apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.ts b/apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.ts index e6526a34c22..7e10179b2cd 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.ts +++ b/apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management-oauth.service.ts @@ -2,7 +2,7 @@ import { HttpService } from '@nestjs/axios'; import { Inject, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { DefaultEncryptionService, IEncryptionService } from '@shared/infra/encryption'; -import { OauthConfigDto } from '@src/modules/system/service'; +import { OauthConfigDto } from '@modules/system/service'; import qs from 'qs'; import { lastValueFrom } from 'rxjs'; import { IdentityManagementOauthService } from '../../identity-management-oauth.service'; diff --git a/apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management.service.integration.spec.ts b/apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management.service.integration.spec.ts index b4e6535011d..fd66603f730 100644 --- a/apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management.service.integration.spec.ts +++ b/apps/server/src/shared/infra/identity-management/keycloak/service/keycloak-identity-management.service.integration.spec.ts @@ -6,7 +6,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { IdmAccount, IdmAccountUpdate } from '@shared/domain'; import { KeycloakAdministrationService } from '@shared/infra/identity-management/keycloak-administration/service/keycloak-administration.service'; import { KeycloakModule } from '@shared/infra/identity-management/keycloak/keycloak.module'; -import { ServerModule } from '@src/modules/server'; +import { ServerModule } from '@modules/server'; import { v1 } from 'uuid'; import { IdentityManagementService } from '../../identity-management.service'; import { KeycloakIdentityManagementService } from './keycloak-identity-management.service'; diff --git a/apps/server/src/shared/infra/oauth-provider/dto/request/accept-consent-request.body.ts b/apps/server/src/shared/infra/oauth-provider/dto/request/accept-consent-request.body.ts index 6c553afdd50..235cb9191ea 100644 --- a/apps/server/src/shared/infra/oauth-provider/dto/request/accept-consent-request.body.ts +++ b/apps/server/src/shared/infra/oauth-provider/dto/request/accept-consent-request.body.ts @@ -1,4 +1,4 @@ -import { IdToken } from '@src/modules/oauth-provider/interface/id-token'; +import { IdToken } from '@modules/oauth-provider/interface/id-token'; export interface AcceptConsentRequestBody { grant_access_token_audience?: string[]; diff --git a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.integration.spec.ts b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.integration.spec.ts index 7c02cbc8a75..0a9151d8c9c 100644 --- a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.integration.spec.ts @@ -12,12 +12,12 @@ import { schoolFactory, } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { CustomParameterEntry } from '@src/modules/tool/common/domain'; -import { ToolContextType } from '@src/modules/tool/common/enum'; -import { ContextExternalTool, ContextExternalToolProps } from '@src/modules/tool/context-external-tool/domain'; -import { ContextExternalToolEntity, ContextExternalToolType } from '@src/modules/tool/context-external-tool/entity'; -import { ContextExternalToolQuery } from '@src/modules/tool/context-external-tool/uc/dto/context-external-tool.types'; -import { SchoolExternalToolEntity } from '@src/modules/tool/school-external-tool/entity'; +import { CustomParameterEntry } from '@modules/tool/common/domain'; +import { ToolContextType } from '@modules/tool/common/enum'; +import { ContextExternalTool, ContextExternalToolProps } from '@modules/tool/context-external-tool/domain'; +import { ContextExternalToolEntity, ContextExternalToolType } from '@modules/tool/context-external-tool/entity'; +import { ContextExternalToolQuery } from '@modules/tool/context-external-tool/uc/dto/context-external-tool.types'; +import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; import { ContextExternalToolRepo } from './context-external-tool.repo'; describe('ContextExternalToolRepo', () => { @@ -127,7 +127,7 @@ describe('ContextExternalToolRepo', () => { }); describe('save', () => { - describe('when context is known', () => { + describe('when context is course', () => { function setup() { const domainObject: ContextExternalTool = contextExternalToolFactory.build({ displayName: 'displayName', @@ -159,6 +159,38 @@ describe('ContextExternalToolRepo', () => { }); }); + describe('when context is board card', () => { + function setup() { + const domainObject: ContextExternalTool = contextExternalToolFactory.build({ + displayName: 'displayName', + contextRef: { + id: new ObjectId().toHexString(), + type: ToolContextType.BOARD_ELEMENT, + }, + parameters: [new CustomParameterEntry({ name: 'param', value: 'value' })], + schoolToolRef: { + schoolToolId: new ObjectId().toHexString(), + schoolId: undefined, + }, + toolVersion: 1, + }); + + return { + domainObject, + }; + } + + it('should save a ContextExternalToolDO', async () => { + const { domainObject } = setup(); + const { id, ...expected } = domainObject; + + const result: ContextExternalTool = await repo.save(domainObject); + + expect(result).toMatchObject(expected); + expect(result.id).toBeDefined(); + }); + }); + describe('when context is unknown', () => { const setup = () => { const domainObject: ContextExternalTool = contextExternalToolFactory.build({ diff --git a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.ts b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.ts index b766828beff..5ad1629f0c2 100644 --- a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.ts +++ b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.repo.ts @@ -3,16 +3,16 @@ import { EntityManager } from '@mikro-orm/mongodb'; import { Injectable } from '@nestjs/common'; import { BaseDORepo } from '@shared/repo'; import { LegacyLogger } from '@src/core/logger'; -import { ToolContextType } from '@src/modules/tool/common/enum/tool-context-type.enum'; -import { ContextExternalTool, ContextRef } from '@src/modules/tool/context-external-tool/domain'; +import { ToolContextType } from '@modules/tool/common/enum/tool-context-type.enum'; +import { ContextExternalTool, ContextRef } from '@modules/tool/context-external-tool/domain'; import { ContextExternalToolEntity, ContextExternalToolType, IContextExternalToolProperties, -} from '@src/modules/tool/context-external-tool/entity'; -import { ContextExternalToolQuery } from '@src/modules/tool/context-external-tool/uc/dto/context-external-tool.types'; -import { SchoolExternalToolRefDO } from '@src/modules/tool/school-external-tool/domain'; -import { SchoolExternalToolEntity } from '@src/modules/tool/school-external-tool/entity'; +} from '@modules/tool/context-external-tool/entity'; +import { ContextExternalToolQuery } from '@modules/tool/context-external-tool/uc/dto/context-external-tool.types'; +import { SchoolExternalToolRefDO } from '@modules/tool/school-external-tool/domain'; +import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; import { EntityId } from '../../domain'; import { ExternalToolRepoMapper } from '../externaltool'; import { ContextExternalToolScope } from './context-external-tool.scope'; @@ -115,6 +115,8 @@ export class ContextExternalToolRepo extends BaseDORepo< switch (type) { case ToolContextType.COURSE: return ContextExternalToolType.COURSE; + case ToolContextType.BOARD_ELEMENT: + return ContextExternalToolType.BOARD_ELEMENT; default: throw new Error('Unknown ToolContextType'); } @@ -124,6 +126,8 @@ export class ContextExternalToolRepo extends BaseDORepo< switch (type) { case ContextExternalToolType.COURSE: return ToolContextType.COURSE; + case ContextExternalToolType.BOARD_ELEMENT: + return ToolContextType.BOARD_ELEMENT; default: throw new Error('Unknown ContextExternalToolType'); } diff --git a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.scope.spec.ts b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.scope.spec.ts index b6896ae497f..25c77bf5beb 100644 --- a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.scope.spec.ts +++ b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.scope.spec.ts @@ -1,6 +1,6 @@ import { schoolExternalToolEntityFactory } from '@shared/testing'; -import { ToolContextType } from '@src/modules/tool/common/enum'; -import { SchoolExternalToolEntity } from '@src/modules/tool/school-external-tool/entity'; +import { ToolContextType } from '@modules/tool/common/enum'; +import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; import { ContextExternalToolScope } from './context-external-tool.scope'; describe('CourseExternalToolScope', () => { diff --git a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.scope.ts b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.scope.ts index 51540ed17ba..b2382c71c2c 100644 --- a/apps/server/src/shared/repo/contextexternaltool/context-external-tool.scope.ts +++ b/apps/server/src/shared/repo/contextexternaltool/context-external-tool.scope.ts @@ -1,7 +1,7 @@ import { Scope } from '@shared/repo'; import { EntityId } from '@shared/domain'; -import { ToolContextType } from '@src/modules/tool/common/enum'; -import { ContextExternalToolEntity } from '@src/modules/tool/context-external-tool/entity'; +import { ToolContextType } from '@modules/tool/common/enum'; +import { ContextExternalToolEntity } from '@modules/tool/context-external-tool/entity'; export class ContextExternalToolScope extends Scope { byId(id: EntityId | undefined): ContextExternalToolScope { diff --git a/apps/server/src/shared/repo/course/course.repo.integration.spec.ts b/apps/server/src/shared/repo/course/course.repo.integration.spec.ts index 42df9c6ba24..5474c4ec19d 100644 --- a/apps/server/src/shared/repo/course/course.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/course/course.repo.integration.spec.ts @@ -72,6 +72,8 @@ describe('course repo', () => { 'updatedAt', 'students', 'features', + 'classes', + 'groups', ].sort(); expect(keysOfFirstElements).toEqual(expectedResult); }); diff --git a/apps/server/src/shared/repo/externaltool/external-tool-sorting.mapper.spec.ts b/apps/server/src/shared/repo/externaltool/external-tool-sorting.mapper.spec.ts index d40c8654878..d3fe01c1b07 100644 --- a/apps/server/src/shared/repo/externaltool/external-tool-sorting.mapper.spec.ts +++ b/apps/server/src/shared/repo/externaltool/external-tool-sorting.mapper.spec.ts @@ -1,8 +1,8 @@ import { QueryOrderMap } from '@mikro-orm/core'; import { LtiTool, SortOrder, SortOrderMap } from '@shared/domain'; import { ExternalToolSortingMapper } from '@shared/repo'; -import { ExternalTool } from '@src/modules/tool/external-tool/domain'; -import { ExternalToolEntity } from '@src/modules/tool/external-tool/entity'; +import { ExternalTool } from '@modules/tool/external-tool/domain'; +import { ExternalToolEntity } from '@modules/tool/external-tool/entity'; describe('ExternalToolSortingMapper', () => { describe('mapDOSortOrderToQueryOrder', () => { diff --git a/apps/server/src/shared/repo/externaltool/external-tool-sorting.mapper.ts b/apps/server/src/shared/repo/externaltool/external-tool-sorting.mapper.ts index 751130ad784..893f5cbc923 100644 --- a/apps/server/src/shared/repo/externaltool/external-tool-sorting.mapper.ts +++ b/apps/server/src/shared/repo/externaltool/external-tool-sorting.mapper.ts @@ -1,7 +1,7 @@ import { QueryOrderMap } from '@mikro-orm/core'; import { SortOrderMap } from '@shared/domain'; -import { ExternalTool } from '@src/modules/tool/external-tool/domain'; -import { ExternalToolEntity } from '@src/modules/tool/external-tool/entity'; +import { ExternalTool } from '@modules/tool/external-tool/domain'; +import { ExternalToolEntity } from '@modules/tool/external-tool/entity'; export class ExternalToolSortingMapper { static mapDOSortOrderToQueryOrder(sort: SortOrderMap): QueryOrderMap { diff --git a/apps/server/src/shared/repo/externaltool/external-tool.repo.integration.spec.ts b/apps/server/src/shared/repo/externaltool/external-tool.repo.integration.spec.ts index 1ba9fa09aea..56cba80ec69 100644 --- a/apps/server/src/shared/repo/externaltool/external-tool.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/externaltool/external-tool.repo.integration.spec.ts @@ -6,8 +6,8 @@ import { MongoMemoryDatabaseModule } from '@shared/infra/database'; import { ExternalToolRepo, ExternalToolRepoMapper } from '@shared/repo'; import { cleanupCollections, externalToolEntityFactory } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { ExternalToolSearchQuery } from '@src/modules/tool'; -import { CustomParameter } from '@src/modules/tool/common/domain'; +import { ExternalToolSearchQuery } from '@modules/tool'; +import { CustomParameter } from '@modules/tool/common/domain'; import { CustomParameterLocation, CustomParameterScope, @@ -15,14 +15,9 @@ import { LtiMessageType, LtiPrivacyPermission, ToolConfigType, -} from '@src/modules/tool/common/enum'; -import { - BasicToolConfig, - ExternalTool, - Lti11ToolConfig, - Oauth2ToolConfig, -} from '@src/modules/tool/external-tool/domain'; -import { ExternalToolEntity } from '@src/modules/tool/external-tool/entity'; +} from '@modules/tool/common/enum'; +import { BasicToolConfig, ExternalTool, Lti11ToolConfig, Oauth2ToolConfig } from '@modules/tool/external-tool/domain'; +import { ExternalToolEntity } from '@modules/tool/external-tool/entity'; describe('ExternalToolRepo', () => { let module: TestingModule; diff --git a/apps/server/src/shared/repo/externaltool/external-tool.repo.mapper.ts b/apps/server/src/shared/repo/externaltool/external-tool.repo.mapper.ts index bcd319de14d..ab35c7b5fda 100644 --- a/apps/server/src/shared/repo/externaltool/external-tool.repo.mapper.ts +++ b/apps/server/src/shared/repo/externaltool/external-tool.repo.mapper.ts @@ -1,13 +1,8 @@ import { UnprocessableEntityException } from '@nestjs/common'; -import { CustomParameter, CustomParameterEntry } from '@src/modules/tool/common/domain'; -import { CustomParameterEntryEntity } from '@src/modules/tool/common/entity'; -import { ToolConfigType } from '@src/modules/tool/common/enum'; -import { - BasicToolConfig, - ExternalTool, - Lti11ToolConfig, - Oauth2ToolConfig, -} from '@src/modules/tool/external-tool/domain'; +import { CustomParameter, CustomParameterEntry } from '@modules/tool/common/domain'; +import { CustomParameterEntryEntity } from '@modules/tool/common/entity'; +import { ToolConfigType } from '@modules/tool/common/enum'; +import { BasicToolConfig, ExternalTool, Lti11ToolConfig, Oauth2ToolConfig } from '@modules/tool/external-tool/domain'; import { BasicToolConfigEntity, CustomParameterEntity, @@ -15,7 +10,7 @@ import { IExternalToolProperties, Lti11ToolConfigEntity, Oauth2ToolConfigEntity, -} from '@src/modules/tool/external-tool/entity'; +} from '@modules/tool/external-tool/entity'; // TODO: maybe rename because of usage in external tool repo and school external tool repo export class ExternalToolRepoMapper { diff --git a/apps/server/src/shared/repo/externaltool/external-tool.repo.ts b/apps/server/src/shared/repo/externaltool/external-tool.repo.ts index 05f3a2d47d1..4ea69a54855 100644 --- a/apps/server/src/shared/repo/externaltool/external-tool.repo.ts +++ b/apps/server/src/shared/repo/externaltool/external-tool.repo.ts @@ -4,10 +4,10 @@ import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator' import { IFindOptions, IPagination, Page, SortOrder } from '@shared/domain'; import { BaseDORepo, ExternalToolRepoMapper, ExternalToolSortingMapper, Scope } from '@shared/repo'; import { LegacyLogger } from '@src/core/logger'; -import { ToolConfigType } from '@src/modules/tool/common/enum'; -import { ExternalToolSearchQuery } from '@src/modules/tool/common/interface'; -import { ExternalTool } from '@src/modules/tool/external-tool/domain'; -import { ExternalToolEntity, IExternalToolProperties } from '@src/modules/tool/external-tool/entity'; +import { ToolConfigType } from '@modules/tool/common/enum'; +import { ExternalToolSearchQuery } from '@modules/tool/common/interface'; +import { ExternalTool } from '@modules/tool/external-tool/domain'; +import { ExternalToolEntity, IExternalToolProperties } from '@modules/tool/external-tool/entity'; import { ExternalToolScope } from './external-tool.scope'; @Injectable() diff --git a/apps/server/src/shared/repo/externaltool/external-tool.scope.ts b/apps/server/src/shared/repo/externaltool/external-tool.scope.ts index 38e7fbf7754..5065c84413c 100644 --- a/apps/server/src/shared/repo/externaltool/external-tool.scope.ts +++ b/apps/server/src/shared/repo/externaltool/external-tool.scope.ts @@ -1,5 +1,5 @@ import { Scope } from '@shared/repo/scope'; -import { ExternalToolEntity } from '@src/modules/tool/external-tool/entity'; +import { ExternalToolEntity } from '@modules/tool/external-tool/entity'; export class ExternalToolScope extends Scope { byName(name: string | undefined): this { diff --git a/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.integration.spec.ts b/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.integration.spec.ts index 98fceceee5f..a2844e8e426 100644 --- a/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.integration.spec.ts @@ -11,11 +11,11 @@ import { } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; import { createMock } from '@golevelup/ts-jest'; -import { SchoolExternalToolQuery } from '@src/modules/tool/school-external-tool/uc/dto/school-external-tool.types'; -import { ExternalToolEntity } from '@src/modules/tool/external-tool/entity'; -import { SchoolExternalToolEntity } from '@src/modules/tool/school-external-tool/entity'; -import { CustomParameterEntry } from '@src/modules/tool/common/domain'; -import { SchoolExternalTool } from '@src/modules/tool/school-external-tool/domain'; +import { SchoolExternalToolQuery } from '@modules/tool/school-external-tool/uc/dto/school-external-tool.types'; +import { ExternalToolEntity } from '@modules/tool/external-tool/entity'; +import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; +import { CustomParameterEntry } from '@modules/tool/common/domain'; +import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; import { SchoolExternalToolRepo } from './school-external-tool.repo'; describe('SchoolExternalToolRepo', () => { diff --git a/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.ts b/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.ts index bc9ab0cc868..64f55c715e8 100644 --- a/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.ts +++ b/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.repo.ts @@ -4,10 +4,10 @@ import { Injectable } from '@nestjs/common/decorators/core/injectable.decorator' import { SchoolEntity } from '@shared/domain'; import { BaseDORepo } from '@shared/repo/base.do.repo'; import { LegacyLogger } from '@src/core/logger'; -import { SchoolExternalToolQuery } from '@src/modules/tool/school-external-tool/uc/dto/school-external-tool.types'; -import { ISchoolExternalToolProperties, SchoolExternalToolEntity } from '@src/modules/tool/school-external-tool/entity'; -import { SchoolExternalTool } from '@src/modules/tool/school-external-tool/domain'; -import { ExternalToolEntity } from '@src/modules/tool/external-tool/entity'; +import { SchoolExternalToolQuery } from '@modules/tool/school-external-tool/uc/dto/school-external-tool.types'; +import { ISchoolExternalToolProperties, SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; +import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; +import { ExternalToolEntity } from '@modules/tool/external-tool/entity'; import { SchoolExternalToolScope } from './school-external-tool.scope'; import { ExternalToolRepoMapper } from '../externaltool'; diff --git a/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.scope.ts b/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.scope.ts index 89ca466b72a..eed938cd7f3 100644 --- a/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.scope.ts +++ b/apps/server/src/shared/repo/schoolexternaltool/school-external-tool.scope.ts @@ -1,6 +1,6 @@ import { Scope } from '@shared/repo/scope'; import { EntityId } from '@shared/domain'; -import { SchoolExternalToolEntity } from '@src/modules/tool/school-external-tool/entity'; +import { SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; export class SchoolExternalToolScope extends Scope { bySchoolId(schoolId: EntityId | undefined): this { diff --git a/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts b/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts index 0d273283ec2..ddcfb55b520 100644 --- a/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/user/user-do.repo.integration.spec.ts @@ -27,7 +27,7 @@ import { userFactory, } from '@shared/testing'; import { LegacyLogger } from '@src/core/logger'; -import { UserQuery } from '@src/modules/user/service/user-query.type'; +import { UserQuery } from '@modules/user/service/user-query.type'; describe('UserRepo', () => { let module: TestingModule; diff --git a/apps/server/src/shared/repo/user/user-do.repo.ts b/apps/server/src/shared/repo/user/user-do.repo.ts index 1856befc18b..e9eae128a1f 100644 --- a/apps/server/src/shared/repo/user/user-do.repo.ts +++ b/apps/server/src/shared/repo/user/user-do.repo.ts @@ -17,7 +17,7 @@ import { RoleReference } from '@shared/domain/domainobject'; import { Page } from '@shared/domain/domainobject/page'; import { UserDO } from '@shared/domain/domainobject/user.do'; import { BaseDORepo, Scope } from '@shared/repo'; -import { UserQuery } from '@src/modules/user/service/user-query.type'; +import { UserQuery } from '@modules/user/service/user-query.type'; import { UserScope } from './user.scope'; @Injectable() diff --git a/apps/server/src/shared/repo/user/user.repo.integration.spec.ts b/apps/server/src/shared/repo/user/user.repo.integration.spec.ts index 04e19284040..d469b59c14b 100644 --- a/apps/server/src/shared/repo/user/user.repo.integration.spec.ts +++ b/apps/server/src/shared/repo/user/user.repo.integration.spec.ts @@ -399,4 +399,45 @@ describe('user repo', () => { expect(user.id).not.toBeNull(); }); }); + + describe('delete', () => { + const setup = async () => { + const user1: User = userFactory.buildWithId(); + const user2: User = userFactory.buildWithId(); + const user3: User = userFactory.buildWithId(); + await em.persistAndFlush([user1, user2, user3]); + + return { + user1, + user2, + user3, + }; + }; + it('should delete user', async () => { + const { user1, user2, user3 } = await setup(); + const deleteResult = await repo.deleteUser(user1.id); + expect(deleteResult).toEqual(1); + + const result1 = await em.find(User, { id: user1.id }); + expect(result1).toHaveLength(0); + + const result2 = await repo.findById(user2.id); + expect(result2).toMatchObject({ + firstName: user2.firstName, + lastName: user2.lastName, + email: user2.email, + roles: user2.roles, + school: user2.school, + }); + + const result3 = await repo.findById(user3.id); + expect(result3).toMatchObject({ + firstName: user3.firstName, + lastName: user3.lastName, + email: user3.email, + roles: user3.roles, + school: user3.school, + }); + }); + }); }); diff --git a/apps/server/src/shared/repo/user/user.repo.ts b/apps/server/src/shared/repo/user/user.repo.ts index 32aa38578a0..a067953faba 100644 --- a/apps/server/src/shared/repo/user/user.repo.ts +++ b/apps/server/src/shared/repo/user/user.repo.ts @@ -163,6 +163,13 @@ export class UserRepo extends BaseRepo { return promise; } + async deleteUser(userId: EntityId): Promise { + const deletedUserNumber: Promise = this._em.nativeDelete(User, { + id: userId, + }); + return deletedUserNumber; + } + private async populateRoles(roles: Role[]): Promise { for (let i = 0; i < roles.length; i += 1) { const role = roles[i]; diff --git a/apps/server/src/shared/testing/factory/account-dto.factory.ts b/apps/server/src/shared/testing/factory/account-dto.factory.ts index c8c5b07e183..126d469e644 100644 --- a/apps/server/src/shared/testing/factory/account-dto.factory.ts +++ b/apps/server/src/shared/testing/factory/account-dto.factory.ts @@ -1,4 +1,4 @@ -import { AccountDto } from '@src/modules/account/services/dto'; +import { AccountDto } from '@modules/account/services/dto'; import { ObjectId } from 'bson'; import { defaultTestPasswordHash } from './account.factory'; import { BaseFactory } from './base.factory'; diff --git a/apps/server/src/shared/testing/factory/boardnode/index.ts b/apps/server/src/shared/testing/factory/boardnode/index.ts index 410a399ccff..14ae5c29312 100644 --- a/apps/server/src/shared/testing/factory/boardnode/index.ts +++ b/apps/server/src/shared/testing/factory/boardnode/index.ts @@ -1,8 +1,9 @@ export * from './card-node.factory'; export * from './column-board-node.factory'; export * from './column-node.factory'; +export * from './external-tool-element-node.factory'; export * from './file-element-node.factory'; +export * from './link-element-node.factory'; export * from './rich-text-element-node.factory'; export * from './submission-container-element-node.factory'; export * from './submission-item-node.factory'; -export * from './external-tool-element-node.factory'; diff --git a/apps/server/src/shared/testing/factory/boardnode/link-element-node.factory.ts b/apps/server/src/shared/testing/factory/boardnode/link-element-node.factory.ts new file mode 100644 index 00000000000..1725634705f --- /dev/null +++ b/apps/server/src/shared/testing/factory/boardnode/link-element-node.factory.ts @@ -0,0 +1,14 @@ +/* istanbul ignore file */ +import { LinkElementNode, LinkElementNodeProps } from '@shared/domain'; +import { BaseFactory } from '../base.factory'; + +export const linkElementNodeFactory = BaseFactory.define( + LinkElementNode, + ({ sequence }) => { + const url = `https://www.example.com/link/${sequence}`; + return { + url, + title: `The example page ${sequence}`, + }; + } +); diff --git a/apps/server/src/shared/testing/factory/boardnode/submission-container-element-node.factory.ts b/apps/server/src/shared/testing/factory/boardnode/submission-container-element-node.factory.ts index 1d62edf6051..a0d65c152cb 100644 --- a/apps/server/src/shared/testing/factory/boardnode/submission-container-element-node.factory.ts +++ b/apps/server/src/shared/testing/factory/boardnode/submission-container-element-node.factory.ts @@ -6,5 +6,7 @@ export const submissionContainerElementNodeFactory = BaseFactory.define< SubmissionContainerElementNode, SubmissionContainerNodeProps >(SubmissionContainerElementNode, () => { - return {}; + return { + dueDate: null, + }; }); diff --git a/apps/server/src/shared/testing/factory/context-external-tool-entity.factory.ts b/apps/server/src/shared/testing/factory/context-external-tool-entity.factory.ts index 1299340095e..fb545c1e2e7 100644 --- a/apps/server/src/shared/testing/factory/context-external-tool-entity.factory.ts +++ b/apps/server/src/shared/testing/factory/context-external-tool-entity.factory.ts @@ -1,10 +1,10 @@ import { BaseFactory } from '@shared/testing/factory/base.factory'; -import { CustomParameterEntryEntity } from '@src/modules/tool/common/entity'; +import { CustomParameterEntryEntity } from '@modules/tool/common/entity'; import { ContextExternalToolEntity, ContextExternalToolType, IContextExternalToolProperties, -} from '@src/modules/tool/context-external-tool/entity'; +} from '@modules/tool/context-external-tool/entity'; import { courseFactory } from './course.factory'; import { schoolExternalToolEntityFactory } from './school-external-tool-entity.factory'; diff --git a/apps/server/src/shared/testing/factory/domainobject/board/board-do-authorizable.factory.ts b/apps/server/src/shared/testing/factory/domainobject/board/board-do-authorizable.factory.ts new file mode 100644 index 00000000000..f774ba97bb4 --- /dev/null +++ b/apps/server/src/shared/testing/factory/domainobject/board/board-do-authorizable.factory.ts @@ -0,0 +1,14 @@ +import { BoardDoAuthorizable, BoardDoAuthorizableProps, UserRoleEnum } from '@shared/domain/domainobject/board'; +import { ObjectId } from 'bson'; +import { DomainObjectFactory } from '../domain-object.factory'; + +export const boardDoAuthorizableFactory = DomainObjectFactory.define( + BoardDoAuthorizable, + () => { + return { + id: new ObjectId().toHexString(), + users: [], + requiredUserRole: UserRoleEnum.STUDENT, + }; + } +); diff --git a/apps/server/src/shared/testing/factory/domainobject/board/external-tool.do.factory.ts b/apps/server/src/shared/testing/factory/domainobject/board/external-tool-element.do.factory.ts similarity index 100% rename from apps/server/src/shared/testing/factory/domainobject/board/external-tool.do.factory.ts rename to apps/server/src/shared/testing/factory/domainobject/board/external-tool-element.do.factory.ts diff --git a/apps/server/src/shared/testing/factory/domainobject/board/index.ts b/apps/server/src/shared/testing/factory/domainobject/board/index.ts index e7b3bae56ed..802dcf744f3 100644 --- a/apps/server/src/shared/testing/factory/domainobject/board/index.ts +++ b/apps/server/src/shared/testing/factory/domainobject/board/index.ts @@ -1,8 +1,9 @@ export * from './card.do.factory'; export * from './column-board.do.factory'; export * from './column.do.factory'; +export * from './external-tool-element.do.factory'; export * from './file-element.do.factory'; +export * from './link-element.do.factory'; export * from './rich-text-element.do.factory'; export * from './submission-container-element.do.factory'; export * from './submission-item.do.factory'; -export * from './external-tool.do.factory'; diff --git a/apps/server/src/shared/testing/factory/domainobject/board/link-element.do.factory.ts b/apps/server/src/shared/testing/factory/domainobject/board/link-element.do.factory.ts new file mode 100644 index 00000000000..af0e55a1912 --- /dev/null +++ b/apps/server/src/shared/testing/factory/domainobject/board/link-element.do.factory.ts @@ -0,0 +1,15 @@ +/* istanbul ignore file */ +import { LinkElement, LinkElementProps } from '@shared/domain'; +import { ObjectId } from 'bson'; +import { BaseFactory } from '../../base.factory'; + +export const linkElementFactory = BaseFactory.define(LinkElement, ({ sequence }) => { + return { + id: new ObjectId().toHexString(), + url: `https://www.example.com/link/${sequence}`, + title: 'Website opengraph title', + children: [], + createdAt: new Date(), + updatedAt: new Date(), + }; +}); 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 index a65d5141b61..ba0f8899249 100644 --- a/apps/server/src/shared/testing/factory/domainobject/groups/group.factory.ts +++ b/apps/server/src/shared/testing/factory/domainobject/groups/group.factory.ts @@ -1,5 +1,5 @@ import { ExternalSource } from '@shared/domain'; -import { Group, GroupProps, GroupTypes } from '@src/modules/group/domain'; +import { Group, GroupProps, GroupTypes } from '@modules/group/domain'; import { ObjectId } from 'bson'; import { DomainObjectFactory } from '../domain-object.factory'; diff --git a/apps/server/src/shared/testing/factory/domainobject/tool/context-external-tool.factory.ts b/apps/server/src/shared/testing/factory/domainobject/tool/context-external-tool.factory.ts index 015713dd997..8bb20364db1 100644 --- a/apps/server/src/shared/testing/factory/domainobject/tool/context-external-tool.factory.ts +++ b/apps/server/src/shared/testing/factory/domainobject/tool/context-external-tool.factory.ts @@ -1,7 +1,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; -import { CustomParameterEntry } from '@src/modules/tool/common/domain'; -import { ToolContextType } from '@src/modules/tool/common/enum'; -import { ContextExternalTool, ContextExternalToolProps } from '@src/modules/tool/context-external-tool/domain'; +import { CustomParameterEntry } from '@modules/tool/common/domain'; +import { ToolContextType } from '@modules/tool/common/enum'; +import { ContextExternalTool, ContextExternalToolProps } from '@modules/tool/context-external-tool/domain'; import { DeepPartial } from 'fishery'; import { DoBaseFactory } from '../do-base.factory'; diff --git a/apps/server/src/shared/testing/factory/domainobject/tool/external-tool.factory.ts b/apps/server/src/shared/testing/factory/domainobject/tool/external-tool.factory.ts index cc33b3d63b9..7815d863a69 100644 --- a/apps/server/src/shared/testing/factory/domainobject/tool/external-tool.factory.ts +++ b/apps/server/src/shared/testing/factory/domainobject/tool/external-tool.factory.ts @@ -1,4 +1,4 @@ -import { CustomParameter } from '@src/modules/tool/common/domain'; +import { CustomParameter } from '@modules/tool/common/domain'; import { CustomParameterLocation, CustomParameterScope, @@ -7,14 +7,14 @@ import { LtiPrivacyPermission, TokenEndpointAuthMethod, ToolConfigType, -} from '@src/modules/tool/common/enum'; +} from '@modules/tool/common/enum'; import { BasicToolConfig, ExternalTool, ExternalToolProps, Lti11ToolConfig, Oauth2ToolConfig, -} from '@src/modules/tool/external-tool/domain'; +} from '@modules/tool/external-tool/domain'; import { DeepPartial } from 'fishery'; import { DoBaseFactory } from '../do-base.factory'; diff --git a/apps/server/src/shared/testing/factory/domainobject/tool/school-external-tool.factory.ts b/apps/server/src/shared/testing/factory/domainobject/tool/school-external-tool.factory.ts index 08c52e487cc..a10ae7797fe 100644 --- a/apps/server/src/shared/testing/factory/domainobject/tool/school-external-tool.factory.ts +++ b/apps/server/src/shared/testing/factory/domainobject/tool/school-external-tool.factory.ts @@ -1,5 +1,5 @@ -import { CustomParameterEntry, ToolConfigurationStatus } from '@src/modules/tool/common/domain'; -import { SchoolExternalTool, SchoolExternalToolProps } from '@src/modules/tool/school-external-tool/domain'; +import { CustomParameterEntry, ToolConfigurationStatus } from '@modules/tool/common/domain'; +import { SchoolExternalTool, SchoolExternalToolProps } from '@modules/tool/school-external-tool/domain'; import { DeepPartial } from 'fishery'; import { DoBaseFactory } from '../do-base.factory'; 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 index 241b1fb45bd..562b68f8767 100644 --- a/apps/server/src/shared/testing/factory/external-group-dto.factory.ts +++ b/apps/server/src/shared/testing/factory/external-group-dto.factory.ts @@ -1,7 +1,7 @@ import { RoleName } from '@shared/domain'; import { ObjectId } from 'bson'; -import { ExternalGroupDto } from '@src/modules/provisioning/dto'; -import { GroupTypes } from '@src/modules/group'; +import { ExternalGroupDto } from '@modules/provisioning/dto'; +import { GroupTypes } from '@modules/group'; import { BaseFactory } from './base.factory'; export const externalGroupDtoFactory = BaseFactory.define( diff --git a/apps/server/src/shared/testing/factory/external-tool-entity.factory.ts b/apps/server/src/shared/testing/factory/external-tool-entity.factory.ts index ed47f93b28b..32c077e2529 100644 --- a/apps/server/src/shared/testing/factory/external-tool-entity.factory.ts +++ b/apps/server/src/shared/testing/factory/external-tool-entity.factory.ts @@ -5,7 +5,7 @@ import { LtiMessageType, LtiPrivacyPermission, ToolConfigType, -} from '@src/modules/tool/common/enum'; +} from '@modules/tool/common/enum'; import { BasicToolConfigEntity, CustomParameterEntity, @@ -13,7 +13,7 @@ import { IExternalToolProperties, Lti11ToolConfigEntity, Oauth2ToolConfigEntity, -} from '@src/modules/tool/external-tool/entity'; +} from '@modules/tool/external-tool/entity'; import { DeepPartial } from 'fishery'; import { BaseFactory } from './base.factory'; diff --git a/apps/server/src/shared/testing/factory/external-tool-pseudonym.factory.ts b/apps/server/src/shared/testing/factory/external-tool-pseudonym.factory.ts index 3e263dc9ae5..b3ee2595412 100644 --- a/apps/server/src/shared/testing/factory/external-tool-pseudonym.factory.ts +++ b/apps/server/src/shared/testing/factory/external-tool-pseudonym.factory.ts @@ -1,6 +1,6 @@ import { BaseFactory } from '@shared/testing/factory/base.factory'; import { ObjectId } from '@mikro-orm/mongodb'; -import { ExternalToolPseudonymEntity, IExternalToolPseudonymEntityProps } from '@src/modules/pseudonym/entity'; +import { ExternalToolPseudonymEntity, IExternalToolPseudonymEntityProps } from '@modules/pseudonym/entity'; export const externalToolPseudonymEntityFactory = BaseFactory.define< ExternalToolPseudonymEntity, diff --git a/apps/server/src/shared/testing/factory/filerecord.factory.ts b/apps/server/src/shared/testing/factory/filerecord.factory.ts index 4a0c73966fd..36811ed9752 100644 --- a/apps/server/src/shared/testing/factory/filerecord.factory.ts +++ b/apps/server/src/shared/testing/factory/filerecord.factory.ts @@ -1,5 +1,5 @@ import { FileRecordParentType } from '@shared/infra/rabbitmq'; -import { FileRecord, FileRecordSecurityCheck, IFileRecordProperties } from '@src/modules/files-storage/entity'; +import { FileRecord, FileRecordSecurityCheck, IFileRecordProperties } from '@modules/files-storage/entity'; import { ObjectId } from 'bson'; import { DeepPartial } from 'fishery'; import { BaseFactory } from './base.factory'; diff --git a/apps/server/src/shared/testing/factory/group-entity.factory.ts b/apps/server/src/shared/testing/factory/group-entity.factory.ts index 591b7c37d41..482aca971cf 100644 --- a/apps/server/src/shared/testing/factory/group-entity.factory.ts +++ b/apps/server/src/shared/testing/factory/group-entity.factory.ts @@ -1,5 +1,5 @@ import { ExternalSourceEntity, RoleName } from '@shared/domain'; -import { GroupEntity, GroupEntityProps, GroupEntityTypes, GroupValidPeriodEntity } from '@src/modules/group/entity'; +import { GroupEntity, GroupEntityProps, GroupEntityTypes, GroupValidPeriodEntity } from '@modules/group/entity'; import { BaseFactory } from './base.factory'; import { roleFactory } from './role.factory'; import { schoolFactory } from './school.factory'; diff --git a/apps/server/src/shared/testing/factory/pseudonym.factory.ts b/apps/server/src/shared/testing/factory/pseudonym.factory.ts index 6f2f60e371c..96be2d1c3c0 100644 --- a/apps/server/src/shared/testing/factory/pseudonym.factory.ts +++ b/apps/server/src/shared/testing/factory/pseudonym.factory.ts @@ -1,6 +1,6 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { BaseFactory } from '@shared/testing/factory/base.factory'; -import { PseudonymEntity, PseudonymEntityProps } from '@src/modules/pseudonym/entity'; +import { PseudonymEntity, PseudonymEntityProps } from '@modules/pseudonym/entity'; export const pseudonymEntityFactory = BaseFactory.define( PseudonymEntity, diff --git a/apps/server/src/shared/testing/factory/role-dto.factory.ts b/apps/server/src/shared/testing/factory/role-dto.factory.ts index 03d14965d41..2158580753d 100644 --- a/apps/server/src/shared/testing/factory/role-dto.factory.ts +++ b/apps/server/src/shared/testing/factory/role-dto.factory.ts @@ -1,6 +1,6 @@ import { RoleName } from '@shared/domain'; import { ObjectId } from 'bson'; -import { RoleDto } from '@src/modules/role/service/dto/role.dto'; +import { RoleDto } from '@modules/role/service/dto/role.dto'; import { BaseFactory } from './base.factory'; import { userPermissions } from '../user-role-permissions'; diff --git a/apps/server/src/shared/testing/factory/school-external-tool-entity.factory.ts b/apps/server/src/shared/testing/factory/school-external-tool-entity.factory.ts index c991d5abc75..456356e6e23 100644 --- a/apps/server/src/shared/testing/factory/school-external-tool-entity.factory.ts +++ b/apps/server/src/shared/testing/factory/school-external-tool-entity.factory.ts @@ -1,5 +1,5 @@ import { BaseFactory } from '@shared/testing/factory/base.factory'; -import { ISchoolExternalToolProperties, SchoolExternalToolEntity } from '@src/modules/tool/school-external-tool/entity'; +import { ISchoolExternalToolProperties, SchoolExternalToolEntity } from '@modules/tool/school-external-tool/entity'; import { externalToolEntityFactory } from './external-tool-entity.factory'; import { schoolFactory } from './school.factory'; diff --git a/apps/server/src/shared/testing/factory/share-token.do.factory.ts b/apps/server/src/shared/testing/factory/share-token.do.factory.ts index 02262f4d175..2c23ca904be 100644 --- a/apps/server/src/shared/testing/factory/share-token.do.factory.ts +++ b/apps/server/src/shared/testing/factory/share-token.do.factory.ts @@ -1,6 +1,6 @@ /* istanbul ignore file */ import { EntityId } from '@shared/domain'; -import { ShareTokenDO, ShareTokenParentType } from '@src/modules/sharing/domainobject/share-token.do'; +import { ShareTokenDO, ShareTokenParentType } from '@modules/sharing/domainobject/share-token.do'; import { ObjectId } from 'bson'; import { Factory } from 'fishery'; diff --git a/apps/server/src/shared/testing/map-user-to-current-user.ts b/apps/server/src/shared/testing/map-user-to-current-user.ts index 1d77d8d2bb3..d835c822066 100644 --- a/apps/server/src/shared/testing/map-user-to-current-user.ts +++ b/apps/server/src/shared/testing/map-user-to-current-user.ts @@ -1,6 +1,6 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { Account, EntityId, User } from '@shared/domain'; -import { ICurrentUser } from '@src/modules/authentication'; +import { ICurrentUser } from '@modules/authentication'; export const mapUserToCurrentUser = ( user: User, diff --git a/apps/server/src/shared/testing/test-api-client.ts b/apps/server/src/shared/testing/test-api-client.ts index 733dcdd5cfc..75be7b7dc5f 100644 --- a/apps/server/src/shared/testing/test-api-client.ts +++ b/apps/server/src/shared/testing/test-api-client.ts @@ -140,6 +140,10 @@ export class TestApiClient { } private getJwtFromResponse(response: Response): string { + if (response.error) { + const error = JSON.stringify(response.error); + throw new Error(error); + } if (!this.isAuthenticationResponse(response.body)) { const body = JSON.stringify(response.body); throw new Error(`${testReqestConst.errorMessage} ${body}`); diff --git a/apps/server/src/shared/testing/user-role-permissions.ts b/apps/server/src/shared/testing/user-role-permissions.ts index c2a75fe478d..cfd38cea3a3 100644 --- a/apps/server/src/shared/testing/user-role-permissions.ts +++ b/apps/server/src/shared/testing/user-role-permissions.ts @@ -53,6 +53,7 @@ export const userPermissions = [ Permission.CLASS_VIEW, Permission.COURSE_VIEW, Permission.LERNSTORE_VIEW, + Permission.GROUP_VIEW, ] as Permission[]; export const studentPermissions = [ @@ -73,6 +74,7 @@ const sharedAdminPermissions = [ Permission.COURSE_CREATE, Permission.COURSE_EDIT, Permission.COURSE_REMOVE, + Permission.GROUP_LIST, Permission.NEWS_CREATE, Permission.NEWS_EDIT, Permission.STUDENT_SKIP_REGISTRATION, diff --git a/backup/setup/groups.json b/backup/setup/groups.json new file mode 100644 index 00000000000..36e5e226896 --- /dev/null +++ b/backup/setup/groups.json @@ -0,0 +1,115 @@ +[ + { + "createdAt": { + "$date": "2023-10-17T12:15:26.458Z" + }, + "updatedAt": { + "$date": "2023-10-17T12:15:26.461Z" + }, + "name": "Cypress-Test-Group", + "type": "class", + "externalSource_externalId": "fd84869b-56e8-41d2-a3dd-6c7239068ed5", + "externalSource_system": { + "$oid": "0000d186816abba584714c93" + }, + "users": [ + { + "user": { + "$oid": "5fa2c71bb229544f2c6966d9" + }, + "role": { + "$oid": "0000d186816abba584714c98" + } + }, + { + "user": { + "$oid": "5fa2cccab229544f2c696917" + }, + "role": { + "$oid": "0000d186816abba584714c99" + } + } + ], + "organization": { + "$oid": "5fa2c5ccb229544f2c69666c" + } + }, + { + "createdAt": { + "$date": "2023-10-17T12:15:26.458Z" + }, + "updatedAt": { + "$date": "2023-10-17T12:15:26.461Z" + }, + "name": "Cypress-Test-Group1", + "type": "class", + "externalSource_externalId": "fd84869b-56e8-41d2-a3dd-6c7239068ed5", + "externalSource_system": { + "$oid": "0000d186816abba584714c93" + }, + "users": [ + { + "user": { + "$oid": "5fa2c71bb229544f2c6966d9" + }, + "role": { + "$oid": "0000d186816abba584714c98" + } + }, + { + "user": { + "$oid": "5fa2cccab229544f2c696917" + }, + "role": { + "$oid": "0000d186816abba584714c99" + } + } + ], + "organization": { + "$oid": "5fa2c5ccb229544f2c69666c" + } + }, + { + "createdAt": { + "$date": "2023-10-17T12:15:26.458Z" + }, + "updatedAt": { + "$date": "2023-10-17T12:15:26.461Z" + }, + "name": "Cypress-Test-Group2", + "type": "class", + "externalSource_externalId": "fd84869b-56e8-41d2-a3dd-6c7239068ed5", + "externalSource_system": { + "$oid": "0000d186816abba584714c93" + }, + "users": [ + { + "user": { + "$oid": "5fa2c71bb229544f2c6966d9" + }, + "role": { + "$oid": "0000d186816abba584714c98" + } + }, + { + "user": { + "$oid": "5fa2cccab229544f2c696917" + }, + "role": { + "$oid": "0000d186816abba584714c99" + } + }, + { + "user": { + "$oid": "5fa30079b229544f2c6969ff" + }, + "role": { + "$oid": "0000d186816abba584714c99" + } + } + ], + "organization": { + "$oid": "5fa2c5ccb229544f2c69666c" + } + } +] diff --git a/backup/setup/migrations.json b/backup/setup/migrations.json index 96c0f276522..8292d8fc62f 100644 --- a/backup/setup/migrations.json +++ b/backup/setup/migrations.json @@ -306,5 +306,27 @@ "$date": "2023-09-01T13:14:13.453Z" }, "__v": 0 + }, + { + "_id": { + "$oid": "652686eb35521c3d90686845" + }, + "state": "up", + "name": "remove-moin-schule-logout-endpoint", + "createdAt": { + "$date": "2023-10-11T10:40:18.782Z" + }, + "__v": 0 + }, + { + "_id": { + "$oid": "652ea0196ddf74176cb57561" + }, + "state": "up", + "name": "add-group-view-and-list-permission", + "createdAt": { + "$date": "2023-10-17T14:38:44.886Z" + }, + "__v": 0 } ] diff --git a/backup/setup/roles.json b/backup/setup/roles.json index 3d7909a84da..0ad460fc526 100644 --- a/backup/setup/roles.json +++ b/backup/setup/roles.json @@ -6,7 +6,7 @@ "name": "user", "roles": [], "updatedAt": { - "$date": "2023-05-16T11:11:21.297Z" + "$date": "2023-10-18T05:58:52.716Z" }, "createdAt": { "$date": "2017-01-01T00:06:37.148Z" @@ -58,7 +58,8 @@ "TOOL_VIEW", "TOPIC_VIEW", "NEXTCLOUD_USER", - "CONTEXT_TOOL_USER" + "CONTEXT_TOOL_USER", + "GROUP_VIEW" ], "__v": 0 }, @@ -68,7 +69,7 @@ }, "name": "administrator", "updatedAt": { - "$date": "2023-09-04T12:13:44.069Z" + "$date": "2023-10-18T05:58:52.729Z" }, "createdAt": { "$date": "2017-01-01T00:06:37.148Z" @@ -132,7 +133,8 @@ "SCHOOL_TOOL_ADMIN", "USER_LOGIN_MIGRATION_ADMIN", "START_MEETING", - "JOIN_MEETING" + "JOIN_MEETING", + "GROUP_LIST" ], "__v": 2 }, @@ -142,7 +144,7 @@ }, "name": "superhero", "updatedAt": { - "$date": "2022-09-22T13:14:23.839Z" + "$date": "2023-10-18T05:58:52.729Z" }, "createdAt": { "$date": "2017-01-01T00:06:37.148Z" @@ -188,7 +190,8 @@ "TEAM_EDIT", "TOOL_CREATE", "TOOL_EDIT", - "YEARS_EDIT" + "YEARS_EDIT", + "GROUP_LIST" ], "__v": 2 }, @@ -198,7 +201,7 @@ }, "name": "teacher", "updatedAt": { - "$date": "2023-09-04T12:54:47.237Z" + "$date": "2023-10-18T05:59:16.621Z" }, "createdAt": { "$date": "2017-01-01T00:06:37.148Z" @@ -245,7 +248,8 @@ "HOMEWORK_CREATE", "HOMEWORK_EDIT", "CONTEXT_TOOL_ADMIN", - "JOIN_MEETING" + "JOIN_MEETING", + "GROUP_LIST" ], "__v": 2 }, diff --git a/config/default.schema.json b/config/default.schema.json index 9103cfd7d4f..ef92d3f1db5 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -259,6 +259,11 @@ "type": "string", "default": "files-storage", "description": "rabbitmq exchange name for antivirus" + }, + "USE_STREAM_TO_ANTIVIRUS": { + "type": "boolean", + "default": false, + "description": "send file to antivirus by uploading" } } }, @@ -454,6 +459,25 @@ } } }, + "CLAMAV": { + "type": "object", + "description": "Properties of the ClamAV server", + "required": [], + "properties": { + "SERVICE_HOSTNAME": { + "type": "string", + "description": "IP of host to connect to TCP interface" + }, + "SERVICE_PORT": { + "type": "number", + "description": "Port of host to use when connecting via TCP interface" + } + }, + "default": { + "SERVICE_HOSTNAME": "localhost", + "SERVICE_PORT": 3310 + } + }, "RABBITMQ_URI": { "type": "string", "format": "uri", @@ -1050,6 +1074,11 @@ "default": false, "description": "Enable submissions in column board." }, + "FEATURE_COLUMN_BOARD_LINK_ELEMENT_ENABLED": { + "type": "boolean", + "default": false, + "description": "Enable link elements in column board." + }, "COLUMN_BOARD_HELP_LINK": { "type": "string", "default": "https://docs.dbildungscloud.de/pages/viewpage.action?pageId=270827606", @@ -1293,6 +1322,11 @@ "default": false, "description": "Enables the new class list view" }, + "FEATURE_GROUPS_IN_COURSE_ENABLED": { + "type": "boolean", + "default": false, + "description": "Enables groups of type class in courses" + }, "TSP_SCHOOL_SYNCER": { "type": "object", "description": "TSP School Syncer properties", diff --git a/config/development.json b/config/development.json index a2b8ba524a9..43d1b18640f 100644 --- a/config/development.json +++ b/config/development.json @@ -68,5 +68,6 @@ "FEATURE_COURSE_SHARE": true, "FEATURE_COLUMN_BOARD_ENABLED": true, "FEATURE_COLUMN_BOARD_SUBMISSIONS_ENABLED": true, + "FEATURE_COLUMN_BOARD_LINK_ELEMENT_ENABLED": true, "FEATURE_COLUMN_BOARD_EXTERNAL_TOOLS_ENABLED": true } diff --git a/jest.config.ts b/jest.config.ts index dde0a796711..032f4828dde 100644 --- a/jest.config.ts +++ b/jest.config.ts @@ -25,6 +25,7 @@ let config: Config.InitialOptions = { // add ts-config path's here as regex '^@shared/(.*)$': '/apps/server/src/shared/$1', '^@src/(.*)$': '/apps/server/src/$1', + '^@modules/(.*)$': '/apps/server/src/modules/$1', }, maxWorkers: 2, // limited for not taking all workers within of a single github action }; diff --git a/migrations/1697020818782-remove-moin-schule-logout-endpoint.js b/migrations/1697020818782-remove-moin-schule-logout-endpoint.js new file mode 100644 index 00000000000..793db440b46 --- /dev/null +++ b/migrations/1697020818782-remove-moin-schule-logout-endpoint.js @@ -0,0 +1,87 @@ +const mongoose = require('mongoose'); +// eslint-disable-next-line no-unused-vars +const { alert, error } = require('../src/logger'); + +const { connect, close } = require('../src/utils/database'); + +const Systems = mongoose.model( + 'system2023101111140', + new mongoose.Schema( + { + alias: { type: String }, + oauthConfig: { + type: { + clientId: { type: String, required: true }, + clientSecret: { type: String, required: true }, + grantType: { type: String, required: true }, + redirectUri: { type: String, required: true }, + scope: { type: String, required: true }, + responseType: { type: String, required: true }, + authEndpoint: { type: String, required: true }, + provider: { type: String, required: true }, + logoutEndpoint: { type: String, required: false }, + issuer: { type: String, required: true }, + jwksEndpoint: { type: String, required: true }, + }, + required: false, + }, + }, + { + timestamps: true, + } + ), + 'systems' +); + +module.exports = { + up: async function up() { + await connect(); + + const result = await Systems.findOneAndUpdate( + { alias: 'SANIS' }, + { + $unset: { + 'oauthConfig.logoutEndpoint': 1, + }, + } + ) + .lean() + .exec(); + + if (result) { + alert(`Removed logoutEndpoint from oauthConfig of sanis/moin.schule system`); + } else { + alert('No matching document found with alias "SANIS" and logoutEndpoint'); + } + + await close(); + }, + + down: async function down() { + await connect(); + + const system = await Systems.findOne({ alias: 'SANIS' }).lean().exec(); + + if (system) { + const { authEndpoint } = system.oauthConfig; + const logoutEndpoint = authEndpoint.replace(/\/auth$/, '/logout'); + + const result = await Systems.findOneAndUpdate( + { alias: 'SANIS' }, + { + $set: { + 'oauthConfig.logoutEndpoint': logoutEndpoint, + }, + } + ) + .lean() + .exec(); + + if (result) { + alert(`Added logoutEndpoint to oauthConfig of sanis/moin.schule system`); + } + } + + await close(); + }, +}; diff --git a/migrations/1697553524886-add-group-view-and-list-permission.js b/migrations/1697553524886-add-group-view-and-list-permission.js new file mode 100644 index 00000000000..2bf46755aed --- /dev/null +++ b/migrations/1697553524886-add-group-view-and-list-permission.js @@ -0,0 +1,100 @@ +const mongoose = require('mongoose'); +// eslint-disable-next-line no-unused-vars +const { info } = require('winston'); +const { alert, error } = require('../src/logger'); + +const { connect, close } = require('../src/utils/database'); + +const Roles = mongoose.model( + 'roles2023101716394', + new mongoose.Schema( + { + name: { type: String, required: true }, + permissions: [{ type: String }], + }, + { + timestamps: true, + } + ), + 'roles' +); + +module.exports = { + up: async function up() { + // eslint-disable-next-line no-process-env + if (process.env.SC_THEME !== 'n21') { + info('Permissions GROUP_VIEW and GROUP_LIST will not be added for this instance.'); + return; + } + + await connect(); + + const groupViewPermission = await Roles.updateOne( + { name: 'user' }, + { + $addToSet: { + permissions: { + $each: ['GROUP_VIEW'], + }, + }, + } + ).exec(); + if (groupViewPermission) { + alert(`Permission GROUP_VIEW added to role user`); + } + + const groupListPermission = await Roles.updateMany( + { name: { $in: ['teacher', 'administrator', 'superhero'] } }, + { + $addToSet: { + permissions: { + $each: ['GROUP_LIST'], + }, + }, + } + ).exec(); + if (groupListPermission) { + alert(`Permission GROUP_LIST added to role user and administrator`); + } + + await close(); + }, + + down: async function down() { + // eslint-disable-next-line no-process-env + if (process.env.SC_THEME !== 'n21') { + info('Permissions GROUP_VIEW and GROUP_LIST will not be removed for this instance.'); + return; + } + + await connect(); + + const groupViewRollback = await Roles.updateOne( + { name: 'user' }, + { + $pull: { + permissions: 'GROUP_VIEW', + }, + } + ).exec(); + + if (groupViewRollback) { + alert(`Rollback: Removed permission GROUP_VIEW from role user`); + } + + const groupListRollback = await Roles.updateMany( + { name: { $in: ['teacher', 'administrator', 'superhero'] } }, + { + $pull: { + permissions: 'GROUP_LIST', + }, + } + ).exec(); + + if (groupListRollback) { + alert(`Rollback: Removed permission GROUP_LIST from roles teacher and administrator`); + } + + await close(); + }, +}; diff --git a/package-lock.json b/package-lock.json index c19ca347f18..e36ecdd9873 100644 --- a/package-lock.json +++ b/package-lock.json @@ -58,6 +58,7 @@ "cache-manager": "^2.9.0", "cache-manager-redis-store": "^2.0.0", "chalk": "^5.0.0", + "clamscan": "^2.1.2", "class-transformer": "^0.4.0", "class-validator": "^0.14.0", "client-oauth2": "^4.2.5", @@ -99,6 +100,7 @@ "nest-winston": "^1.9.4", "nestjs-console": "^9.0.0", "oauth-1.0a": "^2.2.6", + "open-graph-scraper": "^6.2.2", "p-limit": "^3.1.0", "papaparse": "^5.1.1", "passport": "^0.6.0", @@ -132,6 +134,7 @@ }, "devDependencies": { "@aws-sdk/client-s3": "^3.352.0", + "@faker-js/faker": "^8.0.2", "@golevelup/ts-jest": "^0.3.4", "@jest-mock/express": "^1.4.5", "@nestjs/cli": "^10.1.17", @@ -141,6 +144,7 @@ "@types/amqplib": "^0.8.2", "@types/bcryptjs": "^2.4.2", "@types/busboy": "^1.5.0", + "@types/clamscan": "^2.0.5", "@types/cookie": "^0.4.1", "@types/crypto-js": "^4.0.2", "@types/express": "^4.17.11", @@ -2911,6 +2915,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@faker-js/faker": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.1.0.tgz", + "integrity": "sha512-38DT60rumHfBYynif3lmtxMqMqmsOQIxQgEuPZxCk2yUYN0eqWpTACgxi0VpidvsJB8CRxCpvP7B3anK85FjtQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0", + "npm": ">=6.14.13" + } + }, "node_modules/@feathers-plus/batch-loader": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/@feathers-plus/batch-loader/-/batch-loader-0.3.6.tgz", @@ -5442,6 +5462,25 @@ "integrity": "sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==", "dev": true }, + "node_modules/@types/clamscan": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/clamscan/-/clamscan-2.0.5.tgz", + "integrity": "sha512-bFqdscswqBia3yKEJZVVWELOVvWKHUR1dCmH4xshYwu0T9YSfZd35Q8Z9jYW0ygxqGlHjLXMb2/7C6CJITbDgg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "axios": "^0.24.0" + } + }, + "node_modules/@types/clamscan/node_modules/axios": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", + "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", + "dev": true, + "dependencies": { + "follow-redirects": "^1.14.4" + } + }, "node_modules/@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -7410,6 +7449,11 @@ "node": ">= 0.8" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "node_modules/bowser": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", @@ -7885,6 +7929,162 @@ "node": "*" } }, + "node_modules/cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/cheerio-select/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/cheerio-select/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/cheerio-select/node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/cheerio-select/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/cheerio/node_modules/htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + }, "node_modules/chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -7943,6 +8143,14 @@ "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", "dev": true }, + "node_modules/clamscan": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clamscan/-/clamscan-2.1.2.tgz", + "integrity": "sha512-pcovgLHcrg3l/mI51Kuk0kN++07pSZdBTskISw0UFvsm8UXda8oNCm0eLeODxFg85Mz+k+TtSS9+XPlriJ8/Fg==", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/class-transformer": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.4.0.tgz", @@ -8865,6 +9073,83 @@ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-select/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/css-select/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/css-select/node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/css-select/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, "node_modules/daemon": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/daemon/-/daemon-1.1.0.tgz", @@ -9137,9 +9422,9 @@ } }, "node_modules/domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", "funding": [ { "type": "github", @@ -18012,6 +18297,17 @@ "gauge": "~1.2.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/number-allocator": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.9.tgz", @@ -18435,6 +18731,25 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/open-graph-scraper": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/open-graph-scraper/-/open-graph-scraper-6.2.2.tgz", + "integrity": "sha512-cQO0c0HF9ZMhSoIEOKMyxbSYwKn6qWBDEdQeCvZnAVwKCxSWj2DV8AwC1J4JCiwZbn/C4grGCJXpvmlAyTXrBg==", + "dependencies": { + "chardet": "^1.6.0", + "cheerio": "^1.0.0-rc.12", + "undici": "^5.22.1", + "validator": "^13.9.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/open-graph-scraper/node_modules/chardet": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-1.6.0.tgz", + "integrity": "sha512-+QOTw3otC4+FxdjK9RopGpNOglADbr4WPFi0SonkO99JbpkTPbMxmdm4NenhF5Zs+4gPXLI1+y2uazws5TMe8w==" + }, "node_modules/optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -18752,6 +19067,54 @@ "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", "integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE=" }, + "node_modules/parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "dependencies": { + "entities": "^4.4.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "dependencies": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, "node_modules/parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -23495,6 +23858,17 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" }, + "node_modules/undici": { + "version": "5.25.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.25.2.tgz", + "integrity": "sha512-tch8RbCfn1UUH1PeVCXva4V8gDpGAud/w0WubD6sHC46vYQ3KDxL+xv1A2UxK0N6jrVedutuPHxe1XIoqerwMw==", + "dependencies": { + "busboy": "^1.6.0" + }, + "engines": { + "node": ">=14.0" + } + }, "node_modules/universal-analytics": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.5.3.tgz", @@ -23709,9 +24083,9 @@ } }, "node_modules/validator": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", - "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==", + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==", "engines": { "node": ">= 0.10" } @@ -26605,6 +26979,12 @@ } } }, + "@faker-js/faker": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-8.1.0.tgz", + "integrity": "sha512-38DT60rumHfBYynif3lmtxMqMqmsOQIxQgEuPZxCk2yUYN0eqWpTACgxi0VpidvsJB8CRxCpvP7B3anK85FjtQ==", + "dev": true + }, "@feathers-plus/batch-loader": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/@feathers-plus/batch-loader/-/batch-loader-0.3.6.tgz", @@ -28431,6 +28811,27 @@ "integrity": "sha512-/ceqdqeRraGolFTcfoXNiqjyQhZzbINDngeoAq9GoHa8PPK1yNzTaxWjA6BFWp5Ua9JpXEMSS4s5i9tS0hOJtw==", "dev": true }, + "@types/clamscan": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@types/clamscan/-/clamscan-2.0.5.tgz", + "integrity": "sha512-bFqdscswqBia3yKEJZVVWELOVvWKHUR1dCmH4xshYwu0T9YSfZd35Q8Z9jYW0ygxqGlHjLXMb2/7C6CJITbDgg==", + "dev": true, + "requires": { + "@types/node": "*", + "axios": "^0.24.0" + }, + "dependencies": { + "axios": { + "version": "0.24.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-0.24.0.tgz", + "integrity": "sha512-Q6cWsys88HoPgAaFAVUb0WpPk0O8iTeisR9IMqy9G8AbO4NlpVknrnQS03zzF9PGAWgO3cgletO3VjV/P7VztA==", + "dev": true, + "requires": { + "follow-redirects": "^1.14.4" + } + } + } + }, "@types/connect": { "version": "3.4.35", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", @@ -30041,6 +30442,11 @@ } } }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" + }, "bowser": { "version": "2.11.0", "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.11.0.tgz", @@ -30408,6 +30814,114 @@ "integrity": "sha1-V00xLt2Iu13YkS6Sht1sCu1KrII=", "dev": true }, + "cheerio": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", + "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", + "requires": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "htmlparser2": "^8.0.1", + "parse5": "^7.0.0", + "parse5-htmlparser2-tree-adapter": "^7.0.0" + }, + "dependencies": { + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + }, + "htmlparser2": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", + "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1", + "entities": "^4.4.0" + } + } + } + }, + "cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "requires": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "dependencies": { + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + } + } + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -30449,6 +30963,11 @@ "integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==", "dev": true }, + "clamscan": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clamscan/-/clamscan-2.1.2.tgz", + "integrity": "sha512-pcovgLHcrg3l/mI51Kuk0kN++07pSZdBTskISw0UFvsm8UXda8oNCm0eLeODxFg85Mz+k+TtSS9+XPlriJ8/Fg==" + }, "class-transformer": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/class-transformer/-/class-transformer-0.4.0.tgz", @@ -31206,6 +31725,58 @@ "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-4.1.1.tgz", "integrity": "sha512-o2JlM7ydqd3Qk9CA0L4NL6mTzU2sdx96a+oOfPu8Mkl/PK51vSyoi8/rQ8NknZtk44vq15lmhAj9CIAGwgeWKw==" }, + "css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "dependencies": { + "dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "requires": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + } + }, + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + }, + "domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "requires": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + } + }, + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + } + } + }, + "css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==" + }, "daemon": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/daemon/-/daemon-1.1.0.tgz", @@ -31404,9 +31975,9 @@ } }, "domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==" }, "domhandler": { "version": "4.3.0", @@ -38115,6 +38686,14 @@ "gauge": "~1.2.0" } }, + "nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "requires": { + "boolbase": "^1.0.0" + } + }, "number-allocator": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/number-allocator/-/number-allocator-1.0.9.tgz", @@ -38441,6 +39020,24 @@ "is-wsl": "^2.2.0" } }, + "open-graph-scraper": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/open-graph-scraper/-/open-graph-scraper-6.2.2.tgz", + "integrity": "sha512-cQO0c0HF9ZMhSoIEOKMyxbSYwKn6qWBDEdQeCvZnAVwKCxSWj2DV8AwC1J4JCiwZbn/C4grGCJXpvmlAyTXrBg==", + "requires": { + "chardet": "^1.6.0", + "cheerio": "^1.0.0-rc.12", + "undici": "^5.22.1", + "validator": "^13.9.0" + }, + "dependencies": { + "chardet": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-1.6.0.tgz", + "integrity": "sha512-+QOTw3otC4+FxdjK9RopGpNOglADbr4WPFi0SonkO99JbpkTPbMxmdm4NenhF5Zs+4gPXLI1+y2uazws5TMe8w==" + } + } + }, "optionator": { "version": "0.9.1", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", @@ -38673,6 +39270,40 @@ "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", "integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE=" }, + "parse5": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.1.2.tgz", + "integrity": "sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw==", + "requires": { + "entities": "^4.4.0" + }, + "dependencies": { + "entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==" + } + } + }, + "parse5-htmlparser2-tree-adapter": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.0.0.tgz", + "integrity": "sha512-B77tOZrqqfUfnVcOrUvfdLbz4pu4RopLD/4vmu3HUPswwTA8OH0EMW9BlWR2B0RCoiZRAHEUu7IxeP1Pd1UU+g==", + "requires": { + "domhandler": "^5.0.2", + "parse5": "^7.0.0" + }, + "dependencies": { + "domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "requires": { + "domelementtype": "^2.3.0" + } + } + } + }, "parseurl": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", @@ -42249,6 +42880,14 @@ "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.12.1.tgz", "integrity": "sha512-hEQt0+ZLDVUMhebKxL4x1BTtDY7bavVofhZ9KZ4aI26X9SRaE+Y3m83XUL1UP2jn8ynjndwCCpEHdUG+9pP1Tw==" }, + "undici": { + "version": "5.25.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.25.2.tgz", + "integrity": "sha512-tch8RbCfn1UUH1PeVCXva4V8gDpGAud/w0WubD6sHC46vYQ3KDxL+xv1A2UxK0N6jrVedutuPHxe1XIoqerwMw==", + "requires": { + "busboy": "^1.6.0" + } + }, "universal-analytics": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/universal-analytics/-/universal-analytics-0.5.3.tgz", @@ -42439,9 +43078,9 @@ } }, "validator": { - "version": "13.7.0", - "resolved": "https://registry.npmjs.org/validator/-/validator-13.7.0.tgz", - "integrity": "sha512-nYXQLCBkpJ8X6ltALua9dRrZDHVYxjJ1wgskNt1lH9fzGjs3tgojGSCBjmEPwkWS1y29+DrizMTW19Pr9uB2nw==" + "version": "13.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz", + "integrity": "sha512-Ii+sehpSfZy+At5nPdnyMhx78fEoPDkR2XW/zimHEL3MyGJQOCQ7WeP20jPYRz7ZCpcKLB21NxuXHF3bxjStBQ==" }, "vary": { "version": "1.1.2", diff --git a/package.json b/package.json index 99a99b681d4..3c5a73df7d1 100644 --- a/package.json +++ b/package.json @@ -140,6 +140,7 @@ "cache-manager": "^2.9.0", "cache-manager-redis-store": "^2.0.0", "chalk": "^5.0.0", + "clamscan": "^2.1.2", "class-transformer": "^0.4.0", "class-validator": "^0.14.0", "client-oauth2": "^4.2.5", @@ -181,6 +182,7 @@ "nest-winston": "^1.9.4", "nestjs-console": "^9.0.0", "oauth-1.0a": "^2.2.6", + "open-graph-scraper": "^6.2.2", "p-limit": "^3.1.0", "papaparse": "^5.1.1", "passport": "^0.6.0", @@ -214,6 +216,7 @@ }, "devDependencies": { "@aws-sdk/client-s3": "^3.352.0", + "@faker-js/faker": "^8.0.2", "@golevelup/ts-jest": "^0.3.4", "@jest-mock/express": "^1.4.5", "@nestjs/cli": "^10.1.17", @@ -223,6 +226,7 @@ "@types/amqplib": "^0.8.2", "@types/bcryptjs": "^2.4.2", "@types/busboy": "^1.5.0", + "@types/clamscan": "^2.0.5", "@types/cookie": "^0.4.1", "@types/crypto-js": "^4.0.2", "@types/express": "^4.17.11", diff --git a/src/middleware/errorHandler.js b/src/middleware/errorHandler.js index 0cc57312116..463e924ee98 100644 --- a/src/middleware/errorHandler.js +++ b/src/middleware/errorHandler.js @@ -125,6 +125,7 @@ const secretDataKeys = (() => 'gradeComment', '_csrf', 'searchUserPassword', + 'authorization', ].map((k) => k.toLocaleLowerCase()))(); const filterSecretValue = (key, value) => { @@ -174,6 +175,7 @@ const filterSecrets = (error, req, res, next) => { if (error) { // req.url = filterQuery(req.url); req.originalUrl = filterQuery(req.originalUrl); + req.headers = filter(req.headers); req.body = filter(req.body); error.data = filter(error.data); error.options = filter(error.options); diff --git a/src/services/config/publicAppConfigService.js b/src/services/config/publicAppConfigService.js index 31a3ae22224..06a54c6cf96 100644 --- a/src/services/config/publicAppConfigService.js +++ b/src/services/config/publicAppConfigService.js @@ -39,6 +39,7 @@ const exposedVars = [ 'SC_TITLE', 'FEATURE_COLUMN_BOARD_ENABLED', 'FEATURE_COLUMN_BOARD_SUBMISSIONS_ENABLED', + 'FEATURE_COLUMN_BOARD_LINK_ELEMENT_ENABLED', 'FEATURE_COLUMN_BOARD_EXTERNAL_TOOLS_ENABLED', 'FEATURE_COURSE_SHARE', 'FEATURE_COURSE_SHARE_NEW', diff --git a/src/services/edusharing/index.js b/src/services/edusharing/index.js index d002686ebea..27a45244494 100644 --- a/src/services/edusharing/index.js +++ b/src/services/edusharing/index.js @@ -35,7 +35,7 @@ class EduSharingPlayer { throw new MethodNotAllowed('This feature is disabled on this instance'); } const esPlayer = EduSharingConnectorV7.getPlayerForNode(uuid); - + return esPlayer; } } @@ -49,7 +49,7 @@ class MerlinToken { module.exports = (app) => { const eduSharingRoute = '/edu-sharing'; const eduSharingPlayerRoute = '/edu-sharing/player'; - const merlinRoute = '/edu-sharing/merlinToken'; + const merlinRoute = '/edu-sharing-merlinToken'; const docRoute = '/edu-sharing/api'; app.use(eduSharingRoute, new EduSharing()); diff --git a/src/services/edusharing/services/EduSharingConnectorV6.js b/src/services/edusharing/services/EduSharingConnectorV6.js index 95a924a1eda..cc9f67f8230 100644 --- a/src/services/edusharing/services/EduSharingConnectorV6.js +++ b/src/services/edusharing/services/EduSharingConnectorV6.js @@ -114,7 +114,9 @@ class EduSharingConnector { if (err.statusCode === 404) { return null; } - logger.error(`Edu-Sharing failed request with error ${err.statusCode} ${err.message}`, options); + // eslint-disable-next-line no-unused-vars + const { headers, ...logOptions } = options; + logger.error(`Edu-Sharing failed request with error ${err.statusCode} ${err.message}`, logOptions); if (retried === true) { throw new GeneralError('Edu-Sharing Request failed'); } else { diff --git a/src/services/lesson/hooks/index.js b/src/services/lesson/hooks/index.js index b7b0b0ea5e7..458b64785ab 100644 --- a/src/services/lesson/hooks/index.js +++ b/src/services/lesson/hooks/index.js @@ -60,7 +60,7 @@ const convertMerlinUrl = async (context) => { await Promise.all( content.content.resources.map(async (resource) => { if (resource && resource.merlinReference) { - resource.url = await context.app.service('edu-sharing/merlinToken').find({ + resource.url = await context.app.service('edu-sharing-merlinToken').find({ ...context.params, query: { ...context.params.query, merlinReference: resource.merlinReference }, }); @@ -82,7 +82,7 @@ const convertMerlinUrl = async (context) => { await Promise.all( materialIds.map(async (material) => { if (material.merlinReference) { - material.url = await context.app.service('edu-sharing/merlinToken').find({ + material.url = await context.app.service('edu-sharing-merlinToken').find({ ...context.params, query: { ...context.params.query, @@ -229,48 +229,50 @@ const populateWhitelist = { ], }; -exports.before = () => ({ - all: [authenticate('jwt'), mapUsers], - find: [ - hasPermission('TOPIC_VIEW'), - iff(isProvider('external'), validateLessonFind), - iff(isProvider('external'), getRestrictPopulatesHook(populateWhitelist)), - iff(isProvider('external'), restrictToUsersCoursesLessons), - ], - get: [ - hasPermission('TOPIC_VIEW'), - iff(isProvider('external'), getRestrictPopulatesHook(populateWhitelist)), - iff(isProvider('external'), restrictToUsersCoursesLessons), - ], - create: [ - checkIfCourseGroupLesson.bind(this, 'COURSEGROUP_CREATE', 'TOPIC_CREATE', true), - injectUserId, - checkCorrectCourseOrTeamId, - setPosition, - iff(isProvider('external'), preventPopulate), - ], - update: [ - iff(isProvider('external'), preventPopulate), - permitGroupOperation, - ifNotLocal(checkCorrectCourseOrTeamId), - iff(isProvider('external'), restrictToUsersCoursesLessons), - checkIfCourseGroupLesson.bind(this, 'COURSEGROUP_EDIT', 'TOPIC_EDIT', false), - ], - patch: [ - attachMerlinReferenceToLesson, - checkIfCourseGroupLesson.bind(this, 'COURSEGROUP_EDIT', 'TOPIC_EDIT', false), - permitGroupOperation, - ifNotLocal(checkCorrectCourseOrTeamId), - iff(isProvider('external'), restrictToUsersCoursesLessons), - iff(isProvider('external'), preventPopulate), - ], - remove: [ - checkIfCourseGroupLesson.bind(this, 'COURSEGROUP_CREATE', 'TOPIC_CREATE', false), - permitGroupOperation, - iff(isProvider('external'), restrictToUsersCoursesLessons), - iff(isProvider('external'), preventPopulate), - ], -}); +exports.before = () => { + return { + all: [authenticate('jwt'), mapUsers], + find: [ + hasPermission('TOPIC_VIEW'), + iff(isProvider('external'), validateLessonFind), + iff(isProvider('external'), getRestrictPopulatesHook(populateWhitelist)), + iff(isProvider('external'), restrictToUsersCoursesLessons), + ], + get: [ + hasPermission('TOPIC_VIEW'), + iff(isProvider('external'), getRestrictPopulatesHook(populateWhitelist)), + iff(isProvider('external'), restrictToUsersCoursesLessons), + ], + create: [ + checkIfCourseGroupLesson.bind(this, 'COURSEGROUP_CREATE', 'TOPIC_CREATE', true), + injectUserId, + checkCorrectCourseOrTeamId, + setPosition, + iff(isProvider('external'), preventPopulate), + ], + update: [ + iff(isProvider('external'), preventPopulate), + permitGroupOperation, + ifNotLocal(checkCorrectCourseOrTeamId), + iff(isProvider('external'), restrictToUsersCoursesLessons), + checkIfCourseGroupLesson.bind(this, 'COURSEGROUP_EDIT', 'TOPIC_EDIT', false), + ], + patch: [ + attachMerlinReferenceToLesson, + checkIfCourseGroupLesson.bind(this, 'COURSEGROUP_EDIT', 'TOPIC_EDIT', false), + permitGroupOperation, + ifNotLocal(checkCorrectCourseOrTeamId), + iff(isProvider('external'), restrictToUsersCoursesLessons), + iff(isProvider('external'), preventPopulate), + ], + remove: [ + checkIfCourseGroupLesson.bind(this, 'COURSEGROUP_CREATE', 'TOPIC_CREATE', false), + permitGroupOperation, + iff(isProvider('external'), restrictToUsersCoursesLessons), + iff(isProvider('external'), preventPopulate), + ], + }; +}; exports.after = { all: [], diff --git a/src/services/user-group/hooks/courses.js b/src/services/user-group/hooks/courses.js index e5cdb9a7de4..92a5b987a7c 100644 --- a/src/services/user-group/hooks/courses.js +++ b/src/services/user-group/hooks/courses.js @@ -1,4 +1,6 @@ const _ = require('lodash'); +const { Configuration } = require('@hpi-schul-cloud/commons/lib'); +const { service } = require('feathers-mongoose'); const { BadRequest } = require('../../../errors'); const globalHooks = require('../../../hooks'); @@ -10,17 +12,34 @@ const restrictToCurrentSchool = globalHooks.ifNotLocal(globalHooks.restrictToCur const restrictToUsersOwnCourses = globalHooks.ifNotLocal(globalHooks.restrictToUsersOwnCourses); const { checkScopePermissions } = require('../../helpers/scopePermissions/hooks'); - /** * adds all students to a course when a class is added to the course * @param hook - contains created/patched object and request body */ -const addWholeClassToCourse = (hook) => { +const addWholeClassToCourse = async (hook) => { + const { app } = hook; const requestBody = hook.data; const course = hook.result; - if (requestBody.classIds === undefined) { - return hook; + + if (Configuration.get('FEATURE_GROUPS_IN_COURSE_ENABLED') && (requestBody.groupIds || []).length > 0) { + await Promise.all( + requestBody.groupIds.map((groupId) => + app + .service('nest-group-service') + .findById(groupId) + .then((group) => group.users) + ) + ).then(async (groupUsers) => { + // flatten deep arrays and remove duplicates + const userIds = _.flattenDeep(groupUsers).map((groupUser) => groupUser.userId); + const uniqueUserIds = _.uniqWith(userIds, (a, b) => a === b); + + await CourseModel.update({ _id: course._id }, { $addToSet: { userIds: { $each: uniqueUserIds } } }).exec(); + + return undefined; + }); } + if ((requestBody.classIds || []).length > 0) { // just courses do have a property "classIds" return Promise.all( @@ -34,6 +53,7 @@ const addWholeClassToCourse = (hook) => { studentIds = _.uniqWith(_.flattenDeep(studentIds), (e1, e2) => JSON.stringify(e1) === JSON.stringify(e2)); await CourseModel.update({ _id: course._id }, { $addToSet: { userIds: { $each: studentIds } } }).exec(); + return hook; }); } @@ -47,16 +67,42 @@ const addWholeClassToCourse = (hook) => { * @param hook - contains and request body */ const deleteWholeClassFromCourse = (hook) => { + const { app } = hook; const requestBody = hook.data; const courseId = hook.id; - if (requestBody.classIds === undefined && requestBody.user === undefined) { + if (requestBody.classIds === undefined && requestBody.user === undefined && requestBody.groupIds === undefined) { return hook; } return CourseModel.findById(courseId) .exec() - .then((course) => { + .then(async (course) => { if (!course) return hook; + const removedGroups = _.differenceBy(course.groupIds, requestBody.groupIds, (v) => JSON.stringify(v)); + if (Configuration.get('FEATURE_GROUPS_IN_COURSE_ENABLED') && removedGroups.length > 0) { + await Promise.all( + removedGroups.map((groupId) => + app + .service('nest-group-service') + .findById(groupId) + .then((group) => group.users) + ) + ).then(async (groupUsers) => { + // flatten deep arrays and remove duplicates + const userIds = _.flattenDeep(groupUsers).map((groupUser) => groupUser.userId); + const uniqueUserIds = _.uniqWith(userIds, (a, b) => a === b); + + await CourseModel.update( + { _id: course._id }, + { $pull: { userIds: { $in: uniqueUserIds } } }, + { multi: true } + ).exec(); + hook.data.userIds = hook.data.userIds.filter((value) => !uniqueUserIds.some((id) => equalIds(id, value))); + + return undefined; + }); + } + const removedClasses = _.differenceBy(course.classIds, requestBody.classIds, (v) => JSON.stringify(v)); if (removedClasses.length < 1) return hook; return Promise.all( diff --git a/src/services/user-group/model.js b/src/services/user-group/model.js index 70a7bb7bab3..2bdf688b2c3 100644 --- a/src/services/user-group/model.js +++ b/src/services/user-group/model.js @@ -45,6 +45,7 @@ const timeSchema = new Schema({ const courseSchema = getUserGroupSchema({ description: { type: String }, classIds: [{ type: Schema.Types.ObjectId, required: true, ref: 'class' }], + groupIds: [{ type: Schema.Types.ObjectId }], teacherIds: [{ type: Schema.Types.ObjectId, required: true, ref: 'user' }], substitutionIds: [{ type: Schema.Types.ObjectId, required: true, ref: 'user' }], ltiToolIds: [{ type: Schema.Types.ObjectId, required: true, ref: 'ltiTool' }], diff --git a/test/services/edusharing/services/merlinGenerator.test.js b/test/services/edusharing/services/merlinGenerator.test.js index 43d5fbde13e..00e83f80461 100644 --- a/test/services/edusharing/services/merlinGenerator.test.js +++ b/test/services/edusharing/services/merlinGenerator.test.js @@ -17,7 +17,7 @@ describe('Merlin Token Generator', () => { before(async () => { app = await appPromise(); - MerlinTokenGeneratorService = app.service('edu-sharing/merlinToken'); + MerlinTokenGeneratorService = app.service('edu-sharing-merlinToken'); server = await app.listen(0); nestServices = await setupNestServices(app); }); diff --git a/test/services/user-group/hooks/classes.test.js b/test/services/user-group/hooks/classes.test.js index 33f547e0e38..7353f1520f0 100644 --- a/test/services/user-group/hooks/classes.test.js +++ b/test/services/user-group/hooks/classes.test.js @@ -68,6 +68,7 @@ describe('class hooks', () => { configBefore = Configuration.toObject({}); app = await appPromise(); Configuration.set('TEACHER_STUDENT_VISIBILITY__IS_ENABLED_BY_DEFAULT', 'false'); + Configuration.set('FEATURE_GROUPS_IN_COURSE_ENABLED', 'false'); server = await app.listen(0); nestServices = await setupNestServices(app); }); diff --git a/tsconfig.json b/tsconfig.json index 912ea1b5a78..9bb7c6f72f0 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -19,7 +19,8 @@ "incremental": true, "paths": { "@shared/*": ["apps/server/src/shared/*"], - "@src/*": ["apps/server/src/*"] + "@src/*": ["apps/server/src/*"], + "@modules/*": ["apps/server/src/modules/*"], }, } }