From 5be12384492c24d9a713f1744fad3fdcb791b432 Mon Sep 17 00:00:00 2001 From: mrikallab <93978883+mrikallab@users.noreply.github.com> Date: Mon, 7 Oct 2024 11:58:42 +0200 Subject: [PATCH 01/10] N21-2191 lti encryption (#5274) --- .../templates/configmap_file_init.yml.j2 | 5 + .../mikro-orm/Migration20240926205656.ts | 58 ++ .../service/external-tool.service.spec.ts | 21 + .../service/external-tool.service.ts | 5 +- .../lti11-tool-launch.strategy.spec.ts | 14 +- .../lti11-tool-launch.strategy.ts | 9 +- .../tool/tool-launch/tool-launch.module.ts | 2 + backup/setup/external-tools.json | 738 ++++++++---------- backup/setup/migrations.json | 47 +- backup/setup/school-external-tools.json | 23 - 10 files changed, 454 insertions(+), 468 deletions(-) create mode 100644 apps/server/src/migrations/mikro-orm/Migration20240926205656.ts 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 43c876279ae..f191ce69ca8 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 @@ -517,6 +517,11 @@ data: # ========== Start of the CTL seed data configuration section. echo "Inserting ctl seed data secrets to external-tools..." + + # Encrypt secrets of external tools that contain an lti11 config. + $CTL_SEED_SECRET_ONLINE_DIA_MATHE=$(node scripts/secret.js -s $AES_KEY -e $CTL_SEED_SECRET_ONLINE_DIA_MATHE) + $CTL_SEED_SECRET_ONLINE_DIA_DEUTSCH=$(node scripts/secret.js -s $AES_KEY -e $CTL_SEED_SECRET_ONLINE_DIA_DEUTSCH) + mongosh $DATABASE__URL --quiet --eval 'db.getCollection("external-tools").updateOne( { "name": "Product Test Onlinediagnose Grundschule - Mathematik", diff --git a/apps/server/src/migrations/mikro-orm/Migration20240926205656.ts b/apps/server/src/migrations/mikro-orm/Migration20240926205656.ts new file mode 100644 index 00000000000..7ffd9be3173 --- /dev/null +++ b/apps/server/src/migrations/mikro-orm/Migration20240926205656.ts @@ -0,0 +1,58 @@ +import { Migration } from '@mikro-orm/migrations-mongodb'; +import CryptoJs from 'crypto-js'; + +// eslint-disable-next-line no-process-env + +export class Migration20240926205656 extends Migration { + async up(): Promise { + // eslint-disable-next-line no-process-env + const { AES_KEY } = process.env; + + if (AES_KEY) { + const tools = await this.driver.aggregate('external-tools', [{ $match: { config_type: { $eq: 'lti11' } } }]); + + for await (const tool of tools) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access + if (tool.config_secret) { + // eslint-disable-next-line no-await-in-loop + await this.driver.nativeUpdate( + 'external-tools', + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access + { _id: tool._id }, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access + { $set: { config_secret: CryptoJs.AES.encrypt(tool.config_secret, AES_KEY).toString() } } + ); + } + } + console.info(`Encrypt field config_secret with AES_KEY of the svs.`); + } else { + console.info(`FAILED: Encrypt field config_secret with AES_KEY of the svs. REASON: AES KEY is not provided.`); + } + } + + async down(): Promise { + // eslint-disable-next-line no-process-env + const { AES_KEY } = process.env; + + if (AES_KEY) { + const tools = await this.driver.aggregate('external-tools', [{ $match: { config_type: { $eq: 'lti11' } } }]); + + for await (const tool of tools) { + if (tool) { + // eslint-disable-next-line no-await-in-loop + await this.driver.nativeUpdate( + 'external-tools', + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access + { _id: tool._id }, + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-argument,@typescript-eslint/no-unsafe-member-access + { $set: { config_secret: CryptoJs.AES.decrypt(tool.config_secret, AES_KEY).toString(CryptoJs.enc.Utf8) } } + ); + } + } + + console.info(`Rollback: Encrypt field config_secret with AES_KEY of the svs.`); + } else { + console.info(`FAILED: Encrypt field config_secret with AES_KEY of the svs. REASON: AES KEY is not provided.`); + } + } +} 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 ea54061dd37..5e08193da2c 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 @@ -510,6 +510,27 @@ describe(ExternalToolService.name, () => { }); describe('updateExternalTool', () => { + describe('when external tool with lti11 config is given', () => { + const setup = () => { + encryptionService.encrypt.mockReturnValue('newEncryptedSecret'); + const changedTool: ExternalTool = externalToolFactory + .withLti11Config({ secret: 'newEncryptedSecret' }) + .build({ name: 'newName' }); + + return { + changedTool, + }; + }; + + it('should call externalToolServiceMapper', async () => { + const { changedTool } = setup(); + + await service.updateExternalTool(changedTool); + + expect(externalToolRepo.save).toHaveBeenLastCalledWith(changedTool); + }); + }); + describe('when external tool with oauthConfig is given', () => { const setup = () => { const existingTool: ExternalTool = externalToolFactory.withOauth2Config().buildWithId(); 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 62d1bb39b58..784e64c5643 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 @@ -42,7 +42,10 @@ export class ExternalToolService { } public async updateExternalTool(toUpdate: ExternalTool): Promise { - // TODO N21-2097 use encryption for secret + if (ExternalTool.isLti11Config(toUpdate.config) && toUpdate.config.secret) { + toUpdate.config.secret = this.encryptionService.encrypt(toUpdate.config.secret); + } + await this.updateOauth2ToolConfig(toUpdate); const externalTool: ExternalTool = await this.externalToolRepo.save(toUpdate); diff --git a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.spec.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.spec.ts index 05fecd75d56..74b45a6024d 100644 --- a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.spec.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.spec.ts @@ -1,4 +1,5 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; import { ObjectId } from '@mikro-orm/mongodb'; import { PseudonymService } from '@modules/pseudonym/service'; import { UserService } from '@modules/user'; @@ -36,6 +37,7 @@ describe('Lti11ToolLaunchStrategy', () => { let userService: DeepMocked; let pseudonymService: DeepMocked; let lti11EncryptionService: DeepMocked; + let encryptionService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -77,6 +79,10 @@ describe('Lti11ToolLaunchStrategy', () => { provide: AutoGroupExternalUuidStrategy, useValue: createMock(), }, + { + provide: DefaultEncryptionService, + useValue: createMock(), + }, ], }).compile(); @@ -85,6 +91,7 @@ describe('Lti11ToolLaunchStrategy', () => { userService = module.get(UserService); pseudonymService = module.get(PseudonymService); lti11EncryptionService = module.get(Lti11EncryptionService); + encryptionService = module.get(DefaultEncryptionService); }); afterAll(async () => { @@ -134,10 +141,13 @@ describe('Lti11ToolLaunchStrategy', () => { ], }); + const decrypted = 'decryptedSecret'; + encryptionService.decrypt.mockReturnValue(decrypted); userService.findById.mockResolvedValue(user); return { data, + decrypted, user, mockKey, mockSecret, @@ -148,14 +158,14 @@ describe('Lti11ToolLaunchStrategy', () => { }; it('should contain lti key and secret without location', async () => { - const { data, mockKey, mockSecret } = setup(); + const { data, mockKey, decrypted } = setup(); const result: PropertyData[] = await strategy.buildToolLaunchDataFromConcreteConfig('userId', data); expect(result).toEqual( expect.arrayContaining([ new PropertyData({ name: 'key', value: mockKey }), - new PropertyData({ name: 'secret', value: mockSecret }), + new PropertyData({ name: 'secret', value: decrypted }), ]) ); }); diff --git a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.ts b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.ts index 948df83c295..c0977743e54 100644 --- a/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.ts +++ b/apps/server/src/modules/tool/tool-launch/service/launch-strategy/lti11-tool-launch.strategy.ts @@ -1,7 +1,8 @@ +import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; import { ObjectId } from '@mikro-orm/mongodb'; import { PseudonymService } from '@modules/pseudonym/service'; import { UserService } from '@modules/user'; -import { Injectable, InternalServerErrorException, UnprocessableEntityException } from '@nestjs/common'; +import { Inject, Injectable, InternalServerErrorException, UnprocessableEntityException } from '@nestjs/common'; import { Pseudonym, RoleReference, UserDO } from '@shared/domain/domainobject'; import { RoleName } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; @@ -28,6 +29,7 @@ export class Lti11ToolLaunchStrategy extends AbstractLaunchStrategy { private readonly userService: UserService, private readonly pseudonymService: PseudonymService, private readonly lti11EncryptionService: Lti11EncryptionService, + @Inject(DefaultEncryptionService) private readonly encryptionService: EncryptionService, autoSchoolIdStrategy: AutoSchoolIdStrategy, autoSchoolNumberStrategy: AutoSchoolNumberStrategy, autoContextIdStrategy: AutoContextIdStrategy, @@ -63,10 +65,11 @@ export class Lti11ToolLaunchStrategy extends AbstractLaunchStrategy { const roleNames: RoleName[] = user.roles.map((roleRef: RoleReference): RoleName => roleRef.name); const ltiRoles: LtiRole[] = LtiRoleMapper.mapRolesToLtiRoles(roleNames); + const decrypted = this.encryptionService.decrypt(config.secret); + const additionalProperties: PropertyData[] = [ new PropertyData({ name: 'key', value: config.key }), - // TODO N21-2097 use decryption for secret - new PropertyData({ name: 'secret', value: config.secret }), + new PropertyData({ name: 'secret', value: decrypted }), new PropertyData({ name: 'lti_message_type', value: config.lti_message_type, location: PropertyLocation.BODY }), new PropertyData({ name: 'lti_version', value: 'LTI-1p0', location: PropertyLocation.BODY }), 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 15344278bcd..2644dc7f5fc 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,3 +1,4 @@ +import { EncryptionModule } from '@infra/encryption'; import { BoardModule } from '@modules/board'; import { LearnroomModule } from '@modules/learnroom'; import { LegacySchoolModule } from '@modules/legacy-school'; @@ -32,6 +33,7 @@ import { BasicToolLaunchStrategy, Lti11ToolLaunchStrategy, OAuth2ToolLaunchStrat LearnroomModule, BoardModule, GroupModule, + EncryptionModule, ], providers: [ ToolLaunchService, diff --git a/backup/setup/external-tools.json b/backup/setup/external-tools.json index 1a88cae3c20..120dfa0f8dd 100644 --- a/backup/setup/external-tools.json +++ b/backup/setup/external-tools.json @@ -4,14 +4,10 @@ "$oid": "644a4593d0a8301e6cf25d85" }, "createdAt": { - "$date": { - "$numberLong": "1682589075592" - } + "$date": "2023-04-27T09:51:15.592Z" }, "updatedAt": { - "$date": { - "$numberLong": "1682589075592" - } + "$date": "2023-04-27T09:51:15.592Z" }, "name": "TestTool", "url": "https://google.de/", @@ -71,88 +67,15 @@ "isDeactivated": false, "restrictToContexts": [] }, - { - "_id": { - "$oid": "647de247cf6a427b9d39e5b9" - }, - "createdAt": { - "$date": { - "$numberLong": "1685971527243" - } - }, - "updatedAt": { - "$date": { - "$numberLong": "1685973728239" - } - }, - "name": "LTI Test Tool", - "url": "https://saltire.lti.app", - "config_type": "lti11", - "config_baseUrl": "https://saltire.lti.app/tool", - "config_key": "12345", - "config_secret": "secret", - "config_lti_message_type": "basic-lti-launch-request", - "config_privacy_permission": "name", - "config_launch_presentation_locale": "de-DE", - "parameters": [ - { - "name": "custom_test", - "displayName": "Custom Test Parameter", - "description": "just a test", - "default": "test", - "scope": "global", - "location": "body", - "type": "string", - "isOptional": false, - "isProtected": false - } - ], - "isHidden": false, - "openNewTab": false, - "version": 1, - "isDeactivated": false, - "restrictToContexts": [] - }, - { - "_id": { - "$oid": "667e4fe648ea6a22a5474359" - }, - "createdAt": { - "$date": { - "$numberLong": "1682589075592" - } - }, - "updatedAt": { - "$date": { - "$numberLong": "1682589075592" - } - }, - "name": "CY Test Tool Course Restriction", - "url": "https://google.de/", - "config_type": "basic", - "config_baseUrl": "https://google.de/", - "parameters": [], - "isHidden": false, - "openNewTab": true, - "version": 1, - "isDeactivated": false, - "restrictToContexts": [ - "course" - ] - }, { "_id": { "$oid": "644a4593d0a8301e6cf25d86" }, "createdAt": { - "$date": { - "$numberLong": "1682589075592" - } + "$date": "2023-04-27T09:51:15.592Z" }, "updatedAt": { - "$date": { - "$numberLong": "1682589075592" - } + "$date": "2023-04-27T09:51:15.592Z" }, "name": "CY Test Tool Board-Element Restriction", "url": "https://google.de/", @@ -169,82 +92,62 @@ }, { "_id": { - "$oid": "667e50f6162707ce02b9ac02" + "$oid": "647de247cf6a427b9d39e5b1" }, "createdAt": { - "$date": { - "$numberLong": "1682589075592" - } + "$date": "2023-11-30T12:37:54.977Z" }, "updatedAt": { - "$date": { - "$numberLong": "1682589075592" - } + "$date": "2023-11-30T15:31:47.749Z" }, - "name": "CY Test Tool Media-Board Restriction", - "url": "https://google.de/", + "name": "CY Test Tool School Scope", "config_type": "basic", - "config_baseUrl": "https://google.de/", - "parameters": [], - "isHidden": false, - "openNewTab": true, - "version": 1, - "isDeactivated": false, - "restrictToContexts": [ - "media-board" - ] - }, - { - "_id": { - "$oid": "667e52a4162707ce02b9ac04" - }, - "createdAt": { - "$date": { - "$numberLong": "1682589075592" - } - }, - "updatedAt": { - "$date": { - "$numberLong": "1682589075592" + "config_baseUrl": "http:google.com", + "parameters": [ + { + "name": "searchparam", + "displayName": "searchparameter", + "description": "", + "scope": "school", + "location": "path", + "type": "string", + "isOptional": false, + "isProtected": false } - }, - "name": "CY Test Tool All Restrictions", - "url": "https://google.de/", - "config_type": "basic", - "config_baseUrl": "https://google.de/", - "parameters": [], + ], "isHidden": false, - "openNewTab": true, + "openNewTab": false, "version": 1, "isDeactivated": false, - "restrictToContexts": [ - "course","board-element","media-board" - ] + "restrictToContexts": [] }, { "_id": { - "$oid": "647de247cf6a427b9d39e5b1" + "$oid": "647de247cf6a427b9d39e5b9" }, "createdAt": { - "$date": { - "$numberLong": "1701347874977" - } + "$date": "2023-06-05T13:25:27.243Z" }, "updatedAt": { - "$date": { - "$numberLong": "1701358307749" - } + "$date": "2023-06-05T14:02:08.239Z" }, - "name": "CY Test Tool School Scope", - "config_type": "basic", - "config_baseUrl": "http:google.com", + "name": "LTI Test Tool", + "url": "https://saltire.lti.app", + "config_type": "lti11", + "config_baseUrl": "https://saltire.lti.app/tool", + "config_key": "12345", + "config_secret": "U2FsdGVkX188+4Kh4t/eADwUS7hh0mwOjCOAIbd64Og=", + "config_lti_message_type": "basic-lti-launch-request", + "config_privacy_permission": "name", + "config_launch_presentation_locale": "de-DE", "parameters": [ { - "name": "searchparam", - "displayName": "searchparameter", - "description": "", - "scope": "school", - "location": "path", + "name": "custom_test", + "displayName": "Custom Test Parameter", + "description": "just a test", + "default": "test", + "scope": "global", + "location": "body", "type": "string", "isOptional": false, "isProtected": false @@ -261,14 +164,10 @@ "$oid": "647de247cf6a427b9d39e5c2" }, "createdAt": { - "$date": { - "$numberLong": "1701348029049" - } + "$date": "2023-11-30T12:40:29.049Z" }, "updatedAt": { - "$date": { - "$numberLong": "1701358325991" - } + "$date": "2023-11-30T15:32:05.991Z" }, "name": "CY Test Tool Context Scope", "config_type": "basic", @@ -296,14 +195,10 @@ "$oid": "647de247cf6a427b9d39e6c3" }, "createdAt": { - "$date": { - "$numberLong": "1701358084733" - } + "$date": "2023-11-30T15:28:04.733Z" }, "updatedAt": { - "$date": { - "$numberLong": "1701358362888" - } + "$date": "2023-11-30T15:32:42.888Z" }, "name": "CY Test Tool deactivated External Tool", "config_type": "basic", @@ -320,14 +215,10 @@ "$oid": "659bf6f049e52dedff83a8f1" }, "createdAt": { - "$date": { - "$numberLong": "1701358084733" - } + "$date": "2023-11-30T15:28:04.733Z" }, "updatedAt": { - "$date": { - "$numberLong": "1701358362888" - } + "$date": "2023-11-30T15:32:42.888Z" }, "name": "CY Test Tool Protected Parameter", "config_type": "basic", @@ -362,235 +253,109 @@ }, { "_id": { - "$oid": "65fc0fcda519d4a3b71193e0" - }, - "createdAt": { - "$date": { - "$numberLong": "1701358084733" - } - }, - "updatedAt": { - "$date": { - "$numberLong": "1701358362888" - } - }, - "name": "CY Test Tool Optional Protected Parameter", - "config_type": "basic", - "config_baseUrl": "https://google.com/search", - "parameters": [ - { - "name": "search", - "displayName": "Suchparameter", - "description": "Danch wird gesucht", - "scope": "context", - "location": "query", - "type": "string", - "isOptional": false, - "isProtected": false - }, - { - "name": "protected", - "displayName": "geschützter Parameter", - "description": "Dieser parameter wird nicht mitkopiert", - "scope": "context", - "location": "query", - "type": "string", - "isOptional": true, - "isProtected": true - } - ], - "isHidden": false, - "openNewTab": false, - "version": 1, - "isDeactivated": false, - "restrictToContexts": [] - }, - { - "_id": { - "$oid": "666829b6ea0c14353cec2056" + "$oid": "65f958bdd8b35469f14032b1" }, + "config_type": "oauth2", + "name": "nextcloud", + "config_baseUrl": "https://nextcloud-nbc.dbildungscloud.dev/", + "config_clientId": "neWZs5MIKnAHUbbuO9TzeClZQF", + "config_skipConsent": true, "createdAt": { - "$date": { - "$numberLong": "1701358084733" - } - }, - "updatedAt": { - "$date": { - "$numberLong": "1701358362888" - } + "$date": "2024-03-19T09:19:57.984Z" }, - "name": "CY Test Tool Hidden", - "config_type": "basic", - "config_baseUrl": "https://google.com/search", - "parameters": [], "isHidden": true, - "openNewTab": false, - "version": 1, - "isDeactivated": false, - "restrictToContexts": [] - }, - { - "_id": { - "$oid": "6667ec1c243527c9139bd799" - }, - "createdAt": { - "$date": { - "$numberLong": "1701358084733" - } - }, - "updatedAt": { - "$date": { - "$numberLong": "1701358362888" - } - }, - "name": "CY Test Tool 1", - "config_type": "basic", - "config_baseUrl": "https://google.com/search", + "logoUrl": "", + "openNewTab": true, "parameters": [], - "isHidden": false, - "openNewTab": false, - "version": 1, - "isDeactivated": false, - "restrictToContexts": [] - }, - { - "_id": { - "$oid": "66682949ea0c14353cec2054" - }, - "createdAt": { - "$date": { - "$numberLong": "1701358084733" - } - }, "updatedAt": { - "$date": { - "$numberLong": "1701358362888" - } + "$date": "2024-03-19T09:19:57.984Z" }, - "name": "CY Test Tool 2", - "config_type": "basic", - "config_baseUrl": "https://google.com/search", - "parameters": [], - "isHidden": false, - "openNewTab": false, + "url": "https://nextcloud-nbc.dbildungscloud.dev/", "version": 1, "isDeactivated": false, "restrictToContexts": [] }, { "_id": { - "$oid": "6667ec58243527c9139bd79b" + "$oid": "65fad93bbe8ce15df1279d9b" }, "createdAt": { - "$date": { - "$numberLong": "1701358084733" - } + "$date": "2024-03-20T12:40:27.057Z" }, "updatedAt": { - "$date": { - "$numberLong": "1701358362888" - } + "$date": "2024-03-25T09:13:39.585Z" }, - "name": "CY Test Tool Optional Parameters", + "name": "OSM Route", + "url": "https://www.openstreetmap.org/", + "logoUrl": "https://wiki.openstreetmap.org/w/images/7/7e/Logo_by_hind_128x128.png?20100124154543", + "logoBase64": "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", "config_type": "basic", - "config_baseUrl": "https://google.com/search", + "config_baseUrl": "https://wiki.openstreetmap.org/w/images/c/c8/Public-images-osm_logo.png", "parameters": [ { - "name": "schoolParam", - "displayName": "school parameter", + "name": "from", + "displayName": "Start", "description": "", - "scope": "school", - "location": "path", + "scope": "context", + "location": "query", "type": "string", - "isOptional": true, + "isOptional": false, "isProtected": false }, { - "name": "contextParam", - "displayName": "context parameter", + "name": "to", + "displayName": "Ziel", "description": "", "scope": "context", "location": "query", "type": "string", - "isOptional": true, + "isOptional": false, "isProtected": false } ], "isHidden": false, - "openNewTab": false, - "version": 1, "isDeactivated": false, + "openNewTab": false, + "version": 3, "restrictToContexts": [] }, { "_id": { - "$oid": "6667ec85243527c9139bd79d" + "$oid": "65fc0fcda519d4a3b71193e0" }, "createdAt": { - "$date": { - "$numberLong": "1701358084733" - } + "$date": "2023-11-30T15:28:04.733Z" }, "updatedAt": { - "$date": { - "$numberLong": "1701358362888" - } + "$date": "2023-11-30T15:32:42.888Z" }, - "name": "CY Test Tool Required Parameters", + "name": "CY Test Tool Optional Protected Parameter", "config_type": "basic", "config_baseUrl": "https://google.com/search", "parameters": [ { - "name": "schoolParam", - "displayName": "school parameter", - "description": "", - "scope": "school", - "location": "path", + "name": "search", + "displayName": "Suchparameter", + "description": "Danch wird gesucht", + "scope": "context", + "location": "query", "type": "string", "isOptional": false, "isProtected": false }, { - "name": "contextParam", - "displayName": "context parameter", - "description": "", + "name": "protected", + "displayName": "geschützter Parameter", + "description": "Dieser parameter wird nicht mitkopiert", "scope": "context", "location": "query", "type": "string", - "isOptional": false, - "isProtected": false - } - ], - "isHidden": false, - "openNewTab": false, - "version": 1, - "isDeactivated": false, - "restrictToContexts": [] - }, - { - "_id": { - "$oid": "65f958bdd8b35469f14032b1" - }, - "config_type": "oauth2", - "name": "nextcloud", - "config_baseUrl": "https://nextcloud-nbc.dbildungscloud.dev/", - "config_clientId": "neWZs5MIKnAHUbbuO9TzeClZQF", - "config_skipConsent": true, - "createdAt": { - "$date": { - "$numberLong": "1710839997984" - } - }, - "isHidden": true, - "logoUrl": "", - "openNewTab": true, - "parameters": [], - "updatedAt": { - "$date": { - "$numberLong": "1710839997984" + "isOptional": true, + "isProtected": true } - }, - "url": "https://nextcloud-nbc.dbildungscloud.dev/", + ], + "isHidden": false, + "openNewTab": false, "version": 1, "isDeactivated": false, "restrictToContexts": [] @@ -600,14 +365,10 @@ "$oid": "65fc0fcde519d4a3b71193e0" }, "createdAt": { - "$date": { - "$numberLong": "1711017933720" - } + "$date": "2024-03-21T10:45:33.720Z" }, "updatedAt": { - "$date": { - "$numberLong": "1711018099651" - } + "$date": "2024-03-21T10:48:19.651Z" }, "name": "Youtube Videoausschnitt", "url": "https://www.youtube.com", @@ -658,14 +419,10 @@ "$oid": "65fc113ce519d4a3b71193e1" }, "createdAt": { - "$date": { - "$numberLong": "1711018300466" - } + "$date": "2024-03-21T10:51:40.466Z" }, "updatedAt": { - "$date": { - "$numberLong": "1711018300466" - } + "$date": "2024-03-21T10:51:40.466Z" }, "name": "Invidious Videoausschnitt", "url": "https://yt.cdaut.de/", @@ -716,14 +473,10 @@ "$oid": "65fc11a5e519d4a3b71193e2" }, "createdAt": { - "$date": { - "$numberLong": "1711018405712" - } + "$date": "2024-03-21T10:53:25.712Z" }, "updatedAt": { - "$date": { - "$numberLong": "1711018405712" - } + "$date": "2024-03-21T10:53:25.712Z" }, "name": "Classtime Session", "url": "https://classtime.com/", @@ -754,14 +507,10 @@ "$oid": "65fc1285e519d4a3b71193e3" }, "createdAt": { - "$date": { - "$numberLong": "1711018629196" - } + "$date": "2024-03-21T10:57:09.196Z" }, "updatedAt": { - "$date": { - "$numberLong": "1711018629196" - } + "$date": "2024-03-21T10:57:09.196Z" }, "name": "Lichtblick-Filmsequenz", "logoUrl": "data:image/png;base64,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", @@ -791,14 +540,10 @@ "$oid": "65fc1488e519d4a3b71193e4" }, "createdAt": { - "$date": { - "$numberLong": "1711019144780" - } + "$date": "2024-03-21T11:05:44.780Z" }, "updatedAt": { - "$date": { - "$numberLong": "1711019144780" - } + "$date": "2024-03-21T11:05:44.780Z" }, "name": "Product Test Onlinediagnose Grundschule - Deutsch", "url": "https://onlinediagnose.westermann.de/", @@ -863,14 +608,10 @@ "$oid": "65fc15b5e519d4a3b71193e5" }, "createdAt": { - "$date": { - "$numberLong": "1711019445098" - } + "$date": "2024-03-21T11:10:45.098Z" }, "updatedAt": { - "$date": { - "$numberLong": "1711019445098" - } + "$date": "2024-03-21T11:10:45.098Z" }, "name": "Product Test Onlinediagnose Grundschule - Mathematik", "url": "https://onlinediagnose.westermann.de/", @@ -935,14 +676,10 @@ "$oid": "65fd9736cb3d21d77bee50a6" }, "createdAt": { - "$date": { - "$numberLong": "1711118134160" - } + "$date": "2024-03-22T14:35:34.160Z" }, "updatedAt": { - "$date": { - "$numberLong": "1711358224688" - } + "$date": "2024-03-25T09:17:04.688Z" }, "name": "OpenStreetMap", "url": "https://www.openstreetmap.org/", @@ -993,40 +730,49 @@ }, { "_id": { - "$oid": "65fad93bbe8ce15df1279d9b" + "$oid": "65fd9dabcb3d21d77bee50ae" }, "createdAt": { - "$date": { - "$numberLong": "1710938427057" - } + "$date": "2024-03-22T15:03:07.052Z" }, "updatedAt": { - "$date": { - "$numberLong": "1711358019585" - } + "$date": "2024-03-22T15:03:07.052Z" }, - "name": "OSM Route", - "url": "https://www.openstreetmap.org/", - "logoUrl": "https://wiki.openstreetmap.org/w/images/7/7e/Logo_by_hind_128x128.png?20100124154543", - "logoBase64": "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", + "name": "Übersetzer", + "url": "https://translate.google.com/", + "logoUrl": "https://cdn1.iconfinder.com/data/icons/google-s-logo/150/Google_Icons-09-512.png", + "logoBase64": "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", "config_type": "basic", - "config_baseUrl": "https://wiki.openstreetmap.org/w/images/c/c8/Public-images-osm_logo.png", + "config_baseUrl": "https://translate.google.com/", "parameters": [ { - "name": "from", - "displayName": "Start", - "description": "", - "scope": "context", + "name": "op", + "displayName": "Operation", + "description": "Operation der Anwendung", + "default": "op", + "scope": "global", "location": "query", "type": "string", "isOptional": false, "isProtected": false }, { - "name": "to", - "displayName": "Ziel", - "description": "", - "scope": "context", + "name": "sl", + "displayName": "Quell-Sprache", + "description": "geben Sie die Quell-Sprache ein", + "default": "de", + "scope": "global", + "location": "query", + "type": "string", + "isOptional": false, + "isProtected": false + }, + { + "name": "tl", + "displayName": "Ziel-Sprache", + "description": "Geben Sie die Ziel-Sprache ein", + "default": "en", + "scope": "global", "location": "query", "type": "string", "isOptional": false, @@ -1035,59 +781,100 @@ ], "isHidden": false, "isDeactivated": false, + "openNewTab": true, + "version": 1, + "restrictToContexts": [] + }, + { + "_id": { + "$oid": "6667ec1c243527c9139bd799" + }, + "createdAt": { + "$date": "2023-11-30T15:28:04.733Z" + }, + "updatedAt": { + "$date": "2023-11-30T15:32:42.888Z" + }, + "name": "CY Test Tool 1", + "config_type": "basic", + "config_baseUrl": "https://google.com/search", + "parameters": [], + "isHidden": false, "openNewTab": false, - "version": 3, + "version": 1, + "isDeactivated": false, "restrictToContexts": [] }, { "_id": { - "$oid": "65fd9dabcb3d21d77bee50ae" + "$oid": "6667ec58243527c9139bd79b" }, "createdAt": { - "$date": { - "$numberLong": "1711119787052" - } + "$date": "2023-11-30T15:28:04.733Z" }, "updatedAt": { - "$date": { - "$numberLong": "1711119787052" - } + "$date": "2023-11-30T15:32:42.888Z" }, - "name": "Übersetzer", - "url": "https://translate.google.com/", - "logoUrl": "https://cdn1.iconfinder.com/data/icons/google-s-logo/150/Google_Icons-09-512.png", - "logoBase64": "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", + "name": "CY Test Tool Optional Parameters", "config_type": "basic", - "config_baseUrl": "https://translate.google.com/", + "config_baseUrl": "https://google.com/search", "parameters": [ { - "name": "op", - "displayName": "Operation", - "description": "Operation der Anwendung", - "default": "op", - "scope": "global", - "location": "query", + "name": "schoolParam", + "displayName": "school parameter", + "description": "", + "scope": "school", + "location": "path", "type": "string", - "isOptional": false, + "isOptional": true, "isProtected": false }, { - "name": "sl", - "displayName": "Quell-Sprache", - "description": "geben Sie die Quell-Sprache ein", - "default": "de", - "scope": "global", + "name": "contextParam", + "displayName": "context parameter", + "description": "", + "scope": "context", "location": "query", "type": "string", + "isOptional": true, + "isProtected": false + } + ], + "isHidden": false, + "openNewTab": false, + "version": 1, + "isDeactivated": false, + "restrictToContexts": [] + }, + { + "_id": { + "$oid": "6667ec85243527c9139bd79d" + }, + "createdAt": { + "$date": "2023-11-30T15:28:04.733Z" + }, + "updatedAt": { + "$date": "2023-11-30T15:32:42.888Z" + }, + "name": "CY Test Tool Required Parameters", + "config_type": "basic", + "config_baseUrl": "https://google.com/search", + "parameters": [ + { + "name": "schoolParam", + "displayName": "school parameter", + "description": "", + "scope": "school", + "location": "path", + "type": "string", "isOptional": false, "isProtected": false }, { - "name": "tl", - "displayName": "Ziel-Sprache", - "description": "Geben Sie die Ziel-Sprache ein", - "default": "en", - "scope": "global", + "name": "contextParam", + "displayName": "context parameter", + "description": "", + "scope": "context", "location": "query", "type": "string", "isOptional": false, @@ -1095,9 +882,120 @@ } ], "isHidden": false, + "openNewTab": false, + "version": 1, "isDeactivated": false, - "openNewTab": true, + "restrictToContexts": [] + }, + { + "_id": { + "$oid": "66682949ea0c14353cec2054" + }, + "createdAt": { + "$date": "2023-11-30T15:28:04.733Z" + }, + "updatedAt": { + "$date": "2023-11-30T15:32:42.888Z" + }, + "name": "CY Test Tool 2", + "config_type": "basic", + "config_baseUrl": "https://google.com/search", + "parameters": [], + "isHidden": false, + "openNewTab": false, + "version": 1, + "isDeactivated": false, + "restrictToContexts": [] + }, + { + "_id": { + "$oid": "666829b6ea0c14353cec2056" + }, + "createdAt": { + "$date": "2023-11-30T15:28:04.733Z" + }, + "updatedAt": { + "$date": "2023-11-30T15:32:42.888Z" + }, + "name": "CY Test Tool Hidden", + "config_type": "basic", + "config_baseUrl": "https://google.com/search", + "parameters": [], + "isHidden": true, + "openNewTab": false, "version": 1, + "isDeactivated": false, "restrictToContexts": [] + }, + { + "_id": { + "$oid": "667e4fe648ea6a22a5474359" + }, + "createdAt": { + "$date": "2023-04-27T09:51:15.592Z" + }, + "updatedAt": { + "$date": "2023-04-27T09:51:15.592Z" + }, + "name": "CY Test Tool Course Restriction", + "url": "https://google.de/", + "config_type": "basic", + "config_baseUrl": "https://google.de/", + "parameters": [], + "isHidden": false, + "openNewTab": true, + "version": 1, + "isDeactivated": false, + "restrictToContexts": [ + "course" + ] + }, + { + "_id": { + "$oid": "667e50f6162707ce02b9ac02" + }, + "createdAt": { + "$date": "2023-04-27T09:51:15.592Z" + }, + "updatedAt": { + "$date": "2023-04-27T09:51:15.592Z" + }, + "name": "CY Test Tool Media-Board Restriction", + "url": "https://google.de/", + "config_type": "basic", + "config_baseUrl": "https://google.de/", + "parameters": [], + "isHidden": false, + "openNewTab": true, + "version": 1, + "isDeactivated": false, + "restrictToContexts": [ + "media-board" + ] + }, + { + "_id": { + "$oid": "667e52a4162707ce02b9ac04" + }, + "createdAt": { + "$date": "2023-04-27T09:51:15.592Z" + }, + "updatedAt": { + "$date": "2023-04-27T09:51:15.592Z" + }, + "name": "CY Test Tool All Restrictions", + "url": "https://google.de/", + "config_type": "basic", + "config_baseUrl": "https://google.de/", + "parameters": [], + "isHidden": false, + "openNewTab": true, + "version": 1, + "isDeactivated": false, + "restrictToContexts": [ + "course", + "board-element", + "media-board" + ] } ] diff --git a/backup/setup/migrations.json b/backup/setup/migrations.json index 2a7feac0983..c09fc0c4799 100644 --- a/backup/setup/migrations.json +++ b/backup/setup/migrations.json @@ -107,6 +107,15 @@ "$date": "2024-05-17T14:00:42.414Z" } }, + { + "_id": { + "$oid": "6655e94f06722f2a434c135f" + }, + "name": "Migration20240528140356", + "created_at": { + "$date": "2024-05-28T14:25:19.577Z" + } + }, { "_id": { "$oid": "6656f4835290f6d36be31830" @@ -116,6 +125,15 @@ "$date": "2024-05-29T09:25:23.454Z" } }, + { + "_id": { + "$oid": "6668485aadfd9c4d7be91ca3" + }, + "name": "Migration20240611081033", + "created_at": { + "$date": "2024-06-11T12:51:38.379Z" + } + }, { "_id": { "$oid": "66684c3db14698848e23c0c2" @@ -152,24 +170,6 @@ "$date": "2024-06-12T12:26:01.665Z" } }, - { - "_id": { - "$oid": "6655e94f06722f2a434c135f" - }, - "name": "Migration20240528140356", - "created_at": { - "$date": "2024-05-28T14:25:19.577Z" - } - }, - { - "_id": { - "$oid": "6668485aadfd9c4d7be91ca3" - }, - "name": "Migration20240611081033", - "created_at": { - "$date": "2024-06-11T12:51:38.379Z" - } - }, { "_id": { "$oid": "667e611e207a39b02c306406" @@ -185,7 +185,7 @@ }, "name": "Migration20240719115036", "created_at": { - "$date": "2024-07-24T014:50:10.278Z" + "$date": "1970-01-01T00:00:00Z" } }, { @@ -214,5 +214,14 @@ "created_at": { "$date": "2024-08-23T15:25:05.360Z" } + }, + { + "_id": { + "$oid": "66fda9462a63b5749b3a64c9" + }, + "name": "Migration20240926205656", + "created_at": { + "$date": "2024-10-02T20:12:54.209Z" + } } ] diff --git a/backup/setup/school-external-tools.json b/backup/setup/school-external-tools.json index b9909e5a5c5..0f87303d154 100644 --- a/backup/setup/school-external-tools.json +++ b/backup/setup/school-external-tools.json @@ -50,29 +50,6 @@ "schoolParameters": [], "isDeactivated": false }, - { - "_id": { - "$oid": "65fd74c4d1c1ddf3bb2b05de" - }, - "createdAt": { - "$date": { - "$numberLong": "1711109316850" - } - }, - "updatedAt": { - "$date": { - "$numberLong": "1711109316850" - } - }, - "tool": { - "$oid": "65fd44ba09e6ffd0bae3b8d3" - }, - "school": { - "$oid": "5f2987e020834114b8efd6f8" - }, - "schoolParameters": [], - "isDeactivated": false - }, { "_id": { "$oid": "65fd9882cb3d21d77bee50a7" From a6b710f63da953047b8b4d019bd68d5da0854752 Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov <133751031+sdinkov@users.noreply.github.com> Date: Mon, 7 Oct 2024 13:10:21 +0200 Subject: [PATCH 02/10] N21 2202 fix shd school data cannot be updated (#5261) * update school rule * migration: Migration20240925165112 --------- Co-authored-by: Alexander Weber <103171324+alweber-cap@users.noreply.github.com> --- .../mikro-orm/Migration20240925165112.ts | 37 ++++++++++++++++++ .../domain/rules/school.rule.spec.ts | 39 +++++++++++++++++-- .../authorization/domain/rules/school.rule.ts | 11 ++++-- .../domain/interface/permission.enum.ts | 1 + backup/setup/migrations.json | 9 +++++ backup/setup/roles.json | 3 +- 6 files changed, 91 insertions(+), 9 deletions(-) create mode 100644 apps/server/src/migrations/mikro-orm/Migration20240925165112.ts diff --git a/apps/server/src/migrations/mikro-orm/Migration20240925165112.ts b/apps/server/src/migrations/mikro-orm/Migration20240925165112.ts new file mode 100644 index 00000000000..144aad8cfd3 --- /dev/null +++ b/apps/server/src/migrations/mikro-orm/Migration20240925165112.ts @@ -0,0 +1,37 @@ +import { Migration } from '@mikro-orm/migrations-mongodb'; + +export class Migration20240925165112 extends Migration { + async up(): Promise { + const adminRoleUpdate = await this.getCollection('roles').updateOne( + { name: 'superhero' }, + { + $addToSet: { + permissions: { + $each: ['SCHOOL_EDIT_ALL'], + }, + }, + } + ); + + if (adminRoleUpdate.modifiedCount > 0) { + console.info('Permission SCHOOL_EDIT_ALL added to role superhero.'); + } + } + + async down(): Promise { + const adminRoleUpdate = await this.getCollection('roles').updateOne( + { name: 'superhero' }, + { + $pull: { + permissions: { + $in: ['SCHOOL_EDIT_ALL'], + }, + }, + } + ); + + if (adminRoleUpdate.modifiedCount > 0) { + console.info('Rollback: Permission SCHOOL_EDIT_ALL added to role superhero.'); + } + } +} diff --git a/apps/server/src/modules/authorization/domain/rules/school.rule.spec.ts b/apps/server/src/modules/authorization/domain/rules/school.rule.spec.ts index 9b1ac7446c6..cf82db40d32 100644 --- a/apps/server/src/modules/authorization/domain/rules/school.rule.spec.ts +++ b/apps/server/src/modules/authorization/domain/rules/school.rule.spec.ts @@ -1,3 +1,4 @@ +import { Permission } from '@shared/domain/interface/permission.enum'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { schoolFactory } from '@modules/school/testing/school.factory'; import { Test, TestingModule } from '@nestjs/testing'; @@ -9,11 +10,12 @@ import { SchoolRule } from './school.rule'; describe('SchoolRule', () => { let rule: SchoolRule; let authorizationHelper: DeepMocked; + let module: TestingModule; beforeAll(async () => { await setupEntities(); - const module: TestingModule = await Test.createTestingModule({ + module = await Test.createTestingModule({ providers: [SchoolRule, { provide: AuthorizationHelper, useValue: createMock() }], }).compile(); @@ -24,10 +26,19 @@ describe('SchoolRule', () => { const setupSchoolAndUser = () => { const school = schoolFactory.build(); const user = userFactory.build({ school: schoolEntityFactory.buildWithId(undefined, school.id) }); + const superUser = userFactory.asSuperhero([Permission.SCHOOL_EDIT_ALL]).build(); - return { school, user }; + return { school, user, superUser }; }; + afterEach(() => { + jest.clearAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + describe('isApplicable', () => { describe('when object is instance of School', () => { const setup = () => { @@ -88,7 +99,7 @@ describe('SchoolRule', () => { const { user, school } = setupSchoolAndUser(); const context = AuthorizationContextBuilder.read([]); - authorizationHelper.hasAllPermissions.mockReturnValueOnce(false); + authorizationHelper.hasAllPermissions.mockReturnValue(false); return { user, school, context }; }; @@ -108,7 +119,7 @@ describe('SchoolRule', () => { const someOtherSchool = schoolFactory.build(); const context = AuthorizationContextBuilder.read([]); - authorizationHelper.hasAllPermissions.mockReturnValueOnce(true); + authorizationHelper.hasAllPermissions.mockReturnValueOnce(false); return { user, someOtherSchool, context }; }; @@ -121,5 +132,25 @@ describe('SchoolRule', () => { expect(result).toBe(false); }); }); + + describe('when the user has super powers', () => { + const setup = () => { + const { superUser } = setupSchoolAndUser(); + const someOtherSchool = schoolFactory.build(); + const context = AuthorizationContextBuilder.read([]); + + authorizationHelper.hasAllPermissions.mockReturnValueOnce(true); + + return { superUser, someOtherSchool, context }; + }; + + it('should return true', () => { + const { superUser, someOtherSchool, context } = setup(); + + const result = rule.hasPermission(superUser, someOtherSchool, context); + + expect(result).toBe(true); + }); + }); }); }); diff --git a/apps/server/src/modules/authorization/domain/rules/school.rule.ts b/apps/server/src/modules/authorization/domain/rules/school.rule.ts index d960e62c3dd..ad87a582294 100644 --- a/apps/server/src/modules/authorization/domain/rules/school.rule.ts +++ b/apps/server/src/modules/authorization/domain/rules/school.rule.ts @@ -1,5 +1,6 @@ import { Injectable } from '@nestjs/common'; import { User } from '@shared/domain/entity'; +import { Permission } from '@shared/domain/interface/permission.enum'; import { School } from '@src/modules/school/domain/do'; import { AuthorizationHelper } from '../service/authorization.helper'; import { AuthorizationContext, Rule } from '../type'; @@ -15,11 +16,13 @@ export class SchoolRule implements Rule { } public hasPermission(user: User, school: School, context: AuthorizationContext): boolean { - const hasRequiredPermissions = this.authorizationHelper.hasAllPermissions(user, context.requiredPermissions); - + let hasPermission = false; const isUsersSchool = user.school.id === school.id; - - const hasPermission = hasRequiredPermissions && isUsersSchool; + if (isUsersSchool) { + hasPermission = this.authorizationHelper.hasAllPermissions(user, context.requiredPermissions); + } else { + hasPermission = this.authorizationHelper.hasAllPermissions(user, [Permission.SCHOOL_EDIT_ALL]); + } return hasPermission; } diff --git a/apps/server/src/shared/domain/interface/permission.enum.ts b/apps/server/src/shared/domain/interface/permission.enum.ts index 70d57507164..24b1536a4d2 100644 --- a/apps/server/src/shared/domain/interface/permission.enum.ts +++ b/apps/server/src/shared/domain/interface/permission.enum.ts @@ -103,6 +103,7 @@ export enum Permission { SCHOOL_CHAT_MANAGE = 'SCHOOL_CHAT_MANAGE', SCHOOL_CREATE = 'SCHOOL_CREATE', SCHOOL_EDIT = 'SCHOOL_EDIT', + SCHOOL_EDIT_ALL = 'SCHOOL_EDIT_ALL', SCHOOL_LOGO_MANAGE = 'SCHOOL_LOGO_MANAGE', SCHOOL_NEWS_EDIT = 'SCHOOL_NEWS_EDIT', SCHOOL_PERMISSION_CHANGE = 'SCHOOL_PERMISSION_CHANGE', diff --git a/backup/setup/migrations.json b/backup/setup/migrations.json index c09fc0c4799..225324cd518 100644 --- a/backup/setup/migrations.json +++ b/backup/setup/migrations.json @@ -215,6 +215,15 @@ "$date": "2024-08-23T15:25:05.360Z" } }, + { + "_id": { + "$oid": "66f440bf0dbeeb6747a4242c" + }, + "name": "Migration20240925165112", + "created_at": { + "$date": "2024-09-25T16:56:31.889Z" + } + }, { "_id": { "$oid": "66fda9462a63b5749b3a64c9" diff --git a/backup/setup/roles.json b/backup/setup/roles.json index 94454494dba..01fc0562cce 100644 --- a/backup/setup/roles.json +++ b/backup/setup/roles.json @@ -203,7 +203,8 @@ "ACCOUNT_DELETE", "USER_LOGIN_MIGRATION_FORCE", "USER_LOGIN_MIGRATION_ROLLBACK", - "INSTANCE_VIEW" + "INSTANCE_VIEW", + "SCHOOL_EDIT_ALL" ], "__v": 2 }, From 52c8f2be89b2798c59f73678363c2cf759f7f08e Mon Sep 17 00:00:00 2001 From: MBergCap <111343628+MBergCap@users.noreply.github.com> Date: Tue, 8 Oct 2024 13:36:22 +0200 Subject: [PATCH 03/10] N21-2229 delete context external tools during course deletion (#5275) --- apps/server/src/apps/server.app.ts | 3 +++ .../common-tool-delete.service.spec.ts | 22 +++++++++++++++++++ .../service/common-tool-delete.service.ts | 10 +++++++++ .../context-external-tool.service.spec.ts | 21 ++++++++++++++++++ .../service/context-external-tool.service.ts | 5 +++++ src/services/user-group/hooks/courses.js | 6 +++++ src/services/user-group/services/courses.js | 3 ++- 7 files changed, 69 insertions(+), 1 deletion(-) diff --git a/apps/server/src/apps/server.app.ts b/apps/server/src/apps/server.app.ts index b3394bf7567..2409153fc89 100644 --- a/apps/server/src/apps/server.app.ts +++ b/apps/server/src/apps/server.app.ts @@ -7,6 +7,7 @@ import { AccountService } from '@modules/account'; import { SystemRule } from '@modules/authorization/domain/rules'; import { ColumnBoardService } from '@modules/board'; +import { ContextExternalToolService } from '@src/modules/tool/context-external-tool'; import { CollaborativeStorageUc } from '@modules/collaborative-storage/uc/collaborative-storage.uc'; import { GroupService } from '@modules/group'; import { InternalServerModule } from '@modules/internal-server'; @@ -94,6 +95,8 @@ async function bootstrap() { // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access feathersExpress.services['nest-column-board-service'] = nestApp.get(ColumnBoardService); // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access + feathersExpress.services['nest-context-external-tool-service'] = nestApp.get(ContextExternalToolService); + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-member-access feathersExpress.services['nest-system-rule'] = nestApp.get(SystemRule); // 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/modules/tool/common/service/common-tool-delete.service.spec.ts b/apps/server/src/modules/tool/common/service/common-tool-delete.service.spec.ts index 9ba1cf5ac7f..ca8b4f6cd07 100644 --- a/apps/server/src/modules/tool/common/service/common-tool-delete.service.spec.ts +++ b/apps/server/src/modules/tool/common/service/common-tool-delete.service.spec.ts @@ -262,4 +262,26 @@ describe(CommonToolDeleteService.name, () => { }); }); }); + + describe('deleteContextExternalToolsByCourseId', () => { + describe('when deleting a context external tool', () => { + const setup = () => { + const contextExternalTool = contextExternalToolFactory.build(); + + contextExternalToolRepo.find.mockResolvedValueOnce([contextExternalTool]); + + return { + contextExternalTool, + }; + }; + + it('should delete the context external tool', async () => { + const { contextExternalTool } = setup(); + + await service.deleteContextExternalToolsByCourseId(contextExternalTool.contextRef.id); + + expect(contextExternalToolRepo.delete).toHaveBeenCalledWith([contextExternalTool]); + }); + }); + }); }); diff --git a/apps/server/src/modules/tool/common/service/common-tool-delete.service.ts b/apps/server/src/modules/tool/common/service/common-tool-delete.service.ts index 4cfdd33795f..efaca8f9fc0 100644 --- a/apps/server/src/modules/tool/common/service/common-tool-delete.service.ts +++ b/apps/server/src/modules/tool/common/service/common-tool-delete.service.ts @@ -1,6 +1,8 @@ import { Injectable } from '@nestjs/common'; import { EventBus } from '@nestjs/cqrs'; import { ContextExternalToolRepo, ExternalToolRepo, SchoolExternalToolRepo } from '@shared/repo'; +import { EntityId } from '@shared/domain/types'; +import { ToolContextType } from '@modules/tool/common/enum'; import { ContextExternalTool, ContextExternalToolDeletedEvent } from '../../context-external-tool/domain'; import type { ExternalTool } from '../../external-tool/domain'; import type { SchoolExternalTool } from '../../school-external-tool/domain'; @@ -44,6 +46,14 @@ export class CommonToolDeleteService { await this.deleteContextExternalToolInternal(externalTool, contextExternalTool); } + public async deleteContextExternalToolsByCourseId(courseId: EntityId): Promise { + const contextExternalTools: ContextExternalTool[] = await this.contextExternalToolRepo.find({ + context: { id: courseId, type: ToolContextType.COURSE }, + }); + + await this.contextExternalToolRepo.delete(contextExternalTools); + } + private async deleteSchoolExternalToolInternal( externalTool: ExternalTool, schoolExternalTool: SchoolExternalTool 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 deaf6330fd5..ac626ba0c19 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 @@ -123,6 +123,27 @@ describe(ContextExternalToolService.name, () => { }); }); + describe('deleteContextExternalToolsByCourseId', () => { + describe('when deleting a context external tool', () => { + const setup = () => { + const contextExternalTool: ContextExternalTool = contextExternalToolFactory.build(); + const courseId = contextExternalTool.contextRef.id; + + return { + courseId, + }; + }; + + it('should delete the context external tool', async () => { + const { courseId } = setup(); + + await service.deleteContextExternalToolsByCourseId(courseId); + + expect(commonToolDeleteService.deleteContextExternalToolsByCourseId).toHaveBeenCalledWith(courseId); + }); + }); + }); + describe('saveContextExternalTool', () => { describe('when contextExternalTool is given', () => { const setup = () => { 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 43f7d5f82ae..c1746cdf777 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 @@ -54,6 +54,11 @@ export class ContextExternalToolService { await this.commonToolDeleteService.deleteContextExternalTool(contextExternalTool); } + // called from feathers + public async deleteContextExternalToolsByCourseId(courseId: EntityId): Promise { + await this.commonToolDeleteService.deleteContextExternalToolsByCourseId(courseId); + } + public async findAllByContext(contextRef: ContextRef): Promise { const contextExternalTools: ContextExternalTool[] = await this.contextExternalToolRepo.find({ context: contextRef, diff --git a/src/services/user-group/hooks/courses.js b/src/services/user-group/hooks/courses.js index aa50cc52a78..cf73427112c 100644 --- a/src/services/user-group/hooks/courses.js +++ b/src/services/user-group/hooks/courses.js @@ -190,6 +190,11 @@ const removeColumnBoard = async (context) => { await context.app.service('nest-column-board-service').deleteByCourseId(courseId); }; +const removeContextExternalTools = async (context) => { + const courseId = context.id; + await context.app.service('nest-context-external-tool-service').deleteContextExternalToolsByCourseId(courseId); +}; + /** * remove all substitution teacher which are also teachers * @param hook - contains and request body @@ -250,6 +255,7 @@ module.exports = { addWholeClassToCourse, deleteWholeClassFromCourse, removeColumnBoard, + removeContextExternalTools, removeSubstitutionDuplicates, courseInviteHook, patchPermissionHook, diff --git a/src/services/user-group/services/courses.js b/src/services/user-group/services/courses.js index 2d358210e6e..52bdd59deb5 100644 --- a/src/services/user-group/services/courses.js +++ b/src/services/user-group/services/courses.js @@ -26,6 +26,7 @@ const { addWholeClassToCourse, deleteWholeClassFromCourse, removeColumnBoard, + removeContextExternalTools, courseInviteHook, patchPermissionHook, restrictChangesToArchivedCourse, @@ -146,7 +147,7 @@ const courseHooks = { create: [addWholeClassToCourse], update: [], patch: [addWholeClassToCourse], - remove: [removeColumnBoard], + remove: [removeColumnBoard, removeContextExternalTools], }, }; From d95880a6e16a4e1e4e017e964214c650d65dcd3a Mon Sep 17 00:00:00 2001 From: Patrick Sachmann <20001160+psachmann@users.noreply.github.com> Date: Tue, 8 Oct 2024 15:03:00 +0200 Subject: [PATCH 04/10] EW-1039 tsp client requesting access from tsp token endpoint (#5273) * tsp client requesting access to from tsp token endpoint --------- Co-authored-by: Alexander Weber <103171324+alweber-cap@users.noreply.github.com> --- apps/server/src/infra/tsp-client/README.md | 6 + .../tsp-client/generated/api/export-api.ts | 21 + apps/server/src/infra/tsp-client/index.ts | 8 +- apps/server/src/infra/tsp-client/openapi.json | 4780 +++++++++++++++++ .../src/infra/tsp-client/tsp-client-config.ts | 7 +- .../tsp-client-factory.integration.spec.ts | 72 + .../tsp-client/tsp-client-factory.spec.ts | 103 +- .../infra/tsp-client/tsp-client-factory.ts | 70 +- .../src/infra/tsp-client/tsp-client.module.ts | 2 + apps/server/src/modules/oauth/index.ts | 1 + .../src/modules/server/server.config.ts | 11 +- openapitools.json | 2 +- 12 files changed, 5010 insertions(+), 73 deletions(-) create mode 100644 apps/server/src/infra/tsp-client/openapi.json create mode 100644 apps/server/src/infra/tsp-client/tsp-client-factory.integration.spec.ts diff --git a/apps/server/src/infra/tsp-client/README.md b/apps/server/src/infra/tsp-client/README.md index 5f4e66829b7..f88fb33859b 100644 --- a/apps/server/src/infra/tsp-client/README.md +++ b/apps/server/src/infra/tsp-client/README.md @@ -29,6 +29,12 @@ export class MyNewService { ## How the code generation works +> IMPORTANT: Currently we are using the `openapi.json` and not the spec from +> https://test2.schulportal-thueringen.de/tip-ms/api/swagger.json, because we have to patch the security schemas +> manually into to the specification so the generator can generate them correctly. The provided +> specification does not contain all necessary definitions. Only the `Export` endpoints are +> decorated with the security definitions. + We are using the openapi-generator-cli to generate apis, models and supporting files in the `generated` directory. **DO NOT** modify anything in the `generated` folder, because it will be deleted on the next client generation. diff --git a/apps/server/src/infra/tsp-client/generated/api/export-api.ts b/apps/server/src/infra/tsp-client/generated/api/export-api.ts index c1b2f2b1579..3aaf7761827 100644 --- a/apps/server/src/infra/tsp-client/generated/api/export-api.ts +++ b/apps/server/src/infra/tsp-client/generated/api/export-api.ts @@ -61,6 +61,9 @@ export const ExportApiAxiosParamCreator = function (configuration?: Configuratio const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; + // authentication Bearer required + await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) + if (dtLetzteAenderung !== undefined) { localVarQueryParameter['dtLetzteAenderung'] = dtLetzteAenderung; } @@ -96,6 +99,9 @@ export const ExportApiAxiosParamCreator = function (configuration?: Configuratio const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; + // authentication Bearer required + await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) + if (dtLetzteAenderung !== undefined) { localVarQueryParameter['dtLetzteAenderung'] = dtLetzteAenderung; } @@ -130,6 +136,9 @@ export const ExportApiAxiosParamCreator = function (configuration?: Configuratio const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; + // authentication Bearer required + await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) + setSearchParams(localVarUrlObj, localVarQueryParameter); @@ -161,6 +170,9 @@ export const ExportApiAxiosParamCreator = function (configuration?: Configuratio const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; + // authentication Bearer required + await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) + if (dtLetzteAenderung !== undefined) { localVarQueryParameter['dtLetzteAenderung'] = dtLetzteAenderung; } @@ -195,6 +207,9 @@ export const ExportApiAxiosParamCreator = function (configuration?: Configuratio const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; + // authentication Bearer required + await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) + setSearchParams(localVarUrlObj, localVarQueryParameter); @@ -226,6 +241,9 @@ export const ExportApiAxiosParamCreator = function (configuration?: Configuratio const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; + // authentication Bearer required + await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) + if (dtLetzteAenderung !== undefined) { localVarQueryParameter['dtLetzteAenderung'] = dtLetzteAenderung; } @@ -260,6 +278,9 @@ export const ExportApiAxiosParamCreator = function (configuration?: Configuratio const localVarHeaderParameter = {} as any; const localVarQueryParameter = {} as any; + // authentication Bearer required + await setApiKeyToObject(localVarHeaderParameter, "Authorization", configuration) + setSearchParams(localVarUrlObj, localVarQueryParameter); diff --git a/apps/server/src/infra/tsp-client/index.ts b/apps/server/src/infra/tsp-client/index.ts index 66f5a6ebced..54d121063e1 100644 --- a/apps/server/src/infra/tsp-client/index.ts +++ b/apps/server/src/infra/tsp-client/index.ts @@ -1,3 +1,5 @@ -export * from './generated/api'; -export * from './generated/models'; -export { TspClientFactory } from './tsp-client-factory'; +export * from './generated/api'; +export * from './generated/models'; +export * from './tsp-client-config'; +export * from './tsp-client-factory'; +export * from './tsp-client.module'; diff --git a/apps/server/src/infra/tsp-client/openapi.json b/apps/server/src/infra/tsp-client/openapi.json new file mode 100644 index 00000000000..384e129af10 --- /dev/null +++ b/apps/server/src/infra/tsp-client/openapi.json @@ -0,0 +1,4780 @@ +{ + "swagger": "2.0", + "info": { "description": "TIP-Rest Api v1", "version": "1.0.0", "title": "" }, + "basePath": "/tip-ms/api", + "tags": [ + { "name": "Zugangsdaten prüfen und verwalten" }, + { "name": "geschaeftspartneradmin_aktuellestammdatenbearbeitung" }, + { "name": "geschaeftspartneradmin_benutzergruppezuordnung" }, + { "name": "geschaeftspartneradmin_benutzerrollezuordnung" }, + { "name": "geschaeftspartneradmin_kommunikation" }, + { "name": "geschaeftspartneradmin_zugangsdaten" }, + { "name": "Ilea" }, + { "name": "klasse_detail" }, + { "name": "klasse_personalzuordnung" }, + { "name": "klasse_schuelerzuordnung" }, + { "name": "Klassenbildung" }, + { "name": "Klassenuebersicht" }, + { "name": "kurs_detail" }, + { "name": "kurs_personalzuordnung" }, + { "name": "kurs_schuelerzuordnung" }, + { "name": "Kursbildung" }, + { "name": "schueler_detail" }, + { "name": "schueler_uebernahme" }, + { "name": "Schueleruebersicht" }, + { "name": "backend_schule_tsc_sso_links" }, + { "name": "backend_stammdaten_ilea_aufgabenpaketjahrgang" }, + { "name": "backend_stammdaten_tooltipbearbeiten" }, + { "name": "backend_systempflege_protokolleintrag" }, + { "name": "Mediotheksexport" }, + { "name": "Berechtigungen" }, + { "name": "Export" }, + { "name": "Klassenverwaltung" }, + { "name": "Schulpersonalverwaltung" } + ], + "schemes": ["https"], + "securityDefinitions": { + "Bearer": { + "type": "apiKey", + "name": "Authorization", + "in": "header" + } + }, + "consumes": ["application/json"], + "produces": ["application/json"], + "paths": { + "/info": { + "get": { + "operationId": "getInfo", + "produces": ["text/plain"], + "parameters": [], + "responses": { "200": { "description": "successful operation", "headers": {}, "schema": { "type": "string" } } } + } + }, + "/authentication": { + "post": { + "tags": ["Zugangsdaten prüfen und verwalten"], + "summary": "Zugangsdaten hinzufügen", + "description": "", + "operationId": "insertZugang", + "parameters": [ + { + "name": "benutzername", + "in": "formData", + "description": "benutzername", + "required": true, + "type": "string" + }, + { "name": "kennwort", "in": "formData", "description": "kennwort", "required": true, "type": "string" }, + { + "name": "kennwortWiederholung", + "in": "formData", + "description": "kennwortWiederholung", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "200": { "description": "successful operation", "schema": { "type": "string" } } } + }, + "put": { + "tags": ["Zugangsdaten prüfen und verwalten"], + "summary": "Zugangsdaten ändern", + "description": "", + "operationId": "updateZugang", + "parameters": [ + { + "name": "benutzername", + "in": "formData", + "description": "benutzername", + "required": true, + "type": "string" + }, + { + "name": "currentBenutzername", + "in": "formData", + "description": "currentBenutzername", + "required": true, + "type": "string" + }, + { + "name": "currentKennwort", + "in": "formData", + "description": "Aktuelles Kennwort", + "required": true, + "type": "string" + }, + { "name": "kennwort", "in": "formData", "description": "kennwort", "required": true, "type": "string" }, + { + "name": "kennwortWiederholung", + "in": "formData", + "description": "kennwortWiederholung", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "200": { "description": "successful operation", "schema": { "type": "boolean" } } } + } + }, + "/authentication/loginId": { + "get": { + "tags": ["Zugangsdaten prüfen und verwalten"], + "summary": "Liefert den aktuellen Benutzername", + "description": "", + "operationId": "getCurrentUser", + "parameters": [ + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "200": { "description": "successful operation", "schema": { "type": "string" } } } + } + }, + "/backend_geschaeftspartneradmin_aktuellestammdatenbearbeitung/standortzuordnungen": { + "get": { + "tags": ["geschaeftspartneradmin_aktuellestammdatenbearbeitung"], + "summary": "liefert die Stammdaten eines Geschäftspartners", + "description": "", + "operationId": "getStandortZuordnungen", + "produces": ["application/json"], + "parameters": [ + { + "name": "gepaId", + "in": "query", + "description": "ID des Geschäftspartners", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjStandortZuordnung" } } + } + } + } + }, + "/backend_geschaeftspartneradmin_aktuellestammdatenbearbeitung/dienststellen": { + "get": { + "tags": ["geschaeftspartneradmin_aktuellestammdatenbearbeitung"], + "summary": "liefert die Dienststellenliste", + "description": "", + "operationId": "getListDienststelle", + "produces": ["application/json"], + "parameters": [ + { + "name": "dienststelleNummer", + "in": "query", + "description": "Nummer der Dienststelle eingeben", + "required": false, + "type": "string" + }, + { + "name": "dienststelleName", + "in": "query", + "description": "Name der Dienststelle eingeben", + "required": false, + "type": "string" + }, + { + "name": "strasse", + "in": "query", + "description": "Straße der Dienststelle eingeben", + "required": false, + "type": "string" + }, + { + "name": "postleitzahl", + "in": "query", + "description": "Postleitzahl der Dienststelle eingeben", + "required": false, + "type": "string" + }, + { + "name": "ort", + "in": "query", + "description": "Ort der Dienststelle eingeben", + "required": false, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjDienststelle" } } + } + } + } + }, + "/backend_geschaeftspartneradmin_aktuellestammdatenbearbeitung": { + "post": { + "tags": ["geschaeftspartneradmin_aktuellestammdatenbearbeitung"], + "summary": "Setze neue Stammdienststelle", + "description": "", + "operationId": "insertStammdienststelle", + "consumes": ["application/x-www-form-urlencoded"], + "parameters": [ + { + "name": "gepaId", + "in": "formData", + "description": "ID des Geschäftspartners", + "required": true, + "type": "string" + }, + { + "name": "dienId", + "in": "formData", + "description": "ID der neuen Stammdienststelle", + "required": true, + "type": "string" + }, + { + "name": "persUser", + "in": "formData", + "description": "User der zu ändernden Person", + "required": true, + "type": "string" + }, + { + "name": "persXPts", + "in": "formData", + "description": "XPts der zu ändernden Person", + "required": true, + "type": "string" + }, + { + "name": "altPestId", + "in": "formData", + "description": "ID der letzen Person-Standort-Zuordnung", + "required": false, + "type": "string" + }, + { + "name": "altPestUser", + "in": "formData", + "description": "User der letzen Person-Standort-Zuordnung", + "required": false, + "type": "string" + }, + { + "name": "altPestXPts", + "in": "formData", + "description": "XPts der letzen Person-Standort-Zuordnung", + "required": false, + "type": "string" + }, + { + "name": "altAnscId", + "in": "formData", + "description": "ID der letzen Anschrift", + "required": false, + "type": "string" + }, + { + "name": "altAnscUser", + "in": "formData", + "description": "User der letzen Anschrift", + "required": false, + "type": "string" + }, + { + "name": "altAnscXPts", + "in": "formData", + "description": "XPts der letzen Anschrift", + "required": false, + "type": "string" + }, + { + "name": "neuPestId", + "in": "formData", + "description": "ID der neuen Person-Standort-Zuordnung", + "required": false, + "type": "string" + }, + { + "name": "neuPestUser", + "in": "formData", + "description": "User der neuen Person-Standort-Zuordnung", + "required": false, + "type": "string" + }, + { + "name": "neuPestXPts", + "in": "formData", + "description": "XPts der neuen Person-Standort-Zuordnung", + "required": false, + "type": "string" + }, + { + "name": "neuAnscId", + "in": "formData", + "description": "ID der neuen Anschrift", + "required": false, + "type": "string" + }, + { + "name": "neuAnscUser", + "in": "formData", + "description": "User der neuen Anschrift", + "required": false, + "type": "string" + }, + { + "name": "neuAnscXPts", + "in": "formData", + "description": "XPts der neuen Anschrift", + "required": false, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "default": { "description": "successful operation" } } + } + }, + "/backend_geschaeftspartneradmin_aktuellestammdatenbearbeitung/person": { + "get": { + "tags": ["geschaeftspartneradmin_aktuellestammdatenbearbeitung"], + "summary": "liefert die Stammdaten eines Geschäftspartners", + "description": "", + "operationId": "getPerson", + "produces": ["application/json"], + "parameters": [ + { + "name": "gepaId", + "in": "query", + "description": "ID des Geschäftspartners", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { "description": "successful operation", "schema": { "$ref": "#/definitions/RobjPerson" } } + } + } + }, + "/backend_geschaeftspartneradmin_benutzergruppezuordnung/zuordnungen": { + "get": { + "tags": ["geschaeftspartneradmin_benutzergruppezuordnung"], + "summary": "liefert die Gruppenzuordnungen eines Geschäftspartners", + "description": "", + "operationId": "getListBenutzergruppeZuordnungByGepa", + "produces": ["application/json"], + "parameters": [ + { + "name": "gepaId", + "in": "query", + "description": "ID des Geschäftspartners", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjBenutzergruppeZuordnung" } } + } + } + } + }, + "/backend_geschaeftspartneradmin_benutzergruppezuordnung/gruppen": { + "get": { + "tags": ["geschaeftspartneradmin_benutzergruppezuordnung"], + "summary": "liefert die Gruppen ohne die, die dem übergebenen Geschäftspartner zugeordnet sind", + "description": "", + "operationId": "getListBenutzergruppeOhneGepaGruppen", + "produces": ["application/json"], + "parameters": [ + { + "name": "gepaId", + "in": "query", + "description": "ID des Geschäftspartners", + "required": true, + "type": "string" + }, + { + "name": "benutzergruppeName", + "in": "query", + "description": "Name der Benutzergruppe", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjBenutzergruppe" } } + } + } + } + }, + "/backend_geschaeftspartneradmin_benutzergruppezuordnung/save": { + "put": { + "tags": ["geschaeftspartneradmin_benutzergruppezuordnung"], + "summary": "ermöglicht das Einfügen, Updaten, Löschen von Benutzergruppenzuordnungen eines Geschäftspartners", + "description": "", + "operationId": "insertGruppenzuordnungen", + "produces": ["application/json"], + "parameters": [ + { + "name": "jsonZuordnung", + "in": "formData", + "description": "Daten der Benutzergruppenzuordnungen im JSON Format", + "required": true, + "type": "string" + }, + { + "name": "gepaId", + "in": "query", + "description": "ID des Geschäftspartners", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "default": { "description": "successful operation" } } + } + }, + "/backend_geschaeftspartneradmin_benutzerrollezuordnung/auswahl_klasse": { + "get": { + "tags": ["geschaeftspartneradmin_benutzerrollezuordnung"], + "summary": "liefert die Klassenliste", + "description": "", + "operationId": "getListAuswahlKlassen", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjKlasse" } } + } + } + } + }, + "/backend_geschaeftspartneradmin_benutzerrollezuordnung/schulen": { + "get": { + "tags": ["geschaeftspartneradmin_benutzerrollezuordnung"], + "summary": "liefert die Schulenliste", + "description": "", + "operationId": "getListSchulen", + "produces": ["application/json"], + "parameters": [ + { + "name": "dienststellennummer", + "in": "query", + "description": "Nummer der Schule eingeben", + "required": false, + "type": "string" + }, + { + "name": "name", + "in": "query", + "description": "Name der Schule eingeben", + "required": false, + "type": "string" + }, + { + "name": "strasse", + "in": "query", + "description": "Straße der Schule eingeben", + "required": false, + "type": "string" + }, + { + "name": "postleitzahl", + "in": "query", + "description": "Postleitzahl der Schule eingeben", + "required": false, + "type": "string" + }, + { + "name": "ort", + "in": "query", + "description": "Ort der Schule eingeben", + "required": false, + "type": "string" + }, + { + "name": "schulartId", + "in": "query", + "description": "ID der Schulart eingeben", + "required": false, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjSchule" } } + } + } + } + }, + "/backend_geschaeftspartneradmin_benutzerrollezuordnung/save": { + "put": { + "tags": ["geschaeftspartneradmin_benutzerrollezuordnung"], + "summary": "ermöglicht das Einfügen, Updaten, Löschen von Benutzerrollenzuordnungen eines Geschäftspartners", + "description": "", + "operationId": "updateBenutzerrolleZuordnung", + "produces": ["application/json"], + "parameters": [ + { + "name": "jsonZuordnung", + "in": "formData", + "description": "Daten der Benutzerrollenzuordnungen im JSON Format", + "required": true, + "type": "string" + }, + { + "name": "gepaId", + "in": "query", + "description": "ID des Geschäftspartners", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "default": { "description": "successful operation" } } + } + }, + "/backend_geschaeftspartneradmin_benutzerrollezuordnung/listeRolleZuordnung": { + "get": { + "tags": ["geschaeftspartneradmin_benutzerrollezuordnung"], + "summary": "liefert die Rollenzuordnungen eines Geschäftspartners", + "description": "", + "operationId": "getListBenutzerrolleZuordnungByGepa", + "produces": ["application/json"], + "parameters": [ + { + "name": "gepaId", + "in": "query", + "description": "GepaId des Geschäftspartners", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjBenutzerrolleZuordnung" } } + } + } + } + }, + "/backend_geschaeftspartneradmin_kommunikation": { + "get": { + "tags": ["geschaeftspartneradmin_kommunikation"], + "summary": "liefert die Kommunikationsverbindungen eines Geschäftspartners", + "description": "", + "operationId": "getListeKommunikationByGepaId", + "produces": ["application/json"], + "parameters": [ + { + "name": "gepaId", + "in": "query", + "description": "GepaId des Geschäftspartners", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjKommunikation" } } + } + } + }, + "put": { + "tags": ["geschaeftspartneradmin_kommunikation"], + "summary": "ermöglicht das Einfügen und Löschen von Kommunikationsverbindungen eines Geschäftspartners", + "description": "", + "operationId": "updateGepaKommunikation", + "parameters": [ + { "name": "gepaId", "in": "formData", "required": false, "type": "string" }, + { "name": "kommunikationJson", "in": "formData", "required": false, "type": "string" }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "default": { "description": "successful operation" } } + } + }, + "/backend_geschaeftspartneradmin_kommunikation/auswahl_kommunikationsart": { + "get": { + "tags": ["geschaeftspartneradmin_kommunikation"], + "summary": "liefert die Kommunikationsarten als Auswahlliste", + "description": "", + "operationId": "getAuswahlKommunikationsart", + "produces": ["application/json"], + "parameters": [ + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_geschaeftspartneradmin_zugangsdaten": { + "get": { + "tags": ["geschaeftspartneradmin_zugangsdaten"], + "summary": "liefert die Zugangsdaten eines Geschäftspartners", + "description": "", + "operationId": "getZugangsdaten", + "produces": ["application/json"], + "parameters": [ + { + "name": "gepaId", + "in": "query", + "description": "GepaId des Geschäftspartners", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { "description": "successful operation", "schema": { "$ref": "#/definitions/DobjZugangsdaten" } } + } + }, + "put": { + "tags": ["geschaeftspartneradmin_zugangsdaten"], + "summary": "ändern die Zugangsdaten eines Geschäftspartners", + "description": "", + "operationId": "updateZugangsdaten", + "parameters": [ + { + "name": "jsonZugang", + "in": "formData", + "description": "Zugangsdaten im JSON Format", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "default": { "description": "successful operation" } } + } + }, + "/public_backend_kennwortneu": { + "put": { + "summary": "Zugangsdaten ändern", + "description": "", + "operationId": "updateZugang_1", + "consumes": ["application/x-www-form-urlencoded"], + "produces": ["application/json"], + "parameters": [ + { + "name": "currentBenutzername", + "in": "formData", + "description": "currentBenutzername", + "required": true, + "type": "string" + }, + { + "name": "currentKennwort", + "in": "formData", + "description": "currentKennwort", + "required": true, + "type": "string" + }, + { "name": "kennwort", "in": "formData", "description": "kennwort", "required": true, "type": "string" }, + { + "name": "kennwortWiederholung", + "in": "formData", + "description": "kennwortWiederholung", + "required": true, + "type": "string" + } + ], + "responses": { "200": { "description": "successful operation", "schema": { "type": "boolean" } } } + } + }, + "/public_backend_kennwortneu/getKennwortLaenge": { + "get": { + "summary": "Liefert den zulaessigen Wert für die Kennwortlaenge.", + "description": "", + "operationId": "getKennwortLaenge", + "parameters": [], + "responses": { "200": { "description": "successful operation", "schema": { "type": "string" } } } + } + }, + "/public_backend_report": { + "get": { + "summary": "Liefert einen Kopfzeileninformationen für Reports", + "description": "", + "operationId": "getKopfzeilenInformationen", + "produces": ["application/json"], + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { "$ref": "#/definitions/DobjKopfzeilenInformationen" } + } + } + } + }, + "/backend_schule_ilea/searchExamen": { + "get": { + "tags": ["Ilea"], + "summary": "liefert alle Examen einer Schule", + "description": "", + "operationId": "getListExamen", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjExamen" } } + } + } + } + }, + "/backend_schule_ilea/examen": { + "get": { + "tags": ["Ilea"], + "summary": "liefert ein Examen samt Teilnehmer einer Schule", + "description": "", + "operationId": "getExamen", + "produces": ["application/json"], + "parameters": [ + { + "name": "examenId", + "in": "query", + "description": "ID des Examen eingeben", + "required": true, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { "description": "successful operation", "schema": { "$ref": "#/definitions/DobjExamen" } } + } + }, + "post": { + "tags": ["Ilea"], + "summary": "Einfügen eines Examen", + "description": "", + "operationId": "insertExam", + "consumes": ["application/x-www-form-urlencoded"], + "produces": ["application/json"], + "parameters": [ + { + "name": "jsonExamen", + "in": "formData", + "description": "Daten des Examen im JSON Format", + "required": true, + "type": "string" + }, + { "name": "schuleId", "in": "query", "description": "Schule ID", "required": true, "type": "string" }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "200": { "description": "successful operation", "schema": { "type": "string" } } } + }, + "delete": { + "tags": ["Ilea"], + "summary": "löscht ein Examen samt aller Unterdaten", + "description": "", + "operationId": "deleteExamen", + "produces": ["application/json"], + "parameters": [ + { + "name": "jsonExamenIds", + "in": "formData", + "description": "ID des Examen eingeben", + "required": true, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "default": { "description": "successful operation" } } + } + }, + "/backend_schule_ilea/teilnehmer": { + "get": { + "tags": ["Ilea"], + "summary": "liefert alle Teilnehmer eines Examen", + "description": "", + "operationId": "getListExamenTeilnehmer", + "produces": ["application/json"], + "parameters": [ + { "name": "schuleId", "in": "query", "description": "ID der Schule", "required": true, "type": "string" }, + { "name": "examenId", "in": "query", "description": "ID des Examen", "required": true, "type": "string" }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjSchueler" } } + } + } + } + }, + "/backend_schule_ilea/schueler": { + "get": { + "tags": ["Ilea"], + "summary": "liefert alle Schueler einer Klasse", + "description": "", + "operationId": "getListSchueler", + "produces": ["application/json"], + "parameters": [ + { "name": "schuleId", "in": "query", "description": "ID der Schule", "required": true, "type": "string" }, + { "name": "kursId", "in": "query", "description": "ID der Klasse", "required": true, "type": "string" }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjSchueler" } } + } + } + } + }, + "/backend_schule_ilea/auswertung": { + "get": { + "tags": ["Ilea"], + "summary": "liefert ein Examen samt Teilnehmer einer Schule", + "description": "", + "operationId": "getAuswertung", + "produces": ["application/json"], + "parameters": [ + { + "name": "examenId", + "in": "query", + "description": "ID des Examen eingeben", + "required": true, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { "description": "successful operation", "schema": { "$ref": "#/definitions/DobjDatei" } } + } + } + }, + "/backend_schule_ilea/auswahl_kurs": { + "get": { + "tags": ["Ilea"], + "summary": "liefert alle aktiven Kurse der schule", + "description": "", + "operationId": "getListkurs", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { + "name": "ileaPaketCode", + "in": "query", + "description": "ileaPaketCode", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_schule_ilea/aufgaben": { + "get": { + "tags": ["Ilea"], + "summary": "liefert alle aufgaben", + "description": "", + "operationId": "getListAufgaben", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" }, + { "name": "paketCode", "in": "query", "description": "paketCode", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/IleaAufgabe" } } + } + } + } + }, + "/backend_schule_ilea/auswahl_aufgabenpaket": { + "get": { + "tags": ["Ilea"], + "summary": "liefert alle Aufgabenpakete", + "description": "", + "operationId": "getListAufgabenpaket", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAufgabenpaketAuswahl" } } + } + } + } + }, + "/backend_schule_ilea/printExamen": { + "get": { + "tags": ["Ilea"], + "summary": "Liefert Examen im Format PDF zurück.", + "description": "", + "operationId": "print", + "parameters": [ + { + "name": "examenId", + "in": "query", + "description": "ID des Examen eingeben", + "required": true, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "hostname", "in": "query", "description": "hostname", "required": true, "type": "string" }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "default": { "description": "successful operation" } } + } + }, + "/backend_schule_klasse_detail/schueler_vorgaengerklasse": { + "get": { + "tags": ["klasse_detail"], + "summary": "Liefert die Schueler einer bestimmten Klasse.", + "description": "", + "operationId": "getListSchueler_1", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "klasseId", "in": "query", "description": "KlasseID eingeben", "required": true, "type": "string" }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjSchueler" } } + } + } + } + }, + "/backend_schule_klasse_detail/auswahl_halbjahr": { + "get": { + "tags": ["klasse_detail"], + "summary": "liefert die Halbjahreliste", + "description": "", + "operationId": "getListHalbjahr", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjHalbjahr" } } + } + } + } + }, + "/backend_schule_klasse_detail/{id}": { + "get": { + "tags": ["klasse_detail"], + "summary": "liest eine Klasse", + "description": "", + "operationId": "getKlasse", + "produces": ["application/json"], + "parameters": [ + { "name": "id", "in": "path", "description": "ID der Klasse eingeben", "required": true, "type": "string" }, + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { "description": "successful operation", "schema": { "$ref": "#/definitions/DobjKlasse" } } + } + }, + "put": { + "tags": ["klasse_detail"], + "summary": "modifiziert eine Klasse", + "description": "", + "operationId": "updateKlasse", + "produces": ["application/json"], + "parameters": [ + { + "name": "jsonKlasse", + "in": "formData", + "description": "Daten der Klasse im JSON Format", + "required": false, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "200": { "description": "successful operation", "schema": { "type": "string" } } } + }, + "delete": { + "tags": ["klasse_detail"], + "summary": "löscht eine Klasse", + "description": "", + "operationId": "deleteKlasse", + "parameters": [ + { "name": "id", "in": "path", "description": "ID der Klasse", "required": true, "type": "string" }, + { + "name": "version", + "in": "formData", + "description": "Zeitstempel der Klasse", + "required": true, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "200": { "description": "successful operation", "schema": { "type": "string" } } } + } + }, + "/backend_schule_klasse_detail": { + "post": { + "tags": ["klasse_detail"], + "summary": "Einfügen einer Klasse", + "description": "", + "operationId": "insertKlasse", + "consumes": ["application/x-www-form-urlencoded"], + "produces": ["application/json"], + "parameters": [ + { + "name": "jsonKlasse", + "in": "formData", + "description": "Daten der Klasse im JSON Format", + "required": true, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "200": { "description": "successful operation", "schema": { "type": "string" } } } + } + }, + "/backend_schule_klasse_detail/auswahl_vorgaengerKlasse": { + "get": { + "tags": ["klasse_detail"], + "summary": "Liefert die Klassen eines des vorangegangen Halbjahres für eine Auswahlliste.", + "description": "", + "operationId": "getListAuswahlVorgaengerKlassen", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { + "name": "halbjahrId", + "in": "query", + "description": "HalbjahrID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjVorgaengerklasse" } } + } + } + } + }, + "/backend_schule_klasse_personalzuordnung/auswahl_klassenlehrerFunktion": { + "get": { + "tags": ["klasse_personalzuordnung"], + "summary": "Liefert die möglichen Funktionen von Lehrern für eine Auswahlliste", + "description": "", + "operationId": "getListAuswahlKlassenlehrerFunktion", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjKodierung" } } + } + } + } + }, + "/backend_schule_klasse_personalzuordnung/auswahl_klassenlehrer": { + "get": { + "tags": ["klasse_personalzuordnung"], + "summary": "Liefert die Lehrer einer Schule für eine Auswahlliste", + "description": "", + "operationId": "getListAuswahlKlassenlehrer", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjKodierung" } } + } + } + } + }, + "/backend_schule_klasse_personalzuordnung": { + "get": { + "tags": ["klasse_personalzuordnung"], + "summary": "liest die Personalzuordnungen", + "description": "", + "operationId": "getPersonalzuordnung", + "produces": ["application/json"], + "parameters": [ + { + "name": "klasseId", + "in": "query", + "description": "ID der Klasse eingeben", + "required": true, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjKlassenlehrerZuordnung" } } + } + } + }, + "put": { + "tags": ["klasse_personalzuordnung"], + "summary": "modifiziert die Personalzuordnungen", + "description": "", + "operationId": "updatePersonalzuordnung", + "produces": ["application/json"], + "parameters": [ + { + "name": "jsonPersonalzuordnungen", + "in": "formData", + "description": "Daten der Personalzuordnungen im JSON Format", + "required": false, + "type": "string" + }, + { "name": "klasseId", "in": "formData", "description": "ID der Klasse", "required": true, "type": "string" }, + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "200": { "description": "successful operation", "schema": { "type": "string" } } } + } + }, + "/backend_schule_klasse_schuelerzuordnung/auswahl_klasse": { + "get": { + "tags": ["klasse_schuelerzuordnung"], + "summary": "liefert die Klassenliste", + "description": "", + "operationId": "getListAuswahlKlassenByHalbjahr", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { + "name": "halbjahrId", + "in": "query", + "description": "HalbjahrID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjKlasse" } } + } + } + } + }, + "/backend_schule_klasse_schuelerzuordnung/auswahl_halbjahr": { + "get": { + "tags": ["klasse_schuelerzuordnung"], + "summary": "liefert die Halbjahreliste", + "description": "", + "operationId": "getListHalbjahr_1", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjHalbjahr" } } + } + } + } + }, + "/backend_schule_klasse_schuelerzuordnung": { + "get": { + "tags": ["klasse_schuelerzuordnung"], + "summary": "liefert Schülerzuordnungen zu Klassen", + "description": "", + "operationId": "getListSchuelerzuordnung", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuelerNachname", + "in": "query", + "description": "Nachname des Schülers eingeben", + "required": false, + "type": "string" + }, + { + "name": "schuelerVorname", + "in": "query", + "description": "Vorname des Schülers eingeben", + "required": false, + "type": "string" + }, + { + "name": "klasseId", + "in": "query", + "description": "Id der Klasse eingeben", + "required": false, + "type": "string" + }, + { + "name": "halbjahrId", + "in": "query", + "description": "ID des Halbjahres eingeben", + "required": false, + "type": "string" + }, + { "name": "kzAktiv", "in": "query", "description": "Ist Aktiv", "required": false, "type": "string" }, + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" }, + { + "name": "currentKlasseId", + "in": "query", + "description": "ID der aktuellen Klasse", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjSchuelerzuordnung" } } + } + } + }, + "put": { + "tags": ["klasse_schuelerzuordnung"], + "summary": "Überträgt Schüler in eine neue Klasse", + "description": "", + "operationId": "updateSchuelerzuordnung", + "produces": ["application/json"], + "parameters": [ + { + "name": "jsonSchueler", + "in": "formData", + "description": "Daten der Schüler im JSON Format", + "required": true, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { + "name": "zielKlasseId", + "in": "query", + "description": "ID der Zielklasse eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjHalbjahr" } } + } + } + } + }, + "/backend_schule_klassenbildung/auswahl_halbjahr": { + "get": { + "tags": ["Klassenbildung"], + "summary": "liefert die Halbjahreliste", + "description": "", + "operationId": "getListHalbjahr_2", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjHalbjahr" } } + } + } + } + }, + "/backend_schule_klassenbildung": { + "get": { + "tags": ["Klassenbildung"], + "summary": "liefert Klassen", + "description": "", + "operationId": "getListKlasse", + "produces": ["application/json"], + "parameters": [ + { + "name": "name", + "in": "query", + "description": "Name der Klasse eingeben", + "required": false, + "type": "string" + }, + { + "name": "halbjahrId", + "in": "query", + "description": "ID des Halbjahres eingeben", + "required": false, + "type": "string" + }, + { "name": "kzAktiv", "in": "query", "description": "Ist Aktiv", "required": false, "type": "string" }, + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjKlasse" } } + } + } + } + }, + "/backend_schule_klassenbildung/print": { + "get": { + "tags": ["Klassenbildung"], + "summary": "Liefert die Statistik als Dokument im Format PDF zurück.", + "description": "", + "operationId": "print_1", + "produces": ["application/json"], + "parameters": [ + { "name": "schuleId", "in": "query", "description": "schuleId", "required": true, "type": "string" }, + { "name": "name", "in": "query", "description": "name", "required": true, "type": "string" }, + { "name": "halbjahrId", "in": "query", "description": "halbjahrId", "required": true, "type": "string" }, + { "name": "kzAktiv", "in": "query", "description": "kzAktiv", "required": true, "type": "string" }, + { "name": "selectedIds", "in": "query", "description": "selectedIds", "required": true, "type": "string" }, + { "name": "reportTyp", "in": "query", "description": "reportTyp", "required": true, "type": "string" }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { "description": "successful operation", "schema": { "$ref": "#/definitions/DobjDatei" } } + } + } + }, + "/backend_schule_klassenuebersicht/schueler": { + "get": { + "tags": ["Klassenuebersicht"], + "summary": "liest einen Schüler", + "description": "", + "operationId": "getSchueler", + "produces": ["application/json"], + "parameters": [ + { "name": "id", "in": "query", "description": "Schüler ID eingeben", "required": true, "type": "string" }, + { + "name": "klasseId", + "in": "query", + "description": "Klassen ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { "description": "successful operation", "schema": { "$ref": "#/definitions/RobjSchueler" } } + } + } + }, + "/backend_schule_kurs_detail/schueler_klasse": { + "get": { + "tags": ["kurs_detail"], + "summary": "Liefert die Schueler einer bestimmten Kurs.", + "description": "", + "operationId": "getListSchueler_2", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "klasseId", "in": "query", "description": "KlasseId eingeben", "required": true, "type": "string" }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjSchueler" } } + } + } + } + }, + "/backend_schule_kurs_detail/{id}": { + "get": { + "tags": ["kurs_detail"], + "summary": "liest eine Kurs", + "description": "", + "operationId": "getKurs", + "produces": ["application/json"], + "parameters": [ + { "name": "id", "in": "path", "description": "ID der Kurs eingeben", "required": true, "type": "string" }, + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { "description": "successful operation", "schema": { "$ref": "#/definitions/DobjKurs" } } + } + }, + "put": { + "tags": ["kurs_detail"], + "summary": "modifiziert eine Kurs", + "description": "", + "operationId": "updateKurs", + "produces": ["application/json"], + "parameters": [ + { + "name": "jsonKurs", + "in": "formData", + "description": "Daten der Kurs im JSON Format", + "required": false, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "200": { "description": "successful operation", "schema": { "type": "string" } } } + }, + "delete": { + "tags": ["kurs_detail"], + "summary": "löscht eine Kurs", + "description": "", + "operationId": "deleteKurs", + "parameters": [ + { "name": "id", "in": "path", "description": "ID der Kurs", "required": true, "type": "string" }, + { + "name": "version", + "in": "formData", + "description": "Zeitstempel der Kurs", + "required": true, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "200": { "description": "successful operation", "schema": { "type": "string" } } } + } + }, + "/backend_schule_kurs_detail/auswahl_fachrichtung": { + "get": { + "tags": ["kurs_detail"], + "summary": "liefert die Fachrichtungen", + "description": "", + "operationId": "getListFachrichtung", + "produces": ["application/json"], + "parameters": [ + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_schule_kurs_detail/auswahl_stufe": { + "get": { + "tags": ["kurs_detail"], + "summary": "liefert die Stufe", + "description": "", + "operationId": "getListStufe", + "produces": ["application/json"], + "parameters": [ + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_schule_kurs_detail": { + "post": { + "tags": ["kurs_detail"], + "summary": "Einfügen einer Kurs", + "description": "", + "operationId": "insertKurs", + "consumes": ["application/x-www-form-urlencoded"], + "produces": ["application/json"], + "parameters": [ + { + "name": "jsonKurs", + "in": "formData", + "description": "Daten der Kurs im JSON Format", + "required": true, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "200": { "description": "successful operation", "schema": { "type": "string" } } } + } + }, + "/backend_schule_kurs_detail/auswahl_Klasse": { + "get": { + "tags": ["kurs_detail"], + "summary": "Liefert die Kurs eines des vorangegangen Halbjahres für eine Auswahlliste.", + "description": "", + "operationId": "getListAuswahlVorgaengerKurs", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { + "name": "halbjahrId", + "in": "query", + "description": "HalbjahrID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjVorgaengerkurs" } } + } + } + } + }, + "/backend_schule_kurs_detail/auswahl_halbjahr": { + "get": { + "tags": ["kurs_detail"], + "summary": "liefert die Halbjahreliste", + "description": "", + "operationId": "getListHalbjahr_3", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjHalbjahr" } } + } + } + } + }, + "/backend_schule_kurs_personalzuordnung/auswahl_kurslehrer": { + "get": { + "tags": ["kurs_personalzuordnung"], + "summary": "Liefert die Lehrer einer Schule für eine Auswahlliste", + "description": "", + "operationId": "getListAuswahlKurslehrer", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjKodierung" } } + } + } + } + }, + "/backend_schule_kurs_personalzuordnung/auswahl_kurslehrerFunktion": { + "get": { + "tags": ["kurs_personalzuordnung"], + "summary": "Liefert die möglichen Funktionen von Lehrern für eine Auswahlliste", + "description": "", + "operationId": "getListAuswahlKurslehrerFunktion", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjKodierung" } } + } + } + } + }, + "/backend_schule_kurs_personalzuordnung": { + "get": { + "tags": ["kurs_personalzuordnung"], + "summary": "liest die Personalzuordnungen", + "description": "", + "operationId": "getPersonalzuordnung_1", + "produces": ["application/json"], + "parameters": [ + { + "name": "kursId", + "in": "query", + "description": "ID der Kurs eingeben", + "required": true, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjKurslehrerZuordnung" } } + } + } + }, + "put": { + "tags": ["kurs_personalzuordnung"], + "summary": "modifiziert die Personalzuordnungen", + "description": "", + "operationId": "updatePersonalzuordnung_1", + "produces": ["application/json"], + "parameters": [ + { + "name": "jsonPersonalzuordnungen", + "in": "formData", + "description": "Daten der Personalzuordnungen im JSON Format", + "required": false, + "type": "string" + }, + { "name": "kursId", "in": "formData", "description": "ID der Kurs", "required": true, "type": "string" }, + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "200": { "description": "successful operation", "schema": { "type": "string" } } } + } + }, + "/backend_schule_kurs_schuelerzuordnung/suchen_schueler": { + "get": { + "tags": ["kurs_schuelerzuordnung"], + "summary": "liefert Schülerzuordnungen zu Kurs", + "description": "", + "operationId": "getListSchueler_3", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuelerNachname", + "in": "query", + "description": "Nachname des Schülers eingeben", + "required": false, + "type": "string" + }, + { + "name": "schuelerVorname", + "in": "query", + "description": "Vorname des Schülers eingeben", + "required": false, + "type": "string" + }, + { + "name": "kursId", + "in": "query", + "description": "Id der Kurs eingeben", + "required": false, + "type": "string" + }, + { + "name": "klasseId", + "in": "query", + "description": "Id der Klasse eingeben", + "required": false, + "type": "string" + }, + { + "name": "halbjahrId", + "in": "query", + "description": "ID des Halbjahres eingeben", + "required": false, + "type": "string" + }, + { "name": "kzAktiv", "in": "query", "description": "Ist Aktiv", "required": false, "type": "string" }, + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" }, + { + "name": "currentKursId", + "in": "query", + "description": "ID dem aktuellen Kurs", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjSchuelerzuordnung" } } + } + } + } + }, + "/backend_schule_kurs_schuelerzuordnung/auswahl_halbjahr": { + "get": { + "tags": ["kurs_schuelerzuordnung"], + "summary": "liefert die Halbjahreliste", + "description": "", + "operationId": "getListHalbjahr_4", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjHalbjahr" } } + } + } + } + }, + "/backend_schule_kurs_schuelerzuordnung": { + "get": { + "tags": ["kurs_schuelerzuordnung"], + "operationId": "getListSchuelerzuordnung_1", + "consumes": ["text/html"], + "produces": ["application/json"], + "parameters": [ + { "name": "kursId", "in": "query", "description": "Klasse ID eingeben", "required": false, "type": "string" }, + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "headers": {}, + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjSchuelerzuordnung" } } + } + } + }, + "put": { + "tags": ["kurs_schuelerzuordnung"], + "summary": "Überträgt Schüler in einen neuen Kurs", + "description": "", + "operationId": "updateSchuelerzuordnung_1", + "produces": ["application/json"], + "parameters": [ + { + "name": "jsonSchueler", + "in": "formData", + "description": "Daten der Schüler im JSON Format", + "required": true, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { + "name": "zielKursId", + "in": "query", + "description": "ID der Zielkurs eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "default": { "description": "successful operation" } } + } + }, + "/backend_schule_kurs_schuelerzuordnung/auswahl_kurs": { + "get": { + "tags": ["kurs_schuelerzuordnung"], + "summary": "liefert die Kursliste", + "description": "", + "operationId": "getListAuswahlKursByHalbjahr", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { + "name": "halbjahrId", + "in": "query", + "description": "HalbjahrID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjKurs" } } + } + } + } + }, + "/backend_schule_kursbildung": { + "get": { + "tags": ["Kursbildung"], + "summary": "liefert Kurse", + "description": "", + "operationId": "getListKurs_1", + "produces": ["application/json"], + "parameters": [ + { + "name": "name", + "in": "query", + "description": "Name der Kurs eingeben", + "required": false, + "type": "string" + }, + { + "name": "halbjahrId", + "in": "query", + "description": "ID des Halbjahres eingeben", + "required": false, + "type": "string" + }, + { "name": "kzAktiv", "in": "query", "description": "Ist Aktiv", "required": false, "type": "string" }, + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjKurs" } } + } + } + } + }, + "/backend_schule_kursbildung/auswahl_halbjahr": { + "get": { + "tags": ["Kursbildung"], + "summary": "liefert die Halbjahreliste", + "description": "", + "operationId": "getListHalbjahr_5", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjHalbjahr" } } + } + } + } + }, + "/backend_schule_kursbildung/print": { + "get": { + "tags": ["Kursbildung"], + "summary": "Liefert die Statistik als Dokument im Format PDF zurück.", + "description": "", + "operationId": "print_2", + "produces": ["application/json"], + "parameters": [ + { "name": "schuleId", "in": "query", "description": "schuleId", "required": true, "type": "string" }, + { "name": "name", "in": "query", "description": "name", "required": true, "type": "string" }, + { "name": "halbjahrId", "in": "query", "description": "halbjahrId", "required": true, "type": "string" }, + { "name": "kzAktiv", "in": "query", "description": "kzAktiv", "required": true, "type": "string" }, + { "name": "selectedIds", "in": "query", "description": "selectedIds", "required": true, "type": "string" }, + { "name": "reportTyp", "in": "query", "description": "reportTyp", "required": true, "type": "string" }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { "description": "successful operation", "schema": { "$ref": "#/definitions/DobjDatei" } } + } + } + }, + "/backend_schule_schueler_detail/{id}": { + "get": { + "tags": ["schueler_detail"], + "summary": "liest einen Schüler", + "description": "", + "operationId": "getSchueler_1", + "produces": ["application/json"], + "parameters": [ + { "name": "id", "in": "path", "description": "Schüler ID eingeben", "required": true, "type": "string" }, + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { "description": "successful operation", "schema": { "$ref": "#/definitions/DobjSchueler" } } + } + }, + "put": { + "tags": ["schueler_detail"], + "summary": "modifiziert einen Schüler", + "description": "", + "operationId": "updateSchueler", + "produces": ["application/json"], + "parameters": [ + { "name": "id", "in": "path", "description": "Schüler ID", "required": true, "type": "string" }, + { "name": "version", "in": "formData", "description": "Version", "required": true, "type": "string" }, + { + "name": "vorname", + "in": "formData", + "description": "Vorname des Schülers", + "required": true, + "type": "string" + }, + { + "name": "nachname", + "in": "formData", + "description": "Nachname des Schülers", + "required": true, + "type": "string" + }, + { + "name": "geburtsdatum", + "in": "formData", + "description": "geburtsdatum", + "required": true, + "type": "string" + }, + { + "name": "email", + "in": "formData", + "description": "Email des Schülers", + "required": false, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "200": { "description": "successful operation", "schema": { "type": "string" } } } + } + }, + "/backend_schule_schueler_detail/auswahl_halbjahr_aktuell": { + "get": { + "tags": ["schueler_detail"], + "summary": "liefert die Halbjahreliste", + "description": "", + "operationId": "getListHalbjahrAktuell", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjHalbjahr" } } + } + } + } + }, + "/backend_schule_schueler_detail/auswahl_klasse": { + "get": { + "tags": ["schueler_detail"], + "summary": "liefert die Klassenliste", + "description": "", + "operationId": "getListAuswahlKlassenByHalbjahr_1", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { + "name": "halbjahrId", + "in": "query", + "description": "HalbjahrID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjKlasse" } } + } + } + } + }, + "/backend_schule_schueler_detail": { + "post": { + "tags": ["schueler_detail"], + "summary": "Einfügen eines Schülers", + "description": "", + "operationId": "insertSchueler", + "consumes": ["application/x-www-form-urlencoded"], + "parameters": [ + { + "name": "vorname", + "in": "formData", + "description": "Vorname des Schülers", + "required": true, + "type": "string" + }, + { + "name": "nachname", + "in": "formData", + "description": "Nachname des Schülers", + "required": true, + "type": "string" + }, + { + "name": "geburtsdatum", + "in": "formData", + "description": "geburtsdatum", + "required": true, + "type": "string" + }, + { + "name": "klasseId", + "in": "formData", + "description": "Klasse-ID eingeben", + "required": true, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "200": { "description": "successful operation", "schema": { "type": "string" } } } + } + }, + "/backend_schule_schueler_uebernahme/auswahl_halbjahr": { + "get": { + "tags": ["schueler_uebernahme"], + "summary": "liefert die Halbjahreliste", + "description": "", + "operationId": "getListHalbjahr_6", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjHalbjahr" } } + } + } + } + }, + "/backend_schule_schueler_uebernahme/klasse": { + "get": { + "tags": ["schueler_uebernahme"], + "summary": "liefert Klassen", + "description": "", + "operationId": "getListKlasse_1", + "produces": ["application/json"], + "parameters": [ + { + "name": "name", + "in": "query", + "description": "Name der Klasse eingeben", + "required": false, + "type": "string" + }, + { + "name": "halbjahrId", + "in": "query", + "description": "ID des Halbjahres eingeben", + "required": false, + "type": "string" + }, + { "name": "kzAktiv", "in": "query", "description": "Ist Aktiv", "required": false, "type": "string" }, + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjKlasse" } } + } + } + } + }, + "/backend_schule_schueler_uebernahme": { + "get": { + "tags": ["schueler_uebernahme"], + "summary": "liefert freigegebene Schüler", + "description": "", + "operationId": "getListFreigegebeneSchueler", + "consumes": ["text/html"], + "produces": ["application/json"], + "parameters": [ + { + "name": "nachname", + "in": "query", + "description": "Nachname des Schülers", + "required": false, + "type": "string" + }, + { + "name": "vorname", + "in": "query", + "description": "Vorname des Schülers", + "required": false, + "type": "string" + }, + { + "name": "geburtsdatum", + "in": "query", + "description": "Geburtsdatum des Schülers", + "required": false, + "type": "string" + }, + { + "name": "kzAktuellerSchuleZugeordnet", + "in": "query", + "description": "Kennzeichen, dass die Schüler der übergebenen Schule zugeordnet sind", + "required": false, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjSchueler" } } + } + } + }, + "put": { + "tags": ["schueler_uebernahme"], + "summary": "übernimmt Schüler in gewählte Klasse", + "description": "", + "operationId": "transferSchueler", + "produces": ["application/json"], + "parameters": [ + { + "name": "jsonSchueler", + "in": "formData", + "description": "Daten der Schüler im JSON Format", + "required": true, + "type": "string" + }, + { + "name": "zielKlasseId", + "in": "query", + "description": "ID der Zielklasse eingeben", + "required": true, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "default": { "description": "successful operation" } } + } + }, + "/backend_schule_schueleruebersicht/schulen": { + "get": { + "tags": ["Schueleruebersicht"], + "summary": "liefert die Schulenliste", + "description": "", + "operationId": "getListSchulen_1", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { + "name": "schulnummer", + "in": "query", + "description": "Nummer der Schule eingeben", + "required": false, + "type": "string" + }, + { + "name": "schulname", + "in": "query", + "description": "Name der Schule eingeben", + "required": false, + "type": "string" + }, + { + "name": "strasse", + "in": "query", + "description": "Straße der Schule eingeben", + "required": false, + "type": "string" + }, + { + "name": "postleitzahl", + "in": "query", + "description": "Postleitzahl der Schule eingeben", + "required": false, + "type": "string" + }, + { + "name": "ort", + "in": "query", + "description": "Ort der Schule eingeben", + "required": false, + "type": "string" + }, + { + "name": "schulartId", + "in": "query", + "description": "ID der Schulart eingeben", + "required": false, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjSchule" } } + } + } + } + }, + "/backend_schule_schueleruebersicht/auswahl_klasse": { + "get": { + "tags": ["Schueleruebersicht"], + "summary": "liefert die Klassenliste", + "description": "", + "operationId": "getListAuswahlKlassenByHalbjahr_2", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { + "name": "halbjahrId", + "in": "query", + "description": "HalbjahrID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjKlasse" } } + } + } + } + }, + "/backend_schule_schueleruebersicht/auswahl_halbjahr": { + "get": { + "tags": ["Schueleruebersicht"], + "summary": "liefert die Halbjahreliste", + "description": "", + "operationId": "getListHalbjahr_7", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjHalbjahr" } } + } + } + } + }, + "/backend_schule_schueleruebersicht": { + "get": { + "tags": ["Schueleruebersicht"], + "summary": "liefert die Schüler", + "description": "", + "operationId": "getListSchuelerzuordnung_2", + "consumes": ["text/html"], + "produces": ["application/json"], + "parameters": [ + { + "name": "name", + "in": "query", + "description": "Name der Klasse eingeben", + "required": false, + "type": "string" + }, + { + "name": "halbjahrId", + "in": "query", + "description": "ID des Halbjahres eingeben", + "required": false, + "type": "string" + }, + { + "name": "klasseId", + "in": "query", + "description": "Klasse ID eingeben", + "required": false, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjSchuelerzuordnung" } } + } + } + } + }, + "/backend_schule_schueleruebersicht/schuelerfreigabe": { + "put": { + "tags": ["Schueleruebersicht"], + "summary": "Gibt die ausgewählten Schüler zur Übernahme frei.", + "description": "", + "operationId": "schuelerFreigeben", + "produces": ["application/json"], + "parameters": [ + { + "name": "listSchuelerId", + "in": "query", + "description": "Liste der Schüler-IDs", + "required": true, + "type": "array", + "items": { "type": "string" }, + "collectionFormat": "multi" + }, + { + "name": "wunschschuleId", + "in": "formData", + "description": "ID der Wunschschule eingeben", + "required": false, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "default": { "description": "successful operation" } } + } + }, + "/backend_schule_schueleruebersicht/schueler_archivieren": { + "delete": { + "tags": ["Schueleruebersicht"], + "summary": "Archiviert alle ausgewählten Schüler.", + "description": "", + "operationId": "deleteKlasse_1", + "parameters": [ + { + "name": "listSchuelerId", + "in": "query", + "description": "Liste der Schüler-IDs", + "required": true, + "type": "array", + "items": { "type": "string" }, + "collectionFormat": "multi" + }, + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "default": { "description": "successful operation" } } + } + }, + "/backend_schule_schueleruebersicht/print": { + "get": { + "tags": ["Schueleruebersicht"], + "summary": "Liefert die Statistik als Dokument im Format PDF zurück.", + "description": "", + "operationId": "print_3", + "produces": ["application/json"], + "parameters": [ + { "name": "schuleId", "in": "query", "description": "schuleId", "required": true, "type": "string" }, + { "name": "selectedIds", "in": "query", "description": "selectedIds", "required": true, "type": "string" }, + { "name": "reportTyp", "in": "query", "description": "reportTyp", "required": true, "type": "string" }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { "description": "successful operation", "schema": { "$ref": "#/definitions/DobjDatei" } } + } + } + }, + "/backend_schule_tsc_sso_links": { + "get": { + "tags": ["backend_schule_tsc_sso_links"], + "summary": "liest eine Klasse", + "description": "", + "operationId": "getList", + "produces": ["application/json"], + "parameters": [ + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjSsoLink" } } + } + } + } + }, + "/backend_stammdaten_ilea_aufgabenpaketjahrgang/suche": { + "get": { + "tags": ["backend_stammdaten_ilea_aufgabenpaketjahrgang"], + "summary": "Liefert die Ilea-Aufgabenpakete", + "description": "", + "operationId": "getIleaAufgabenpakete", + "produces": ["application/json"], + "parameters": [ + { "name": "jahrgang", "in": "query", "description": "jahrgang", "required": true, "type": "string" }, + { + "name": "listAufgabenpaketCode", + "in": "query", + "description": "aufgabenpaketCode", + "required": true, + "type": "array", + "items": { "type": "string" }, + "collectionFormat": "multi" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjAufgabepaket" } } + } + } + } + }, + "/backend_stammdaten_ilea_aufgabenpaketjahrgang": { + "post": { + "tags": ["backend_stammdaten_ilea_aufgabenpaketjahrgang"], + "summary": "Einfügen eine Aufgabenpaket-Zuordnung", + "description": "", + "operationId": "insertZuordnung", + "consumes": ["application/x-www-form-urlencoded"], + "produces": ["application/json"], + "parameters": [ + { + "name": "jsonAufgabenpaket", + "in": "formData", + "description": "Daten des Aufgabenpakets im JSON Format", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "default": { "description": "successful operation" } } + }, + "delete": { + "tags": ["backend_stammdaten_ilea_aufgabenpaketjahrgang"], + "summary": "Entfernt Aufgabenpaket Zuordnung/en.", + "description": "", + "operationId": "listZuordnungId", + "parameters": [ + { + "name": "listZuordnungId", + "in": "query", + "description": "Liste der Zuordnung-IDs", + "required": true, + "type": "array", + "items": { "type": "string" }, + "collectionFormat": "multi" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "default": { "description": "successful operation" } } + } + }, + "/backend_stammdaten_ilea_aufgabenpaketjahrgang/auswahl_jahrgang": { + "get": { + "tags": ["backend_stammdaten_ilea_aufgabenpaketjahrgang"], + "summary": "liefert die Jahrgänge", + "description": "", + "operationId": "getListJahrgang", + "produces": ["application/json"], + "parameters": [ + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_stammdaten_ilea_aufgabenpaketjahrgang/auswahl_aufgabenpaket": { + "get": { + "tags": ["backend_stammdaten_ilea_aufgabenpaketjahrgang"], + "summary": "liefert alle Aufgabenpakete", + "description": "", + "operationId": "getListAufgabenpaket_1", + "produces": ["application/json"], + "parameters": [ + { + "name": "aufgabenpaket", + "in": "query", + "description": "aufgabenpaket", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAufgabenpaketAuswahl" } } + } + } + } + }, + "/backend_stammdaten_tooltipbearbeiten/suche": { + "get": { + "tags": ["backend_stammdaten_tooltipbearbeiten"], + "summary": "Liefert eine Liste mit Tooltips.", + "description": "", + "operationId": "getListTooltip", + "produces": ["application/json"], + "parameters": [ + { + "name": "komponente", + "in": "query", + "description": "Code der Komponente", + "required": false, + "type": "string" + }, + { "name": "feld", "in": "query", "description": "Name des Feldes", "required": false, "type": "string" }, + { "name": "text", "in": "query", "description": "Text des Tooltips", "required": false, "type": "string" }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjTooltip" } } + } + } + } + }, + "/backend_stammdaten_tooltipbearbeiten/auswahl_komponente": { + "get": { + "tags": ["backend_stammdaten_tooltipbearbeiten"], + "summary": "Liefert eine Auswahlliste der Komponenten für die Tooltipkonfiguration.", + "description": "", + "operationId": "getAuswahlKomponente", + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_stammdaten_tooltipbearbeiten/tooltiptext_update": { + "put": { + "tags": ["backend_stammdaten_tooltipbearbeiten"], + "summary": "Ändert den Text eines Tooltips.", + "description": "", + "operationId": "tooltiptextUpdate", + "produces": ["application/json"], + "parameters": [ + { "name": "id", "in": "formData", "description": "ID des Tooltips", "required": true, "type": "string" }, + { "name": "text", "in": "formData", "description": "Text des Tooltips", "required": true, "type": "string" }, + { "name": "version", "in": "formData", "description": "Version", "required": true, "type": "string" }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "default": { "description": "successful operation" } } + } + }, + "/backend_stammdaten_tooltipbearbeiten/{id}": { + "get": { + "tags": ["backend_stammdaten_tooltipbearbeiten"], + "summary": "Liefert die Daten eines Tooltips.", + "description": "", + "operationId": "getTooltip", + "produces": ["application/json"], + "parameters": [ + { "name": "id", "in": "path", "description": "ID des Tooltips", "required": true, "type": "string" }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { "description": "successful operation", "schema": { "$ref": "#/definitions/RobjTooltip" } } + } + } + }, + "/backend_statistik_veranstaltung/auswahl_auswertungstyp": { + "get": { + "summary": "Liefert Auswahlliste Auswertungstyp", + "description": "", + "operationId": "getAuswahlAuswertungstyp", + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahlGruppiert" } } + } + } + } + }, + "/backend_statistik_veranstaltung/auswahl_mandant": { + "get": { + "summary": "Liefert Auswahlliste Mandant", + "description": "", + "operationId": "getAuswahlMandant", + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_statistik_veranstaltung/auswahl_eigentuemer": { + "get": { + "summary": "Liefert Auswahlliste Eigentümer", + "description": "", + "operationId": "getAuswahlEigentuemer", + "parameters": [ + { "name": "mandId", "in": "query", "description": "mandId", "required": false, "type": "string" }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_statistik_veranstaltung/auswahl_arbeitsbereich": { + "get": { + "summary": "Liefert Auswahlliste Arbeitsbereich", + "description": "", + "operationId": "getAuswahlArbeitsbereich", + "parameters": [ + { "name": "mandId", "in": "query", "description": "mandId", "required": false, "type": "string" }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_statistik_veranstaltung/auswahl_kostenstelle": { + "get": { + "summary": "Liefert Auswahlliste Kostenstelle", + "description": "", + "operationId": "getAuswahlKostenstelle", + "parameters": [ + { "name": "mandId", "in": "query", "description": "mandId", "required": false, "type": "string" }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_statistik_veranstaltung/auswahl_organisationsform": { + "get": { + "summary": "Liefert Auswahlliste Organisationsform", + "description": "", + "operationId": "getAuswahlOrganisationsform", + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_statistik_veranstaltung/auswahl_veranstaltungsstatus": { + "get": { + "summary": "Liefert Auswahlliste Veranstaltungsstatus", + "description": "", + "operationId": "getAuswahlVeranstaltungsstatus", + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_statistik_veranstaltung/auswahl_zielgruppe": { + "get": { + "summary": "Liefert Auswahlliste Zielgruppe", + "description": "", + "operationId": "getAuswahlZielgruppe", + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_statistik_veranstaltung/auswahl_schulart": { + "get": { + "summary": "Liefert Auswahlliste Schulart", + "description": "", + "operationId": "getAuswahlSchulart", + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_statistik_veranstaltung/auswahl_gueltigkeitsbereich": { + "get": { + "summary": "Liefert Auswahlliste Gültigkeitsbereich", + "description": "", + "operationId": "getAuswahlGueltigkeitsbereich", + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_statistik_veranstaltung/auswahl_fachrichtung": { + "get": { + "summary": "Liefert Auswahlliste Fachrichtung", + "description": "", + "operationId": "getAuswahlFachrichtung", + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_statistik_veranstaltung/auswahl_abrechnungsstatus": { + "get": { + "summary": "Liefert Auswahlliste Fachrichtung", + "description": "", + "operationId": "getAuswahlAbrechnungsstatus", + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_statistik_veranstaltung/auswahl_fortbildungsart": { + "get": { + "summary": "Liefert Auswahlliste Fortbildungsart", + "description": "", + "operationId": "getAuswahlFortbildungsart", + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_statistik_veranstaltung/auswahl_veranstaltungsart": { + "get": { + "summary": "Liefert Auswahlliste Veranstaltungsart", + "description": "", + "operationId": "getAuswahlVeranstaltungsart", + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_statistik_veranstaltung/auswahl_schwerpunkt": { + "get": { + "summary": "Liefert Auswahlliste Schwerpunkt", + "description": "", + "operationId": "getAuswahlSchwerpunkt", + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_statistik_veranstaltung/auswahl_stichwort": { + "get": { + "summary": "Liefert Auswahlliste Stichwort", + "description": "", + "operationId": "getAuswahlStichwort", + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_statistik_veranstaltung/auswahl_dozent": { + "get": { + "summary": "Liefert Auswahlliste Stichwort", + "description": "", + "operationId": "getAuswahlDozent", + "parameters": [ + { "name": "name", "in": "query", "description": "name", "required": true, "type": "string" }, + { "name": "ort", "in": "query", "description": "ort", "required": true, "type": "string" }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_statistik_veranstaltung/auswahl_leitung": { + "get": { + "summary": "Liefert Auswahlliste Stichwort", + "description": "", + "operationId": "getAuswahlLeitung", + "parameters": [ + { "name": "name", "in": "query", "description": "name", "required": true, "type": "string" }, + { "name": "ort", "in": "query", "description": "ort", "required": true, "type": "string" }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_statistik_veranstaltung/spaltenoption": { + "get": { + "summary": "Liefert die Spaltenoptionen für die Ergebnisliste.", + "description": "", + "operationId": "getSpaltenoptionen", + "parameters": [ + { + "name": "auswertungstypGruppeCode", + "in": "query", + "description": "auswertungstypGruppeCode", + "required": true, + "type": "string" + }, + { "name": "resultsetId", "in": "query", "description": "resultsetId", "required": true, "type": "string" }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjSpaltenoption" } } + } + } + } + }, + "/backend_statistik_veranstaltung/print": { + "get": { + "summary": "Liefert die Statistik als Dokument im Format PDF oder Excel zurück.", + "description": "", + "operationId": "print_4", + "produces": ["application/json"], + "parameters": [ + { + "name": "jsonReportBobj", + "in": "query", + "description": "JSON Objekt mit Suchparametern", + "required": false, + "type": "string" + }, + { "name": "reportFormat", "in": "query", "description": "reportFormat", "required": true, "type": "string" }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { "description": "successful operation", "schema": { "$ref": "#/definitions/DobjDatei" } } + } + } + }, + "/backend_statistik_veranstaltung": { + "get": { + "summary": "Liefert Vesg Statistik", + "description": "", + "operationId": "search", + "parameters": [ + { + "name": "jsonSearchBobj", + "in": "query", + "description": "JSON Objekt mit Suchparametern", + "required": false, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { "description": "successful operation", "schema": { "$ref": "#/definitions/DobjStatistik" } } + } + } + }, + "/backend_systempflege_protokolleintrag/auswahl_protokolleintragSchweregrad": { + "get": { + "tags": ["backend_systempflege_protokolleintrag"], + "summary": "Liefert eine Auswahlliste der Schweregrade des Logeintritts.", + "description": "", + "operationId": "getAuswahlProtokolleintragSchweregrad", + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/backend_systempflege_protokolleintrag/details": { + "get": { + "tags": ["backend_systempflege_protokolleintrag"], + "summary": "Liefert die Daten eines Fehlereintritts.", + "description": "", + "operationId": "getProtokolleintrag", + "produces": ["application/json"], + "parameters": [ + { + "name": "logUID", + "in": "query", + "description": "Nummer des Fehlereintritts", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { "description": "successful operation", "schema": { "$ref": "#/definitions/RobjProtokolleintrag" } } + } + } + }, + "/backend_systempflege_protokolleintrag/suche": { + "get": { + "tags": ["backend_systempflege_protokolleintrag"], + "summary": "Liefert eine Liste mit Fehlereintritten.", + "description": "", + "operationId": "getListProtokolleintrag", + "produces": ["application/json"], + "parameters": [ + { + "name": "jsonSearchBobj", + "in": "query", + "description": "JSON Objekt mit Suchparametern", + "required": false, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjProtokolleintrag" } } + } + } + } + }, + "/backend_systempflege_protokolleintrag/auswahl_protokolleintragTyp": { + "get": { + "tags": ["backend_systempflege_protokolleintrag"], + "summary": "Liefert eine Auswahlliste der Logeintrittstypen.", + "description": "", + "operationId": "getAuswahlProtokolleintragTyp", + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahl" } } + } + } + } + }, + "/public_backend_tooltip": { + "get": { + "summary": "Liefert einen Tooltip", + "description": "", + "operationId": "get", + "produces": ["application/json"], + "parameters": [{ "name": "key", "in": "query", "description": "key", "required": true, "type": "string" }], + "responses": { "200": { "description": "successful operation", "schema": { "$ref": "#/definitions/Value" } } } + } + }, + "/public_mediothek_metadatenexport/publicMediendatei": { + "get": { + "tags": ["Mediotheksexport"], + "summary": "Liefert alle Metadaten.", + "description": "Sortiert nach letzte Änderung (pts)", + "operationId": "getAll", + "produces": ["application/json"], + "parameters": [ + { + "name": "pts", + "in": "query", + "description": "Letzte Änderung der Datei", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjMediumdatei" } } + } + } + } + }, + "/permission/reload_syei": { + "get": { + "tags": ["Berechtigungen"], + "operationId": "reload_syei", + "parameters": [], + "responses": { "default": { "description": "successful operation" } } + } + }, + "/permission/reload_bere": { + "get": { + "tags": ["Berechtigungen"], + "operationId": "reload_bere", + "parameters": [], + "responses": { "default": { "description": "successful operation" } } + } + }, + "/permission": { + "get": { + "tags": ["Berechtigungen"], + "summary": "Ruft technische Objekte auf Basis eines Präfix ab.", + "description": "", + "operationId": "get_1", + "produces": ["application/json"], + "parameters": [ + { + "name": "prefix", + "in": "query", + "description": "Präfix auf deren Basis die technischen Objekte geholt werden", + "required": false, + "type": "string" + }, + { + "name": "rolleObjektId", + "in": "query", + "description": "Id bei Rollenberechtigungen", + "required": false, + "type": "string" + }, + { + "name": "rolleObjektName", + "in": "query", + "description": "Name bei Rollenberechtigungen", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "$ref": "#/definitions/Objekt zur Beantwortung von Berechtigungsanfragen" } + }, + "401": { + "description": "Fehler mit oder bei der Authentifizierung.", + "schema": { "$ref": "#/definitions/Fehlerantwort" } + }, + "400": { + "description": "Fehlerhafte Authentifizierungsdaten.", + "schema": { "$ref": "#/definitions/Fehlerantwort" } + } + } + } + }, + "/schulverwaltung_export_klasse": { + "get": { + "tags": ["Export"], + "security": [ + { + "Bearer": [] + } + ], + "summary": "liefert eine Liste von allen geändert oder erstellten Klassen seit dem gegebenen Datum", + "description": "", + "operationId": "exportKlasseList", + "produces": ["application/json"], + "parameters": [ + { + "name": "dtLetzteAenderung", + "in": "query", + "description": "Datum der letzten Änderung eingeben", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjExportKlasse" } } + } + } + } + }, + "/schulverwaltung_export_lehrer_migration": { + "get": { + "tags": ["Export"], + "security": [ + { + "Bearer": [] + } + ], + "summary": "liefert eine Liste von allen Lehrern. Zu einem Lehrer wird die alte und die neue uid geliefert.", + "description": "", + "operationId": "exportLehrerListMigration", + "produces": ["application/json"], + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjExportLehrerMigration" } } + } + } + } + }, + "/schulverwaltung_export_lehrer": { + "get": { + "tags": ["Export"], + "security": [ + { + "Bearer": [] + } + ], + "summary": "liefert eine Liste von allen geändert oder erstellten Lehrer seit dem gegebenen Datum", + "description": "", + "operationId": "exportLehrerList", + "produces": ["application/json"], + "parameters": [ + { + "name": "dtLetzteAenderung", + "in": "query", + "description": "Datum der letzten Änderung eingeben", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjExportLehrer" } } + } + } + } + }, + "/schulverwaltung_export_schueler_migration": { + "get": { + "tags": ["Export"], + "security": [ + { + "Bearer": [] + } + ], + "summary": "liefert eine Liste von allen Lehrern. Zu einem Schüler wird die alte und die neue uid geliefert.", + "description": "", + "operationId": "exportSchuelerListMigration", + "produces": ["application/json"], + "parameters": [], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjExportSchuelerMigration" } } + } + } + } + }, + "/schulverwaltung_export_schueler": { + "get": { + "tags": ["Export"], + "security": [ + { + "Bearer": [] + } + ], + "summary": "liefert eine Liste von allen geändert oder erstellten Schüler seit dem gegebenen Datum", + "description": "", + "operationId": "exportSchuelerList", + "produces": ["application/json"], + "parameters": [ + { + "name": "dtLetzteAenderung", + "in": "query", + "description": "Datum der letzten Änderung eingeben", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjExportSchueler" } } + } + } + } + }, + "/schulverwaltung_export_schule": { + "get": { + "tags": ["Export"], + "security": [ + { + "Bearer": [] + } + ], + "summary": "liefert eine Liste von allen geändert oder erstellten Schulen seit dem gegebenen Datum", + "description": "", + "operationId": "exportSchuleList", + "produces": ["application/json"], + "parameters": [ + { + "name": "dtLetzteAenderung", + "in": "query", + "description": "Datum der letzten Änderung eingeben", + "required": false, + "type": "string" + } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjExportSchule" } } + } + } + } + }, + "/schulverwaltung_export_version": { + "get": { + "tags": ["Export"], + "security": [ + { + "Bearer": [] + } + ], + "summary": "liefert die aktuelle Version zurück", + "description": "", + "operationId": "version", + "produces": ["application/json"], + "parameters": [], + "responses": { + "200": { "description": "successful operation", "schema": { "$ref": "#/definitions/VersionResponse" } } + } + } + }, + "/schulverwaltung_klasse": { + "get": { + "tags": ["Klassenverwaltung"], + "summary": "liefert Klassen", + "description": "", + "operationId": "getListKlasse_2", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjKlasse" } } + } + } + }, + "post": { + "tags": ["Klassenverwaltung"], + "summary": "Einfügen einer Klasse", + "description": "", + "operationId": "insertKlasse_1", + "consumes": ["application/x-www-form-urlencoded"], + "produces": ["application/json"], + "parameters": [ + { "name": "name", "in": "formData", "description": "Name der Klasse", "required": true, "type": "string" }, + { + "name": "klassenlehrerId", + "in": "formData", + "description": "Lehrer-ID", + "required": false, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "200": { "description": "successful operation", "schema": { "type": "string" } } } + } + }, + "/schulverwaltung_klasse/{id}": { + "put": { + "tags": ["Klassenverwaltung"], + "summary": "modifiziert eine Klasse", + "description": "", + "operationId": "updateKlasse_1", + "produces": ["application/json"], + "parameters": [ + { "name": "id", "in": "path", "description": "Klasse-ID", "required": true, "type": "string" }, + { + "name": "version", + "in": "formData", + "description": "Zeitstempel der Klasse", + "required": false, + "type": "string" + }, + { "name": "name", "in": "formData", "description": "Name der Klasse", "required": true, "type": "string" }, + { + "name": "klassenlehrerId", + "in": "formData", + "description": "Lehrer-ID", + "required": false, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "200": { "description": "successful operation", "schema": { "type": "string" } } } + }, + "delete": { + "tags": ["Klassenverwaltung"], + "summary": "löscht eine Klasse", + "description": "", + "operationId": "deleteKlasse_2", + "parameters": [ + { "name": "id", "in": "path", "description": "ID der Klasse", "required": true, "type": "string" }, + { + "name": "version", + "in": "formData", + "description": "Zeitstempel der Klasse", + "required": true, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "200": { "description": "successful operation", "schema": { "type": "string" } } } + } + }, + "/public_schuelerregistrierung/bestaetigungscode": { + "post": { + "summary": "Es testet die Formularangaben und erzeugt einen Bestätigungscode", + "description": "", + "operationId": "createBestaetigungscode", + "parameters": [ + { "name": "vorname", "in": "formData", "description": "vorname", "required": true, "type": "string" }, + { "name": "nachname", "in": "formData", "description": "nachname", "required": true, "type": "string" }, + { + "name": "geburtsdatum", + "in": "formData", + "description": "geburtsdatum", + "required": true, + "type": "string" + }, + { "name": "email", "in": "formData", "description": "email", "required": true, "type": "string" }, + { + "name": "emailWiederholung", + "in": "formData", + "description": "wiederholung", + "required": true, + "type": "string" + }, + { + "name": "registrierungCode", + "in": "formData", + "description": "registrierungscode", + "required": true, + "type": "string" + }, + { + "name": "kzEinverstaendnis1", + "in": "formData", + "description": "kzEinverstaendnis1", + "required": true, + "type": "string" + }, + { + "name": "kzEinverstaendnis2", + "in": "formData", + "description": "kzEinverstaendnis2", + "required": true, + "type": "string" + }, + { + "name": "kzEinverstaendnis3", + "in": "formData", + "description": "kzEinverstaendnis3", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "200": { "description": "successful operation", "schema": { "type": "boolean" } } } + } + }, + "/public_schuelerregistrierung": { + "post": { + "summary": "Zugangsdaten einpflegen", + "description": "", + "operationId": "insertZugangsdaten", + "parameters": [ + { "name": "vorname", "in": "formData", "description": "vorname", "required": true, "type": "string" }, + { "name": "nachname", "in": "formData", "description": "nachname", "required": true, "type": "string" }, + { + "name": "geburtsdatum", + "in": "formData", + "description": "geburtsdatum", + "required": true, + "type": "string" + }, + { "name": "email", "in": "formData", "description": "email", "required": true, "type": "string" }, + { + "name": "emailWiederholung", + "in": "formData", + "description": "wiederholung", + "required": true, + "type": "string" + }, + { + "name": "registrierungCode", + "in": "formData", + "description": "registrierungscode", + "required": true, + "type": "string" + }, + { + "name": "kzEinverstaendnis1", + "in": "formData", + "description": "kzEinverstaendnis1", + "required": true, + "type": "string" + }, + { + "name": "kzEinverstaendnis2", + "in": "formData", + "description": "kzEinverstaendnis2", + "required": true, + "type": "string" + }, + { + "name": "kzEinverstaendnis3", + "in": "formData", + "description": "kzEinverstaendnis3", + "required": true, + "type": "string" + }, + { + "name": "benutzername", + "in": "formData", + "description": "benutzername", + "required": true, + "type": "string" + }, + { "name": "kennwort", "in": "formData", "description": "kennwort", "required": true, "type": "string" }, + { + "name": "kennwortWiederholung", + "in": "formData", + "description": "kennwortWiederholung", + "required": true, + "type": "string" + }, + { + "name": "bestaetigungscode", + "in": "formData", + "description": "bestaetigungscode", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "200": { "description": "successful operation", "schema": { "type": "boolean" } } } + } + }, + "/schulverwaltung_schueler": { + "get": { + "summary": "liefert die Schüler", + "description": "", + "operationId": "getListSchueler_4", + "consumes": ["text/html"], + "produces": ["application/json"], + "parameters": [ + { + "name": "klasseId", + "in": "query", + "description": "Klasse ID eingeben", + "required": false, + "type": "string" + }, + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { + "name": "maxResults", + "in": "query", + "description": "Max zum anzeigen", + "required": false, + "type": "integer", + "format": "int32" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjSchueler" } } + } + } + } + }, + "/schulverwaltung_schueler/einladung": { + "get": { + "summary": "Einfügen eines Schülers", + "description": "", + "operationId": "getEinladung", + "produces": ["application/json"], + "parameters": [ + { + "name": "listEinladungId", + "in": "query", + "description": "einladungId", + "required": true, + "type": "array", + "items": { "type": "string" }, + "collectionFormat": "multi" + }, + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "klasseId", "in": "query", "description": "klasseId eingeben", "required": true, "type": "string" }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { "description": "successful operation", "schema": { "$ref": "#/definitions/DobjDatei" } } + } + }, + "post": { + "summary": "Einfügen eines Schülers", + "description": "", + "operationId": "createEinladung", + "produces": ["application/json"], + "parameters": [ + { + "name": "listSchuelerId", + "in": "query", + "description": "Liste der Schüler-IDs", + "required": true, + "type": "array", + "items": { "type": "string" }, + "collectionFormat": "multi" + }, + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { "description": "successful operation", "schema": { "type": "array", "items": { "type": "string" } } } + } + } + }, + "/schulverwaltung_schule/{schuleId}": { + "get": { + "summary": "liefert eine Schule", + "description": "", + "operationId": "getSchule", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "path", + "description": "ID der Schule eingeben", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { "description": "successful operation", "schema": { "$ref": "#/definitions/DobjSchule" } } + } + } + }, + "/schulverwaltung_schule": { + "get": { + "summary": "liefert Schulen", + "description": "", + "operationId": "getListSchule", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "ID der Schule eingeben", + "required": false, + "type": "string" + }, + { + "name": "schulenName", + "in": "query", + "description": "Name der Schule eingeben", + "required": false, + "type": "string" + }, + { + "name": "dienststellennummer", + "in": "query", + "description": "Dienststellennummer", + "required": false, + "type": "string" + }, + { "name": "schultyp", "in": "query", "description": "Schultyp", "required": false, "type": "string" }, + { "name": "plz", "in": "query", "description": "Plz", "required": false, "type": "string" }, + { "name": "ort", "in": "query", "description": "Ort", "required": false, "type": "string" }, + { "name": "hausnummer", "in": "query", "description": "Hausnummer", "required": false, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/DobjSchule" } } + } + } + } + }, + "/schulverwaltung_schulpersonal": { + "get": { + "tags": ["Schulpersonalverwaltung"], + "summary": "Liefert die Lehrer einer Schule", + "description": "", + "operationId": "getListSchulpersonal", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjSchulpersonal" } } + } + } + } + }, + "/schulverwaltung_schulpersonal/auswahlliste": { + "get": { + "tags": ["Schulpersonalverwaltung"], + "summary": "Liefert die Lehrer einer Schule in einer Auswahlliste", + "description": "", + "operationId": "getListAuswahl", + "produces": ["application/json"], + "parameters": [ + { + "name": "schuleId", + "in": "query", + "description": "Schule ID eingeben", + "required": true, + "type": "string" + }, + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { + "200": { + "description": "successful operation", + "schema": { "type": "array", "items": { "$ref": "#/definitions/RobjSchulpersonal" } } + } + } + } + }, + "/zugangsdaten_bearbeiten/current_user": { + "get": { + "summary": "Lifert den aktuellen Benutzername", + "description": "", + "operationId": "getCurrentUser_1", + "parameters": [ + { "name": "authToken", "in": "query", "description": "authToken", "required": true, "type": "string" } + ], + "responses": { "200": { "description": "successful operation", "schema": { "type": "string" } } } + } + } + }, + "definitions": { + "RobjStandortZuordnung": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "dienName1": { "type": "string" }, + "dienName2": { "type": "string" }, + "dienNr": { "type": "string" }, + "dienId": { "type": "string" }, + "orgUser": { "type": "string" }, + "orgXPts": { "type": "string" }, + "kzBevorzugt": { "type": "string" }, + "anschriftId": { "type": "string" }, + "anschriftUser": { "type": "string" }, + "anschriftXPts": { "type": "string" }, + "anschriftIdReferenz": { "type": "string" } + } + }, + "RobjDienststelle": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "gepa_id": { "type": "string" }, + "GEPA_NAME_1": { "type": "string" }, + "gepa_ortv_name": { "type": "string" }, + "gepa_strv_name": { "type": "string" }, + "anschriftId": { "type": "string" } + } + }, + "RobjPerson": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "orgUser": { "type": "string" }, + "orgXPts": { "type": "string" } + } + }, + "RobjBenutzergruppeZuordnung": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "name": { "type": "string" }, + "orgUser": { "type": "string" }, + "orgXPts": { "type": "string" } + } + }, + "RobjBenutzergruppe": { + "type": "object", + "properties": { "id": { "type": "string" }, "version": { "type": "string" }, "name": { "type": "string" } } + }, + "RobjKlasse": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "name": { "type": "string" }, + "schuleId": { "type": "string" }, + "klassenlehrerName": { "type": "string" }, + "listKlassenlehrer": { "type": "array", "items": { "$ref": "#/definitions/RobjKlassenlehrerZuordnung" } }, + "kzMeineKlasse": { "type": "string" } + } + }, + "RobjSchule": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "ROWS_TOTAL": { "type": "integer", "format": "int32" }, + "RESULTSET_ID": { "type": "string" }, + "schulnummer": { "type": "string" }, + "schulname": { "type": "string" }, + "schulartDecode": { "type": "string" }, + "ort": { "type": "string" }, + "strasse": { "type": "string" } + } + }, + "RobjBenutzerrolleZuordnung": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "tscAuthUID": { "type": "string" }, + "tscAuthUID_label": { "type": "string" }, + "dienststellennummer": { "type": "string" }, + "rolle": { "type": "string" }, + "dtGueltigAb": { "type": "string" }, + "dtGueltigBis": { "type": "string" }, + "kzEdit": { "type": "string" }, + "kzDelete": { "type": "string" } + } + }, + "DobjAuswahl": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "fieldLabel": { "type": "string" }, + "selectLabel": { "type": "string" } + } + }, + "RobjKommunikation": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "artCode": { "type": "string" }, + "artDecode": { "type": "string" }, + "verbindung": { "type": "string" }, + "kzBevorzugt": { "type": "string" }, + "orgUser": { "type": "string" }, + "orgXPts": { "type": "string" } + } + }, + "DobjZugangsdaten": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "loginId": { "type": "string" }, + "kzKennwortAbgelaufen": { "type": "string" }, + "kennwortGueltigBis": { "type": "string" }, + "zugangGueltigAb": { "type": "string" }, + "zugangGueltigBis": { "type": "string" } + } + }, + "DobjKopfzeilenInformationen": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "KopfSpalteLinksZeile1": { "type": "string" }, + "KopfSpalteLinksZeile2": { "type": "string" }, + "KopfSpalteLinksZeile3": { "type": "string" }, + "KopfSpalteRechtsZeile1": { "type": "string" }, + "KopfSpalteRechtsZeile2": { "type": "string" }, + "KopfSpalteRechtsZeile3": { "type": "string" } + } + }, + "RobjExamen": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "examenUid": { "type": "string" }, + "schuleId": { "type": "string" }, + "schuleName": { "type": "string" }, + "schuleNummer": { "type": "string" }, + "kursName": { "type": "string" }, + "kursId": { "type": "string" }, + "ileaPaketCode": { "type": "string" }, + "ileaPaketFachCode": { "type": "string" }, + "ileaPaketJahrgangsstufe": { "type": "string" }, + "dtExamenBeginn": { "type": "string" }, + "dtExamenEnde": { "type": "string" }, + "dtExamenErstelltAm": { "type": "string" }, + "dtExamenGeaendertAm": { "type": "string" } + } + }, + "DobjExamen": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "examenUID": { "type": "string" }, + "schuleId": { "type": "string" }, + "schuleName": { "type": "string" }, + "schuleNummer": { "type": "string" }, + "kursName": { "type": "string" }, + "kursId": { "type": "string" }, + "ileaPaketCode": { "type": "string" }, + "ileaPaketFachCode": { "type": "string" }, + "ileaPaketJahrgangsstufe": { "type": "string" }, + "dtExamenBeginn": { "type": "string" }, + "beginnDatum": { "type": "string" }, + "dtExamenEnde": { "type": "string" }, + "endeDatum": { "type": "string" }, + "dtErstelltAm": { "type": "string" }, + "erstelltDurch": { "type": "string" }, + "dtGeaendertAm": { "type": "string" }, + "geandertDurch": { "type": "string" }, + "listSchueler": { "type": "array", "items": { "$ref": "#/definitions/DobjTeilnehmer" } } + } + }, + "DobjTeilnehmer": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "teilnahmeCode": { "type": "string" }, + "schuelerId": { "type": "string" }, + "nachname": { "type": "string" }, + "vorname": { "type": "string" }, + "geburtsdatum": { "type": "string" }, + "checked": { "type": "boolean" }, + "isNew": { "type": "boolean" } + } + }, + "RobjSchueler": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "nachname": { "type": "string" }, + "vorname": { "type": "string" }, + "geburtsdatum": { "type": "string" }, + "email": { "type": "string" }, + "kzZugang": { "type": "string" }, + "klasseId": { "type": "string" }, + "klasseName": { "type": "string" }, + "schuleId": { "type": "string" }, + "benutzerName": { "type": "string" }, + "dtEinladungAblauf": { "type": "string" } + } + }, + "DobjDatei": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "dateiname": { "type": "string" }, + "base64Data": { "type": "string" }, + "mimetype": { "type": "string" } + } + }, + "IleaAufgabe": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "art": { "type": "string" }, + "displayId": { "type": "string" }, + "bezeichnung": { "type": "string" }, + "beschreibung": { "type": "string" } + } + }, + "DobjAufgabenpaketAuswahl": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "fieldLabel": { "type": "string" }, + "selectLabel": { "type": "string" }, + "jahrgangsstufe": { "type": "integer", "format": "int32" }, + "ileaFachId": { "type": "string" } + } + }, + "RobjHalbjahr": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "beschreibung": { "type": "string" }, + "dtBeginn": { "type": "string" }, + "dtEnde": { "type": "string" }, + "kzAktuell": { "type": "string" } + } + }, + "DobjKlasse": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "kzAktiv": { "type": "string" }, + "halbjahrId": { "type": "string" }, + "halbjahrBeschreibung": { "type": "string" }, + "name": { "type": "string" }, + "schuleId": { "type": "string" }, + "vorgaengerKlasseId": { "type": "string" }, + "kzSchuelerUebernehmen": { "type": "string" }, + "schuelerAnzahl": { "type": "string" }, + "listSchuelerUebernahme": { "type": "array", "items": { "type": "string" } } + } + }, + "RobjVorgaengerklasse": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "name": { "type": "string" }, + "halbjahrId": { "type": "string" } + } + }, + "RobjKodierung": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "code": { "type": "string" }, + "decode": { "type": "string" }, + "ext1": { "type": "string" } + } + }, + "DobjKlassenlehrerZuordnung": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "klasseId": { "type": "string" }, + "personalSchuleVerhaeltnisId": { "type": "string" }, + "lehrerName": { "type": "string" }, + "funktionsbezeichnungCode": { "type": "string" }, + "funktionsbezeichnungDecode": { "type": "string" } + } + }, + "RobjSchuelerzuordnung": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "schuelerId": { "type": "string" }, + "nachname": { "type": "string" }, + "vorname": { "type": "string" }, + "geburtsdatum": { "type": "string" }, + "email": { "type": "string" }, + "kzZugang": { "type": "string" }, + "klasseId": { "type": "string" }, + "zeitraum": { "type": "string" }, + "klasseName": { "type": "string" }, + "schuleId": { "type": "string" }, + "benutzerName": { "type": "string" }, + "dtEinladungAblauf": { "type": "string" } + } + }, + "RobjKlassenlehrerZuordnung": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "klasseId": { "type": "string" }, + "klassenlehrerId": { "type": "string" }, + "klassenlehrerName": { "type": "string" }, + "funktionsbezeichnungId": { "type": "string" }, + "funktionsbezeichnung": { "type": "string" } + } + }, + "DobjKurs": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "kzAktiv": { "type": "string" }, + "halbjahrId": { "type": "string" }, + "halbjahrBeschreibung": { "type": "string" }, + "name": { "type": "string" }, + "schuleId": { "type": "string" }, + "vorgaengerKursId": { "type": "string" }, + "klasseId": { "type": "string" }, + "klasseName": { "type": "string" }, + "fachId": { "type": "string" }, + "fachBezeichnung": { "type": "string" }, + "stufeId": { "type": "string" }, + "stufeBezeichnung": { "type": "string" }, + "kzSchuelerUebernehmen": { "type": "string" }, + "schuelerAnzahl": { "type": "string" }, + "listSchuelerUebernahme": { "type": "array", "items": { "type": "string" } } + } + }, + "RobjVorgaengerkurs": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "name": { "type": "string" }, + "halbjahrId": { "type": "string" } + } + }, + "DobjKurslehrerZuordnung": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "kursId": { "type": "string" }, + "personalSchuleVerhaeltnisId": { "type": "string" }, + "lehrerName": { "type": "string" }, + "erstelltVon": { "type": "string" } + } + }, + "DobjSchuelerzuordnung": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "schuleId": { "type": "string" }, + "klasseId": { "type": "string" }, + "kursId": { "type": "string" }, + "kurszuordnungId": { "type": "string" }, + "nachname": { "type": "string" }, + "vorname": { "type": "string" }, + "geburtsdatum": { "type": "string" }, + "email": { "type": "string" }, + "kzZugang": { "type": "string" } + } + }, + "RobjKurs": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "name": { "type": "string" }, + "schuleId": { "type": "string" }, + "halbjahrId": { "type": "string" }, + "halbjahrBeschreibung": { "type": "string" }, + "dtBeginn": { "type": "string" }, + "dtEnde": { "type": "string" }, + "schuelerAnzahl": { "type": "string" }, + "kzMeineKurs": { "type": "string" }, + "kzAktiv": { "type": "string" }, + "listKurslehrer": { "type": "array", "items": { "$ref": "#/definitions/RobjKurslehrerZuordnung" } } + } + }, + "RobjKurslehrerZuordnung": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "KursId": { "type": "string" }, + "KurslehrerId": { "type": "string" }, + "KurslehrerName": { "type": "string" }, + "funktionsbezeichnungId": { "type": "string" }, + "funktionsbezeichnung": { "type": "string" } + } + }, + "DobjSchueler": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "schuleId": { "type": "string" }, + "klasseId": { "type": "string" }, + "kursId": { "type": "string" }, + "nachname": { "type": "string" }, + "vorname": { "type": "string" }, + "geburtsdatum": { "type": "string" }, + "email": { "type": "string" }, + "kzZugang": { "type": "string" }, + "geandertDurch": { "type": "string" } + } + }, + "DobjSsoLink": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "tscAuthUID": { "type": "string" }, + "tscAuthUID_label": { "type": "string" } + } + }, + "RobjAufgabepaket": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "jahrgang": { "type": "string" }, + "ileaAufgabenpaket": { "type": "string" }, + "stufen": { "type": "string" }, + "dtErstelltAm": { "type": "string" }, + "erstelltVon": { "type": "string" } + } + }, + "RobjTooltip": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "komponente": { "type": "string" }, + "feld": { "type": "string" }, + "text": { "type": "string" } + } + }, + "DobjAuswahlGruppiert": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "parentId": { "type": "string" }, + "fieldLabel": { "type": "string" }, + "selectLabel": { "type": "string" }, + "ext1": { "type": "string" }, + "ext2": { "type": "string" }, + "hasFocus": { "type": "boolean" }, + "isSelected": { "type": "boolean" }, + "listChild": { "type": "array", "items": { "$ref": "#/definitions/DobjAuswahlGruppiert" } } + } + }, + "DobjSpaltenoption": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "fieldLabel": { "type": "string" }, + "selectLabel": { "type": "string" }, + "kzUntergruppe": { "type": "boolean" }, + "kzDefault": { "type": "boolean" } + } + }, + "BobjStandard": { "type": "object", "properties": { "id": { "type": "string" }, "version": { "type": "string" } } }, + "DobjStatistik": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "kopfSpalte1": { "type": "string" }, + "resultsetId": { "type": "string" }, + "resultsetId2": { "type": "string" }, + "auswertungstypGruppeCode": { "type": "string" }, + "fusszeile": { "type": "string" }, + "results": { "type": "array", "items": { "$ref": "#/definitions/BobjStandard" } } + } + }, + "RobjProtokolleintrag": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "logUID": { "type": "string" }, + "lognummer": { "type": "string" }, + "typCode": { "type": "string" }, + "typDecode": { "type": "string" }, + "schweregradCode": { "type": "string" }, + "schweregradDecode": { "type": "string" }, + "meldung": { "type": "string" }, + "server": { "type": "string" }, + "zeitstempel": { "type": "string" }, + "kategorie": { "type": "string" }, + "datenbank": { "type": "string" }, + "objekt": { "type": "string" }, + "ausloesenderBenutzer": { "type": "string" }, + "betroffenerBenutzer": { "type": "string" } + } + }, + "Value": { "type": "object", "properties": { "value": { "type": "string" } } }, + "DobjMediumdatei": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "mediumId": { "type": "string" }, + "pts": { "type": "string" }, + "dateiName": { "type": "string" }, + "mediumNummer": { "type": "string" }, + "oeffentlich": { "type": "string" }, + "pixiothek": { "type": "string" }, + "serientitel": { "type": "string" }, + "serienuntertitel": { "type": "string" }, + "einzeltitel": { "type": "string" }, + "einzeluntertitel": { "type": "string" }, + "anzahlLaufzeitFarben": { "type": "string" }, + "mediumStatusCode": { "type": "string" }, + "inhalt": { "type": "string" }, + "kurzinhalt": { "type": "string" }, + "fskVermerkCode": { "type": "string" }, + "fskVermerkDecode": { "type": "string" }, + "previewImageId": { "type": "string" }, + "downloadUrl": { "type": "string" }, + "previewImageUrl": { "type": "string" }, + "dateiBezeichnung": { "type": "string" }, + "dateiGroesse": { "type": "string" }, + "dateiVerwendungCode": { "type": "string" }, + "dateiSortierung": { "type": "string" }, + "dateiAufloesung": { "type": "string" }, + "listeStichwort": { "type": "array", "items": { "type": "string" } }, + "listeUrheber": { "type": "array", "items": { "type": "string" } } + } + }, + "Objekt zur Beantwortung von Berechtigungsanfragen": { + "type": "object", + "properties": { + "authToken": { + "type": "string", + "description": "Dient in erster Linie zur Verhinderung von Cross-Site-Request-Forgery.", + "readOnly": true + }, + "listTechObj": { + "type": "array", + "description": "Beinhaltet die Liste aller technischen Objekte für die der Anfrager berechtigt ist und welche zu dem angegebenen prefix passen.", + "items": { "type": "string" } + }, + "listSystemeinstellung": { + "type": "object", + "description": "Beinhaltet die Liste aller relevanten syeis für den Prefix", + "additionalProperties": { "type": "string" } + }, + "listToolTipKey": { + "type": "array", + "description": "Beinhaltet die Liste aller relevanten listToolTipKeys für den Prefix", + "items": { "type": "string" } + } + } + }, + "Fehler": { + "type": "object", + "properties": { + "reference": { + "type": "string", + "description": "Welchen Aspekt / welchen Bereich / welche Unterliste ist betroffen?" + }, + "fieldName": { + "type": "string", + "description": "Feldname, falls ein konkretes (Eingabe)feld betroffen/ursächlich ist." + }, + "errorText": { "type": "string", "description": "Der, dem Nutzer anzuzeigende Fehlertext." }, + "errorSource": { + "type": "string", + "description": "Technische Zusatzinformationen, StackTrace, Prozedurname, Statement etc. insofern keine sicherheitsrelevanten Informationen preisgegeben würden." + } + } + }, + "Fehlerantwort": { + "type": "object", + "properties": { "listError": { "type": "array", "items": { "$ref": "#/definitions/Fehler" } } } + }, + "RobjExportKlasse": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "klasseId": { "type": "string" }, + "klasseName": { "type": "string" }, + "schuleNummer": { "type": "string" }, + "lehrerUid": { "type": "string" } + } + }, + "RobjExportLehrerMigration": { + "type": "object", + "properties": { "lehrerUidAlt": { "type": "string" }, "lehrerUidNeu": { "type": "string" } } + }, + "RobjExportLehrer": { + "type": "object", + "properties": { + "lehrerUid": { "type": "string" }, + "lehrerTitel": { "type": "string" }, + "lehrerVorname": { "type": "string" }, + "lehrerNachname": { "type": "string" }, + "schuleNummer": { "type": "string" } + } + }, + "RobjExportSchuelerMigration": { + "type": "object", + "properties": { "schuelerUidAlt": { "type": "string" }, "schuelerUidNeu": { "type": "string" } } + }, + "RobjExportSchueler": { + "type": "object", + "properties": { + "schuelerUid": { "type": "string" }, + "schuelerVorname": { "type": "string" }, + "schuelerNachname": { "type": "string" }, + "schuleNummer": { "type": "string" }, + "klasseId": { "type": "string" } + } + }, + "RobjExportSchule": { + "type": "object", + "properties": { "schuleNummer": { "type": "string" }, "schuleName": { "type": "string" } } + }, + "VersionResponse": { "type": "object", "properties": { "version": { "type": "string" } } }, + "DobjSchule": { + "type": "object", + "properties": { + "schuleId": { "type": "string" }, + "name": { "type": "string" }, + "dienststellenNummer": { "type": "string" }, + "schultyp": { "type": "string" }, + "plz": { "type": "string" }, + "ort": { "type": "string" }, + "hausnummer": { "type": "string" } + } + }, + "RobjSchulpersonal": { + "type": "object", + "properties": { + "id": { "type": "string" }, + "version": { "type": "string" }, + "code": { "type": "string" }, + "decode": { "type": "string" }, + "ext1": { "type": "string" }, + "nachname": { "type": "string" }, + "vorname": { "type": "string" }, + "titel": { "type": "string" }, + "schuleId": { "type": "string" } + } + } + } +} diff --git a/apps/server/src/infra/tsp-client/tsp-client-config.ts b/apps/server/src/infra/tsp-client/tsp-client-config.ts index 95c1da4131b..1ca78959cbe 100644 --- a/apps/server/src/infra/tsp-client/tsp-client-config.ts +++ b/apps/server/src/infra/tsp-client/tsp-client-config.ts @@ -1,9 +1,4 @@ -export interface TspRestClientConfig { - SC_DOMAIN: string; - HOST: string; +export interface TspClientConfig { TSP_API_BASE_URL: string; - TSP_API_CLIENT_ID: string; - TSP_API_CLIENT_SECRET: string; TSP_API_TOKEN_LIFETIME_MS: number; - TSP_API_SIGNATURE_KEY: string; } diff --git a/apps/server/src/infra/tsp-client/tsp-client-factory.integration.spec.ts b/apps/server/src/infra/tsp-client/tsp-client-factory.integration.spec.ts new file mode 100644 index 00000000000..90d798ea650 --- /dev/null +++ b/apps/server/src/infra/tsp-client/tsp-client-factory.integration.spec.ts @@ -0,0 +1,72 @@ +import { createMock } from '@golevelup/ts-jest'; +import { ServerTestModule } from '@modules/server'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { TspClientFactory } from './tsp-client-factory'; +import { TspClientModule } from './tsp-client.module'; + +// NOTE: This test is skipped because it requires a valid client id, secret and token endpoint. +// It is meant to be used for manual testing only. +describe('TspClientFactory Integration', () => { + let module: TestingModule; + let sut: TspClientFactory; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ServerTestModule, TspClientModule], + }) + .overrideProvider(ConfigService) + .useValue( + createMock({ + getOrThrow: (key: string) => { + switch (key) { + case 'TSP_API_BASE_URL': + return 'https://test2.schulportal-thueringen.de/tip-ms/api'; + case 'TSP_API_TOKEN_LIFETIME_MS': + return 30_000; + default: + throw new Error(`Unknown key: ${key}`); + } + }, + }) + ) + .compile(); + + sut = module.get(TspClientFactory); + }); + + afterAll(async () => { + await module.close(); + }); + + it('should be defined', () => { + expect(sut).toBeDefined(); + }); + + describe.skip('when requesting the version', () => { + const setup = () => { + // The client id, secret and token endpoint can be found in 1Password, + // search for "test2 test" + const api = sut.createExportClient({ + clientId: '', + clientSecret: '', + tokenEndpoint: '', + }); + + return { api }; + }; + + it( + 'should return the version', + async () => { + const { api } = setup(); + + const result = await api.version(); + + expect(result.status).toEqual(200); + expect(result.data.version).toEqual('1.1'); + }, + 10 * 1000 + ); + }); +}); diff --git a/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts b/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts index 45c7c13072a..2efc2437934 100644 --- a/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts +++ b/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts @@ -1,36 +1,33 @@ import { faker } from '@faker-js/faker'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { OauthAdapterService } from '@modules/oauth'; +import { ServerConfig } from '@modules/server'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; -import { ServerConfig } from '@src/modules/server'; +import axios from 'axios'; import { TspClientFactory } from './tsp-client-factory'; describe('TspClientFactory', () => { let module: TestingModule; let sut: TspClientFactory; let configServiceMock: DeepMocked>; + let oauthAdapterServiceMock: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ providers: [ TspClientFactory, + { + provide: OauthAdapterService, + useValue: createMock(), + }, { provide: ConfigService, useValue: createMock>({ getOrThrow: (key: string) => { switch (key) { - case 'SC_DOMAIN': - return faker.internet.domainName(); - case 'HOST': - return faker.internet.url(); case 'TSP_API_BASE_URL': - return 'https://test2.schulportal-thueringen.de/tip-ms/api/'; - case 'TSP_API_CLIENT_ID': - return faker.string.uuid(); - case 'TSP_API_CLIENT_SECRET': - return faker.string.uuid(); - case 'TSP_API_SIGNATURE_KEY': - return faker.string.uuid(); + return faker.internet.url(); case 'TSP_API_TOKEN_LIFETIME_MS': return faker.number.int(); default: @@ -44,6 +41,7 @@ describe('TspClientFactory', () => { sut = module.get(TspClientFactory); configServiceMock = module.get(ConfigService); + oauthAdapterServiceMock = module.get(OauthAdapterService); }); afterAll(async () => { @@ -61,7 +59,11 @@ describe('TspClientFactory', () => { describe('createExportClient', () => { describe('when createExportClient is called', () => { it('should return ExportApiInterface', () => { - const result = sut.createExportClient(); + const result = sut.createExportClient({ + clientId: faker.string.alpha(), + clientSecret: faker.string.alpha(), + tokenEndpoint: faker.internet.url(), + }); expect(result).toBeDefined(); expect(configServiceMock.getOrThrow).toHaveBeenCalledTimes(0); @@ -70,8 +72,13 @@ describe('TspClientFactory', () => { describe('when token is cached', () => { const setup = () => { + const client = sut.createExportClient({ + clientId: faker.string.alpha(), + clientSecret: faker.string.alpha(), + tokenEndpoint: faker.internet.url(), + }); + Reflect.set(sut, 'cachedToken', faker.string.alpha()); - const client = sut.createExportClient(); return client; }; @@ -85,21 +92,73 @@ describe('TspClientFactory', () => { }); }); - // TODO: add a working integration test - describe.skip('when using the created client', () => { + describe('getAccessToken', () => { const setup = () => { - const client = sut.createExportClient(); + const clientId = faker.string.alpha(); + const clientSecret = faker.string.alpha(); + const tokenEndpoint = faker.internet.url(); + + oauthAdapterServiceMock.sendTokenRequest.mockResolvedValue({ + accessToken: faker.string.alpha(), + idToken: faker.string.alpha(), + refreshToken: faker.string.alpha(), + }); - return client; + return { + clientId, + clientSecret, + tokenEndpoint, + }; + }; + + it('should return access token', async () => { + const params = setup(); + + const response = await sut.getAccessToken(params); + + expect(response).toBeDefined(); + expect(configServiceMock.getOrThrow).toHaveBeenCalledTimes(0); + }); + }); + + describe('when using the created client', () => { + const setup = () => { + const client = sut.createExportClient({ + clientId: '', + clientSecret: '', + tokenEndpoint: '', + }); + + jest.mock('axios'); + + oauthAdapterServiceMock.sendTokenRequest.mockResolvedValue({ + accessToken: faker.string.alpha(), + idToken: faker.string.alpha(), + refreshToken: faker.string.alpha(), + }); + + const axiosMock = axios as jest.Mocked; + + axiosMock.request = jest.fn(); + axiosMock.request.mockResolvedValue({ + data: { + version: '1.1', + }, + }); + + return { + client, + axiosMock, + }; }; it('should return the migration version', async () => { - const client = setup(); + const { client, axiosMock } = setup(); - const result = await client.version(); + const response = await client.version(); - expect(result.status).toBe(200); - expect(result.data.version).toBeDefined(); + expect(axiosMock.request).toHaveBeenCalledTimes(1); + expect(response.data.version).toBe('1.1'); }); }); }); diff --git a/apps/server/src/infra/tsp-client/tsp-client-factory.ts b/apps/server/src/infra/tsp-client/tsp-client-factory.ts index 835cd8e333d..b4d54208269 100644 --- a/apps/server/src/infra/tsp-client/tsp-client-factory.ts +++ b/apps/server/src/infra/tsp-client/tsp-client-factory.ts @@ -1,44 +1,42 @@ import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { randomUUID } from 'crypto'; +import { OauthAdapterService } from '@src/modules/oauth'; +import { OAuthGrantType } from '@src/modules/oauth/interface/oauth-grant-type.enum'; +import { ClientCredentialsGrantTokenRequest } from '@src/modules/oauth/service/dto'; import * as jwt from 'jsonwebtoken'; import { Configuration, ExportApiFactory, ExportApiInterface } from './generated'; -import { TspRestClientConfig } from './tsp-client-config'; +import { TspClientConfig } from './tsp-client-config'; + +type FactoryParams = { + clientId: string; + clientSecret: string; + tokenEndpoint: string; +}; @Injectable() export class TspClientFactory { - private readonly domain: string; - - private readonly host: string; - private readonly baseUrl: string; - private readonly clientId: string; - - private readonly clientSecret: string; - - private readonly signingKey: string; - private readonly tokenLifetime: number; private cachedToken: string | undefined; private tokenExpiresAt: number | undefined; - constructor(configService: ConfigService) { - this.domain = configService.getOrThrow('SC_DOMAIN'); - this.host = configService.getOrThrow('HOST'); + constructor( + private readonly oauthAdapterService: OauthAdapterService, + configService: ConfigService + ) { this.baseUrl = configService.getOrThrow('TSP_API_BASE_URL'); - this.clientId = configService.getOrThrow('TSP_API_CLIENT_ID'); - this.clientSecret = configService.getOrThrow('TSP_API_CLIENT_SECRET'); - this.signingKey = configService.getOrThrow('TSP_API_SIGNATURE_KEY'); this.tokenLifetime = configService.getOrThrow('TSP_API_TOKEN_LIFETIME_MS'); } - public createExportClient(): ExportApiInterface { + public createExportClient(params: FactoryParams): ExportApiInterface { const factory = ExportApiFactory( new Configuration({ - accessToken: this.createJwt(), + // accessToken has to be a function otherwise it will be called once + // and will not be refresh the access token when it expires + apiKey: async () => this.getAccessToken(params), basePath: this.baseUrl, }) ); @@ -46,28 +44,32 @@ export class TspClientFactory { return factory; } - private createJwt(): string { + public async getAccessToken(params: FactoryParams): Promise { const now = Date.now(); if (this.cachedToken && this.tokenExpiresAt && this.tokenExpiresAt > now) { return this.cachedToken; } - this.tokenExpiresAt = now + this.tokenLifetime; + const payload = new ClientCredentialsGrantTokenRequest({ + client_id: params.clientId, + client_secret: params.clientSecret, + grant_type: OAuthGrantType.CLIENT_CREDENTIALS_GRANT, + }); + + const response = await this.oauthAdapterService.sendTokenRequest(params.tokenEndpoint, payload); - const payload = { - apiClientId: this.clientId, - apiClientSecret: this.clientSecret, - iss: this.domain, - aud: this.baseUrl, - sub: this.host, - exp: this.tokenExpiresAt, - iat: this.tokenExpiresAt - this.tokenLifetime, - jti: randomUUID(), - }; + this.cachedToken = response.accessToken; + this.tokenExpiresAt = this.getExpiresAt(now, response.accessToken); + + // We need the Bearer prefix for the generated client, because OAS 2 does not support Bearer token type + return `Bearer ${this.cachedToken}`; + } - this.cachedToken = jwt.sign(payload, this.signingKey); + private getExpiresAt(now: number, token: string): number { + const decoded = jwt.decode(token, { json: true }); + const expiresAt = decoded?.exp || now + this.tokenLifetime; - return this.cachedToken; + return expiresAt; } } diff --git a/apps/server/src/infra/tsp-client/tsp-client.module.ts b/apps/server/src/infra/tsp-client/tsp-client.module.ts index c5b459df3f8..b7dd40df24d 100644 --- a/apps/server/src/infra/tsp-client/tsp-client.module.ts +++ b/apps/server/src/infra/tsp-client/tsp-client.module.ts @@ -1,7 +1,9 @@ +import { OauthModule } from '@modules/oauth'; import { Module } from '@nestjs/common'; import { TspClientFactory } from './tsp-client-factory'; @Module({ + imports: [OauthModule], providers: [TspClientFactory], exports: [TspClientFactory], }) diff --git a/apps/server/src/modules/oauth/index.ts b/apps/server/src/modules/oauth/index.ts index 9cd1d26e142..72bfafa518b 100644 --- a/apps/server/src/modules/oauth/index.ts +++ b/apps/server/src/modules/oauth/index.ts @@ -1,2 +1,3 @@ export * from './interface'; +export * from './oauth.module'; export * from './service'; diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index cd7ef5329fa..ca5aa6f9d02 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -3,6 +3,7 @@ import { XApiKeyConfig } from '@infra/auth-guard'; import type { IdentityManagementConfig } from '@infra/identity-management'; import type { MailConfig } from '@infra/mail/interfaces/mail-config'; import type { SchulconnexClientConfig } from '@infra/schulconnex-client'; +import type { TspClientConfig } from '@infra/tsp-client'; import type { AccountConfig } from '@modules/account'; import { AlertConfig } from '@modules/alert'; import type { AuthenticationConfig } from '@modules/authentication'; @@ -18,18 +19,17 @@ import { ProvisioningConfig } from '@modules/provisioning'; import { RoomConfig } from '@modules/room'; import type { SchoolConfig } from '@modules/school'; import type { SharingConfig } from '@modules/sharing'; +import type { ShdConfig } from '@modules/shd'; import { getTldrawClientConfig, type TldrawClientConfig } from '@modules/tldraw-client'; import type { ToolConfig } from '@modules/tool'; import type { UserConfig } from '@modules/user'; import type { UserImportConfig } from '@modules/user-import'; import type { UserLoginMigrationConfig } from '@modules/user-login-migration'; import type { VideoConferenceConfig } from '@modules/video-conference'; +import type { BbbConfig } from '@modules/video-conference/bbb'; import type { LanguageType } from '@shared/domain/interface'; import type { SchulcloudTheme } from '@shared/domain/types'; import type { CoreModuleConfig } from '@src/core'; -import { TspRestClientConfig } from '@src/infra/tsp-client/tsp-client-config'; -import type { ShdConfig } from '@modules/shd'; -import type { BbbConfig } from '@modules/video-conference/bbb'; import type { Timezone } from './types/timezone.enum'; export enum NodeEnvType { @@ -69,7 +69,7 @@ export interface ServerConfig UserImportConfig, VideoConferenceConfig, BbbConfig, - TspRestClientConfig, + TspClientConfig, AlertConfig, ShdConfig { NODE_ENV: NodeEnvType; @@ -309,10 +309,7 @@ const config: ServerConfig = { FEATURE_AI_TUTOR_ENABLED: Configuration.get('FEATURE_AI_TUTOR_ENABLED') as boolean, FEATURE_ROOMS_ENABLED: Configuration.get('FEATURE_ROOMS_ENABLED') as boolean, TSP_API_BASE_URL: Configuration.get('TSP_API_BASE_URL') as string, - TSP_API_CLIENT_ID: Configuration.get('TSP_API_CLIENT_ID') as string, - TSP_API_CLIENT_SECRET: Configuration.get('TSP_API_CLIENT_SECRET') as string, TSP_API_TOKEN_LIFETIME_MS: Configuration.get('TSP_API_TOKEN_LIFETIME_MS') as number, - TSP_API_SIGNATURE_KEY: Configuration.get('TSP_API_SIGNATURE_KEY') as string, ROCKET_CHAT_URI: Configuration.get('ROCKET_CHAT_URI') as string, ROCKET_CHAT_ADMIN_ID: Configuration.get('ROCKET_CHAT_ADMIN_ID') as string, ROCKET_CHAT_ADMIN_TOKEN: Configuration.get('ROCKET_CHAT_ADMIN_TOKEN') as string, diff --git a/openapitools.json b/openapitools.json index 01614e969dc..bd567305968 100644 --- a/openapitools.json +++ b/openapitools.json @@ -6,7 +6,7 @@ "generators": { "tsp-api": { "generatorName": "typescript-axios", - "inputSpec": "https://test2.schulportal-thueringen.de/tip-ms/api/swagger.json", + "inputSpec": "./apps/server/src/infra/tsp-client/openapi.json", "output": "./apps/server/src/infra/tsp-client/generated", "skipValidateSpec": true, "enablePostProcessFile": true, From ac4f35d0a6c90a1bb1166edc23948f272bec5dd3 Mon Sep 17 00:00:00 2001 From: Steliyan Dinkov <133751031+sdinkov@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:08:24 +0200 Subject: [PATCH 05/10] N21 2198 media shelf extend database for ctl metadata (#5272) * update media source model --- .../entity/external-tool.entity.ts | 1 - .../domain/media-source-oauth-config.ts | 17 ++++ .../user-license/domain/media-source.ts | 18 ++++- .../src/modules/user-license/entity/index.ts | 4 +- .../media-source-oauth-config.embeddable.ts | 41 ++++++++++ .../entity/media-source.entity.ts | 26 ++++-- .../entity/media-user-license.entity.ts | 2 +- .../entity/user-license.entity.ts | 2 +- .../src/modules/user-license/enum/index.ts | 3 + .../enum/media-source-auth-method.enum.ts | 3 + .../enum/media-source-data-format.enum.ts | 3 + .../{entity => enum}/user-license-type.ts | 0 apps/server/src/modules/user-license/index.ts | 13 +-- .../src/modules/user-license/repo/index.ts | 3 +- .../repo/media-source-config.mapper.spec.ts | 79 +++++++++++++++++++ .../repo/media-source-config.mapper.ts | 25 ++++++ .../user-license/repo/media-source.mapper.ts | 5 ++ .../repo/media-source.repo.spec.ts | 12 ++- .../repo/media-user-license.repo.spec.ts | 11 ++- .../repo/media-user-license.repo.ts | 3 + .../media-source-config.embeddable.factory.ts | 20 +++++ .../testing/media-source-config.factory.ts | 19 +++++ .../testing/media-source-entity.factory.ts | 5 +- .../testing/media-source.factory.ts | 4 + 24 files changed, 295 insertions(+), 24 deletions(-) create mode 100644 apps/server/src/modules/user-license/domain/media-source-oauth-config.ts create mode 100644 apps/server/src/modules/user-license/entity/media-source-oauth-config.embeddable.ts create mode 100644 apps/server/src/modules/user-license/enum/index.ts create mode 100644 apps/server/src/modules/user-license/enum/media-source-auth-method.enum.ts create mode 100644 apps/server/src/modules/user-license/enum/media-source-data-format.enum.ts rename apps/server/src/modules/user-license/{entity => enum}/user-license-type.ts (100%) create mode 100644 apps/server/src/modules/user-license/repo/media-source-config.mapper.spec.ts create mode 100644 apps/server/src/modules/user-license/repo/media-source-config.mapper.ts create mode 100644 apps/server/src/modules/user-license/testing/media-source-config.embeddable.factory.ts create mode 100644 apps/server/src/modules/user-license/testing/media-source-config.factory.ts diff --git a/apps/server/src/modules/tool/external-tool/entity/external-tool.entity.ts b/apps/server/src/modules/tool/external-tool/entity/external-tool.entity.ts index d73a57b54f4..e5a6fbbf6f8 100644 --- a/apps/server/src/modules/tool/external-tool/entity/external-tool.entity.ts +++ b/apps/server/src/modules/tool/external-tool/entity/external-tool.entity.ts @@ -1,5 +1,4 @@ import { Embedded, Entity, Property, Unique } from '@mikro-orm/core'; - import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; import { EntityId } from '@shared/domain/types'; import { ToolContextType } from '../../common/enum'; diff --git a/apps/server/src/modules/user-license/domain/media-source-oauth-config.ts b/apps/server/src/modules/user-license/domain/media-source-oauth-config.ts new file mode 100644 index 00000000000..7846c782191 --- /dev/null +++ b/apps/server/src/modules/user-license/domain/media-source-oauth-config.ts @@ -0,0 +1,17 @@ +import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; +import { EntityId } from '@shared/domain/types'; +import { MediaSourceAuthMethod } from '../enum'; + +export interface MediaSourceOauthConfigProps extends AuthorizableObject { + id: EntityId; + + clientId: string; + + clientSecret: string; + + authEndpoint: string; + + method: MediaSourceAuthMethod; +} + +export class MediaSourceOauthConfig extends DomainObject {} diff --git a/apps/server/src/modules/user-license/domain/media-source.ts b/apps/server/src/modules/user-license/domain/media-source.ts index 7759d36bb4e..62147d86d0a 100644 --- a/apps/server/src/modules/user-license/domain/media-source.ts +++ b/apps/server/src/modules/user-license/domain/media-source.ts @@ -1,11 +1,17 @@ -import { DomainObject } from '@shared/domain/domain-object'; +import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; +import { MediaSourceOauthConfig } from './media-source-oauth-config'; +import { MediaSourceDataFormat } from '../enum'; -export interface MediaSourceProps { +export interface MediaSourceProps extends AuthorizableObject { id: string; name?: string; sourceId: string; + + format?: MediaSourceDataFormat; + + config?: MediaSourceOauthConfig; } export class MediaSource extends DomainObject { @@ -16,4 +22,12 @@ export class MediaSource extends DomainObject { get sourceId(): string { return this.props.sourceId; } + + get format(): MediaSourceDataFormat | undefined { + return this.props.format; + } + + get config(): MediaSourceOauthConfig | undefined { + return this.props.config; + } } diff --git a/apps/server/src/modules/user-license/entity/index.ts b/apps/server/src/modules/user-license/entity/index.ts index 7b7925040ed..7343a42f18b 100644 --- a/apps/server/src/modules/user-license/entity/index.ts +++ b/apps/server/src/modules/user-license/entity/index.ts @@ -1,4 +1,6 @@ -export { UserLicenseType } from './user-license-type'; +export { UserLicenseType } from '../enum/user-license-type'; export { MediaUserLicenseEntity, MediaUserLicenseEntityProps } from './media-user-license.entity'; export { UserLicenseEntity } from './user-license.entity'; export { MediaSourceEntity, MediaSourceEntityProps } from './media-source.entity'; +export { MediaSourceDataFormat } from '../enum/media-source-data-format.enum'; +export { MediaSourceAuthMethod } from '../enum/media-source-auth-method.enum'; diff --git a/apps/server/src/modules/user-license/entity/media-source-oauth-config.embeddable.ts b/apps/server/src/modules/user-license/entity/media-source-oauth-config.embeddable.ts new file mode 100644 index 00000000000..a236c308865 --- /dev/null +++ b/apps/server/src/modules/user-license/entity/media-source-oauth-config.embeddable.ts @@ -0,0 +1,41 @@ +import { Embeddable, Enum, Property } from '@mikro-orm/core'; +import { ObjectId } from '@mikro-orm/mongodb'; +import { MediaSourceAuthMethod } from '../enum/media-source-auth-method.enum'; + +export interface MediaSourceConfigEmbeddableProps { + _id: ObjectId; + + clientId: string; + + clientSecret: string; + + authEndpoint: string; + + method: MediaSourceAuthMethod; +} + +@Embeddable() +export class MediaSourceConfigEmbeddable { + @Property() + _id: ObjectId; + + @Property() + clientId: string; + + @Property() + clientSecret: string; + + @Property() + authEndpoint: string; + + @Enum({ nullable: false }) + method: MediaSourceAuthMethod; + + constructor(props: MediaSourceConfigEmbeddableProps) { + this._id = props._id; + this.clientId = props.clientId; + this.clientSecret = props.clientSecret; + this.authEndpoint = props.authEndpoint; + this.method = props.method; + } +} diff --git a/apps/server/src/modules/user-license/entity/media-source.entity.ts b/apps/server/src/modules/user-license/entity/media-source.entity.ts index 57ee3b55cfa..bd3f9da8b79 100644 --- a/apps/server/src/modules/user-license/entity/media-source.entity.ts +++ b/apps/server/src/modules/user-license/entity/media-source.entity.ts @@ -1,6 +1,8 @@ -import { Entity, Index, Property } from '@mikro-orm/core'; +import { Embedded, Entity, Index, Property } from '@mikro-orm/core'; import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; import { EntityId } from '@shared/domain/types'; +import { MediaSourceDataFormat } from '../enum/media-source-data-format.enum'; +import { MediaSourceConfigEmbeddable } from './media-source-oauth-config.embeddable'; export interface MediaSourceEntityProps { id?: EntityId; @@ -8,23 +10,35 @@ export interface MediaSourceEntityProps { name?: string; sourceId: string; + + config?: MediaSourceConfigEmbeddable; + + format?: MediaSourceDataFormat; } @Entity({ tableName: 'media-sources' }) export class MediaSourceEntity extends BaseEntityWithTimestamps { constructor(props: MediaSourceEntityProps) { super(); - if (props.id != null) { + if (props.id) { this.id = props.id; } - this.name = props.name; this.sourceId = props.sourceId; + this.name = props.name; + this.format = props.format; + this.config = props.config; } - @Property({ nullable: true }) - name?: string; - @Index() @Property() sourceId: string; + + @Property({ nullable: true }) + name?: string; + + @Property({ nullable: true }) + format?: MediaSourceDataFormat; + + @Embedded(() => MediaSourceConfigEmbeddable, { object: true, nullable: true }) + config?: MediaSourceConfigEmbeddable; } diff --git a/apps/server/src/modules/user-license/entity/media-user-license.entity.ts b/apps/server/src/modules/user-license/entity/media-user-license.entity.ts index 4cc501c4975..ded564bdce3 100644 --- a/apps/server/src/modules/user-license/entity/media-user-license.entity.ts +++ b/apps/server/src/modules/user-license/entity/media-user-license.entity.ts @@ -1,6 +1,6 @@ import { Entity, ManyToOne, Property } from '@mikro-orm/core'; import { MediaSourceEntity } from './media-source.entity'; -import { UserLicenseType } from './user-license-type'; +import { UserLicenseType } from '../enum/user-license-type'; import { UserLicenseEntity, UserLicenseProps } from './user-license.entity'; export interface MediaUserLicenseEntityProps extends UserLicenseProps { diff --git a/apps/server/src/modules/user-license/entity/user-license.entity.ts b/apps/server/src/modules/user-license/entity/user-license.entity.ts index 1e3ddcd7b71..0d8948a1126 100644 --- a/apps/server/src/modules/user-license/entity/user-license.entity.ts +++ b/apps/server/src/modules/user-license/entity/user-license.entity.ts @@ -2,7 +2,7 @@ import { Entity, Enum, Index, ManyToOne } from '@mikro-orm/core'; import { BaseEntityWithTimestamps } from '@shared/domain/entity/base.entity'; import { User as UserEntity } from '@shared/domain/entity/user.entity'; import { EntityId } from '@shared/domain/types'; -import { UserLicenseType } from './user-license-type'; +import { UserLicenseType } from '../enum/user-license-type'; export interface UserLicenseProps { id?: EntityId; diff --git a/apps/server/src/modules/user-license/enum/index.ts b/apps/server/src/modules/user-license/enum/index.ts new file mode 100644 index 00000000000..66d96a9a346 --- /dev/null +++ b/apps/server/src/modules/user-license/enum/index.ts @@ -0,0 +1,3 @@ +export { UserLicenseType } from './user-license-type'; +export { MediaSourceDataFormat } from './media-source-data-format.enum'; +export { MediaSourceAuthMethod } from './media-source-auth-method.enum'; diff --git a/apps/server/src/modules/user-license/enum/media-source-auth-method.enum.ts b/apps/server/src/modules/user-license/enum/media-source-auth-method.enum.ts new file mode 100644 index 00000000000..5f839c307c2 --- /dev/null +++ b/apps/server/src/modules/user-license/enum/media-source-auth-method.enum.ts @@ -0,0 +1,3 @@ +export enum MediaSourceAuthMethod { + CLIENT_CREDENTIALS = 'CLIENT_CREDENTIALS', +} diff --git a/apps/server/src/modules/user-license/enum/media-source-data-format.enum.ts b/apps/server/src/modules/user-license/enum/media-source-data-format.enum.ts new file mode 100644 index 00000000000..98bfa8f4b73 --- /dev/null +++ b/apps/server/src/modules/user-license/enum/media-source-data-format.enum.ts @@ -0,0 +1,3 @@ +export enum MediaSourceDataFormat { + BILDUNGSLOGIN = 'BILDUNGSLOGIN', +} diff --git a/apps/server/src/modules/user-license/entity/user-license-type.ts b/apps/server/src/modules/user-license/enum/user-license-type.ts similarity index 100% rename from apps/server/src/modules/user-license/entity/user-license-type.ts rename to apps/server/src/modules/user-license/enum/user-license-type.ts diff --git a/apps/server/src/modules/user-license/index.ts b/apps/server/src/modules/user-license/index.ts index ebb28ca5a46..7a37a14200c 100644 --- a/apps/server/src/modules/user-license/index.ts +++ b/apps/server/src/modules/user-license/index.ts @@ -1,10 +1,11 @@ -export { UserLicenseModule } from './user-license.module'; -export { MediaUserLicenseService, MediaSourceService } from './service'; -export { MediaUserLicense, MediaSource, MediaUserLicenseProps, MediaSourceProps, AnyUserLicense } from './domain'; -export { UserLicenseType } from './entity/user-license-type'; +export { AnyUserLicense, MediaSource, MediaSourceProps, MediaUserLicense, MediaUserLicenseProps } from './domain'; +export { MediaSourceEntity } from './entity'; +export { UserLicenseType } from './enum/user-license-type'; +export { MediaSourceService, MediaUserLicenseService } from './service'; export { - mediaUserLicenseFactory, - mediaSourceFactory, mediaSourceEntityFactory, + mediaSourceFactory, mediaUserLicenseEntityFactory, + mediaUserLicenseFactory, } from './testing'; +export { UserLicenseModule } from './user-license.module'; diff --git a/apps/server/src/modules/user-license/repo/index.ts b/apps/server/src/modules/user-license/repo/index.ts index 102b731f497..0aed70e9853 100644 --- a/apps/server/src/modules/user-license/repo/index.ts +++ b/apps/server/src/modules/user-license/repo/index.ts @@ -1,3 +1,4 @@ +export { MediaSourceConfigMapper } from './media-source-config.mapper'; +export { MediaSourceRepo } from './media-source.repo'; export { MediaUserLicenseRepo } from './media-user-license.repo'; export { UserLicenseQuery } from './user-license-query'; -export { MediaSourceRepo } from './media-source.repo'; diff --git a/apps/server/src/modules/user-license/repo/media-source-config.mapper.spec.ts b/apps/server/src/modules/user-license/repo/media-source-config.mapper.spec.ts new file mode 100644 index 00000000000..0a73a28f018 --- /dev/null +++ b/apps/server/src/modules/user-license/repo/media-source-config.mapper.spec.ts @@ -0,0 +1,79 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { setupEntities } from '@shared/testing'; +import { MediaSourceOauthConfig } from '../domain/media-source-oauth-config'; +import { MediaSourceConfigEmbeddable } from '../entity/media-source-oauth-config.embeddable'; +import { mediaSourceConfigEmbeddableFactory } from '../testing/media-source-config.embeddable.factory'; +import { mediaSourceConfigFactory } from '../testing/media-source-config.factory'; +import { MediaSourceConfigMapper } from './media-source-config.mapper'; + +describe('MediaSourceConfigMapper', () => { + describe('mapToDo', () => { + describe('when entity is passed', () => { + const setup = async () => { + await setupEntities(); + + const entity = mediaSourceConfigEmbeddableFactory.build(); + const expected = new MediaSourceOauthConfig({ + id: entity._id.toHexString(), + clientId: entity.clientId, + clientSecret: entity.clientSecret, + authEndpoint: entity.authEndpoint, + method: entity.method, + }); + + return { entity, expected }; + }; + + it('should return an instance of config', async () => { + const { entity } = await setup(); + + const result = MediaSourceConfigMapper.mapToDo(entity); + + expect(result).toBeInstanceOf(MediaSourceOauthConfig); + }); + + it('should return a do with all properties', async () => { + const { entity, expected } = await setup(); + + const result = MediaSourceConfigMapper.mapToDo(entity); + + expect(result).toEqual(expected); + }); + }); + }); + + describe('mapToEntity', () => { + describe('when config do is passed', () => { + const setup = async () => { + await setupEntities(); + + const configDo = mediaSourceConfigFactory.build(); + const expected = new MediaSourceConfigEmbeddable({ + _id: new ObjectId(configDo.id), + clientId: configDo.getProps().clientId, + clientSecret: configDo.getProps().clientSecret, + authEndpoint: configDo.getProps().authEndpoint, + method: configDo.getProps().method, + }); + + return { configDo, expected }; + }; + + it('should return an instance of config embeddable', async () => { + const { configDo } = await setup(); + + const result = MediaSourceConfigMapper.mapToEntity(configDo); + + expect(result).toBeInstanceOf(MediaSourceConfigEmbeddable); + }); + + it('should return an embeddable with all properties', async () => { + const { configDo, expected } = await setup(); + + const result = MediaSourceConfigMapper.mapToEntity(configDo); + + expect(result).toEqual(expected); + }); + }); + }); +}); diff --git a/apps/server/src/modules/user-license/repo/media-source-config.mapper.ts b/apps/server/src/modules/user-license/repo/media-source-config.mapper.ts new file mode 100644 index 00000000000..5a2f4fcfce1 --- /dev/null +++ b/apps/server/src/modules/user-license/repo/media-source-config.mapper.ts @@ -0,0 +1,25 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { MediaSourceOauthConfig } from '../domain/media-source-oauth-config'; +import { MediaSourceConfigEmbeddable } from '../entity/media-source-oauth-config.embeddable'; + +export class MediaSourceConfigMapper { + static mapToEntity(config: MediaSourceOauthConfig): MediaSourceConfigEmbeddable { + const configProps = config.getProps(); + + const configEmbeddable = new MediaSourceConfigEmbeddable({ ...configProps, _id: new ObjectId(configProps.id) }); + + return configEmbeddable; + } + + static mapToDo(embeddable: MediaSourceConfigEmbeddable): MediaSourceOauthConfig { + const config = new MediaSourceOauthConfig({ + id: embeddable._id.toHexString(), + clientId: embeddable.clientId, + clientSecret: embeddable.clientSecret, + method: embeddable.method, + authEndpoint: embeddable.authEndpoint, + }); + + return config; + } +} diff --git a/apps/server/src/modules/user-license/repo/media-source.mapper.ts b/apps/server/src/modules/user-license/repo/media-source.mapper.ts index f1ae887a3b0..20b46bdd6cb 100644 --- a/apps/server/src/modules/user-license/repo/media-source.mapper.ts +++ b/apps/server/src/modules/user-license/repo/media-source.mapper.ts @@ -1,12 +1,15 @@ import { EntityData } from '@mikro-orm/core'; import { MediaSource } from '../domain'; import { MediaSourceEntity } from '../entity'; +import { MediaSourceConfigMapper } from './media-source-config.mapper'; export class MediaSourceMapper { public static mapToEntityProperties(entityDO: MediaSource): EntityData { const entityProps: EntityData = { name: entityDO.name, sourceId: entityDO.sourceId, + config: entityDO.config ? MediaSourceConfigMapper.mapToEntity(entityDO.config) : undefined, + format: entityDO.format, }; return entityProps; @@ -17,6 +20,8 @@ export class MediaSourceMapper { id: entity.id, name: entity.name, sourceId: entity.sourceId, + config: entity.config ? MediaSourceConfigMapper.mapToDo(entity.config) : undefined, + format: entity.format, }); return domainObject; diff --git a/apps/server/src/modules/user-license/repo/media-source.repo.spec.ts b/apps/server/src/modules/user-license/repo/media-source.repo.spec.ts index 12708211687..63ae61b3a4c 100644 --- a/apps/server/src/modules/user-license/repo/media-source.repo.spec.ts +++ b/apps/server/src/modules/user-license/repo/media-source.repo.spec.ts @@ -6,6 +6,9 @@ import { MediaSource } from '../domain'; import { MediaSourceEntity } from '../entity'; import { mediaSourceEntityFactory, mediaSourceFactory } from '../testing'; import { MediaSourceRepo } from './media-source.repo'; +import { MediaSourceConfigEmbeddable } from '../entity/media-source-oauth-config.embeddable'; +import { mediaSourceConfigEmbeddableFactory } from '../testing/media-source-config.embeddable.factory'; +import { MediaSourceConfigMapper } from './media-source-config.mapper'; describe(MediaSourceRepo.name, () => { let module: TestingModule; @@ -33,7 +36,9 @@ describe(MediaSourceRepo.name, () => { describe('findBySourceId', () => { describe('when a media source exists', () => { const setup = async () => { - const mediaSource: MediaSourceEntity = mediaSourceEntityFactory.build(); + const config: MediaSourceConfigEmbeddable = mediaSourceConfigEmbeddableFactory.build(); + + const mediaSource: MediaSourceEntity = mediaSourceEntityFactory.build({ config }); await em.persistAndFlush([mediaSource]); @@ -41,11 +46,12 @@ describe(MediaSourceRepo.name, () => { return { mediaSource, + config, }; }; it('should return user licenses for user', async () => { - const { mediaSource } = await setup(); + const { mediaSource, config } = await setup(); const result = await repo.findBySourceId(mediaSource.sourceId); @@ -54,6 +60,8 @@ describe(MediaSourceRepo.name, () => { id: mediaSource.id, name: mediaSource.name, sourceId: mediaSource.sourceId, + format: mediaSource.format, + config: MediaSourceConfigMapper.mapToDo(config), }) ); }); diff --git a/apps/server/src/modules/user-license/repo/media-user-license.repo.spec.ts b/apps/server/src/modules/user-license/repo/media-user-license.repo.spec.ts index 5940d568137..f8a29727842 100644 --- a/apps/server/src/modules/user-license/repo/media-user-license.repo.spec.ts +++ b/apps/server/src/modules/user-license/repo/media-user-license.repo.spec.ts @@ -12,6 +12,9 @@ import { mediaUserLicenseFactory, } from '../testing'; import { MediaUserLicenseRepo } from './media-user-license.repo'; +import { MediaSourceConfigMapper } from './media-source-config.mapper'; +import { MediaSourceConfigEmbeddable } from '../entity/media-source-oauth-config.embeddable'; +import { mediaSourceConfigEmbeddableFactory } from '../testing/media-source-config.embeddable.factory'; describe(MediaUserLicenseRepo.name, () => { let module: TestingModule; @@ -40,7 +43,8 @@ describe(MediaUserLicenseRepo.name, () => { describe('when searching for a users media licences', () => { const setup = async () => { const user: UserEntity = userFactory.build(); - const mediaSource: MediaSourceEntity = mediaSourceEntityFactory.build(); + const config: MediaSourceConfigEmbeddable = mediaSourceConfigEmbeddableFactory.build(); + const mediaSource: MediaSourceEntity = mediaSourceEntityFactory.build({ config }); const mediaUserLicense: MediaUserLicenseEntity = mediaUserLicenseEntityFactory.build({ user, mediaSource }); const otherMediaUserLicense: MediaUserLicenseEntity = mediaUserLicenseEntityFactory.build(); @@ -52,11 +56,12 @@ describe(MediaUserLicenseRepo.name, () => { user, mediaUserLicense, mediaSource, + config, }; }; it('should return user licenses for user', async () => { - const { user, mediaUserLicense, mediaSource } = await setup(); + const { user, mediaUserLicense, mediaSource, config } = await setup(); const result: MediaUserLicense[] = await repo.findMediaUserLicensesForUser(user.id); @@ -70,6 +75,8 @@ describe(MediaUserLicenseRepo.name, () => { id: mediaSource.id, name: mediaSource.name, sourceId: mediaSource.sourceId, + format: mediaSource.format, + config: MediaSourceConfigMapper.mapToDo(config), }), }), ]); diff --git a/apps/server/src/modules/user-license/repo/media-user-license.repo.ts b/apps/server/src/modules/user-license/repo/media-user-license.repo.ts index 19685fd8656..004984b07ec 100644 --- a/apps/server/src/modules/user-license/repo/media-user-license.repo.ts +++ b/apps/server/src/modules/user-license/repo/media-user-license.repo.ts @@ -5,6 +5,7 @@ import { EntityId } from '@shared/domain/types'; import { BaseDomainObjectRepo } from '@shared/repo/base-domain-object.repo'; import { MediaSource, MediaUserLicense } from '../domain'; import { MediaSourceEntity, MediaUserLicenseEntity, UserLicenseType } from '../entity'; +import { MediaSourceConfigMapper } from './media-source-config.mapper'; @Injectable() export class MediaUserLicenseRepo extends BaseDomainObjectRepo { @@ -20,6 +21,8 @@ export class MediaUserLicenseRepo extends BaseDomainObjectRepo(MediaSourceConfigEmbeddable, ({ sequence }) => { + return { + _id: new ObjectId(), + clientId: `media-source-client-id-${sequence}`, + clientSecret: `media-source-client-secret-${sequence}`, + authEndpoint: `media-source-auth-endpoint-${sequence}`, + method: MediaSourceAuthMethod.CLIENT_CREDENTIALS, + }; +}); diff --git a/apps/server/src/modules/user-license/testing/media-source-config.factory.ts b/apps/server/src/modules/user-license/testing/media-source-config.factory.ts new file mode 100644 index 00000000000..d43d288b9b7 --- /dev/null +++ b/apps/server/src/modules/user-license/testing/media-source-config.factory.ts @@ -0,0 +1,19 @@ +import { ObjectId } from '@mikro-orm/mongodb'; +import { BaseFactory } from '@shared/testing'; +import { MediaSourceAuthMethod } from '../entity'; +import { MediaSourceOauthConfig, MediaSourceOauthConfigProps } from '../domain/media-source-oauth-config'; + +export const mediaSourceConfigFactory = BaseFactory.define( + MediaSourceOauthConfig, + ({ sequence }) => { + const config = { + id: new ObjectId().toHexString(), + clientId: `media-source-client-id-${sequence}`, + clientSecret: `media-source-client-secret-${sequence}`, + authEndpoint: `media-source-auth-endpoint-${sequence}`, + method: MediaSourceAuthMethod.CLIENT_CREDENTIALS, + }; + + return config; + } +); diff --git a/apps/server/src/modules/user-license/testing/media-source-entity.factory.ts b/apps/server/src/modules/user-license/testing/media-source-entity.factory.ts index 801927dca57..48013f5364c 100644 --- a/apps/server/src/modules/user-license/testing/media-source-entity.factory.ts +++ b/apps/server/src/modules/user-license/testing/media-source-entity.factory.ts @@ -1,6 +1,7 @@ import { ObjectId } from '@mikro-orm/mongodb'; import { BaseFactory } from '@shared/testing'; -import { MediaSourceEntity, MediaSourceEntityProps } from '../entity'; +import { MediaSourceDataFormat, MediaSourceEntity, MediaSourceEntityProps } from '../entity'; +import { mediaSourceConfigEmbeddableFactory } from './media-source-config.embeddable.factory'; export const mediaSourceEntityFactory = BaseFactory.define( MediaSourceEntity, @@ -9,6 +10,8 @@ export const mediaSourceEntityFactory = BaseFactory.define(MediaSource, ({ sequence }) => { return { id: new ObjectId().toHexString(), name: `media-source-${sequence}`, sourceId: `source-id-${sequence}`, + format: MediaSourceDataFormat.BILDUNGSLOGIN, + config: mediaSourceConfigFactory.build(), }; }); From 691acabf21022ac9448f2ad00d09e287f7a0cbe4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= <103562092+MarvinOehlerkingCap@users.noreply.github.com> Date: Thu, 10 Oct 2024 10:26:39 +0200 Subject: [PATCH 06/10] N21-2200 Move logout to nest (#5278) --- apps/server/src/imports-from-feathers.ts | 8 ++ .../authentication-api.module.ts | 9 +- .../controllers/api-test/logout.api.spec.ts | 72 +++++++++++++++ .../authentication/controllers/index.ts | 2 + .../controllers/logout.controller.ts | 20 +++++ .../helper/jwt-whitelist.adapter.spec.ts | 90 +++++++++---------- .../helper/jwt-whitelist.adapter.ts | 24 +++-- .../src/modules/authentication/uc/index.ts | 1 + .../authentication/uc/logout.uc.spec.ts | 47 ++++++++++ .../modules/authentication/uc/logout.uc.ts | 11 +++ .../testing/factory/jwt.test.factory.ts | 3 +- 11 files changed, 222 insertions(+), 65 deletions(-) create mode 100644 apps/server/src/modules/authentication/controllers/api-test/logout.api.spec.ts create mode 100644 apps/server/src/modules/authentication/controllers/index.ts create mode 100644 apps/server/src/modules/authentication/controllers/logout.controller.ts create mode 100644 apps/server/src/modules/authentication/uc/logout.uc.spec.ts create mode 100644 apps/server/src/modules/authentication/uc/logout.uc.ts diff --git a/apps/server/src/imports-from-feathers.ts b/apps/server/src/imports-from-feathers.ts index 0804901a53b..57cf4ad7ea8 100644 --- a/apps/server/src/imports-from-feathers.ts +++ b/apps/server/src/imports-from-feathers.ts @@ -4,5 +4,13 @@ export { addTokenToWhitelist, createRedisIdentifierFromJwtData, ensureTokenIsWhitelisted, + getRedisData, } from '../../../src/services/authentication/logic/whitelist.js'; export * as feathersRedis from '../../../src/utils/redis.js'; +export type JwtRedisData = { + IP: string; + Browser: string; + Device: string; + privateDevice: boolean; + expirationInSeconds: number; +}; diff --git a/apps/server/src/modules/authentication/authentication-api.module.ts b/apps/server/src/modules/authentication/authentication-api.module.ts index 6780e5fe11c..07780ddbe4c 100644 --- a/apps/server/src/modules/authentication/authentication-api.module.ts +++ b/apps/server/src/modules/authentication/authentication-api.module.ts @@ -1,12 +1,11 @@ import { Module } from '@nestjs/common'; import { AuthenticationModule } from './authentication.module'; -import { LoginController } from './controllers/login.controller'; -import { LoginUc } from './uc/login.uc'; +import { LoginController, LogoutController } from './controllers'; +import { LoginUc, LogoutUc } from './uc'; @Module({ imports: [AuthenticationModule], - providers: [LoginUc], - controllers: [LoginController], - exports: [], + providers: [LoginUc, LogoutUc], + controllers: [LoginController, LogoutController], }) export class AuthenticationApiModule {} diff --git a/apps/server/src/modules/authentication/controllers/api-test/logout.api.spec.ts b/apps/server/src/modules/authentication/controllers/api-test/logout.api.spec.ts new file mode 100644 index 00000000000..0ea7fd6bd2b --- /dev/null +++ b/apps/server/src/modules/authentication/controllers/api-test/logout.api.spec.ts @@ -0,0 +1,72 @@ +import { EntityManager } from '@mikro-orm/mongodb'; +import { ServerTestModule } from '@modules/server/server.module'; +import { CACHE_MANAGER } from '@nestjs/cache-manager'; +import { HttpStatus, INestApplication } from '@nestjs/common'; +import { Test, TestingModule } from '@nestjs/testing'; +import { cleanupCollections, TestApiClient, UserAndAccountTestFactory } from '@shared/testing'; +import { Cache } from 'cache-manager'; +import { Response } from 'supertest'; + +describe('Logout Controller (api)', () => { + const baseRouteName = '/logout'; + + let app: INestApplication; + let em: EntityManager; + let cacheManager: Cache; + let testApiClient: TestApiClient; + + beforeAll(async () => { + const moduleFixture: TestingModule = await Test.createTestingModule({ + imports: [ServerTestModule], + }).compile(); + + app = moduleFixture.createNestApplication(); + await app.init(); + em = app.get(EntityManager); + cacheManager = app.get(CACHE_MANAGER); + testApiClient = new TestApiClient(app, baseRouteName); + }); + + beforeEach(async () => { + await cleanupCollections(em); + }); + + afterAll(async () => { + await app.close(); + }); + + describe('logout', () => { + describe('when a valid jwt is provided', () => { + const setup = async () => { + const { studentAccount, studentUser } = UserAndAccountTestFactory.buildStudent(); + + await em.persistAndFlush([studentAccount, studentUser]); + em.clear(); + + const loggedInClient = await testApiClient.login(studentAccount); + + return { + loggedInClient, + studentAccount, + }; + }; + + it('should log out the user', async () => { + const { loggedInClient, studentAccount } = await setup(); + + const response: Response = await loggedInClient.post(''); + + expect(response.status).toEqual(HttpStatus.OK); + expect(await cacheManager.store.keys(`jwt:${studentAccount.id}:*`)).toHaveLength(0); + }); + }); + + describe('when the user is not logged in', () => { + it('should return unauthorized', async () => { + const response: Response = await testApiClient.post(''); + + expect(response.status).toEqual(HttpStatus.UNAUTHORIZED); + }); + }); + }); +}); diff --git a/apps/server/src/modules/authentication/controllers/index.ts b/apps/server/src/modules/authentication/controllers/index.ts new file mode 100644 index 00000000000..94b9a1dc3aa --- /dev/null +++ b/apps/server/src/modules/authentication/controllers/index.ts @@ -0,0 +1,2 @@ +export { LogoutController } from './logout.controller'; +export { LoginController } from './login.controller'; diff --git a/apps/server/src/modules/authentication/controllers/logout.controller.ts b/apps/server/src/modules/authentication/controllers/logout.controller.ts new file mode 100644 index 00000000000..a483a9a1bec --- /dev/null +++ b/apps/server/src/modules/authentication/controllers/logout.controller.ts @@ -0,0 +1,20 @@ +import { JWT, JwtAuthentication } from '@infra/auth-guard'; +import { Controller, HttpCode, HttpStatus, Post } from '@nestjs/common'; +import { ApiOkResponse, ApiOperation, ApiTags, ApiUnauthorizedResponse } from '@nestjs/swagger'; +import { LogoutUc } from '../uc'; + +@ApiTags('Authentication') +@Controller('logout') +export class LogoutController { + constructor(private readonly logoutUc: LogoutUc) {} + + @JwtAuthentication() + @Post() + @HttpCode(HttpStatus.OK) + @ApiOperation({ summary: 'Logs out a user.' }) + @ApiOkResponse({ description: 'Logout was successful.' }) + @ApiUnauthorizedResponse({ description: 'There has been an error while logging out.' }) + async logout(@JWT() jwt: string): Promise { + await this.logoutUc.logout(jwt); + } +} diff --git a/apps/server/src/modules/authentication/helper/jwt-whitelist.adapter.spec.ts b/apps/server/src/modules/authentication/helper/jwt-whitelist.adapter.spec.ts index 2d2662e1dde..471584ea96c 100644 --- a/apps/server/src/modules/authentication/helper/jwt-whitelist.adapter.spec.ts +++ b/apps/server/src/modules/authentication/helper/jwt-whitelist.adapter.spec.ts @@ -1,47 +1,30 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { JwtValidationAdapter } from '@infra/auth-guard/'; -import { CacheService } from '@infra/cache'; -import { CacheStoreType } from '@infra/cache/interface/cache-store-type.enum'; +import { Configuration } from '@hpi-schul-cloud/commons/lib'; import { ObjectId } from '@mikro-orm/mongodb'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Test, TestingModule } from '@nestjs/testing'; -import { feathersRedis } from '@src/imports-from-feathers'; import { Cache } from 'cache-manager'; import { JwtWhitelistAdapter } from './jwt-whitelist.adapter'; -import RedisMock = require('../../../../../../test/utils/redis/redisMock'); describe('jwt strategy', () => { let module: TestingModule; let jwtWhitelistAdapter: JwtWhitelistAdapter; - let jwtValidationAdapter: JwtValidationAdapter; let cacheManager: DeepMocked; - let cacheService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ providers: [ - JwtValidationAdapter, JwtWhitelistAdapter, { provide: CACHE_MANAGER, useValue: createMock(), }, - { - provide: CacheService, - useValue: createMock(), - }, ], }).compile(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment,@typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - const redisClientMock = new RedisMock(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-call,@typescript-eslint/no-unsafe-member-access - feathersRedis.setRedisClient(redisClientMock); cacheManager = module.get(CACHE_MANAGER); - cacheService = module.get(CacheService); jwtWhitelistAdapter = module.get(JwtWhitelistAdapter); - jwtValidationAdapter = module.get(JwtValidationAdapter); }); afterAll(async () => { @@ -52,41 +35,58 @@ describe('jwt strategy', () => { jest.resetAllMocks(); }); - describe('when authenticate a user with jwt', () => { - it('should fail without whitelisted jwt', async () => { - const accountId = new ObjectId().toHexString(); - const jti = new ObjectId().toHexString(); - await expect(jwtValidationAdapter.isWhitelisted(accountId, jti)).rejects.toThrow( - 'Session was expired due to inactivity - autologout.' - ); - }); - it('should pass when jwt has been whitelisted', async () => { - const accountId = new ObjectId().toHexString(); - const jti = new ObjectId().toHexString(); - await jwtWhitelistAdapter.addToWhitelist(accountId, jti); - // might fail when we would wait more than JWT_TIMEOUT_SECONDS - await jwtValidationAdapter.isWhitelisted(accountId, jti); - }); - }); + describe('addToWhitelist', () => { + describe('when adding jwt to the whitelist', () => { + const setup = () => { + const accountId = new ObjectId().toHexString(); + const jti = new ObjectId().toHexString(); + const expirationInSeconds = Configuration.get('JWT_TIMEOUT_SECONDS') as number; - describe('removeFromWhitelist is called', () => { - describe('when redis is used as cache store', () => { - it('should call the cache manager to delete the entry from the cache', async () => { - cacheService.getStoreType.mockReturnValue(CacheStoreType.REDIS); + return { + accountId, + jti, + expirationInSeconds, + }; + }; - await jwtWhitelistAdapter.removeFromWhitelist('accountId', 'jti'); + it('should call the cache manager to set the jwt from the cache', async () => { + const { accountId, jti, expirationInSeconds } = setup(); - expect(cacheManager.del).toHaveBeenCalledWith('jwt:accountId:jti'); + await jwtWhitelistAdapter.addToWhitelist(accountId, jti); + + expect(cacheManager.set).toHaveBeenCalledWith( + `jwt:${accountId}:${jti}`, + { + IP: 'NONE', + Browser: 'NONE', + Device: 'NONE', + privateDevice: false, + expirationInSeconds, + }, + expirationInSeconds * 1000 + ); }); }); + }); + + describe('removeFromWhitelist', () => { + describe('when removing a token from the whitelist', () => { + const setup = () => { + const accountId = new ObjectId().toHexString(); + const jti = new ObjectId().toHexString(); + + return { + accountId, + jti, + }; + }; - describe('when a memory store is used', () => { - it('should do nothing', async () => { - cacheService.getStoreType.mockReturnValue(CacheStoreType.MEMORY); + it('should call the cache manager to jwt the entry from the cache', async () => { + const { accountId, jti } = setup(); - await jwtWhitelistAdapter.removeFromWhitelist('accountId', 'jti'); + await jwtWhitelistAdapter.removeFromWhitelist(accountId, jti); - expect(cacheManager.del).not.toHaveBeenCalled(); + expect(cacheManager.del).toHaveBeenCalledWith(`jwt:${accountId}:${jti}`); }); }); }); diff --git a/apps/server/src/modules/authentication/helper/jwt-whitelist.adapter.ts b/apps/server/src/modules/authentication/helper/jwt-whitelist.adapter.ts index 9a7b9d81f26..c7568545906 100644 --- a/apps/server/src/modules/authentication/helper/jwt-whitelist.adapter.ts +++ b/apps/server/src/modules/authentication/helper/jwt-whitelist.adapter.ts @@ -1,27 +1,23 @@ -import { CacheService } from '@infra/cache'; -import { CacheStoreType } from '@infra/cache/interface/cache-store-type.enum'; import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { Inject, Injectable } from '@nestjs/common'; -import { addTokenToWhitelist, createRedisIdentifierFromJwtData } from '@src/imports-from-feathers'; +import { createRedisIdentifierFromJwtData, getRedisData, JwtRedisData } from '@src/imports-from-feathers'; import { Cache } from 'cache-manager'; @Injectable() export class JwtWhitelistAdapter { - constructor( - @Inject(CACHE_MANAGER) private readonly cacheManager: Cache, - private readonly cacheService: CacheService - ) {} + constructor(@Inject(CACHE_MANAGER) private readonly cacheManager: Cache) {} async addToWhitelist(accountId: string, jti: string): Promise { - const redisIdentifier = createRedisIdentifierFromJwtData(accountId, jti); - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - await addTokenToWhitelist(redisIdentifier); + const redisIdentifier: string = createRedisIdentifierFromJwtData(accountId, jti); + const redisData: JwtRedisData = getRedisData({}); + const expirationInMilliseconds: number = redisData.expirationInSeconds * 1000; + + await this.cacheManager.set(redisIdentifier, redisData, expirationInMilliseconds); } async removeFromWhitelist(accountId: string, jti: string): Promise { - if (this.cacheService.getStoreType() === CacheStoreType.REDIS) { - const redisIdentifier: string = createRedisIdentifierFromJwtData(accountId, jti); - await this.cacheManager.del(redisIdentifier); - } + const redisIdentifier: string = createRedisIdentifierFromJwtData(accountId, jti); + + await this.cacheManager.del(redisIdentifier); } } diff --git a/apps/server/src/modules/authentication/uc/index.ts b/apps/server/src/modules/authentication/uc/index.ts index 8616d4505f9..bd541002fd4 100644 --- a/apps/server/src/modules/authentication/uc/index.ts +++ b/apps/server/src/modules/authentication/uc/index.ts @@ -1,2 +1,3 @@ export { LoginDto } from './dto'; export { LoginUc } from './login.uc'; +export { LogoutUc } from './logout.uc'; diff --git a/apps/server/src/modules/authentication/uc/logout.uc.spec.ts b/apps/server/src/modules/authentication/uc/logout.uc.spec.ts new file mode 100644 index 00000000000..fc06ce13cf6 --- /dev/null +++ b/apps/server/src/modules/authentication/uc/logout.uc.spec.ts @@ -0,0 +1,47 @@ +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { JwtTestFactory } from '@shared/testing'; +import { AuthenticationService } from '../services'; +import { LogoutUc } from './logout.uc'; + +describe(LogoutUc.name, () => { + let module: TestingModule; + let logoutUc: LogoutUc; + + let authenticationService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + LogoutUc, + { + provide: AuthenticationService, + useValue: createMock(), + }, + ], + }).compile(); + + logoutUc = await module.get(LogoutUc); + authenticationService = await module.get(AuthenticationService); + }); + + describe('logout', () => { + describe('when a jwt is given', () => { + const setup = () => { + const jwt = JwtTestFactory.createJwt(); + + return { + jwt, + }; + }; + + it('should remove the user from the whitelist', async () => { + const { jwt } = setup(); + + await logoutUc.logout(jwt); + + expect(authenticationService.removeJwtFromWhitelist).toHaveBeenCalledWith(jwt); + }); + }); + }); +}); diff --git a/apps/server/src/modules/authentication/uc/logout.uc.ts b/apps/server/src/modules/authentication/uc/logout.uc.ts new file mode 100644 index 00000000000..63c792626cb --- /dev/null +++ b/apps/server/src/modules/authentication/uc/logout.uc.ts @@ -0,0 +1,11 @@ +import { Injectable } from '@nestjs/common'; +import { AuthenticationService } from '../services'; + +@Injectable() +export class LogoutUc { + constructor(private readonly authenticationService: AuthenticationService) {} + + async logout(jwt: string): Promise { + await this.authenticationService.removeJwtFromWhitelist(jwt); + } +} diff --git a/apps/server/src/shared/testing/factory/jwt.test.factory.ts b/apps/server/src/shared/testing/factory/jwt.test.factory.ts index 6d63fada5c2..54658c5abeb 100644 --- a/apps/server/src/shared/testing/factory/jwt.test.factory.ts +++ b/apps/server/src/shared/testing/factory/jwt.test.factory.ts @@ -1,5 +1,5 @@ -import jwt from 'jsonwebtoken'; import crypto, { KeyPairKeyObjectResult } from 'crypto'; +import jwt from 'jsonwebtoken'; const keyPair: KeyPairKeyObjectResult = crypto.generateKeyPairSync('rsa', { modulusLength: 4096 }); const publicKey: string | Buffer = keyPair.publicKey.export({ type: 'pkcs1', format: 'pem' }); @@ -36,6 +36,7 @@ export class JwtTestFactory { algorithm: 'RS256', } ); + return validJwt; } } From 943220fbe3b168c6ed0248b78dd340fc81c0aba9 Mon Sep 17 00:00:00 2001 From: mrikallab <93978883+mrikallab@users.noreply.github.com> Date: Thu, 10 Oct 2024 10:57:24 +0200 Subject: [PATCH 07/10] N21-2236 fix lti encryption (#5281) --- .../templates/configmap_file_init.yml.j2 | 4 +- .../service/external-tool.service.spec.ts | 31 +-- .../service/external-tool.service.ts | 12 +- .../external-tool/uc/external-tool.uc.spec.ts | 247 +++++++++++++++++- .../tool/external-tool/uc/external-tool.uc.ts | 36 ++- .../src/modules/tool/tool-api.module.ts | 2 + 6 files changed, 289 insertions(+), 43 deletions(-) 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 f191ce69ca8..238b44226e4 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 @@ -519,8 +519,8 @@ data: echo "Inserting ctl seed data secrets to external-tools..." # Encrypt secrets of external tools that contain an lti11 config. - $CTL_SEED_SECRET_ONLINE_DIA_MATHE=$(node scripts/secret.js -s $AES_KEY -e $CTL_SEED_SECRET_ONLINE_DIA_MATHE) - $CTL_SEED_SECRET_ONLINE_DIA_DEUTSCH=$(node scripts/secret.js -s $AES_KEY -e $CTL_SEED_SECRET_ONLINE_DIA_DEUTSCH) + CTL_SEED_SECRET_ONLINE_DIA_MATHE=$(node scripts/secret.js -s $AES_KEY -e $CTL_SEED_SECRET_ONLINE_DIA_MATHE) + CTL_SEED_SECRET_ONLINE_DIA_DEUTSCH=$(node scripts/secret.js -s $AES_KEY -e $CTL_SEED_SECRET_ONLINE_DIA_DEUTSCH) mongosh $DATABASE__URL --quiet --eval 'db.getCollection("external-tools").updateOne( { 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 5e08193da2c..38a14509f9d 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 @@ -1,5 +1,4 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; import { ProviderOauthClient } from '@modules/oauth-provider/domain'; import { UnprocessableEntityException } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; @@ -24,7 +23,6 @@ describe(ExternalToolService.name, () => { let oauthProviderService: DeepMocked; let commonToolDeleteService: DeepMocked; let mapper: DeepMocked; - let encryptionService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -42,10 +40,6 @@ describe(ExternalToolService.name, () => { provide: ExternalToolServiceMapper, useValue: createMock(), }, - { - provide: DefaultEncryptionService, - useValue: createMock(), - }, { provide: LegacyLogger, useValue: createMock(), @@ -62,7 +56,6 @@ describe(ExternalToolService.name, () => { oauthProviderService = module.get(OauthProviderService); mapper = module.get(ExternalToolServiceMapper); commonToolDeleteService = module.get(CommonToolDeleteService); - encryptionService = module.get(DefaultEncryptionService); }); afterAll(async () => { @@ -160,29 +153,14 @@ describe(ExternalToolService.name, () => { describe('when lti11 config is set', () => { const setup = () => { - const encryptedSecret = 'encryptedSecret'; const { externalTool, lti11ToolConfig } = createTools(); externalTool.config = lti11ToolConfig; - const lti11ToolConfigDOEncrypted: Lti11ToolConfig = { ...lti11ToolConfig, secret: encryptedSecret }; - const externalToolDOEncrypted: ExternalTool = externalToolFactory.build({ - ...externalTool, - config: lti11ToolConfigDOEncrypted, - }); - encryptionService.encrypt.mockReturnValue(encryptedSecret); - externalToolRepo.save.mockResolvedValue(externalToolDOEncrypted); + externalToolRepo.save.mockResolvedValue(externalTool); - return { externalTool, lti11ToolConfig, encryptedSecret, externalToolDOEncrypted }; + return { externalTool, lti11ToolConfig }; }; - it('should encrypt the secret', async () => { - const { externalTool } = setup(); - - await service.createExternalTool(externalTool); - - expect(encryptionService.encrypt).toHaveBeenCalledWith('secret'); - }); - it('should call the repo to save a tool', async () => { const { externalTool } = setup(); @@ -192,11 +170,11 @@ describe(ExternalToolService.name, () => { }); it('should save DO', async () => { - const { externalTool, externalToolDOEncrypted } = setup(); + const { externalTool } = setup(); const result: ExternalTool = await service.createExternalTool(externalTool); - expect(result).toEqual(externalToolDOEncrypted); + expect(result).toEqual(externalTool); }); }); }); @@ -512,7 +490,6 @@ describe(ExternalToolService.name, () => { describe('updateExternalTool', () => { describe('when external tool with lti11 config is given', () => { const setup = () => { - encryptionService.encrypt.mockReturnValue('newEncryptedSecret'); const changedTool: ExternalTool = externalToolFactory .withLti11Config({ secret: 'newEncryptedSecret' }) .build({ name: 'newName' }); 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 784e64c5643..1d320179cd6 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 @@ -1,7 +1,6 @@ -import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; import { ProviderOauthClient } from '@modules/oauth-provider/domain'; import { OauthProviderService } from '@modules/oauth-provider/domain/service/oauth-provider.service'; -import { Inject, Injectable, UnprocessableEntityException } from '@nestjs/common'; +import { Injectable, UnprocessableEntityException } from '@nestjs/common'; import { Page } from '@shared/domain/domainobject'; import { IFindOptions } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; @@ -19,15 +18,12 @@ export class ExternalToolService { private readonly externalToolRepo: ExternalToolRepo, private readonly oauthProviderService: OauthProviderService, private readonly mapper: ExternalToolServiceMapper, - @Inject(DefaultEncryptionService) private readonly encryptionService: EncryptionService, private readonly legacyLogger: LegacyLogger, private readonly commonToolDeleteService: CommonToolDeleteService ) {} public async createExternalTool(externalTool: ExternalTool): Promise { - if (ExternalTool.isLti11Config(externalTool.config) && externalTool.config.secret) { - externalTool.config.secret = this.encryptionService.encrypt(externalTool.config.secret); - } else if (ExternalTool.isOauth2Config(externalTool.config)) { + if (ExternalTool.isOauth2Config(externalTool.config)) { const oauthClient: Partial = this.mapper.mapDoToProviderOauthClient( externalTool.name, externalTool.config @@ -42,10 +38,6 @@ export class ExternalToolService { } public async updateExternalTool(toUpdate: ExternalTool): Promise { - if (ExternalTool.isLti11Config(toUpdate.config) && toUpdate.config.secret) { - toUpdate.config.secret = this.encryptionService.encrypt(toUpdate.config.secret); - } - await this.updateOauth2ToolConfig(toUpdate); const externalTool: ExternalTool = await this.externalToolRepo.save(toUpdate); 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 b3af5f0e83e..625d60eee53 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 @@ -1,5 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; import { ObjectId } from '@mikro-orm/mongodb'; import { Action, AuthorizationService } from '@modules/authorization'; import { School, SchoolService } from '@modules/school'; @@ -13,6 +14,7 @@ import { Role, User } from '@shared/domain/entity'; import { IFindOptions, Permission, SortOrder } from '@shared/domain/interface'; import { currentUserFactory, roleFactory, setupEntities, userFactory } from '@shared/testing'; import { CustomParameter } from '../../common/domain'; +import { LtiMessageType, LtiPrivacyPermission, ToolConfigType } from '../../common/enum'; import { ExternalToolSearchQuery } from '../../common/interface'; import { CommonToolMetadataService } from '../../common/service/common-tool-metadata.service'; import { schoolExternalToolFactory } from '../../school-external-tool/testing'; @@ -22,6 +24,7 @@ import { ExternalToolMetadata, ExternalToolParameterDatasheetTemplateProperty, ExternalToolProps, + Lti11ToolConfig, Oauth2ToolConfig, } from '../domain'; import { @@ -36,9 +39,10 @@ import { externalToolDatasheetTemplateDataFactory, externalToolFactory, fileRecordRefFactory, + lti11ToolConfigFactory, oauth2ToolConfigFactory, } from '../testing'; -import { ExternalToolCreate, ExternalToolImportResult, ExternalToolUpdate } from './dto'; +import { ExternalToolCreate, ExternalToolImportResult, ExternalToolUpdate, Lti11ToolConfigUpdate } from './dto'; import { ExternalToolUc } from './external-tool.uc'; describe(ExternalToolUc.name, () => { @@ -54,6 +58,7 @@ describe(ExternalToolUc.name, () => { let commonToolMetadataService: DeepMocked; let pdfService: DeepMocked; let externalToolImageService: DeepMocked; + let encryptionService: DeepMocked; beforeAll(async () => { await setupEntities(); @@ -99,6 +104,10 @@ describe(ExternalToolUc.name, () => { provide: ExternalToolImageService, useValue: createMock(), }, + { + provide: DefaultEncryptionService, + useValue: createMock(), + }, ], }).compile(); @@ -112,6 +121,7 @@ describe(ExternalToolUc.name, () => { commonToolMetadataService = module.get(CommonToolMetadataService); pdfService = module.get(DatasheetPdfService); externalToolImageService = module.get(ExternalToolImageService); + encryptionService = module.get(DefaultEncryptionService); }); afterAll(async () => { @@ -140,6 +150,7 @@ describe(ExternalToolUc.name, () => { const externalTool: ExternalTool = externalToolFactory.withCustomParameters(1).buildWithId(); const oauth2ConfigWithoutExternalData: Oauth2ToolConfig = oauth2ToolConfigFactory.build(); + const lti11ToolConfig: Lti11ToolConfig = lti11ToolConfigFactory.build(); const query: ExternalToolSearchQuery = { name: externalTool.name, @@ -171,6 +182,7 @@ describe(ExternalToolUc.name, () => { query, toolId, mockLogoBase64, + lti11ToolConfig, }; }; @@ -334,6 +346,44 @@ describe(ExternalToolUc.name, () => { ); }); }); + + describe('when external tool with lti11 config is given', () => { + const setupLTI = () => { + const { currentUser } = setupAuthorization(); + const { externalTool, lti11ToolConfig } = setupDefault(); + externalTool.config = lti11ToolConfig; + + encryptionService.encrypt.mockReturnValue('encrypted'); + + return { + currentUser, + externalTool, + }; + }; + it('should call the encryption service', async () => { + const { currentUser, externalTool } = setupLTI(); + + await uc.createExternalTool(currentUser.userId, externalTool.getProps(), 'jwt'); + + expect(encryptionService.encrypt).toHaveBeenCalledWith('secret'); + }); + + it('should call the service to save a tool', async () => { + const { currentUser, externalTool } = setupLTI(); + + await uc.createExternalTool(currentUser.userId, externalTool.getProps(), 'jwt'); + + expect(externalToolService.createExternalTool).toHaveBeenNthCalledWith( + 1, + new ExternalTool({ + ...externalTool.getProps(), + logo: 'base64LogoString', + config: { ...externalTool.config, secret: 'encrypted' }, + id: expect.any(String), + }) + ); + }); + }); }); describe('importExternalTools', () => { @@ -382,6 +432,14 @@ describe(ExternalToolUc.name, () => { }; }; + it('should not call encryption service', async () => { + const { user, externalTool1, externalTool2 } = setup(); + + await uc.importExternalTools(user.id, [externalTool1.getProps(), externalTool2.getProps()], 'jwt'); + + expect(encryptionService.encrypt).not.toHaveBeenCalled(); + }); + it('should check the users permission', async () => { const { user, externalTool1, externalTool2 } = setup(); @@ -477,6 +535,67 @@ describe(ExternalToolUc.name, () => { }); }); + describe('when importing lti tool', () => { + const setup = () => { + const user = userFactory.buildWithId(); + const externalTool1 = externalToolFactory.build({ + name: 'tool1', + medium: { + mediumId: 'medium1', + mediaSourceId: 'mediumSource1', + }, + }); + + const ltiConfig = lti11ToolConfigFactory.build(); + externalTool1.config = ltiConfig; + + const externalToolCreate1: ExternalToolCreate = { + ...externalTool1.getProps(), + thumbnailUrl: 'https://thumbnail.url1', + }; + + const jwt = 'jwt'; + + authorizationService.getUserWithPermissions.mockResolvedValueOnce(user); + logoService.fetchLogo.mockResolvedValueOnce(undefined); + const thumbnailFileRecordRef = fileRecordRefFactory.build(); + externalToolImageService.uploadImageFileFromUrl.mockResolvedValueOnce(thumbnailFileRecordRef); + externalToolService.createExternalTool.mockResolvedValueOnce(externalTool1); + encryptionService.encrypt.mockReturnValue('encrypted'); + + return { + user, + externalTool1, + externalToolCreate1, + thumbnailFileRecordRef, + jwt, + }; + }; + + it('should call encryption service', async () => { + const { user, externalTool1 } = setup(); + + await uc.importExternalTools(user.id, [externalTool1.getProps()], 'jwt'); + + expect(encryptionService.encrypt).toHaveBeenCalled(); + }); + + it('should save tool', async () => { + const { user, externalTool1 } = setup(); + + await uc.importExternalTools(user.id, [externalTool1.getProps()], 'jwt'); + + expect(externalToolService.createExternalTool).toHaveBeenNthCalledWith( + 1, + new ExternalTool({ + ...externalTool1.getProps(), + config: lti11ToolConfigFactory.build({ ...externalTool1.config, secret: 'encrypted' }), + id: expect.any(String), + }) + ); + }); + }); + describe('when an external tools fails the validation', () => { const setup = () => { const user = userFactory.buildWithId(); @@ -643,6 +762,8 @@ describe(ExternalToolUc.name, () => { url: undefined, }); + const lti11ToolConfig: Lti11ToolConfig = lti11ToolConfigFactory.build(); + externalToolService.updateExternalTool.mockResolvedValue(updatedExternalTool); externalToolService.findById.mockResolvedValue(new ExternalTool(externalToolToUpdate)); @@ -652,6 +773,7 @@ describe(ExternalToolUc.name, () => { externalToolDOtoUpdate: externalToolToUpdate, toolId, mockLogoBase64, + lti11ToolConfig, }; }; @@ -868,6 +990,129 @@ describe(ExternalToolUc.name, () => { ); }); }); + + describe('when lti11 config is given and secret is not encrypted', () => { + const setupLTI = () => { + const { externalTool, toolId, mockLogoBase64 } = setupDefault(); + + const lti11ToolConfig: Lti11ToolConfig = lti11ToolConfigFactory.build(); + const externalToolToUpdate: ExternalToolUpdate = { + ...externalTool.getProps(), + name: 'newName', + config: lti11ToolConfig, + url: undefined, + logo: mockLogoBase64, + }; + + const currentTestTool = new ExternalTool({ ...externalToolToUpdate }); + const expectedLtiConfig = lti11ToolConfigFactory.buildWithId({ secret: 'encrypted' }); + currentTestTool.config = expectedLtiConfig; + + const updatedExternalTool: ExternalTool = externalToolFactory.build({ + ...externalTool.getProps(), + name: 'newName', + config: expectedLtiConfig, + url: undefined, + logo: mockLogoBase64, + }); + + externalToolService.findById.mockResolvedValue(currentTestTool); + encryptionService.encrypt.mockReturnValue(expectedLtiConfig.secret); + externalToolService.updateExternalTool.mockResolvedValue(updatedExternalTool); + + return { + externalTool, + updatedExternalToolDO: updatedExternalTool, + externalToolDOtoUpdate: externalToolToUpdate, + toolId, + mockLogoBase64, + lti11ToolConfig, + }; + }; + + it('should call encryption service', async () => { + const { currentUser } = setupAuthorization(); + const { toolId, externalToolDOtoUpdate, lti11ToolConfig } = setupLTI(); + + await uc.updateExternalTool(currentUser.userId, toolId, externalToolDOtoUpdate, 'jwt'); + + expect(encryptionService.encrypt).toHaveBeenNthCalledWith(1, lti11ToolConfig.secret); + }); + + it('should call the service to update the tool', async () => { + const { currentUser } = setupAuthorization(); + const { toolId, externalToolDOtoUpdate, updatedExternalToolDO } = setupLTI(); + + await uc.updateExternalTool(currentUser.userId, toolId, externalToolDOtoUpdate, 'jwt'); + + expect(externalToolService.updateExternalTool).toHaveBeenCalledWith(updatedExternalToolDO); + }); + }); + + describe('when lti11 config is given and secret is already encrypted', () => { + const setupLTI = () => { + const { externalTool, toolId, mockLogoBase64 } = setupDefault(); + + const lti11ToolConfig: Lti11ToolConfigUpdate = { + type: ToolConfigType.LTI11, + baseUrl: 'https://www.basic-baseUrl.com/', + key: 'key', + privacy_permission: LtiPrivacyPermission.PSEUDONYMOUS, + lti_message_type: LtiMessageType.BASIC_LTI_LAUNCH_REQUEST, + launch_presentation_locale: 'de-DE', + }; + + const externalToolToUpdate: ExternalToolUpdate = { + ...externalTool.getProps(), + config: lti11ToolConfig, + name: 'newName', + url: undefined, + }; + + const updatedExternalTool: ExternalTool = externalToolFactory.build({ + ...externalTool.getProps(), + config: lti11ToolConfigFactory.build({ ...externalTool.config, secret: 'encrypted' }), + name: 'newName', + url: undefined, + logo: mockLogoBase64, + }); + + externalToolService.findById.mockResolvedValue( + new ExternalTool({ + ...externalToolToUpdate, + config: lti11ToolConfigFactory.build({ ...externalToolToUpdate.config, secret: 'encrypted' }), + }) + ); + externalToolService.updateExternalTool.mockResolvedValue(updatedExternalTool); + + return { + externalTool, + updatedExternalToolDO: updatedExternalTool, + externalToolDOtoUpdate: externalToolToUpdate, + toolId, + mockLogoBase64, + lti11ToolConfig, + }; + }; + + it('should not call encryption service', async () => { + const { currentUser } = setupAuthorization(); + const { toolId, externalToolDOtoUpdate } = setupLTI(); + + await uc.updateExternalTool(currentUser.userId, toolId, externalToolDOtoUpdate, 'jwt'); + + expect(encryptionService.encrypt).not.toHaveBeenCalledWith(); + }); + + it('should call the service to update the tool', async () => { + const { currentUser } = setupAuthorization(); + const { toolId, externalToolDOtoUpdate, updatedExternalToolDO } = setupLTI(); + + await uc.updateExternalTool(currentUser.userId, toolId, externalToolDOtoUpdate, 'jwt'); + + expect(externalToolService.updateExternalTool).toHaveBeenCalledWith(updatedExternalToolDO); + }); + }); }); describe('deleteExternalTool', () => { 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 bc79c03f090..f9396b3e299 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,16 +1,25 @@ +import { DefaultEncryptionService, EncryptionService } from '@infra/encryption'; import { ObjectId } from '@mikro-orm/mongodb'; import { AuthorizationService } from '@modules/authorization'; import { School, SchoolService } from '@modules/school'; import { SchoolExternalTool } from '@modules/tool/school-external-tool/domain'; import { SchoolExternalToolService } from '@modules/tool/school-external-tool/service'; -import { Injectable } from '@nestjs/common'; +import { Inject, Injectable } from '@nestjs/common'; import { Page } from '@shared/domain/domainobject'; import { User } from '@shared/domain/entity'; import { IFindOptions, Permission } from '@shared/domain/interface'; import { EntityId } from '@shared/domain/types'; import { ExternalToolSearchQuery } from '../../common/interface'; import { CommonToolMetadataService } from '../../common/service/common-tool-metadata.service'; -import { ExternalTool, ExternalToolConfig, ExternalToolDatasheetTemplateData, ExternalToolMetadata } from '../domain'; +import { + BasicToolConfig, + ExternalTool, + ExternalToolConfig, + ExternalToolDatasheetTemplateData, + ExternalToolMetadata, + Lti11ToolConfig, + Oauth2ToolConfig, +} from '../domain'; import { ExternalToolDatasheetMapper } from '../mapper/external-tool-datasheet.mapper'; import { DatasheetPdfService, @@ -32,7 +41,8 @@ export class ExternalToolUc { private readonly externalToolLogoService: ExternalToolLogoService, private readonly commonToolMetadataService: CommonToolMetadataService, private readonly datasheetPdfService: DatasheetPdfService, - private readonly externalToolImageService: ExternalToolImageService + private readonly externalToolImageService: ExternalToolImageService, + @Inject(DefaultEncryptionService) private readonly encryptionService: EncryptionService ) {} public async createExternalTool( @@ -42,6 +52,8 @@ export class ExternalToolUc { ): Promise { await this.ensurePermission(userId, Permission.TOOL_ADMIN); + externalToolCreate.config = this.encryptLtiSecret(externalToolCreate); + const tool: ExternalTool = await this.validateAndSaveExternalTool(externalToolCreate, jwt); return tool; @@ -64,6 +76,8 @@ export class ExternalToolUc { }); try { + externalTool.config = this.encryptLtiSecret(externalTool); + // eslint-disable-next-line no-await-in-loop const savedTool: ExternalTool = await this.validateAndSaveExternalTool(externalTool, jwt); @@ -118,6 +132,8 @@ export class ExternalToolUc { ): Promise { await this.ensurePermission(userId, Permission.TOOL_ADMIN); + externalToolUpdate.config = this.encryptLtiSecret(externalToolUpdate); + const { thumbnailUrl, ...externalToolUpdateProps } = externalToolUpdate; const currentExternalTool: ExternalTool = await this.externalToolService.findById(toolId); @@ -248,4 +264,18 @@ export class ExternalToolUc { return fileName; } + + private encryptLtiSecret( + externalTool: ExternalToolCreate | ExternalToolUpdate + ): BasicToolConfig | Lti11ToolConfig | Oauth2ToolConfig { + if (ExternalTool.isLti11Config(externalTool.config) && externalTool.config.secret) { + const encrypted = this.encryptionService.encrypt(externalTool.config.secret); + + const updatedConfig = new Lti11ToolConfig({ ...externalTool.config, secret: encrypted }); + + return updatedConfig; + } + + return externalTool.config; + } } diff --git a/apps/server/src/modules/tool/tool-api.module.ts b/apps/server/src/modules/tool/tool-api.module.ts index 12d33bd3d90..51eec1332ff 100644 --- a/apps/server/src/modules/tool/tool-api.module.ts +++ b/apps/server/src/modules/tool/tool-api.module.ts @@ -1,3 +1,4 @@ +import { EncryptionModule } from '@infra/encryption'; import { AuthorizationModule } from '@modules/authorization'; import { BoardModule } from '@modules/board'; import { LegacySchoolModule } from '@modules/legacy-school'; @@ -35,6 +36,7 @@ import { ToolModule } from './tool.module'; BoardModule, SchoolModule, UserLicenseModule, + EncryptionModule, ], controllers: [ ToolLaunchController, From 37b842a562a934226595e016e935df609b5b2d16 Mon Sep 17 00:00:00 2001 From: Fshmit <122355627+Fshmit@users.noreply.github.com> Date: Fri, 11 Oct 2024 10:56:00 +0200 Subject: [PATCH 08/10] EW-1007 decoupled course room dependency (#5263) * EW-1007 decoupled course room dependency --- .../room-client/courses-room-client.config.ts | 5 + .../room-client/dto/board-column-board.dto.ts | 27 ++++ .../room-client/dto/board-element.dto.ts | 15 ++ .../room-client/dto/board-lesson.dto.ts | 30 ++++ .../room-client/dto/board-task-status.dto.ts | 22 +++ .../room-client/dto/board-task.dto.ts | 36 +++++ .../room-client/dto/room-board.dto.ts | 24 +++ .../room-client/enums/board-element.enum.ts | 5 + .../room-client/enums/board-layout.enum.ts | 5 + .../room-client/index.ts | 2 + ...ard-column-board-layout-dto.mapper.spec.ts | 34 ++++ .../board-column-board-layout-dto.mapper.ts | 16 ++ .../board-task-status-dto.mapper.spec.ts | 24 +++ .../mapper/board-task-status-dto.mapper.ts | 15 ++ .../mapper/room-board-dto.mapper.spec.ts | 143 +++++++++++++++++ .../mapper/room-board-dto.mapper.ts | 123 ++++++++++++++ .../room-client/room-api-client/.gitignore | 4 + .../room-client/room-api-client/.npmignore | 1 + .../room-api-client/.openapi-generator-ignore | 23 +++ .../room-api-client/.openapi-generator/FILES | 21 +++ .../room-client/room-api-client/api.ts | 18 +++ .../room-api-client/api/course-rooms-api.ts | 148 +++++++++++++++++ .../room-client/room-api-client/base.ts | 86 ++++++++++ .../room-client/room-api-client/common.ts | 150 ++++++++++++++++++ .../room-api-client/configuration.ts | 110 +++++++++++++ .../room-client/room-api-client/git_push.sh | 57 +++++++ .../room-client/room-api-client/index.ts | 18 +++ .../models/board-column-board-response.ts | 66 ++++++++ .../models/board-element-response-content.ts | 36 +++++ .../models/board-element-response.ts | 48 ++++++ .../models/board-lesson-response.ts | 78 +++++++++ .../models/board-task-response.ts | 87 ++++++++++ .../models/board-task-status-response.ts | 60 +++++++ .../models/copy-api-response.ts | 115 ++++++++++++++ .../room-api-client/models/index.ts | 11 ++ .../models/lesson-copy-api-params.ts | 30 ++++ .../models/patch-order-params.ts | 30 ++++ .../models/patch-visibility-params.ts | 30 ++++ .../models/single-column-board-response.ts | 63 ++++++++ .../room-client/room-client.adapter.spec.ts | 113 +++++++++++++ .../room-client/room-client.adapter.ts | 37 +++++ .../room-client/room-client.module.spec.ts | 29 ++++ .../room-client/room-client.module.ts | 26 +++ .../common-cartridge.module.ts | 4 + .../common-cartridge-export.service.spec.ts | 33 ++++ .../common-cartridge-export.service.ts | 11 +- .../board-element.response.ts | 8 +- sonar-project.properties | 4 +- 48 files changed, 2077 insertions(+), 4 deletions(-) create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/courses-room-client.config.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/board-column-board.dto.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/board-element.dto.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/board-lesson.dto.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/board-task-status.dto.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/board-task.dto.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/room-board.dto.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/enums/board-element.enum.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/enums/board-layout.enum.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/index.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-column-board-layout-dto.mapper.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-column-board-layout-dto.mapper.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-task-status-dto.mapper.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-task-status-dto.mapper.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/room-board-dto.mapper.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/room-board-dto.mapper.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/.gitignore create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/.npmignore create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/.openapi-generator-ignore create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/.openapi-generator/FILES create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/api.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/api/course-rooms-api.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/base.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/common.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/configuration.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/git_push.sh create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/index.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-column-board-response.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-element-response-content.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-element-response.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-lesson-response.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-task-response.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-task-status-response.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/copy-api-response.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/index.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/lesson-copy-api-params.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/patch-order-params.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/patch-visibility-params.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/single-column-board-response.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.adapter.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.adapter.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.module.spec.ts create mode 100644 apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.module.ts diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/courses-room-client.config.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/courses-room-client.config.ts new file mode 100644 index 00000000000..ce503cd939e --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/courses-room-client.config.ts @@ -0,0 +1,5 @@ +import { ConfigurationParameters } from './room-api-client'; + +export interface CourseRoomsClientConfig extends ConfigurationParameters { + basePath?: string; +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/board-column-board.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/board-column-board.dto.ts new file mode 100644 index 00000000000..e911dd3d289 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/board-column-board.dto.ts @@ -0,0 +1,27 @@ +import { BoardLayout } from '../enums/board-layout.enum'; + +export class BoardColumnBoardDto { + id: string; + + title: string; + + published: boolean; + + createdAt: string; + + updatedAt: string; + + columnBoardId: string; + + layout: BoardLayout; + + constructor(props: BoardColumnBoardDto) { + this.id = props.id; + this.title = props.title; + this.published = props.published; + this.createdAt = props.createdAt; + this.updatedAt = props.updatedAt; + this.columnBoardId = props.columnBoardId; + this.layout = props.layout; + } +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/board-element.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/board-element.dto.ts new file mode 100644 index 00000000000..41789553320 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/board-element.dto.ts @@ -0,0 +1,15 @@ +import { BoardElementDtoType } from '../enums/board-element.enum'; +import { BoardColumnBoardDto } from './board-column-board.dto'; +import { BoardLessonDto } from './board-lesson.dto'; +import { BoardTaskDto } from './board-task.dto'; + +export class BoardElementDto { + type: BoardElementDtoType; + + content: BoardTaskDto | BoardLessonDto | BoardColumnBoardDto; + + constructor(props: BoardElementDto) { + this.type = props.type; + this.content = props.content; + } +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/board-lesson.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/board-lesson.dto.ts new file mode 100644 index 00000000000..0c35a3df486 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/board-lesson.dto.ts @@ -0,0 +1,30 @@ +export class BoardLessonDto { + id: string; + + name: string; + + courseName?: string; + + numberOfPublishedTasks: number; + + numberOfDraftTasks?: number; + + numberOfPlannedTasks?: number; + + createdAt: string; + + updatedAt: string; + + hidden: boolean; + + constructor(props: BoardLessonDto) { + this.id = props.id; + this.name = props.name; + this.hidden = props.hidden; + this.createdAt = props.createdAt; + this.updatedAt = props.updatedAt; + this.numberOfPublishedTasks = props.numberOfPublishedTasks; + this.numberOfDraftTasks = props.numberOfDraftTasks; + this.numberOfPlannedTasks = props.numberOfPlannedTasks; + } +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/board-task-status.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/board-task-status.dto.ts new file mode 100644 index 00000000000..1082f2dbf05 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/board-task-status.dto.ts @@ -0,0 +1,22 @@ +export class BoardTaskStatusDto { + submitted: number; + + maxSubmissions: number; + + graded: number; + + isDraft: boolean; + + isSubstitutionTeacher: boolean; + + isFinished: boolean; + + constructor(props: BoardTaskStatusDto) { + this.submitted = props.submitted; + this.maxSubmissions = props.maxSubmissions; + this.graded = props.graded; + this.isDraft = props.isDraft; + this.isSubstitutionTeacher = props.isSubstitutionTeacher; + this.isFinished = props.isFinished; + } +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/board-task.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/board-task.dto.ts new file mode 100644 index 00000000000..00594dd744b --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/board-task.dto.ts @@ -0,0 +1,36 @@ +import { BoardTaskStatusDto } from './board-task-status.dto'; + +export class BoardTaskDto { + id: string; + + name: string; + + availableDate?: string; + + dueDate?: string; + + courseName?: string; + + description?: string; + + displayColor?: string; + + createdAt: string; + + updatedAt: string; + + status: BoardTaskStatusDto; + + constructor(props: BoardTaskDto) { + this.id = props.id; + this.name = props.name; + this.availableDate = props.availableDate; + this.dueDate = props.dueDate; + this.courseName = props.courseName; + this.description = props.description; + this.displayColor = props.displayColor; + this.createdAt = props.createdAt; + this.updatedAt = props.updatedAt; + this.status = props.status; + } +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/room-board.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/room-board.dto.ts new file mode 100644 index 00000000000..a298fd68df9 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/dto/room-board.dto.ts @@ -0,0 +1,24 @@ +import { BoardElementDto } from './board-element.dto'; + +export class RoomBoardDto { + roomId: string; + + title: string; + + displayColor: string; + + elements: Array; + + isArchived: boolean; + + isSynchronized: boolean; + + constructor(props: RoomBoardDto) { + this.roomId = props.roomId; + this.title = props.title; + this.displayColor = props.displayColor; + this.elements = props.elements; + this.isArchived = props.isArchived; + this.isSynchronized = props.isSynchronized; + } +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/enums/board-element.enum.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/enums/board-element.enum.ts new file mode 100644 index 00000000000..9eb19b5ee2b --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/enums/board-element.enum.ts @@ -0,0 +1,5 @@ +export enum BoardElementDtoType { + TASK = 'task', + LESSON = 'lesson', + COLUMN_BOARD = 'column-board', +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/enums/board-layout.enum.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/enums/board-layout.enum.ts new file mode 100644 index 00000000000..afcc059e330 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/enums/board-layout.enum.ts @@ -0,0 +1,5 @@ +export enum BoardLayout { + COLUMNS = 'columns', + LIST = 'list', + GRID = 'grid', +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/index.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/index.ts new file mode 100644 index 00000000000..5cba82619a1 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/index.ts @@ -0,0 +1,2 @@ +export { CourseRoomsModule } from './room-client.module'; +export { CourseRoomsClientAdapter } from './room-client.adapter'; diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-column-board-layout-dto.mapper.spec.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-column-board-layout-dto.mapper.spec.ts new file mode 100644 index 00000000000..1864fd6aaa6 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-column-board-layout-dto.mapper.spec.ts @@ -0,0 +1,34 @@ +import { BoardColumnBoardLayoutMapper } from './board-column-board-layout-dto.mapper'; +import { BoardLayout } from '../enums/board-layout.enum'; + +describe(BoardColumnBoardLayoutMapper.name, () => { + describe('mapColumnBoardLayoutToDto', () => { + it('should map column layout to BoardLayout DTO', () => { + const layout = 'columns'; + const result = BoardColumnBoardLayoutMapper.mapColumnBoardLayoutToDto(layout); + + expect(result).toEqual(BoardLayout.COLUMNS); + }); + + it('should map list layout to BoardLayout DTO', () => { + const layout = 'list'; + const result = BoardColumnBoardLayoutMapper.mapColumnBoardLayoutToDto(layout); + + expect(result).toEqual(BoardLayout.LIST); + }); + + it('should map grid layout to BoardLayout DTO', () => { + const layout = 'grid'; + const result = BoardColumnBoardLayoutMapper.mapColumnBoardLayoutToDto(layout); + + expect(result).toEqual(BoardLayout.GRID); + }); + + it('should map unknown layout to BoardLayout DTO', () => { + const layout = 'unknown'; + const result = BoardColumnBoardLayoutMapper.mapColumnBoardLayoutToDto(layout); + + expect(result).toEqual(BoardLayout.COLUMNS); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-column-board-layout-dto.mapper.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-column-board-layout-dto.mapper.ts new file mode 100644 index 00000000000..00d518be615 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-column-board-layout-dto.mapper.ts @@ -0,0 +1,16 @@ +import { BoardLayout } from '../enums/board-layout.enum'; + +export class BoardColumnBoardLayoutMapper { + public static mapColumnBoardLayoutToDto(layout: string): BoardLayout { + switch (layout) { + case 'columns': + return BoardLayout.COLUMNS; + case 'list': + return BoardLayout.LIST; + case 'grid': + return BoardLayout.GRID; + default: + return BoardLayout.COLUMNS; + } + } +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-task-status-dto.mapper.spec.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-task-status-dto.mapper.spec.ts new file mode 100644 index 00000000000..f38859c9bad --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-task-status-dto.mapper.spec.ts @@ -0,0 +1,24 @@ +import { faker } from '@faker-js/faker'; +import { BoardTaskStatusResponse } from '../room-api-client'; +import { BoardTaskStatusMapper } from './board-task-status-dto.mapper'; +import { BoardTaskStatusDto } from '../dto/board-task-status.dto'; + +describe(BoardTaskStatusMapper.name, () => { + describe('mapBoardTaskStatusToDto', () => { + it('should map BoardTaskStatusResponse to BoardTaskStatusDto', () => { + const statusResponse: BoardTaskStatusResponse = { + submitted: faker.number.int(), + maxSubmissions: faker.number.int(), + graded: faker.number.int(), + isDraft: faker.datatype.boolean(), + isSubstitutionTeacher: faker.datatype.boolean(), + isFinished: faker.datatype.boolean(), + }; + + const statusDto = BoardTaskStatusMapper.mapBoardTaskStatusToDto(statusResponse); + + expect(statusDto).toBeInstanceOf(BoardTaskStatusDto); + expect(statusDto).toEqual(statusResponse); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-task-status-dto.mapper.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-task-status-dto.mapper.ts new file mode 100644 index 00000000000..c4834f148f9 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/board-task-status-dto.mapper.ts @@ -0,0 +1,15 @@ +import { BoardTaskStatusDto } from '../dto/board-task-status.dto'; +import { BoardTaskStatusResponse } from '../room-api-client'; + +export class BoardTaskStatusMapper { + public static mapBoardTaskStatusToDto(status: BoardTaskStatusResponse): BoardTaskStatusDto { + return new BoardTaskStatusDto({ + submitted: status.submitted, + maxSubmissions: status.maxSubmissions, + graded: status.graded, + isDraft: status.isDraft, + isSubstitutionTeacher: status.isSubstitutionTeacher, + isFinished: status.isFinished, + }); + } +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/room-board-dto.mapper.spec.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/room-board-dto.mapper.spec.ts new file mode 100644 index 00000000000..037ba49cae7 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/room-board-dto.mapper.spec.ts @@ -0,0 +1,143 @@ +import { faker } from '@faker-js/faker'; +import { + BoardColumnBoardResponse, + BoardElementResponse, + BoardElementResponseType, + BoardLessonResponse, + BoardTaskResponse, + SingleColumnBoardResponse, +} from '../room-api-client'; +import { RoomBoardDtoMapper } from './room-board-dto.mapper'; +import { BoardTaskStatusDto } from '../dto/board-task-status.dto'; + +describe(RoomBoardDtoMapper.name, () => { + describe('mapResponseToRoomBoardDto', () => { + describe('when response contains tasks', () => { + const setup = () => { + const task: BoardTaskResponse = { + id: faker.string.uuid(), + name: faker.lorem.word(), + createdAt: faker.date.recent().toString(), + updatedAt: faker.date.recent().toString(), + status: new BoardTaskStatusDto({ + submitted: faker.number.int(), + maxSubmissions: faker.number.int(), + graded: faker.number.int(), + isDraft: faker.datatype.boolean(), + isSubstitutionTeacher: faker.datatype.boolean(), + isFinished: faker.datatype.boolean(), + }), + availableDate: faker.date.recent().toString(), + courseName: faker.lorem.word(), + description: faker.lorem.word(), + displayColor: faker.lorem.word(), + dueDate: faker.date.recent().toString(), + }; + const boardElementResponse: BoardElementResponse = { + type: BoardElementResponseType.TASK, + content: { + ...task, + }, + }; + const response: SingleColumnBoardResponse = { + roomId: faker.string.uuid(), + title: faker.lorem.word(), + displayColor: faker.lorem.word(), + elements: [boardElementResponse], + isArchived: faker.datatype.boolean(), + isSynchronized: faker.datatype.boolean(), + }; + + return { response, task }; + }; + it('should map response to RoomBoardDto with tasks elements', () => { + const { response, task } = setup(); + const result = RoomBoardDtoMapper.mapResponseToRoomBoardDto(response); + + expect(result.elements[0].content.id).toEqual(task.id); + expect(result.elements[0].content.createdAt).toEqual(task.createdAt); + expect(result.elements[0].content.updatedAt).toEqual(task.updatedAt); + }); + }); + + describe('when response contains lessons', () => { + const setup = () => { + const lesson: BoardLessonResponse = { + id: faker.string.uuid(), + name: faker.lorem.word(), + courseName: faker.lorem.word(), + numberOfPublishedTasks: faker.number.int(), + numberOfDraftTasks: faker.number.int(), + numberOfPlannedTasks: faker.number.int(), + createdAt: faker.date.recent().toString(), + updatedAt: faker.date.recent().toString(), + hidden: faker.datatype.boolean(), + }; + const boardElementResponse: BoardElementResponse = { + type: BoardElementResponseType.LESSON, + content: { + ...lesson, + }, + }; + const response: SingleColumnBoardResponse = { + roomId: faker.string.uuid(), + title: faker.lorem.word(), + displayColor: faker.lorem.word(), + elements: [boardElementResponse], + isArchived: faker.datatype.boolean(), + isSynchronized: faker.datatype.boolean(), + }; + + return { response, lesson }; + }; + + it('should map response to RoomBoardDto with lesson elements', () => { + const { response, lesson } = setup(); + const result = RoomBoardDtoMapper.mapResponseToRoomBoardDto(response); + + expect(result.elements[0].content.id).toEqual(lesson.id); + expect(result.elements[0].content.createdAt).toEqual(lesson.createdAt); + expect(result.elements[0].content.updatedAt).toEqual(lesson.updatedAt); + }); + }); + + describe('when response contains columnboards', () => { + const setup = () => { + const columnBoard: BoardColumnBoardResponse = { + id: faker.string.uuid(), + title: faker.lorem.word(), + createdAt: faker.date.recent().toString(), + updatedAt: faker.date.recent().toString(), + published: faker.datatype.boolean(), + columnBoardId: faker.string.uuid(), + layout: faker.lorem.word(), + }; + const boardElementResponse: BoardElementResponse = { + type: BoardElementResponseType.COLUMN_BOARD, + content: { + ...columnBoard, + }, + }; + const response: SingleColumnBoardResponse = { + roomId: faker.string.uuid(), + title: faker.lorem.word(), + displayColor: faker.lorem.word(), + elements: [boardElementResponse], + isArchived: faker.datatype.boolean(), + isSynchronized: faker.datatype.boolean(), + }; + + return { response, columnBoard }; + }; + + it('should map response to RoomBoardDto with columnboard elements', () => { + const { response, columnBoard } = setup(); + const result = RoomBoardDtoMapper.mapResponseToRoomBoardDto(response); + + expect(result.elements[0].content.id).toEqual(columnBoard.id); + expect(result.elements[0].content.createdAt).toEqual(columnBoard.createdAt); + expect(result.elements[0].content.updatedAt).toEqual(columnBoard.updatedAt); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/room-board-dto.mapper.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/room-board-dto.mapper.ts new file mode 100644 index 00000000000..6bacca7eff6 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/mapper/room-board-dto.mapper.ts @@ -0,0 +1,123 @@ +import { BoardColumnBoardDto } from '../dto/board-column-board.dto'; +import { BoardElementDto } from '../dto/board-element.dto'; +import { BoardLessonDto } from '../dto/board-lesson.dto'; +import { BoardTaskDto } from '../dto/board-task.dto'; +import { RoomBoardDto } from '../dto/room-board.dto'; +import { BoardElementDtoType } from '../enums/board-element.enum'; +import { + BoardColumnBoardResponse, + BoardElementResponse, + BoardLessonResponse, + BoardTaskResponse, + SingleColumnBoardResponse, +} from '../room-api-client'; +import { BoardTaskStatusMapper } from './board-task-status-dto.mapper'; +import { BoardColumnBoardLayoutMapper } from './board-column-board-layout-dto.mapper'; + +export class RoomBoardDtoMapper { + public static mapResponseToRoomBoardDto(response: SingleColumnBoardResponse): RoomBoardDto { + const elements: BoardElementDto[] = this.mapBoardElements(response); + + const mapped: RoomBoardDto = new RoomBoardDto({ + roomId: response.roomId, + title: response.title, + displayColor: response.displayColor, + elements, + isArchived: response.isArchived, + isSynchronized: response.isSynchronized, + }); + + return mapped; + } + + private static mapBoardElements(response: SingleColumnBoardResponse): BoardElementDto[] { + const elements: BoardElementDto[] = []; + response.elements.forEach((element) => { + if (this.isBoardTaskResponse(element)) { + elements.push(this.mapTask(element.content as BoardTaskResponse)); + } + + if (this.isBoardLessonResponse(element)) { + elements.push(this.mapLesson(element.content as BoardLessonResponse)); + } + + if (this.isBoardColumnBoardResponse(element)) { + elements.push(this.mapColumnBoard(element.content as BoardColumnBoardResponse)); + } + }); + return elements; + } + + private static mapTask(task: BoardTaskResponse): BoardElementDto { + const mappedTask = new BoardTaskDto({ + id: task.id, + name: task.name, + createdAt: task.createdAt, + updatedAt: task.updatedAt, + availableDate: task.availableDate ?? undefined, + dueDate: task.dueDate ?? undefined, + courseName: task.courseName ?? undefined, + description: task.description ?? undefined, + displayColor: task.displayColor ?? undefined, + status: BoardTaskStatusMapper.mapBoardTaskStatusToDto(task.status), + }); + + const boardElmentDto = new BoardElementDto({ + type: BoardElementDtoType.TASK, + content: mappedTask, + }); + + return boardElmentDto; + } + + private static mapLesson(lesson: BoardLessonResponse): BoardElementDto { + const mappedLesson = new BoardLessonDto({ + id: lesson.id, + name: lesson.name, + hidden: lesson.hidden, + createdAt: lesson.createdAt, + updatedAt: lesson.updatedAt, + numberOfPublishedTasks: lesson.numberOfPublishedTasks, + numberOfDraftTasks: lesson.numberOfDraftTasks, + numberOfPlannedTasks: lesson.numberOfPlannedTasks, + }); + + const boardElmentDto = new BoardElementDto({ + type: BoardElementDtoType.LESSON, + content: mappedLesson, + }); + + return boardElmentDto; + } + + private static mapColumnBoard(columnBoard: BoardColumnBoardResponse): BoardElementDto { + const mappedColumnBoard = new BoardColumnBoardDto({ + id: columnBoard.id, + columnBoardId: columnBoard.columnBoardId, + title: columnBoard.title, + published: columnBoard.published, + createdAt: columnBoard.createdAt, + updatedAt: columnBoard.updatedAt, + layout: BoardColumnBoardLayoutMapper.mapColumnBoardLayoutToDto(columnBoard.layout), + }); + + const boardElmentDto = new BoardElementDto({ + type: BoardElementDtoType.COLUMN_BOARD, + content: mappedColumnBoard, + }); + + return boardElmentDto; + } + + private static isBoardTaskResponse(element: BoardElementResponse): element is BoardElementResponse { + return element.type === BoardElementDtoType.TASK; + } + + private static isBoardLessonResponse(element: BoardElementResponse): element is BoardElementResponse { + return element.type === BoardElementDtoType.LESSON; + } + + private static isBoardColumnBoardResponse(element: BoardElementResponse): element is BoardElementResponse { + return element.type === BoardElementDtoType.COLUMN_BOARD; + } +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/.gitignore b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/.gitignore new file mode 100644 index 00000000000..149b5765472 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/.gitignore @@ -0,0 +1,4 @@ +wwwroot/*.js +node_modules +typings +dist diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/.npmignore b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/.npmignore new file mode 100644 index 00000000000..999d88df693 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/.npmignore @@ -0,0 +1 @@ +# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm \ No newline at end of file diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/.openapi-generator-ignore b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/.openapi-generator-ignore new file mode 100644 index 00000000000..7484ee590a3 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/.openapi-generator/FILES b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/.openapi-generator/FILES new file mode 100644 index 00000000000..163bab6b37b --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/.openapi-generator/FILES @@ -0,0 +1,21 @@ +.gitignore +.npmignore +api.ts +api/course-rooms-api.ts +base.ts +common.ts +configuration.ts +git_push.sh +index.ts +models/board-column-board-response.ts +models/board-element-response-content.ts +models/board-element-response.ts +models/board-lesson-response.ts +models/board-task-response.ts +models/board-task-status-response.ts +models/copy-api-response.ts +models/index.ts +models/lesson-copy-api-params.ts +models/patch-order-params.ts +models/patch-visibility-params.ts +models/single-column-board-response.ts diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/api.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/api.ts new file mode 100644 index 00000000000..c8fb09d54bd --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/api.ts @@ -0,0 +1,18 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export * from './api/course-rooms-api'; + diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/api/course-rooms-api.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/api/course-rooms-api.ts new file mode 100644 index 00000000000..f8d4f728efa --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/api/course-rooms-api.ts @@ -0,0 +1,148 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from '../configuration'; +import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; +// Some imports not used depending on template conditions +// @ts-ignore +import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common'; +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; +// @ts-ignore +import type { SingleColumnBoardResponse } from '../models'; +/** + * CourseRoomsApi - axios parameter creator + * @export + */ +export const CourseRoomsApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @param {string} roomId The id of the room. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + courseRoomsControllerGetRoomBoard: async (roomId: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'roomId' is not null or undefined + assertParamExists('courseRoomsControllerGetRoomBoard', 'roomId', roomId) + const localVarPath = `/course-rooms/{roomId}/board` + .replace(`{${"roomId"}}`, encodeURIComponent(String(roomId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * CourseRoomsApi - functional programming interface + * @export + */ +export const CourseRoomsApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = CourseRoomsApiAxiosParamCreator(configuration) + return { + /** + * + * @param {string} roomId The id of the room. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async courseRoomsControllerGetRoomBoard(roomId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.courseRoomsControllerGetRoomBoard(roomId, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['CourseRoomsApi.courseRoomsControllerGetRoomBoard']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * CourseRoomsApi - factory interface + * @export + */ +export const CourseRoomsApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = CourseRoomsApiFp(configuration) + return { + /** + * + * @param {string} roomId The id of the room. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + courseRoomsControllerGetRoomBoard(roomId: string, options?: any): AxiosPromise { + return localVarFp.courseRoomsControllerGetRoomBoard(roomId, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * CourseRoomsApi - interface + * @export + * @interface CourseRoomsApi + */ +export interface CourseRoomsApiInterface { + /** + * + * @param {string} roomId The id of the room. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof CourseRoomsApiInterface + */ + courseRoomsControllerGetRoomBoard(roomId: string, options?: RawAxiosRequestConfig): AxiosPromise; + +} + +/** + * CourseRoomsApi - object-oriented interface + * @export + * @class CourseRoomsApi + * @extends {BaseAPI} + */ +export class CourseRoomsApi extends BaseAPI implements CourseRoomsApiInterface { + /** + * + * @param {string} roomId The id of the room. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof CourseRoomsApi + */ + public courseRoomsControllerGetRoomBoard(roomId: string, options?: RawAxiosRequestConfig) { + return CourseRoomsApiFp(this.configuration).courseRoomsControllerGetRoomBoard(roomId, options).then((request) => request(this.axios, this.basePath)); + } +} + diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/base.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/base.ts new file mode 100644 index 00000000000..82686c7b81b --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/base.ts @@ -0,0 +1,86 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from './configuration'; +// Some imports not used depending on template conditions +// @ts-ignore +import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; + +export const BASE_PATH = "http://localhost/api/v3".replace(/\/+$/, ""); + +/** + * + * @export + */ +export const COLLECTION_FORMATS = { + csv: ",", + ssv: " ", + tsv: "\t", + pipes: "|", +}; + +/** + * + * @export + * @interface RequestArgs + */ +export interface RequestArgs { + url: string; + options: RawAxiosRequestConfig; +} + +/** + * + * @export + * @class BaseAPI + */ +export class BaseAPI { + protected configuration: Configuration | undefined; + + constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) { + if (configuration) { + this.configuration = configuration; + this.basePath = configuration.basePath ?? basePath; + } + } +}; + +/** + * + * @export + * @class RequiredError + * @extends {Error} + */ +export class RequiredError extends Error { + constructor(public field: string, msg?: string) { + super(msg); + this.name = "RequiredError" + } +} + +interface ServerMap { + [key: string]: { + url: string, + description: string, + }[]; +} + +/** + * + * @export + */ +export const operationServerMap: ServerMap = { +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/common.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/common.ts new file mode 100644 index 00000000000..6c119efb60d --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/common.ts @@ -0,0 +1,150 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from "./configuration"; +import type { RequestArgs } from "./base"; +import type { AxiosInstance, AxiosResponse } from 'axios'; +import { RequiredError } from "./base"; + +/** + * + * @export + */ +export const DUMMY_BASE_URL = 'https://example.com' + +/** + * + * @throws {RequiredError} + * @export + */ +export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) { + if (paramValue === null || paramValue === undefined) { + throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`); + } +} + +/** + * + * @export + */ +export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) { + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? await configuration.apiKey(keyParamName) + : await configuration.apiKey; + object[keyParamName] = localVarApiKeyValue; + } +} + +/** + * + * @export + */ +export const setBasicAuthToObject = function (object: any, configuration?: Configuration) { + if (configuration && (configuration.username || configuration.password)) { + object["auth"] = { username: configuration.username, password: configuration.password }; + } +} + +/** + * + * @export + */ +export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) { + if (configuration && configuration.accessToken) { + const accessToken = typeof configuration.accessToken === 'function' + ? await configuration.accessToken() + : await configuration.accessToken; + object["Authorization"] = "Bearer " + accessToken; + } +} + +/** + * + * @export + */ +export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) { + if (configuration && configuration.accessToken) { + const localVarAccessTokenValue = typeof configuration.accessToken === 'function' + ? await configuration.accessToken(name, scopes) + : await configuration.accessToken; + object["Authorization"] = "Bearer " + localVarAccessTokenValue; + } +} + +function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ""): void { + if (parameter == null) return; + if (typeof parameter === "object") { + if (Array.isArray(parameter)) { + (parameter as any[]).forEach(item => setFlattenedQueryParams(urlSearchParams, item, key)); + } + else { + Object.keys(parameter).forEach(currentKey => + setFlattenedQueryParams(urlSearchParams, parameter[currentKey], `${key}${key !== '' ? '.' : ''}${currentKey}`) + ); + } + } + else { + if (urlSearchParams.has(key)) { + urlSearchParams.append(key, parameter); + } + else { + urlSearchParams.set(key, parameter); + } + } +} + +/** + * + * @export + */ +export const setSearchParams = function (url: URL, ...objects: any[]) { + const searchParams = new URLSearchParams(url.search); + setFlattenedQueryParams(searchParams, objects); + url.search = searchParams.toString(); +} + +/** + * + * @export + */ +export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) { + const nonString = typeof value !== 'string'; + const needsSerialization = nonString && configuration && configuration.isJsonMime + ? configuration.isJsonMime(requestOptions.headers['Content-Type']) + : nonString; + return needsSerialization + ? JSON.stringify(value !== undefined ? value : {}) + : (value || ""); +} + +/** + * + * @export + */ +export const toPathString = function (url: URL) { + return url.pathname + url.search + url.hash +} + +/** + * + * @export + */ +export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) { + return >(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { + const axiosRequestArgs = {...axiosArgs.options, url: (axios.defaults.baseURL ? '' : configuration?.basePath ?? basePath) + axiosArgs.url}; + return axios.request(axiosRequestArgs); + }; +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/configuration.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/configuration.ts new file mode 100644 index 00000000000..8c97d307cf4 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/configuration.ts @@ -0,0 +1,110 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface ConfigurationParameters { + apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); + username?: string; + password?: string; + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); + basePath?: string; + serverIndex?: number; + baseOptions?: any; + formDataCtor?: new () => any; +} + +export class Configuration { + /** + * parameter for apiKey security + * @param name security name + * @memberof Configuration + */ + apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); + /** + * parameter for basic security + * + * @type {string} + * @memberof Configuration + */ + username?: string; + /** + * parameter for basic security + * + * @type {string} + * @memberof Configuration + */ + password?: string; + /** + * parameter for oauth2 security + * @param name security name + * @param scopes oauth2 scope + * @memberof Configuration + */ + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); + /** + * override base path + * + * @type {string} + * @memberof Configuration + */ + basePath?: string; + /** + * override server index + * + * @type {number} + * @memberof Configuration + */ + serverIndex?: number; + /** + * base options for axios calls + * + * @type {any} + * @memberof Configuration + */ + baseOptions?: any; + /** + * The FormData constructor that will be used to create multipart form data + * requests. You can inject this here so that execution environments that + * do not support the FormData class can still run the generated client. + * + * @type {new () => FormData} + */ + formDataCtor?: new () => any; + + constructor(param: ConfigurationParameters = {}) { + this.apiKey = param.apiKey; + this.username = param.username; + this.password = param.password; + this.accessToken = param.accessToken; + this.basePath = param.basePath; + this.serverIndex = param.serverIndex; + this.baseOptions = param.baseOptions; + this.formDataCtor = param.formDataCtor; + } + + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * application/json + * application/json; charset=UTF8 + * APPLICATION/JSON + * application/vnd.company+json + * @param mime - MIME (Multipurpose Internet Mail Extensions) + * @return True if the given MIME is JSON, false otherwise. + */ + public isJsonMime(mime: string): boolean { + const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i'); + return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json'); + } +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/git_push.sh b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/git_push.sh new file mode 100644 index 00000000000..f53a75d4fab --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/git_push.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ "$git_host" = "" ]; then + git_host="github.com" + echo "[INFO] No command line input provided. Set \$git_host to $git_host" +fi + +if [ "$git_user_id" = "" ]; then + git_user_id="GIT_USER_ID" + echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" +fi + +if [ "$git_repo_id" = "" ]; then + git_repo_id="GIT_REPO_ID" + echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" +fi + +if [ "$release_note" = "" ]; then + release_note="Minor update" + echo "[INFO] No command line input provided. Set \$release_note to $release_note" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "$release_note" + +# Sets the new remote +git_remote=$(git remote) +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/index.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/index.ts new file mode 100644 index 00000000000..8b762df664e --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/index.ts @@ -0,0 +1,18 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export * from "./api"; +export * from "./configuration"; +export * from "./models"; diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-column-board-response.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-column-board-response.ts new file mode 100644 index 00000000000..f0afe361d78 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-column-board-response.ts @@ -0,0 +1,66 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface BoardColumnBoardResponse + */ +export interface BoardColumnBoardResponse { + /** + * + * @type {string} + * @memberof BoardColumnBoardResponse + */ + 'id': string; + /** + * + * @type {string} + * @memberof BoardColumnBoardResponse + */ + 'title': string; + /** + * + * @type {boolean} + * @memberof BoardColumnBoardResponse + */ + 'published': boolean; + /** + * + * @type {string} + * @memberof BoardColumnBoardResponse + */ + 'createdAt': string; + /** + * + * @type {string} + * @memberof BoardColumnBoardResponse + */ + 'updatedAt': string; + /** + * + * @type {string} + * @memberof BoardColumnBoardResponse + */ + 'columnBoardId': string; + /** + * + * @type {string} + * @memberof BoardColumnBoardResponse + */ + 'layout': string; +} + diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-element-response-content.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-element-response-content.ts new file mode 100644 index 00000000000..55580c32ca4 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-element-response-content.ts @@ -0,0 +1,36 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { BoardColumnBoardResponse } from './board-column-board-response'; +// May contain unused imports in some cases +// @ts-ignore +import type { BoardLessonResponse } from './board-lesson-response'; +// May contain unused imports in some cases +// @ts-ignore +import type { BoardTaskResponse } from './board-task-response'; +// May contain unused imports in some cases +// @ts-ignore +import type { BoardTaskStatusResponse } from './board-task-status-response'; + +/** + * @type BoardElementResponseContent + * Content of the Board, either: a task or a lesson specific for the board + * @export + */ +export type BoardElementResponseContent = BoardColumnBoardResponse | BoardLessonResponse | BoardTaskResponse; + + diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-element-response.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-element-response.ts new file mode 100644 index 00000000000..17eb24e5404 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-element-response.ts @@ -0,0 +1,48 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { BoardElementResponseContent } from './board-element-response-content'; + +/** + * + * @export + * @interface BoardElementResponse + */ +export interface BoardElementResponse { + /** + * the type of the element in the content. For possible types, please refer to the enum + * @type {string} + * @memberof BoardElementResponse + */ + 'type': BoardElementResponseType; + /** + * + * @type {BoardElementResponseContent} + * @memberof BoardElementResponse + */ + 'content': BoardElementResponseContent; +} + +export const BoardElementResponseType = { + TASK: 'task', + LESSON: 'lesson', + COLUMN_BOARD: 'column-board' +} as const; + +export type BoardElementResponseType = typeof BoardElementResponseType[keyof typeof BoardElementResponseType]; + + diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-lesson-response.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-lesson-response.ts new file mode 100644 index 00000000000..e4307965b62 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-lesson-response.ts @@ -0,0 +1,78 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface BoardLessonResponse + */ +export interface BoardLessonResponse { + /** + * + * @type {string} + * @memberof BoardLessonResponse + */ + 'id': string; + /** + * + * @type {string} + * @memberof BoardLessonResponse + */ + 'name': string; + /** + * + * @type {string} + * @memberof BoardLessonResponse + */ + 'courseName'?: string; + /** + * + * @type {number} + * @memberof BoardLessonResponse + */ + 'numberOfPublishedTasks': number; + /** + * + * @type {number} + * @memberof BoardLessonResponse + */ + 'numberOfDraftTasks': number; + /** + * + * @type {number} + * @memberof BoardLessonResponse + */ + 'numberOfPlannedTasks': number; + /** + * + * @type {string} + * @memberof BoardLessonResponse + */ + 'createdAt': string; + /** + * + * @type {string} + * @memberof BoardLessonResponse + */ + 'updatedAt': string; + /** + * + * @type {boolean} + * @memberof BoardLessonResponse + */ + 'hidden': boolean; +} + diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-task-response.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-task-response.ts new file mode 100644 index 00000000000..481bec69751 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-task-response.ts @@ -0,0 +1,87 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { BoardTaskStatusResponse } from './board-task-status-response'; + +/** + * + * @export + * @interface BoardTaskResponse + */ +export interface BoardTaskResponse { + /** + * + * @type {string} + * @memberof BoardTaskResponse + */ + 'id': string; + /** + * + * @type {string} + * @memberof BoardTaskResponse + */ + 'name': string; + /** + * + * @type {string} + * @memberof BoardTaskResponse + */ + 'availableDate'?: string; + /** + * + * @type {string} + * @memberof BoardTaskResponse + */ + 'dueDate'?: string; + /** + * + * @type {string} + * @memberof BoardTaskResponse + */ + 'courseName'?: string; + /** + * + * @type {string} + * @memberof BoardTaskResponse + */ + 'description'?: string; + /** + * + * @type {string} + * @memberof BoardTaskResponse + */ + 'displayColor'?: string; + /** + * + * @type {string} + * @memberof BoardTaskResponse + */ + 'createdAt': string; + /** + * + * @type {string} + * @memberof BoardTaskResponse + */ + 'updatedAt': string; + /** + * + * @type {BoardTaskStatusResponse} + * @memberof BoardTaskResponse + */ + 'status': BoardTaskStatusResponse; +} + diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-task-status-response.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-task-status-response.ts new file mode 100644 index 00000000000..dc177df29d9 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/board-task-status-response.ts @@ -0,0 +1,60 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface BoardTaskStatusResponse + */ +export interface BoardTaskStatusResponse { + /** + * + * @type {number} + * @memberof BoardTaskStatusResponse + */ + 'submitted': number; + /** + * + * @type {number} + * @memberof BoardTaskStatusResponse + */ + 'maxSubmissions': number; + /** + * + * @type {number} + * @memberof BoardTaskStatusResponse + */ + 'graded': number; + /** + * + * @type {boolean} + * @memberof BoardTaskStatusResponse + */ + 'isDraft': boolean; + /** + * + * @type {boolean} + * @memberof BoardTaskStatusResponse + */ + 'isSubstitutionTeacher': boolean; + /** + * + * @type {boolean} + * @memberof BoardTaskStatusResponse + */ + 'isFinished': boolean; +} + diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/copy-api-response.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/copy-api-response.ts new file mode 100644 index 00000000000..dd356d1e871 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/copy-api-response.ts @@ -0,0 +1,115 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface CopyApiResponse + */ +export interface CopyApiResponse { + /** + * Id of copied element + * @type {string} + * @memberof CopyApiResponse + */ + 'id'?: string; + /** + * Title of copied element + * @type {string} + * @memberof CopyApiResponse + */ + 'title'?: string; + /** + * Type of copied element + * @type {string} + * @memberof CopyApiResponse + */ + 'type': CopyApiResponseType; + /** + * Id of destination course + * @type {string} + * @memberof CopyApiResponse + */ + 'destinationCourseId'?: string; + /** + * Copy progress status of copied element + * @type {string} + * @memberof CopyApiResponse + */ + 'status': CopyApiResponseStatus; + /** + * List of included sub elements with recursive type structure + * @type {Array} + * @memberof CopyApiResponse + */ + 'elements'?: Array; +} + +export const CopyApiResponseType = { + BOARD: 'BOARD', + CARD: 'CARD', + COLLABORATIVE_TEXT_EDITOR_ELEMENT: 'COLLABORATIVE_TEXT_EDITOR_ELEMENT', + COLUMN: 'COLUMN', + COLUMNBOARD: 'COLUMNBOARD', + CONTENT: 'CONTENT', + COURSE: 'COURSE', + COURSEGROUP_GROUP: 'COURSEGROUP_GROUP', + DELETED_ELEMENT: 'DELETED_ELEMENT', + EXTERNAL_TOOL: 'EXTERNAL_TOOL', + EXTERNAL_TOOL_ELEMENT: 'EXTERNAL_TOOL_ELEMENT', + FILE: 'FILE', + FILE_ELEMENT: 'FILE_ELEMENT', + DRAWING_ELEMENT: 'DRAWING_ELEMENT', + FILE_GROUP: 'FILE_GROUP', + LEAF: 'LEAF', + LESSON: 'LESSON', + LESSON_CONTENT_ETHERPAD: 'LESSON_CONTENT_ETHERPAD', + LESSON_CONTENT_GEOGEBRA: 'LESSON_CONTENT_GEOGEBRA', + LESSON_CONTENT_GROUP: 'LESSON_CONTENT_GROUP', + LESSON_CONTENT_LERNSTORE: 'LESSON_CONTENT_LERNSTORE', + LESSON_CONTENT_NEXBOARD: 'LESSON_CONTENT_NEXBOARD', + LESSON_CONTENT_TASK: 'LESSON_CONTENT_TASK', + LESSON_CONTENT_TEXT: 'LESSON_CONTENT_TEXT', + LERNSTORE_MATERIAL: 'LERNSTORE_MATERIAL', + LERNSTORE_MATERIAL_GROUP: 'LERNSTORE_MATERIAL_GROUP', + LINK_ELEMENT: 'LINK_ELEMENT', + LTITOOL_GROUP: 'LTITOOL_GROUP', + MEDIA_BOARD: 'MEDIA_BOARD', + MEDIA_LINE: 'MEDIA_LINE', + MEDIA_EXTERNAL_TOOL_ELEMENT: 'MEDIA_EXTERNAL_TOOL_ELEMENT', + METADATA: 'METADATA', + RICHTEXT_ELEMENT: 'RICHTEXT_ELEMENT', + SUBMISSION_CONTAINER_ELEMENT: 'SUBMISSION_CONTAINER_ELEMENT', + SUBMISSION_ITEM: 'SUBMISSION_ITEM', + SUBMISSION_GROUP: 'SUBMISSION_GROUP', + TASK: 'TASK', + TASK_GROUP: 'TASK_GROUP', + TIME_GROUP: 'TIME_GROUP', + USER_GROUP: 'USER_GROUP' +} as const; + +export type CopyApiResponseType = typeof CopyApiResponseType[keyof typeof CopyApiResponseType]; +export const CopyApiResponseStatus = { + SUCCESS: 'success', + FAILURE: 'failure', + NOT_DOING: 'not-doing', + NOT_IMPLEMENTED: 'not-implemented', + PARTIAL: 'partial' +} as const; + +export type CopyApiResponseStatus = typeof CopyApiResponseStatus[keyof typeof CopyApiResponseStatus]; + + diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/index.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/index.ts new file mode 100644 index 00000000000..c92b2f05f3c --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/index.ts @@ -0,0 +1,11 @@ +export * from './board-column-board-response'; +export * from './board-element-response'; +export * from './board-element-response-content'; +export * from './board-lesson-response'; +export * from './board-task-response'; +export * from './board-task-status-response'; +export * from './copy-api-response'; +export * from './lesson-copy-api-params'; +export * from './patch-order-params'; +export * from './patch-visibility-params'; +export * from './single-column-board-response'; diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/lesson-copy-api-params.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/lesson-copy-api-params.ts new file mode 100644 index 00000000000..b7b460e8cce --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/lesson-copy-api-params.ts @@ -0,0 +1,30 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface LessonCopyApiParams + */ +export interface LessonCopyApiParams { + /** + * Destination course parent Id the lesson is copied to + * @type {string} + * @memberof LessonCopyApiParams + */ + 'courseId'?: string; +} + diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/patch-order-params.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/patch-order-params.ts new file mode 100644 index 00000000000..b41db00228d --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/patch-order-params.ts @@ -0,0 +1,30 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface PatchOrderParams + */ +export interface PatchOrderParams { + /** + * Array ids determining the new order + * @type {Array} + * @memberof PatchOrderParams + */ + 'elements': Array; +} + diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/patch-visibility-params.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/patch-visibility-params.ts new file mode 100644 index 00000000000..2e366709780 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/patch-visibility-params.ts @@ -0,0 +1,30 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface PatchVisibilityParams + */ +export interface PatchVisibilityParams { + /** + * true to publish the element, false to unpublish + * @type {boolean} + * @memberof PatchVisibilityParams + */ + 'visibility': boolean; +} + diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/single-column-board-response.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/single-column-board-response.ts new file mode 100644 index 00000000000..f1bf648bb76 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-api-client/models/single-column-board-response.ts @@ -0,0 +1,63 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +// May contain unused imports in some cases +// @ts-ignore +import type { BoardElementResponse } from './board-element-response'; + +/** + * + * @export + * @interface SingleColumnBoardResponse + */ +export interface SingleColumnBoardResponse { + /** + * The id of the room this board belongs to + * @type {string} + * @memberof SingleColumnBoardResponse + */ + 'roomId': string; + /** + * Title of the Board + * @type {string} + * @memberof SingleColumnBoardResponse + */ + 'title': string; + /** + * Color of the Board + * @type {string} + * @memberof SingleColumnBoardResponse + */ + 'displayColor': string; + /** + * Array of board specific tasks or lessons with matching type property + * @type {Array} + * @memberof SingleColumnBoardResponse + */ + 'elements': Array; + /** + * Boolean if the room this board belongs to is archived + * @type {boolean} + * @memberof SingleColumnBoardResponse + */ + 'isArchived': boolean; + /** + * Is the course synchronized with a group? + * @type {boolean} + * @memberof SingleColumnBoardResponse + */ + 'isSynchronized': boolean; +} + diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.adapter.spec.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.adapter.spec.ts new file mode 100644 index 00000000000..63ff0dbf697 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.adapter.spec.ts @@ -0,0 +1,113 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { REQUEST } from '@nestjs/core'; +import { faker } from '@faker-js/faker'; +import { Request } from 'express'; +import { AxiosResponse } from 'axios'; +import { jest } from '@jest/globals'; +import { CourseRoomsApi, SingleColumnBoardResponse } from './room-api-client'; +import { CourseRoomsClientAdapter } from './room-client.adapter'; +import { RoomBoardDtoMapper } from './mapper/room-board-dto.mapper'; +import { RoomBoardDto } from './dto/room-board.dto'; + +const jwtToken = 'dummyJwtToken'; + +describe(CourseRoomsClientAdapter.name, () => { + let module: TestingModule; + let service: CourseRoomsClientAdapter; + let courseRoomsApi: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + CourseRoomsClientAdapter, + { + provide: CourseRoomsApi, + useValue: createMock(), + }, + { + provide: REQUEST, + useValue: createMock({ + headers: { + authorization: `Bearer ${jwtToken}`, + }, + }), + }, + { + provide: RoomBoardDtoMapper, + useValue: createMock(), + }, + ], + }).compile(); + + service = module.get(CourseRoomsClientAdapter); + courseRoomsApi = module.get(CourseRoomsApi); + }); + + afterAll(async () => { + await module.close(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('getRoomBoardByCourseId', () => { + describe('when getRoomBoardByCourseId is called', () => { + const setup = () => { + const roomId = faker.string.uuid(); + const response = createMock>({ + data: { + roomId: faker.string.uuid(), + title: faker.lorem.word(), + displayColor: faker.date.recent().toString(), + isSynchronized: faker.datatype.boolean(), + }, + }); + + const mappedResponse: RoomBoardDto = { + roomId: response.data.roomId, + title: response.data.title, + displayColor: response.data.displayColor, + isSynchronized: response.data.isSynchronized, + elements: [], + isArchived: false, + }; + + jest.spyOn(RoomBoardDtoMapper, 'mapResponseToRoomBoardDto').mockReturnValueOnce(mappedResponse); + + return { roomId, mappedResponse }; + }; + + it('should return a room board with full contents', async () => { + const { roomId, mappedResponse } = setup(); + const result = await service.getRoomBoardByCourseId(roomId); + + expect(result).toEqual(mappedResponse); + }); + }); + }); + + describe('when no JWT token is found', () => { + const setup = () => { + const roomId = faker.string.uuid(); + const error = new Error('Authentication is required.'); + const request = createMock({ + headers: {}, + }); + const adapter: CourseRoomsClientAdapter = new CourseRoomsClientAdapter(courseRoomsApi, request); + + return { roomId, error, adapter }; + }; + + it('should throw an UnauthorizedException', async () => { + const { roomId, error, adapter } = setup(); + + await expect(adapter.getRoomBoardByCourseId(roomId)).rejects.toThrowError(error); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.adapter.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.adapter.ts new file mode 100644 index 00000000000..b53c3932334 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.adapter.ts @@ -0,0 +1,37 @@ +import { Inject, Injectable, UnauthorizedException } from '@nestjs/common'; +import { REQUEST } from '@nestjs/core'; +import { Request } from 'express'; +import { extractJwtFromHeader } from '@shared/common'; +import { RawAxiosRequestConfig } from 'axios'; +import { CourseRoomsApi } from './room-api-client'; +import { RoomBoardDto } from './dto/room-board.dto'; +import { RoomBoardDtoMapper } from './mapper/room-board-dto.mapper'; + +@Injectable() +export class CourseRoomsClientAdapter { + constructor(private readonly courseRoomsApi: CourseRoomsApi, @Inject(REQUEST) private request: Request) {} + + public async getRoomBoardByCourseId(roomId: string): Promise { + const options = this.createOptionParams(); + const response = await this.courseRoomsApi.courseRoomsControllerGetRoomBoard(roomId, options); + + return RoomBoardDtoMapper.mapResponseToRoomBoardDto(response.data); + } + + private createOptionParams(): RawAxiosRequestConfig { + const jwt = this.getJwt(); + const options: RawAxiosRequestConfig = { headers: { authorization: `Bearer ${jwt}` } }; + + return options; + } + + private getJwt(): string { + const jwt = extractJwtFromHeader(this.request) || this.request.headers.authorization; + + if (!jwt) { + throw new UnauthorizedException('Authentication is required.'); + } + + return jwt; + } +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.module.spec.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.module.spec.ts new file mode 100644 index 00000000000..777c37b0f57 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.module.spec.ts @@ -0,0 +1,29 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { CourseRoomsModule } from './room-client.module'; +import { CourseRoomsClientAdapter } from './room-client.adapter'; + +describe(CourseRoomsModule.name, () => { + let module: TestingModule; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + CourseRoomsModule.register({ + basePath: 'http://localhost:3000', + }), + ], + }).compile(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('when module is initialized', () => { + it('it should be defined', () => { + const courseRoomsClientAdapter = module.get(CourseRoomsClientAdapter); + + expect(courseRoomsClientAdapter).toBeDefined(); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.module.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.module.ts new file mode 100644 index 00000000000..f4c9f254991 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/common-cartridge-client/room-client/room-client.module.ts @@ -0,0 +1,26 @@ +import { DynamicModule, Module } from '@nestjs/common'; +import { CourseRoomsClientAdapter } from './room-client.adapter'; +import { Configuration, CourseRoomsApi } from './room-api-client'; +import { CourseRoomsClientConfig } from './courses-room-client.config'; + +@Module({}) +export class CourseRoomsModule { + static register(config: CourseRoomsClientConfig): DynamicModule { + const providers = [ + CourseRoomsClientAdapter, + { + provide: CourseRoomsApi, + useFactory: () => { + const configuration = new Configuration(config); + return new CourseRoomsApi(configuration); + }, + }, + ]; + + return { + module: CourseRoomsModule, + providers, + exports: [CourseRoomsClientAdapter], + }; + } +} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge.module.ts b/apps/server/src/modules/common-cartridge/common-cartridge.module.ts index 85e945167ed..9f54ce75802 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge.module.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge.module.ts @@ -10,6 +10,7 @@ import { BoardClientModule } from './common-cartridge-client/board-client'; import { CoursesClientModule } from './common-cartridge-client/course-client'; import { CommonCartridgeExportService } from './service/common-cartridge-export.service'; import { CommonCartridgeUc } from './uc/common-cartridge.uc'; +import { CourseRoomsModule } from './common-cartridge-client/room-client'; @Module({ imports: [ @@ -29,6 +30,9 @@ import { CommonCartridgeUc } from './uc/common-cartridge.uc'; BoardClientModule.register({ basePath: `${Configuration.get('API_HOST') as string}/v3/`, }), + CourseRoomsModule.register({ + basePath: `${Configuration.get('API_HOST') as string}/v3/`, + }), ], providers: [CommonCartridgeUc, CommonCartridgeExportService], exports: [CommonCartridgeUc], diff --git a/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.spec.ts b/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.spec.ts index beb45bd60b1..4a1b4b53066 100644 --- a/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.spec.ts +++ b/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.spec.ts @@ -5,12 +5,14 @@ import { Test, TestingModule } from '@nestjs/testing'; import { BoardClientAdapter } from '../common-cartridge-client/board-client'; import { CommonCartridgeExportService } from './common-cartridge-export.service'; import { CoursesClientAdapter } from '../common-cartridge-client/course-client'; +import { CourseRoomsClientAdapter } from '../common-cartridge-client/room-client'; describe('CommonCartridgeExportService', () => { let module: TestingModule; let sut: CommonCartridgeExportService; let filesStorageServiceMock: DeepMocked; let coursesClientAdapterMock: DeepMocked; + let courseRoomsClientAdapterMock: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -28,12 +30,17 @@ describe('CommonCartridgeExportService', () => { provide: CoursesClientAdapter, useValue: createMock(), }, + { + provide: CourseRoomsClientAdapter, + useValue: createMock(), + }, ], }).compile(); sut = module.get(CommonCartridgeExportService); filesStorageServiceMock = module.get(FilesStorageClientAdapterService); coursesClientAdapterMock = module.get(CoursesClientAdapter); + courseRoomsClientAdapterMock = module.get(CourseRoomsClientAdapter); }); afterAll(async () => { @@ -85,4 +92,30 @@ describe('CommonCartridgeExportService', () => { expect(result).toEqual(expected); }); }); + + describe('findCourseRoomBoard', () => { + const setup = () => { + const roomId = faker.string.uuid(); + const expected = { + roomId, + title: faker.lorem.word(), + displayColor: faker.date.recent().toString(), + isSynchronized: faker.datatype.boolean(), + elements: [], + isArchived: faker.datatype.boolean(), + }; + + courseRoomsClientAdapterMock.getRoomBoardByCourseId.mockResolvedValue(expected); + + return { roomId, expected }; + }; + + it('should return a room board', async () => { + const { roomId, expected } = setup(); + + const result = await sut.findRoomBoardByCourseId(roomId); + + expect(result).toEqual(expected); + }); + }); }); diff --git a/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.ts b/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.ts index fbd0e81ee8f..9d358dba255 100644 --- a/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.ts +++ b/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.ts @@ -2,13 +2,16 @@ import { FileDto, FilesStorageClientAdapterService } from '@modules/files-storag import { Injectable } from '@nestjs/common'; import { BoardClientAdapter } from '../common-cartridge-client/board-client'; import { CourseCommonCartridgeMetadataDto, CoursesClientAdapter } from '../common-cartridge-client/course-client'; +import { CourseRoomsClientAdapter } from '../common-cartridge-client/room-client'; +import { RoomBoardDto } from '../common-cartridge-client/room-client/dto/room-board.dto'; @Injectable() export class CommonCartridgeExportService { constructor( private readonly filesService: FilesStorageClientAdapterService, private readonly boardClientAdapter: BoardClientAdapter, - private readonly coursesClientAdapter: CoursesClientAdapter + private readonly coursesClientAdapter: CoursesClientAdapter, + private readonly courseRoomsClientAdapter: CourseRoomsClientAdapter ) {} public async findCourseFileRecords(courseId: string): Promise { @@ -22,4 +25,10 @@ export class CommonCartridgeExportService { return courseCommonCartridgeMetadata; } + + public async findRoomBoardByCourseId(courseId: string): Promise { + const courseRooms = await this.courseRoomsClientAdapter.getRoomBoardByCourseId(courseId); + + return courseRooms; + } } 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 4e9d139124b..16a4c828d1f 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,9 +1,10 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiExtraModels, ApiProperty, getSchemaPath } from '@nestjs/swagger'; 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'; +@ApiExtraModels(BoardTaskResponse, BoardLessonResponse, BoardColumnBoardResponse) export class BoardElementResponse { constructor({ type, content }: BoardElementResponse) { this.type = type; @@ -18,6 +19,11 @@ export class BoardElementResponse { @ApiProperty({ description: 'Content of the Board, either: a task or a lesson specific for the board', + oneOf: [ + { $ref: getSchemaPath(BoardTaskResponse) }, + { $ref: getSchemaPath(BoardLessonResponse) }, + { $ref: getSchemaPath(BoardColumnBoardResponse) }, + ], }) content: BoardTaskResponse | BoardLessonResponse | BoardColumnBoardResponse; } diff --git a/sonar-project.properties b/sonar-project.properties index f85109f473b..79aa1797c82 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,8 +3,8 @@ sonar.projectKey=hpi-schul-cloud_schulcloud-server sonar.sources=. sonar.tests=. sonar.test.inclusions=**/*.spec.ts -sonar.exclusions=**/*.js,jest.config.ts,globalSetup.ts,globalTeardown.ts,**/*.app.ts,**/seed-data/*.ts,**/migrations/mikro-orm/*.ts,**/etherpad-api-client/**/*.ts,**/authorization-api-client/**/*.ts, **/course-api-client/**/*.ts,**/board-api-client/**/*.ts,**/generated/**/*.ts -sonar.coverage.exclusions=**/board-management.uc.ts,**/*.module.ts,**/*.factory.ts,**/migrations/mikro-orm/*.ts,**/globalSetup.ts,**/globalTeardown.ts,**/etherpad-api-client/**/*.ts,**/authorization-api-client/**/*.ts, **/course-api-client/**/*.ts,**/board-api-client/**/*.ts,**/generated/**/*.ts +sonar.exclusions=**/*.js,jest.config.ts,globalSetup.ts,globalTeardown.ts,**/*.app.ts,**/seed-data/*.ts,**/migrations/mikro-orm/*.ts,**/etherpad-api-client/**/*.ts,**/authorization-api-client/**/*.ts, **/course-api-client/**/*.ts,**/board-api-client/**/*.ts,**/generated/**/*.ts,**/room-api-client/**/*.ts +sonar.coverage.exclusions=**/board-management.uc.ts,**/*.module.ts,**/*.factory.ts,**/migrations/mikro-orm/*.ts,**/globalSetup.ts,**/globalTeardown.ts,**/etherpad-api-client/**/*.ts,**/authorization-api-client/**/*.ts, **/course-api-client/**/*.ts,**/board-api-client/**/*.ts,**/generated/**/*.ts,**/room-api-client/**/*.ts sonar.cpd.exclusions=**/controller/dto/**/*.ts,**/api/dto/**/*.ts,**/shared/testing/factory/*.factory.ts sonar.javascript.lcov.reportPaths=merged-lcov.info sonar.typescript.tsconfigPaths=tsconfig.json,src/apps/server/tsconfig.app.json From f11a3fa69c94493982f4a4b6d8253cfed922ff73 Mon Sep 17 00:00:00 2001 From: mkreuzkam-cap <144103168+mkreuzkam-cap@users.noreply.github.com> Date: Mon, 14 Oct 2024 15:46:39 +0200 Subject: [PATCH 09/10] EW-1006: Add school part to TSP sync. (#5279) * Add school part to sync. --------- Co-authored-by: Alexander Weber --- apps/server/src/infra/sync/index.ts | 1 + apps/server/src/infra/sync/sync.module.ts | 22 +- apps/server/src/infra/sync/tsp/index.ts | 2 + .../tsp-schools-fetched.loggable.spec.ts | 27 ++ .../loggable/tsp-schools-fetched.loggable.ts | 17 + .../tsp-schools-synced.loggable.spec.ts | 29 ++ .../loggable/tsp-schools-synced.loggable.ts | 24 ++ .../tsp-schulnummer-missing.loggable.spec.ts | 26 ++ .../tsp-schulnummer-missing.loggable.ts | 16 + ...ystem-not-found.loggable-exception.spec.ts | 25 ++ ...tsp-system-not-found.loggable-exception.ts | 26 ++ .../src/infra/sync/tsp/tsp-sync.config.ts | 4 + .../infra/sync/tsp/tsp-sync.service.spec.ts | 333 ++++++++++++++++++ .../src/infra/sync/tsp/tsp-sync.service.ts | 114 ++++++ .../infra/sync/tsp/tsp-sync.strategy.spec.ts | 136 ++++++- .../src/infra/sync/tsp/tsp-sync.strategy.ts | 72 +++- .../types/federal-state-names.enum.ts | 1 + .../src/modules/school/domain/do/school.ts | 4 + .../src/modules/server/server.config.ts | 4 + config/default.schema.json | 10 + 20 files changed, 875 insertions(+), 18 deletions(-) create mode 100644 apps/server/src/infra/sync/index.ts create mode 100644 apps/server/src/infra/sync/tsp/index.ts create mode 100644 apps/server/src/infra/sync/tsp/loggable/tsp-schools-fetched.loggable.spec.ts create mode 100644 apps/server/src/infra/sync/tsp/loggable/tsp-schools-fetched.loggable.ts create mode 100644 apps/server/src/infra/sync/tsp/loggable/tsp-schools-synced.loggable.spec.ts create mode 100644 apps/server/src/infra/sync/tsp/loggable/tsp-schools-synced.loggable.ts create mode 100644 apps/server/src/infra/sync/tsp/loggable/tsp-schulnummer-missing.loggable.spec.ts create mode 100644 apps/server/src/infra/sync/tsp/loggable/tsp-schulnummer-missing.loggable.ts create mode 100644 apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.spec.ts create mode 100644 apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.ts create mode 100644 apps/server/src/infra/sync/tsp/tsp-sync.config.ts create mode 100644 apps/server/src/infra/sync/tsp/tsp-sync.service.spec.ts create mode 100644 apps/server/src/infra/sync/tsp/tsp-sync.service.ts diff --git a/apps/server/src/infra/sync/index.ts b/apps/server/src/infra/sync/index.ts new file mode 100644 index 00000000000..0a3165f6f52 --- /dev/null +++ b/apps/server/src/infra/sync/index.ts @@ -0,0 +1 @@ +export * from './tsp'; diff --git a/apps/server/src/infra/sync/sync.module.ts b/apps/server/src/infra/sync/sync.module.ts index 2516e4b13df..5295e22a922 100644 --- a/apps/server/src/infra/sync/sync.module.ts +++ b/apps/server/src/infra/sync/sync.module.ts @@ -1,19 +1,31 @@ +import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { ConsoleWriterModule } from '@infra/console'; +import { TspClientModule } from '@infra/tsp-client/tsp-client.module'; +import { LegacySchoolModule } from '@modules/legacy-school'; +import { SchoolModule } from '@modules/school'; +import { SystemModule } from '@modules/system'; import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; -import { ConsoleWriterModule } from '@infra/console'; -import { Configuration } from '@hpi-schul-cloud/commons/lib'; +import { RabbitMQWrapperModule } from '@infra/rabbitmq'; import { SyncConsole } from './console/sync.console'; -import { SyncUc } from './uc/sync.uc'; import { SyncService } from './service/sync.service'; +import { TspSyncService } from './tsp/tsp-sync.service'; import { TspSyncStrategy } from './tsp/tsp-sync.strategy'; +import { SyncUc } from './uc/sync.uc'; @Module({ - imports: [LoggerModule, ConsoleWriterModule], + imports: [ + LoggerModule, + ConsoleWriterModule, + ...((Configuration.get('FEATURE_TSP_SYNC_ENABLED') as boolean) + ? [TspClientModule, SystemModule, SchoolModule, LegacySchoolModule, RabbitMQWrapperModule] + : []), + ], providers: [ SyncConsole, SyncUc, SyncService, - ...((Configuration.get('FEATURE_TSP_SYNC_ENABLED') as boolean) ? [TspSyncStrategy] : []), + ...((Configuration.get('FEATURE_TSP_SYNC_ENABLED') as boolean) ? [TspSyncStrategy, TspSyncService] : []), ], exports: [SyncConsole], }) diff --git a/apps/server/src/infra/sync/tsp/index.ts b/apps/server/src/infra/sync/tsp/index.ts new file mode 100644 index 00000000000..86513d07df8 --- /dev/null +++ b/apps/server/src/infra/sync/tsp/index.ts @@ -0,0 +1,2 @@ +export { TspSyncConfig } from './tsp-sync.config'; +export { TspSyncStrategy } from './tsp-sync.strategy'; diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-schools-fetched.loggable.spec.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-schools-fetched.loggable.spec.ts new file mode 100644 index 00000000000..c97e2609e7c --- /dev/null +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-schools-fetched.loggable.spec.ts @@ -0,0 +1,27 @@ +import { TspSchoolsFetchedLoggable } from './tsp-schools-fetched.loggable'; + +describe(TspSchoolsFetchedLoggable.name, () => { + let loggable: TspSchoolsFetchedLoggable; + + beforeAll(() => { + loggable = new TspSchoolsFetchedLoggable(10, 5); + }); + + describe('when loggable is initialized', () => { + it('should be defined', () => { + expect(loggable).toBeDefined(); + }); + }); + + describe('getLogMessage', () => { + it('should return a log message', () => { + expect(loggable.getLogMessage()).toEqual({ + message: `Fetched 10 schools for the last 5 days from TSP`, + data: { + tspSchoolCount: 10, + daysFetched: 5, + }, + }); + }); + }); +}); diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-schools-fetched.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-schools-fetched.loggable.ts new file mode 100644 index 00000000000..62843fee0c3 --- /dev/null +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-schools-fetched.loggable.ts @@ -0,0 +1,17 @@ +import { Loggable, LogMessage } from '@src/core/logger'; + +export class TspSchoolsFetchedLoggable implements Loggable { + constructor(private readonly tspSchoolCount: number, private readonly daysFetched: number) {} + + getLogMessage(): LogMessage { + const message: LogMessage = { + message: `Fetched ${this.tspSchoolCount} schools for the last ${this.daysFetched} days from TSP`, + data: { + tspSchoolCount: this.tspSchoolCount, + daysFetched: this.daysFetched, + }, + }; + + return message; + } +} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-schools-synced.loggable.spec.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-schools-synced.loggable.spec.ts new file mode 100644 index 00000000000..c74ce2ade74 --- /dev/null +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-schools-synced.loggable.spec.ts @@ -0,0 +1,29 @@ +import { TspSchoolsSyncedLoggable } from './tsp-schools-synced.loggable'; + +describe(TspSchoolsSyncedLoggable.name, () => { + let loggable: TspSchoolsSyncedLoggable; + + beforeAll(() => { + loggable = new TspSchoolsSyncedLoggable(10, 10, 5, 5); + }); + + describe('when loggable is initialized', () => { + it('should be defined', () => { + expect(loggable).toBeDefined(); + }); + }); + + describe('getLogMessage', () => { + it('should return a log message', () => { + expect(loggable.getLogMessage()).toEqual({ + message: `Synced schools: Of 10 schools 10 were processed. 5 were created and 5 were updated`, + data: { + tspSchoolCount: 10, + processedSchools: 10, + createdSchools: 5, + updatedSchools: 5, + }, + }); + }); + }); +}); diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-schools-synced.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-schools-synced.loggable.ts new file mode 100644 index 00000000000..e10ef69d194 --- /dev/null +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-schools-synced.loggable.ts @@ -0,0 +1,24 @@ +import { Loggable, LogMessage } from '@src/core/logger'; + +export class TspSchoolsSyncedLoggable implements Loggable { + constructor( + private readonly tspSchoolCount: number, + private readonly processedSchools: number, + private readonly createdSchools: number, + private readonly updatedSchools: number + ) {} + + getLogMessage(): LogMessage { + const message: LogMessage = { + message: `Synced schools: Of ${this.tspSchoolCount} schools ${this.processedSchools} were processed. ${this.createdSchools} were created and ${this.updatedSchools} were updated`, + data: { + tspSchoolCount: this.tspSchoolCount, + processedSchools: this.processedSchools, + createdSchools: this.createdSchools, + updatedSchools: this.updatedSchools, + }, + }; + + return message; + } +} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-schulnummer-missing.loggable.spec.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-schulnummer-missing.loggable.spec.ts new file mode 100644 index 00000000000..4627ff49634 --- /dev/null +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-schulnummer-missing.loggable.spec.ts @@ -0,0 +1,26 @@ +import { TspSchulnummerMissingLoggable } from './tsp-schulnummer-missing.loggable'; + +describe(TspSchulnummerMissingLoggable.name, () => { + let loggable: TspSchulnummerMissingLoggable; + + beforeAll(() => { + loggable = new TspSchulnummerMissingLoggable('Schule'); + }); + + describe('when loggable is initialized', () => { + it('should be defined', () => { + expect(loggable).toBeDefined(); + }); + }); + + describe('getLogMessage', () => { + it('should return a log message', () => { + expect(loggable.getLogMessage()).toEqual({ + message: `The TSP school 'Schule' is missing a Schulnummer. This school is skipped.`, + data: { + schulName: 'Schule', + }, + }); + }); + }); +}); diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-schulnummer-missing.loggable.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-schulnummer-missing.loggable.ts new file mode 100644 index 00000000000..ddd8e68f9df --- /dev/null +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-schulnummer-missing.loggable.ts @@ -0,0 +1,16 @@ +import { Loggable, LogMessage } from '@src/core/logger'; + +export class TspSchulnummerMissingLoggable implements Loggable { + constructor(private readonly schulName?: string) {} + + getLogMessage(): LogMessage { + const message: LogMessage = { + message: `The TSP school '${this.schulName ?? ''}' is missing a Schulnummer. This school is skipped.`, + data: { + schulName: this.schulName, + }, + }; + + return message; + } +} diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.spec.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.spec.ts new file mode 100644 index 00000000000..4b1655271b5 --- /dev/null +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.spec.ts @@ -0,0 +1,25 @@ +import { TspSystemNotFoundLoggableException } from './tsp-system-not-found.loggable-exception'; + +describe(TspSystemNotFoundLoggableException.name, () => { + let loggable: TspSystemNotFoundLoggableException; + + beforeAll(() => { + loggable = new TspSystemNotFoundLoggableException(); + }); + + describe('when loggable is initialized', () => { + it('should be defined', () => { + expect(loggable).toBeDefined(); + }); + }); + + describe('getLogMessage', () => { + it('should return a log message', () => { + expect(loggable.getLogMessage()).toEqual({ + message: 'The TSP system could not be found during the sync', + type: 'TSP_SYSTEM_NOT_FOUND', + stack: expect.any(String), + }); + }); + }); +}); diff --git a/apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.ts b/apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.ts new file mode 100644 index 00000000000..d7225c1cafa --- /dev/null +++ b/apps/server/src/infra/sync/tsp/loggable/tsp-system-not-found.loggable-exception.ts @@ -0,0 +1,26 @@ +import { HttpStatus } from '@nestjs/common'; +import { BusinessError, ErrorLogMessage } from '@shared/common'; +import { Loggable, LogMessage } from '@src/core/logger'; + +export class TspSystemNotFoundLoggableException extends BusinessError implements Loggable { + constructor() { + super( + { + type: 'TSP_SYSTEM_NOT_FOUND', + title: 'The TSP system could not be found', + defaultMessage: 'The TSP system could not be found during the sync', + }, + HttpStatus.BAD_REQUEST + ); + } + + getLogMessage(): LogMessage | ErrorLogMessage { + const message: LogMessage | ErrorLogMessage = { + message: this.message, + type: this.type, + stack: this.stack, + }; + + return message; + } +} diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.config.ts b/apps/server/src/infra/sync/tsp/tsp-sync.config.ts new file mode 100644 index 00000000000..9b1e8064337 --- /dev/null +++ b/apps/server/src/infra/sync/tsp/tsp-sync.config.ts @@ -0,0 +1,4 @@ +export interface TspSyncConfig { + TSP_SYNC_SCHOOL_LIMIT: number; + TSP_SYNC_SCHOOL_DAYS_TO_FETCH: number; +} diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.service.spec.ts b/apps/server/src/infra/sync/tsp/tsp-sync.service.spec.ts new file mode 100644 index 00000000000..486d4c2bb57 --- /dev/null +++ b/apps/server/src/infra/sync/tsp/tsp-sync.service.spec.ts @@ -0,0 +1,333 @@ +import { faker } from '@faker-js/faker'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { ExportApiInterface, RobjExportSchule, TspClientFactory } from '@infra/tsp-client'; +import { School, SchoolService } from '@modules/school'; +import { SystemService, SystemType } from '@modules/system'; +import { Test, TestingModule } from '@nestjs/testing'; +import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; +import { federalStateFactory, schoolYearFactory } from '@shared/testing'; +import { FederalStateService, SchoolYearService } from '@src/modules/legacy-school'; +import { SchoolProps } from '@src/modules/school/domain'; +import { FederalStateEntityMapper, SchoolYearEntityMapper } from '@src/modules/school/repo/mikro-orm/mapper'; +import { schoolFactory } from '@src/modules/school/testing'; +import { systemFactory } from '@src/modules/system/testing'; +import { AxiosResponse } from 'axios'; +import { TspSyncService } from './tsp-sync.service'; + +describe(TspSyncService.name, () => { + let module: TestingModule; + let sut: TspSyncService; + let tspClientFactory: DeepMocked; + let systemService: DeepMocked; + let schoolService: DeepMocked; + let federalStateService: DeepMocked; + let schoolYearService: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + TspSyncService, + { + provide: TspClientFactory, + useValue: createMock(), + }, + { + provide: SystemService, + useValue: createMock(), + }, + { + provide: SchoolService, + useValue: createMock(), + }, + { + provide: FederalStateService, + useValue: createMock(), + }, + { + provide: SchoolYearService, + useValue: createMock(), + }, + ], + }).compile(); + + sut = module.get(TspSyncService); + tspClientFactory = module.get(TspClientFactory); + systemService = module.get(SystemService); + schoolService = module.get(SchoolService); + federalStateService = module.get(FederalStateService); + schoolYearService = module.get(SchoolYearService); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('when sync service is initialized', () => { + it('should be defined', () => { + expect(sut).toBeDefined(); + }); + }); + + describe('findTspSystemOrFail', () => { + describe('when tsp system is found', () => { + const setup = () => { + const system = systemFactory.build({ + type: SystemType.OAUTH, + provisioningStrategy: SystemProvisioningStrategy.TSP, + }); + + systemService.find.mockResolvedValueOnce([system]); + }; + + it('should be returned', async () => { + setup(); + + const system = await sut.findTspSystemOrFail(); + + expect(system).toBeDefined(); + }); + }); + + describe('when tsp system is not found', () => { + const setup = () => { + systemService.find.mockResolvedValueOnce([]); + }; + + it('should throw a TspSystemNotFound exception', async () => { + setup(); + + await expect(sut.findTspSystemOrFail()).rejects.toThrow(); + }); + }); + }); + + describe('fetchTspSchools', () => { + describe('when tsp schools are fetched', () => { + const setup = () => { + const clientId = faker.string.alpha(); + const clientSecret = faker.string.alpha(); + const tokenEndpoint = faker.internet.url(); + const system = systemFactory.build({ + oauthConfig: { + clientId, + clientSecret, + tokenEndpoint, + }, + }); + + const tspSchool: RobjExportSchule = { + schuleName: faker.string.alpha(), + schuleNummer: faker.string.alpha(), + }; + const schools = [tspSchool]; + const response = createMock>>({ + data: schools, + }); + + const exportApiMock = createMock(); + exportApiMock.exportSchuleList.mockResolvedValueOnce(response); + tspClientFactory.createExportClient.mockReturnValueOnce(exportApiMock); + + return { clientId, clientSecret, tokenEndpoint, system, exportApiMock, schools }; + }; + + it('should use the oauthConfig to create the client', async () => { + const { clientId, clientSecret, tokenEndpoint, system } = setup(); + + await sut.fetchTspSchools(system, 1); + + expect(tspClientFactory.createExportClient).toHaveBeenCalledWith({ + clientId, + clientSecret, + tokenEndpoint, + }); + }); + + it('should call exportSchuleList', async () => { + const { system, exportApiMock } = setup(); + + await sut.fetchTspSchools(system, 1); + + expect(exportApiMock.exportSchuleList).toHaveBeenCalledTimes(1); + }); + + it('should return an array of schools', async () => { + const { system } = setup(); + + const schools = await sut.fetchTspSchools(system, 1); + + expect(schools).toBeDefined(); + expect(schools).toBeInstanceOf(Array); + }); + }); + }); + + describe('findSchool', () => { + describe('when school is found', () => { + const setup = () => { + const externalId = faker.string.alpha(); + const system = systemFactory.build(); + const school = schoolFactory.build(); + + schoolService.getSchools.mockResolvedValueOnce([school]); + + return { externalId, system }; + }; + + it('should return the school', async () => { + const { externalId, system } = setup(); + + const result = await sut.findSchool(system, externalId); + + expect(result).toBeInstanceOf(School); + }); + }); + + describe('when school is not found', () => { + const setup = () => { + const externalId = faker.string.alpha(); + const system = systemFactory.build(); + + schoolService.getSchools.mockResolvedValueOnce([]); + + return { externalId, system }; + }; + + it('should return undefined', async () => { + const { externalId, system } = setup(); + + const result = await sut.findSchool(system, externalId); + + expect(result).toBeUndefined(); + }); + }); + }); + + describe('updateSchool', () => { + describe('when school is updated', () => { + const setup = () => { + const newName = faker.string.alpha(); + const oldName = faker.string.alpha(); + const school = schoolFactory.build({ + name: oldName, + }); + + return { newName, school }; + }; + + it('should set the new name', async () => { + const { newName, school } = setup(); + + await sut.updateSchool(school, newName); + + expect(schoolService.save).toHaveBeenCalledWith({ + props: expect.objectContaining>({ + name: newName, + }) as Partial, + }); + }); + }); + + describe('when school name is undefined', () => { + const setup = () => { + const newName = undefined; + const oldName = faker.string.alpha(); + const school = schoolFactory.build({ + name: oldName, + }); + + return { newName, school }; + }; + + it('should not update school', async () => { + const { newName, school } = setup(); + + await sut.updateSchool(school, newName); + + expect(schoolService.save).not.toHaveBeenCalled(); + }); + }); + }); + + describe('createSchool', () => { + describe('when school is created', () => { + const setup = () => { + const system = systemFactory.build(); + const name = faker.string.alpha(); + const externalId = faker.string.alpha(); + + const schoolYearEntity = schoolYearFactory.build(); + const schoolYear = SchoolYearEntityMapper.mapToDo(schoolYearEntity); + schoolYearService.getCurrentSchoolYear.mockResolvedValueOnce(schoolYearEntity); + + const federalStateEntity = federalStateFactory.build(); + const federalState = FederalStateEntityMapper.mapToDo(federalStateEntity); + federalStateService.findFederalStateByName.mockResolvedValueOnce(federalStateEntity); + + schoolService.save.mockResolvedValueOnce(schoolFactory.build()); + + return { system, name, externalId, schoolYear, federalState }; + }; + + it('should be returned', async () => { + const { system, name, externalId, schoolYear, federalState } = setup(); + + const school = await sut.createSchool(system, externalId, name); + + expect(school).toBeDefined(); + expect(schoolService.save).toHaveBeenCalledWith({ + props: expect.objectContaining>({ + name, + externalId, + systemIds: [system.id], + federalState, + currentYear: schoolYear, + }) as Partial, + }); + }); + }); + + describe('when federalState is already cached', () => { + const setup = () => { + const system = systemFactory.build(); + const name = faker.string.alpha(); + const externalId = faker.string.alpha(); + + const schoolYearEntity = schoolYearFactory.build(); + const schoolYear = SchoolYearEntityMapper.mapToDo(schoolYearEntity); + schoolYearService.getCurrentSchoolYear.mockResolvedValueOnce(schoolYearEntity); + + const federalStateEntity = federalStateFactory.build(); + const federalState = FederalStateEntityMapper.mapToDo(federalStateEntity); + Reflect.set(sut, 'federalState', federalState); + + schoolService.save.mockResolvedValueOnce(schoolFactory.build()); + + return { system, name, externalId, schoolYear, federalState }; + }; + + it('should be used and not loaded again', async () => { + const { system, name, externalId, schoolYear, federalState } = setup(); + + const school = await sut.createSchool(system, externalId, name); + + expect(school).toBeDefined(); + expect(schoolService.save).toHaveBeenCalledWith({ + props: expect.objectContaining>({ + name, + externalId, + systemIds: [system.id], + federalState, + currentYear: schoolYear, + }) as Partial, + }); + expect(federalStateService.findFederalStateByName).not.toHaveBeenCalled(); + }); + }); + }); +}); diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.service.ts b/apps/server/src/infra/sync/tsp/tsp-sync.service.ts new file mode 100644 index 00000000000..00ff1d2429e --- /dev/null +++ b/apps/server/src/infra/sync/tsp/tsp-sync.service.ts @@ -0,0 +1,114 @@ +import { RobjExportSchule, TspClientFactory } from '@infra/tsp-client'; +import { FederalStateService, SchoolYearService } from '@modules/legacy-school'; +import { School, SchoolService } from '@modules/school'; +import { System, SystemService, SystemType } from '@modules/system'; +import { Injectable } from '@nestjs/common'; +import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; +import { SchoolFeature } from '@shared/domain/types'; +import { FederalStateNames } from '@src/modules/legacy-school/types'; +import { FederalState } from '@src/modules/school/domain'; +import { SchoolFactory } from '@src/modules/school/domain/factory'; +import { FederalStateEntityMapper, SchoolYearEntityMapper } from '@src/modules/school/repo/mikro-orm/mapper'; +import { ObjectId } from 'bson'; +import moment from 'moment/moment'; +import { TspSystemNotFoundLoggableException } from './loggable/tsp-system-not-found.loggable-exception'; + +@Injectable() +export class TspSyncService { + private federalState: FederalState | undefined; + + constructor( + private readonly tspClientFactory: TspClientFactory, + private readonly systemService: SystemService, + private readonly schoolService: SchoolService, + private readonly federalStateService: FederalStateService, + private readonly schoolYearService: SchoolYearService + ) {} + + public async findTspSystemOrFail(): Promise { + const systems = ( + await this.systemService.find({ + types: [SystemType.OAUTH, SystemType.OIDC], + }) + ).filter((system) => system.provisioningStrategy === SystemProvisioningStrategy.TSP); + + if (systems.length === 0) { + throw new TspSystemNotFoundLoggableException(); + } + + return systems[0]; + } + + public async fetchTspSchools(system: System, daysToFetch: number) { + const client = this.tspClientFactory.createExportClient({ + clientId: system.oauthConfig?.clientId ?? '', + clientSecret: system.oauthConfig?.clientSecret ?? '', + tokenEndpoint: system.oauthConfig?.tokenEndpoint ?? '', + }); + + const lastChangeDate = this.formatChangeDate(daysToFetch); + const schools: RobjExportSchule[] = (await client.exportSchuleList(lastChangeDate)).data; + + return schools; + } + + public async findSchool(system: System, identifier: string): Promise { + const schools = await this.schoolService.getSchools({ + externalId: identifier, + systemId: system.id, + }); + + if (schools.length === 0) { + return undefined; + } + return schools[0]; + } + + public async updateSchool(school: School, name?: string): Promise { + if (!name) { + return school; + } + + school.name = name; + + const updatedSchool = await this.schoolService.save(school); + + return updatedSchool; + } + + public async createSchool(system: System, identifier: string, name: string): Promise { + const schoolYearEntity = await this.schoolYearService.getCurrentSchoolYear(); + const schoolYear = SchoolYearEntityMapper.mapToDo(schoolYearEntity); + const federalState = await this.findFederalState(); + + const school = SchoolFactory.build({ + externalId: identifier, + name, + systemIds: [system.id], + federalState, + currentYear: schoolYear, + features: new Set([SchoolFeature.OAUTH_PROVISIONING_ENABLED]), + createdAt: new Date(), + updatedAt: new Date(), + id: new ObjectId().toHexString(), + }); + + const savedSchool = await this.schoolService.save(school); + + return savedSchool; + } + + private async findFederalState(): Promise { + if (this.federalState) { + return this.federalState; + } + + const federalStateEntity = await this.federalStateService.findFederalStateByName(FederalStateNames.THUERINGEN); + this.federalState = FederalStateEntityMapper.mapToDo(federalStateEntity); + return this.federalState; + } + + private formatChangeDate(daysToFetch: number): string { + return moment(new Date()).subtract(daysToFetch, 'days').subtract(1, 'hours').format('YYYY-MM-DD HH:mm:ss.SSS'); + } +} diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts index fce598102eb..ae1767a934c 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.spec.ts @@ -1,17 +1,57 @@ -import { TestingModule, Test } from '@nestjs/testing'; +import { faker } from '@faker-js/faker'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { RobjExportSchule } from '@infra/tsp-client'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Logger } from '@src/core/logger'; +import { schoolFactory } from '@src/modules/school/testing'; import { SyncStrategyTarget } from '../sync-strategy.types'; +import { TspSyncConfig } from './tsp-sync.config'; +import { TspSyncService } from './tsp-sync.service'; import { TspSyncStrategy } from './tsp-sync.strategy'; describe(TspSyncStrategy.name, () => { let module: TestingModule; - let strategy: TspSyncStrategy; + let sut: TspSyncStrategy; + let tspSyncService: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ - providers: [TspSyncStrategy], + providers: [ + TspSyncStrategy, + { + provide: TspSyncService, + useValue: createMock(), + }, + { + provide: Logger, + useValue: createMock(), + }, + { + provide: ConfigService, + useValue: createMock>({ + getOrThrow: (key: string) => { + switch (key) { + case 'TSP_SYNC_SCHOOL_LIMIT': + return 10; + case 'TSP_SYNC_SCHOOL_DAYS_TO_FETCH': + return 1; + default: + throw new Error(`Unknown key: ${key}`); + } + }, + }), + }, + ], }).compile(); - strategy = module.get(TspSyncStrategy); + sut = module.get(TspSyncStrategy); + tspSyncService = module.get(TspSyncService); + }); + + afterEach(() => { + jest.clearAllMocks(); + jest.resetAllMocks(); }); afterAll(async () => { @@ -20,23 +60,101 @@ describe(TspSyncStrategy.name, () => { describe('when tsp sync strategy is initialized', () => { it('should be defined', () => { - expect(strategy).toBeDefined(); + expect(sut).toBeDefined(); }); }); describe('getType', () => { describe('when tsp sync strategy is initialized', () => { it('should return tsp', () => { - expect(strategy.getType()).toBe(SyncStrategyTarget.TSP); + expect(sut.getType()).toBe(SyncStrategyTarget.TSP); }); }); }); describe('sync', () => { - it('should return a promise', () => { - const result = strategy.sync(); + describe('when sync is called', () => { + const setup = () => { + tspSyncService.fetchTspSchools.mockResolvedValueOnce([]); + }; + + it('should find the tsp system', async () => { + await sut.sync(); + + expect(tspSyncService.findTspSystemOrFail).toHaveBeenCalled(); + }); + + it('should fetch the schools', async () => { + setup(); + + await sut.sync(); + + expect(tspSyncService.fetchTspSchools).toHaveBeenCalled(); + }); + }); + + describe('when school does not exist', () => { + const setup = () => { + const tspSchool: RobjExportSchule = { + schuleNummer: faker.string.alpha(), + schuleName: faker.string.alpha(), + }; + const tspSchools = [tspSchool]; + tspSyncService.fetchTspSchools.mockResolvedValueOnce(tspSchools); + + tspSyncService.findSchool.mockResolvedValueOnce(undefined); + }; + + it('should create the school', async () => { + setup(); + + await sut.sync(); - expect(result).toBeInstanceOf(Promise); + expect(tspSyncService.createSchool).toHaveBeenCalled(); + }); + }); + + describe('when school does exist', () => { + const setup = () => { + const tspSchool: RobjExportSchule = { + schuleNummer: faker.string.alpha(), + schuleName: faker.string.alpha(), + }; + const tspSchools = [tspSchool]; + tspSyncService.fetchTspSchools.mockResolvedValueOnce(tspSchools); + + const school = schoolFactory.build(); + tspSyncService.findSchool.mockResolvedValueOnce(school); + }; + + it('should update the school', async () => { + setup(); + + await sut.sync(); + + expect(tspSyncService.updateSchool).toHaveBeenCalled(); + }); + }); + + describe('when tsp school does not have a schulnummer', () => { + const setup = () => { + const tspSchool: RobjExportSchule = { + schuleNummer: undefined, + schuleName: faker.string.alpha(), + }; + const tspSchools = [tspSchool]; + tspSyncService.fetchTspSchools.mockResolvedValueOnce(tspSchools); + }; + + it('should skip the school', async () => { + setup(); + + await sut.sync(); + + expect(tspSyncService.findSchool).not.toHaveBeenCalled(); + expect(tspSyncService.updateSchool).not.toHaveBeenCalled(); + expect(tspSyncService.createSchool).not.toHaveBeenCalled(); + }); }); }); }); diff --git a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.ts b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.ts index 5f7eb3a44c5..c0cb551c3e3 100644 --- a/apps/server/src/infra/sync/tsp/tsp-sync.strategy.ts +++ b/apps/server/src/infra/sync/tsp/tsp-sync.strategy.ts @@ -1,15 +1,79 @@ +import { School } from '@modules/school'; import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { Logger } from '@src/core/logger'; +import { System } from '@src/modules/system'; +import pLimit from 'p-limit'; import { SyncStrategy } from '../strategy/sync-strategy'; import { SyncStrategyTarget } from '../sync-strategy.types'; +import { TspSchoolsFetchedLoggable } from './loggable/tsp-schools-fetched.loggable'; +import { TspSchoolsSyncedLoggable } from './loggable/tsp-schools-synced.loggable'; +import { TspSchulnummerMissingLoggable } from './loggable/tsp-schulnummer-missing.loggable'; +import { TspSyncConfig } from './tsp-sync.config'; +import { TspSyncService } from './tsp-sync.service'; @Injectable() export class TspSyncStrategy extends SyncStrategy { - getType(): SyncStrategyTarget { + private readonly schoolLimit: pLimit.Limit; + + private readonly schoolDaysToFetch: number; + + constructor( + private readonly logger: Logger, + private readonly tspSyncService: TspSyncService, + configService: ConfigService + ) { + super(); + this.logger.setContext(TspSyncStrategy.name); + this.schoolLimit = pLimit(configService.getOrThrow('TSP_SYNC_SCHOOL_LIMIT')); + this.schoolDaysToFetch = configService.get('TSP_SYNC_SCHOOL_DAYS_TO_FETCH', 1); + } + + public override getType(): SyncStrategyTarget { return SyncStrategyTarget.TSP; } - sync(): Promise { - // implementation - return Promise.resolve(); + public async sync(): Promise { + const system = await this.tspSyncService.findTspSystemOrFail(); + + await this.syncSchools(system); + } + + private async syncSchools(system: System): Promise { + const tspSchools = await this.tspSyncService.fetchTspSchools(system, this.schoolDaysToFetch); + this.logger.info(new TspSchoolsFetchedLoggable(tspSchools.length, this.schoolDaysToFetch)); + + const schoolPromises = tspSchools.map((tspSchool) => + this.schoolLimit(async () => { + if (!tspSchool.schuleNummer) { + this.logger.warning(new TspSchulnummerMissingLoggable()); + return null; + } + + const existingSchool = await this.tspSyncService.findSchool(system, tspSchool.schuleNummer); + + if (existingSchool) { + const updatedSchool = await this.tspSyncService.updateSchool(existingSchool, tspSchool.schuleName); + return { school: updatedSchool, created: false }; + } + + const createdSchool = await this.tspSyncService.createSchool( + system, + tspSchool.schuleNummer, + tspSchool.schuleName ?? '' + ); + return { school: createdSchool, created: true }; + }) + ); + + const scSchools = await Promise.all(schoolPromises); + + const total = tspSchools.length; + const totalProcessed = scSchools.filter((scSchool) => scSchool != null).length; + const createdSchools = scSchools.filter((scSchool) => scSchool != null && scSchool.created).length; + const updatedSchools = scSchools.filter((scSchool) => scSchool != null && !scSchool.created).length; + this.logger.info(new TspSchoolsSyncedLoggable(total, totalProcessed, createdSchools, updatedSchools)); + + return scSchools.filter((scSchool) => scSchool != null).map((scSchool) => scSchool.school); } } diff --git a/apps/server/src/modules/legacy-school/types/federal-state-names.enum.ts b/apps/server/src/modules/legacy-school/types/federal-state-names.enum.ts index e6d426e45f6..b9b74ff4656 100644 --- a/apps/server/src/modules/legacy-school/types/federal-state-names.enum.ts +++ b/apps/server/src/modules/legacy-school/types/federal-state-names.enum.ts @@ -1,3 +1,4 @@ export enum FederalStateNames { NIEDERSACHEN = 'Niedersachsen', + THUERINGEN = 'Thüringen', } diff --git a/apps/server/src/modules/school/domain/do/school.ts b/apps/server/src/modules/school/domain/do/school.ts index eaa237827d9..5a94772db20 100644 --- a/apps/server/src/modules/school/domain/do/school.ts +++ b/apps/server/src/modules/school/domain/do/school.ts @@ -40,6 +40,10 @@ export class School extends DomainObject { this.props.ldapLastSync = ldapLastSync; } + set name(name: string) { + this.props.name = name; + } + public getInfo(): SchoolInfo { const info = { id: this.props.id, diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index ca5aa6f9d02..c967ba2a5de 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -3,6 +3,7 @@ import { XApiKeyConfig } from '@infra/auth-guard'; import type { IdentityManagementConfig } from '@infra/identity-management'; import type { MailConfig } from '@infra/mail/interfaces/mail-config'; import type { SchulconnexClientConfig } from '@infra/schulconnex-client'; +import { TspSyncConfig } from '@infra/sync'; import type { TspClientConfig } from '@infra/tsp-client'; import type { AccountConfig } from '@modules/account'; import { AlertConfig } from '@modules/alert'; @@ -70,6 +71,7 @@ export interface ServerConfig VideoConferenceConfig, BbbConfig, TspClientConfig, + TspSyncConfig, AlertConfig, ShdConfig { NODE_ENV: NodeEnvType; @@ -310,6 +312,8 @@ const config: ServerConfig = { FEATURE_ROOMS_ENABLED: Configuration.get('FEATURE_ROOMS_ENABLED') as boolean, TSP_API_BASE_URL: Configuration.get('TSP_API_BASE_URL') as string, TSP_API_TOKEN_LIFETIME_MS: Configuration.get('TSP_API_TOKEN_LIFETIME_MS') as number, + TSP_SYNC_SCHOOL_LIMIT: Configuration.get('TSP_SYNC_SCHOOL_LIMIT') as number, + TSP_SYNC_SCHOOL_DAYS_TO_FETCH: Configuration.get('TSP_SYNC_SCHOOL_DAYS_TO_FETCH') as number, ROCKET_CHAT_URI: Configuration.get('ROCKET_CHAT_URI') as string, ROCKET_CHAT_ADMIN_ID: Configuration.get('ROCKET_CHAT_ADMIN_ID') as string, ROCKET_CHAT_ADMIN_TOKEN: Configuration.get('ROCKET_CHAT_ADMIN_TOKEN') as string, diff --git a/config/default.schema.json b/config/default.schema.json index 387e7331b1a..d13131bf0e9 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -179,6 +179,16 @@ "default": "30000", "description": "The TSP token lifetime in milliseconds." }, + "TSP_SYNC_SCHOOL_LIMIT": { + "type": "number", + "default": "10", + "description": "The amount of schools the sync handles at once." + }, + "TSP_SYNC_SCHOOL_DAYS_TO_FETCH": { + "type": "number", + "default": "1", + "description": "The amount of days for which the sync fetches schools from the TSP." + }, "FEATURE_TSP_ENABLED": { "type": "boolean", "default": false, From 6752c05cd912b3029a7f3f7e6b0f839ce1c55d73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marvin=20=C3=96hlerking?= <103562092+MarvinOehlerkingCap@users.noreply.github.com> Date: Mon, 14 Oct 2024 16:25:14 +0200 Subject: [PATCH 10/10] N21-2216 Fix outdated users for team invitations (#5286) --- src/services/teams/index.js | 131 ++++++++++++---------- src/services/user/hooks/publicTeachers.js | 2 +- 2 files changed, 71 insertions(+), 62 deletions(-) diff --git a/src/services/teams/index.js b/src/services/teams/index.js index 693b7c37a90..119147cfa33 100644 --- a/src/services/teams/index.js +++ b/src/services/teams/index.js @@ -1,8 +1,8 @@ // eslint-disable-next-line max-classes-per-file -const service = require('../../utils/feathers-mongoose'); const { Configuration } = require('@hpi-schul-cloud/commons'); const { static: staticContent } = require('@feathersjs/express'); const path = require('path'); +const service = require('../../utils/feathers-mongoose'); const { NotFound, BadRequest, GeneralError } = require('../../errors'); const hooks = require('./hooks'); @@ -29,6 +29,7 @@ const { equal: equalIds } = require('../../helper/compare').ObjectId; const HOST = Configuration.get('HOST'); const { AdminOverview } = require('./services'); +const { SCHOOL_FEATURES } = require('../school/model'); class Get { constructor(options) { @@ -94,14 +95,20 @@ class Add { * @param {String} email * @return {Promise::User} */ - async _getUsersByEmail(email) { + async _getUsersByEmail(email, school) { + const query = { + email, + $populate: [{ path: 'roles' }, { path: 'schoolId' }], + }; + + if (!school.features?.includes(SCHOOL_FEATURES.SHOW_OUTDATED_USERS)) { + query.outdatedSince = null; + } + return this.app .service('users') .find({ - query: { - email, - $populate: [{ path: 'roles' }], - }, + query, }) .then((users) => extractOne(users)) .catch((err) => { @@ -154,62 +161,64 @@ class Add { * }} */ async _collectUserAndLinkData({ email, role, teamId }) { - return Promise.all([ - // eslint-disable-next-line no-underscore-dangle - this._getUsersByEmail(email), - // eslint-disable-next-line no-underscore-dangle - this._getExpertSchoolId(), - // eslint-disable-next-line no-underscore-dangle - this._getExpertRoleId(), - getTeam(this, teamId), - ]) - .then(async ([user, schoolId, expertRoleId, team]) => { - let isUserCreated = false; - let isResend = false; - let userRoleName; - if (isUndefined(user) && role === 'teamexpert') { - const newUser = { - email, - schoolId, - roles: [expertRoleId], - firstName: 'Experte', - lastName: 'Experte', - }; - // eslint-disable-next-line no-param-reassign - user = await userModel.create(newUser); - isUserCreated = true; - } - - if (isUserCreated || isDefined(role)) { - userRoleName = role; - } else { - const teamUser = team.invitedUserIds.find((invited) => invited.email === email); - isResend = true; - userRoleName = (teamUser || {}).role || role; - } - - // if role teamadmin by import from teacher over email and - // no user exist, the user is undefined - if (isUndefined(user)) { - throw new BadRequest('User must exist.'); - } - if (isUndefined(userRoleName)) { - throw new BadRequest('For this case the team role for user must be set.'); - } - return { - esid: schoolId, - isUserCreated, - isResend, - user, - team, - userRoleName, - importHash: user.importHash, + try { + const team = await getTeam(this, teamId); + const school = await this.app.service('schools').get(team.schoolId); + // eslint-disable-next-line prefer-const + let [user, schoolId, expertRoleId] = await Promise.all([ + // eslint-disable-next-line no-underscore-dangle + this._getUsersByEmail(email, school), + // eslint-disable-next-line no-underscore-dangle + this._getExpertSchoolId(), + // eslint-disable-next-line no-underscore-dangle + this._getExpertRoleId(), + ]); + let isUserCreated = false; + let isResend = false; + let userRoleName; + if (isUndefined(user) && role === 'teamexpert') { + const newUser = { + email, + schoolId, + roles: [expertRoleId], + firstName: 'Experte', + lastName: 'Experte', }; - }) - .catch((err) => { - warning(err); - throw new BadRequest('Can not resolve the user information.'); - }); + // eslint-disable-next-line no-param-reassign + user = await userModel.create(newUser); + isUserCreated = true; + } + + if (isUserCreated || isDefined(role)) { + userRoleName = role; + } else { + const teamUser = team.invitedUserIds.find((invited) => invited.email === email); + isResend = true; + userRoleName = (teamUser || {}).role || role; + } + + // if role teamadmin by import from teacher over email and + // no user exist, the user is undefined + if (isUndefined(user)) { + throw new BadRequest('User must exist.'); + } + if (isUndefined(userRoleName)) { + throw new BadRequest('For this case the team role for user must be set.'); + } + + return { + esid: schoolId, + isUserCreated, + isResend, + user, + team, + userRoleName, + importHash: user.importHash, + }; + } catch (err) { + warning(err); + throw new BadRequest('Can not resolve the user information.'); + } } /** diff --git a/src/services/user/hooks/publicTeachers.js b/src/services/user/hooks/publicTeachers.js index 9a59e3a6fdd..3c57290b712 100644 --- a/src/services/user/hooks/publicTeachers.js +++ b/src/services/user/hooks/publicTeachers.js @@ -26,7 +26,7 @@ const mapRoleFilterQuery = (hook) => { const filterForPublicTeacher = (hook) => { // Limit accessible fields - hook.params.query.$select = ['_id', 'firstName', 'lastName', 'schoolId']; + hook.params.query.$select = ['_id', 'firstName', 'lastName', 'schoolId', 'outdatedSince']; // Limit accessible user (only teacher which are discoverable) hook.params.query.roles = ['teacher'];