From 430871952845094443e24e25f9254881eae69059 Mon Sep 17 00:00:00 2001 From: mkreuzkam-cap <144103168+mkreuzkam-cap@users.noreply.github.com> Date: Wed, 2 Oct 2024 10:31:24 +0200 Subject: [PATCH 1/8] EW-1025: Add new tsp sync cronjob. (#5258) * EW-1025: Add new tsp sync cronjob. --------- Co-authored-by: mamutmk5 <3045922+mamutmk5@users.noreply.github.com> --- .../defaults/main.yml | 1 + .../schulcloud-server-tspsync/tasks/main.yml | 30 +++++ .../api-tsp-sync-cronjob-configmap.yml.j2 | 18 +++ .../templates/api-tsp-sync-cronjob.yml.j2 | 109 ++++++++++++++++++ 4 files changed, 158 insertions(+) create mode 100644 ansible/roles/schulcloud-server-tspsync/defaults/main.yml create mode 100644 ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-cronjob-configmap.yml.j2 create mode 100644 ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-cronjob.yml.j2 diff --git a/ansible/roles/schulcloud-server-tspsync/defaults/main.yml b/ansible/roles/schulcloud-server-tspsync/defaults/main.yml new file mode 100644 index 00000000000..3d8c26f28b4 --- /dev/null +++ b/ansible/roles/schulcloud-server-tspsync/defaults/main.yml @@ -0,0 +1 @@ +SERVER_TSP_SYNC_CRONJOB_SCHEDULE: "9 3 * * *" \ No newline at end of file diff --git a/ansible/roles/schulcloud-server-tspsync/tasks/main.yml b/ansible/roles/schulcloud-server-tspsync/tasks/main.yml index 3f323efb681..05ea93d07ef 100644 --- a/ansible/roles/schulcloud-server-tspsync/tasks/main.yml +++ b/ansible/roles/schulcloud-server-tspsync/tasks/main.yml @@ -81,3 +81,33 @@ when: not WITH_TSP tags: - cronjob + + - name: API TSP Sync CronJob + kubernetes.core.k8s: + kubeconfig: ~/.kube/config + namespace: "{{ NAMESPACE }}" + template: api-tsp-sync-cronjob.yml.j2 + when: WITH_TSP_SYNC + tags: + - cronjob + + - name: remove API TSP Sync CronJob + kubernetes.core.k8s: + kubeconfig: ~/.kube/config + state: absent + api_version: batch/v1 + kind: CronJob + namespace: "{{ NAMESPACE }}" + name: api-tsp-sync-cronjob + when: not WITH_TSP_SYNC + tags: + - cronjob + + - name: API TSP Sync CronJob ConfigMap + kubernetes.core.k8s: + kubeconfig: ~/.kube/config + namespace: "{{ NAMESPACE }}" + template: api-tsp-sync-cronjob-configmap.yml.j2 + state: "{{ 'present' if WITH_TSP_SYNC|bool else 'absent'}}" + tags: + - configmap diff --git a/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-cronjob-configmap.yml.j2 b/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-cronjob-configmap.yml.j2 new file mode 100644 index 00000000000..c93af4d7c2e --- /dev/null +++ b/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-cronjob-configmap.yml.j2 @@ -0,0 +1,18 @@ +apiVersion: v1 +kind: ConfigMap +metadata: + namespace: {{ NAMESPACE }} + name: api-tsp-sync-cronjob-configmap + labels: + app: api-tsp-sync-cronjob +data: + NODE_OPTIONS: "--max-old-space-size=3072" + NEST_LOG_LEVEL: "error" + EXIT_ON_ERROR: "true" + FEATURE_IDENTITY_MANAGEMENT_ENABLED: "{{ FEATURE_IDENTITY_MANAGEMENT_ENABLED }}" + FEATURE_IDENTITY_MANAGEMENT_STORE_ENABLED: "{{ FEATURE_IDENTITY_MANAGEMENT_STORE_ENABLED }}" + FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED: "{{ FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED }}" + IDENTITY_MANAGEMENT__INTERNAL_URI: "{{ IDENTITY_MANAGEMENT__INTERNAL_URI }}" + IDENTITY_MANAGEMENT__EXTERNAL_URI: "{{ IDENTITY_MANAGEMENT__EXTERNAL_URI }}" + IDENTITY_MANAGEMENT__TENANT: "{{ IDENTITY_MANAGEMENT__TENANT }}" + IDENTITY_MANAGEMENT__CLIENTID: "{{ IDENTITY_MANAGEMENT__CLIENTID }}" \ No newline at end of file diff --git a/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-cronjob.yml.j2 b/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-cronjob.yml.j2 new file mode 100644 index 00000000000..0a526e4028c --- /dev/null +++ b/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-cronjob.yml.j2 @@ -0,0 +1,109 @@ +apiVersion: batch/v1 +kind: CronJob +metadata: + namespace: {{ NAMESPACE }} + labels: + app: api-tsp-sync-cronjob + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-tsp-sync-cronjob + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} + name: api-tsp-sync-cronjob +spec: + concurrencyPolicy: Forbid + schedule: "{{ SERVER_TSP_SYNC_CRONJOB_SCHEDULE }}" + jobTemplate: + metadata: + labels: + app: api-tsp-sync-cronjob + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-tsp-sync-cronjob + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} + spec: + template: + metadata: + labels: + app: api-tsp-sync-cronjob + app.kubernetes.io/part-of: schulcloud-verbund + app.kubernetes.io/version: {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + app.kubernetes.io/name: api-tsp-sync-cronjob + app.kubernetes.io/component: sync + app.kubernetes.io/managed-by: ansible + git.branch: {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + git.repo: {{ SCHULCLOUD_SERVER_REPO_NAME }} + spec: + securityContext: + runAsUser: 1000 + runAsGroup: 1000 + fsGroup: 1000 + runAsNonRoot: true + containers: + - name: api-tsp-sync-cronjob + image: {{ SCHULCLOUD_SERVER_IMAGE }}:{{ SCHULCLOUD_SERVER_IMAGE_TAG }} + envFrom: + - configMapRef: + name: api-tsp-sync-cronjob-configmap + - secretRef: + name: api-secret + command: ['/bin/sh', '-c'] + args: ['npm run nest:start:sync tsp'] + resources: + limits: + cpu: {{ API_CPU_LIMITS|default("2000m", true) }} + memory: {{ API_MEMORY_LIMITS|default("2Gi", true) }} + requests: + cpu: {{ API_CPU_REQUESTS|default("100m", true) }} + memory: {{ API_MEMORY_REQUESTS|default("150Mi", true) }} + restartPolicy: OnFailure +{% if AFFINITY_ENABLE is defined and AFFINITY_ENABLE|bool %} + affinity: + podAffinity: + preferredDuringSchedulingIgnoredDuringExecution: + - weight: 20 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/part-of + operator: In + values: + - schulcloud-verbund + topologyKey: "kubernetes.io/hostname" + namespaceSelector: {} + - weight: 10 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: git.repo + operator: In + values: + - {{ SCHULCLOUD_SERVER_REPO_NAME }} + topologyKey: "kubernetes.io/hostname" + namespaceSelector: {} + - weight: 10 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: git.branch + operator: In + values: + - {{ SCHULCLOUD_SERVER_BRANCH_NAME }} + topologyKey: "kubernetes.io/hostname" + namespaceSelector: {} + - weight: 10 + podAffinityTerm: + labelSelector: + matchExpressions: + - key: app.kubernetes.io/version + operator: In + values: + - {{ SCHULCLOUD_SERVER_IMAGE_TAG }} + topologyKey: "kubernetes.io/hostname" + namespaceSelector: {} +{% endif %} From 6d86aa1afe7b4853cf22b1c4c16ae989e14dd51d Mon Sep 17 00:00:00 2001 From: Gordon Nicholas <160246213+GordonNicholasCap@users.noreply.github.com> Date: Wed, 2 Oct 2024 10:47:40 +0200 Subject: [PATCH 2/8] N21-2166 superhero sets preferred tool (#5259) --- .../board-node-copy-specific.service.spec.ts | 1 + .../modules/server/admin-api-server.config.ts | 1 + .../src/modules/server/server.config.ts | 1 + .../interface/external-tool-search-query.ts | 2 + .../admin-api-external-tool.api.spec.ts | 4 + .../controller/api-test/tool.api.spec.ts | 7 + .../request/external-tool-create.params.ts | 12 + .../request/external-tool-update.params.ts | 12 + .../dto/response/external-tool.response.ts | 11 + .../domain/external-tool.do.spec.ts | 1 + .../external-tool/domain/external-tool.do.ts | 22 + .../entity/external-tool.entity.spec.ts | 2 + .../entity/external-tool.entity.ts | 12 + .../external-tool-request.mapper.spec.ts | 12 + .../mapper/external-tool-request.mapper.ts | 4 + .../external-tool-response.mapper.spec.ts | 3 + .../mapper/external-tool-response.mapper.ts | 2 + .../external-tool-validation.service.spec.ts | 452 ++++++++++++++++++ .../external-tool-validation.service.ts | 39 +- .../testing/external-tool-entity.factory.ts | 11 +- .../testing/external-tool.factory.ts | 11 +- .../uc/dto/external-tool.types.ts | 4 + apps/server/src/modules/tool/tool-config.ts | 1 + .../externaltool/external-tool.repo.mapper.ts | 4 + .../repo/externaltool/external-tool.repo.ts | 1 + .../repo/externaltool/external-tool.scope.ts | 7 + config/default.schema.json | 5 + 27 files changed, 633 insertions(+), 11 deletions(-) diff --git a/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts b/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts index 66bd8b6ea0d..3db0d06fa2a 100644 --- a/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts +++ b/apps/server/src/modules/board/service/internal/board-node-copy-specific.service.spec.ts @@ -54,6 +54,7 @@ describe(BoardNodeCopyService.name, () => { FEATURE_CTL_TOOLS_COPY_ENABLED: false, CTL_TOOLS_RELOAD_TIME_MS: 0, FILES_STORAGE__SERVICE_BASE_URL: '', + CTL_TOOLS__PREFERRED_TOOLS_LIMIT: 10, }; let contextExternalToolService: DeepMocked; let copyHelperService: DeepMocked; diff --git a/apps/server/src/modules/server/admin-api-server.config.ts b/apps/server/src/modules/server/admin-api-server.config.ts index 826f61970f3..336d4deba6a 100644 --- a/apps/server/src/modules/server/admin-api-server.config.ts +++ b/apps/server/src/modules/server/admin-api-server.config.ts @@ -65,6 +65,7 @@ const config: AdminApiServerConfig = { ) as boolean, FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED: Configuration.get('FEATURE_IDENTITY_MANAGEMENT_LOGIN_ENABLED') as boolean, FEATURE_IDENTITY_MANAGEMENT_STORE_ENABLED: Configuration.get('FEATURE_IDENTITY_MANAGEMENT_STORE_ENABLED') as boolean, + CTL_TOOLS__PREFERRED_TOOLS_LIMIT: Configuration.get('CTL_TOOLS__PREFERRED_TOOLS_LIMIT') as number, }; export const adminApiServerConfig = () => config; diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index fa875ec0c60..cd7ef5329fa 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -318,6 +318,7 @@ const config: ServerConfig = { ROCKET_CHAT_ADMIN_TOKEN: Configuration.get('ROCKET_CHAT_ADMIN_TOKEN') as string, ROCKET_CHAT_ADMIN_USER: Configuration.get('ROCKET_CHAT_ADMIN_USER') as string, ROCKET_CHAT_ADMIN_PASSWORD: Configuration.get('ROCKET_CHAT_ADMIN_PASSWORD') as string, + CTL_TOOLS__PREFERRED_TOOLS_LIMIT: Configuration.get('CTL_TOOLS__PREFERRED_TOOLS_LIMIT') as number, }; export const serverConfig = () => config; diff --git a/apps/server/src/modules/tool/common/interface/external-tool-search-query.ts b/apps/server/src/modules/tool/common/interface/external-tool-search-query.ts index 659cd6c8280..05237fd7bb8 100644 --- a/apps/server/src/modules/tool/common/interface/external-tool-search-query.ts +++ b/apps/server/src/modules/tool/common/interface/external-tool-search-query.ts @@ -6,4 +6,6 @@ export interface ExternalToolSearchQuery { isHidden?: boolean; ids?: string[]; + + isPreferred?: boolean; } diff --git a/apps/server/src/modules/tool/external-tool/controller/api-test/admin-api-external-tool.api.spec.ts b/apps/server/src/modules/tool/external-tool/controller/api-test/admin-api-external-tool.api.spec.ts index 09e589efc61..7d7a63d659d 100644 --- a/apps/server/src/modules/tool/external-tool/controller/api-test/admin-api-external-tool.api.spec.ts +++ b/apps/server/src/modules/tool/external-tool/controller/api-test/admin-api-external-tool.api.spec.ts @@ -96,6 +96,8 @@ describe('AdminApiExternalTool (API)', () => { url: 'https://link.to-my-tool.com', openNewTab: true, thumbnailUrl: 'https://link.to-my-thumbnail.com', + isPreferred: true, + iconName: 'mdiAlert', }; return { @@ -137,6 +139,8 @@ describe('AdminApiExternalTool (API)', () => { isDeactivated: false, url: 'https://link.to-my-tool.com', openNewTab: true, + isPreferred: true, + iconName: 'mdiAlert', }); const externalTool = await em.findOne(ExternalToolEntity, { id: body.id }); diff --git a/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts b/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts index c0242b68070..43c3cd75f7d 100644 --- a/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts +++ b/apps/server/src/modules/tool/external-tool/controller/api-test/tool.api.spec.ts @@ -91,6 +91,8 @@ describe('ToolController (API)', () => { url: 'https://link.to-my-tool.com', openNewTab: true, thumbnailUrl: 'https://link.to-my-thumbnail.com', + isPreferred: true, + iconName: 'mdiAlert', }; describe('when valid data is given', () => { @@ -161,6 +163,8 @@ describe('ToolController (API)', () => { url: 'https://link.to-my-tool.com', openNewTab: true, thumbnailUrl: postParams.thumbnailUrl, + isPreferred: true, + iconName: 'mdiAlert', }); }); }); @@ -259,6 +263,7 @@ describe('ToolController (API)', () => { mediaSourceId: 'source:1', }, thumbnailUrl: 'https://link.to-my-thumbnail.com', + isPreferred: false, }; describe('when valid data is given', () => { @@ -506,6 +511,7 @@ describe('ToolController (API)', () => { mediumId: 'mediumId', publisher: 'publisher', }, + isPreferred: false, }; describe('when valid data is given', () => { @@ -587,6 +593,7 @@ describe('ToolController (API)', () => { publisher: params.medium?.publisher, }, thumbnailUrl: 'https://link.to-my-thumbnail2.com', + isPreferred: false, }); }); }); diff --git a/apps/server/src/modules/tool/external-tool/controller/dto/request/external-tool-create.params.ts b/apps/server/src/modules/tool/external-tool/controller/dto/request/external-tool-create.params.ts index 5d5f296ab96..804fb3dbc63 100644 --- a/apps/server/src/modules/tool/external-tool/controller/dto/request/external-tool-create.params.ts +++ b/apps/server/src/modules/tool/external-tool/controller/dto/request/external-tool-create.params.ts @@ -97,4 +97,16 @@ export class ExternalToolCreateParams { @IsOptional() @ApiPropertyOptional({ type: ExternalToolMediumParams, description: 'Medium of the external tool' }) medium?: ExternalToolMediumParams; + + @IsBoolean() + @ApiProperty({ type: Boolean, description: 'Should the tool be a preferred tool', default: false }) + isPreferred!: boolean; + + @IsString() + @IsOptional() + @ApiPropertyOptional({ + type: String, + description: 'Name of the icon to be rendered when displaying it as a preferred tool', + }) + iconName?: string; } diff --git a/apps/server/src/modules/tool/external-tool/controller/dto/request/external-tool-update.params.ts b/apps/server/src/modules/tool/external-tool/controller/dto/request/external-tool-update.params.ts index f52c306cba6..bd50d150e68 100644 --- a/apps/server/src/modules/tool/external-tool/controller/dto/request/external-tool-update.params.ts +++ b/apps/server/src/modules/tool/external-tool/controller/dto/request/external-tool-update.params.ts @@ -101,4 +101,16 @@ export class ExternalToolUpdateParams { @IsOptional() @ApiPropertyOptional({ type: ExternalToolMediumParams, description: 'Medium of the external tool' }) medium?: ExternalToolMediumParams; + + @IsBoolean() + @ApiProperty({ type: Boolean, default: false, description: 'Should the tool be a preferred tool' }) + isPreferred!: boolean; + + @IsString() + @IsOptional() + @ApiPropertyOptional({ + type: String, + description: 'Name of the icon to be rendered when displaying it as a preferred tool', + }) + iconName?: string; } diff --git a/apps/server/src/modules/tool/external-tool/controller/dto/response/external-tool.response.ts b/apps/server/src/modules/tool/external-tool/controller/dto/response/external-tool.response.ts index 571b2856bbf..92ad052ac4d 100644 --- a/apps/server/src/modules/tool/external-tool/controller/dto/response/external-tool.response.ts +++ b/apps/server/src/modules/tool/external-tool/controller/dto/response/external-tool.response.ts @@ -57,6 +57,15 @@ export class ExternalToolResponse { @ApiPropertyOptional({ type: ExternalToolMediumResponse, description: 'Medium of the external tool' }) medium?: ExternalToolMediumResponse; + @ApiProperty({ type: Boolean, description: 'Should the tool be a preferred tool' }) + isPreferred: boolean; + + @ApiPropertyOptional({ + type: String, + description: 'Name of the icon to be rendered when displaying it as a preferred tool', + }) + iconName?: string; + constructor(response: ExternalToolResponse) { this.id = response.id; this.name = response.name; @@ -71,5 +80,7 @@ export class ExternalToolResponse { this.openNewTab = response.openNewTab; this.restrictToContexts = response.restrictToContexts; this.medium = response.medium; + this.isPreferred = response.isPreferred; + this.iconName = response.iconName; } } diff --git a/apps/server/src/modules/tool/external-tool/domain/external-tool.do.spec.ts b/apps/server/src/modules/tool/external-tool/domain/external-tool.do.spec.ts index abef2a4541d..0a719e9e8e8 100644 --- a/apps/server/src/modules/tool/external-tool/domain/external-tool.do.spec.ts +++ b/apps/server/src/modules/tool/external-tool/domain/external-tool.do.spec.ts @@ -62,6 +62,7 @@ describe(ExternalTool.name, () => { openNewTab: false, config: basicToolConfigFactory.build(), isDeactivated: false, + isPreferred: false, }); }).toThrowError(); }); diff --git a/apps/server/src/modules/tool/external-tool/domain/external-tool.do.ts b/apps/server/src/modules/tool/external-tool/domain/external-tool.do.ts index 0ae6e0cc91a..e6d3c1df1ba 100644 --- a/apps/server/src/modules/tool/external-tool/domain/external-tool.do.ts +++ b/apps/server/src/modules/tool/external-tool/domain/external-tool.do.ts @@ -36,6 +36,10 @@ export interface ExternalToolProps extends AuthorizableObject { medium?: ExternalToolMedium; createdAt?: Date; + + isPreferred: boolean; + + iconName?: string; } export class ExternalTool extends DomainObject { @@ -151,6 +155,22 @@ export class ExternalTool extends DomainObject { this.props.createdAt = value; } + get isPreferred(): boolean { + return this.props.isPreferred; + } + + set isPreferred(value: boolean) { + this.props.isPreferred = value; + } + + get iconName(): string | undefined { + return this.props.iconName; + } + + set iconName(value: string | undefined) { + this.props.iconName = value; + } + public static readonly thumbnailNameAffix = 'thumbnail'; constructor(props: ExternalToolProps) { @@ -177,6 +197,8 @@ export class ExternalTool extends DomainObject { this.restrictToContexts = props.restrictToContexts; this.medium = props.medium; this.createdAt = props.createdAt; + this.isPreferred = props.isPreferred; + this.iconName = props.iconName; } static isBasicConfig(config: ExternalToolConfig): config is BasicToolConfig { diff --git a/apps/server/src/modules/tool/external-tool/entity/external-tool.entity.spec.ts b/apps/server/src/modules/tool/external-tool/entity/external-tool.entity.spec.ts index 46f3fba185c..0d25caeded4 100644 --- a/apps/server/src/modules/tool/external-tool/entity/external-tool.entity.spec.ts +++ b/apps/server/src/modules/tool/external-tool/entity/external-tool.entity.spec.ts @@ -60,6 +60,8 @@ describe('ExternalToolEntity', () => { isHidden: true, isDeactivated: false, openNewTab: true, + isPreferred: true, + iconName: 'mdiAlert', }); return { externalToolEntity, 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 2e7063fe9fa..d73a57b54f4 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 @@ -36,6 +36,10 @@ export interface ExternalToolEntityProps { restrictToContexts?: ToolContextType[]; medium?: ExternalToolMediumEntity; + + isPreferred: boolean; + + iconName?: string; } @Entity({ tableName: 'external-tools' }) @@ -80,6 +84,12 @@ export class ExternalToolEntity extends BaseEntityWithTimestamps { @Embedded(() => ExternalToolMediumEntity, { nullable: true, object: true }) medium?: ExternalToolMediumEntity; + @Property() + isPreferred: boolean; + + @Property({ nullable: true }) + iconName?: string; + constructor(props: ExternalToolEntityProps) { super(); if (props.id) { @@ -98,5 +108,7 @@ export class ExternalToolEntity extends BaseEntityWithTimestamps { this.openNewTab = props.openNewTab; this.restrictToContexts = props.restrictToContexts; this.medium = props.medium; + this.isPreferred = props.isPreferred; + this.iconName = props.iconName; } } diff --git a/apps/server/src/modules/tool/external-tool/mapper/external-tool-request.mapper.spec.ts b/apps/server/src/modules/tool/external-tool/mapper/external-tool-request.mapper.spec.ts index 0ca8a677fc5..64711a30649 100644 --- a/apps/server/src/modules/tool/external-tool/mapper/external-tool-request.mapper.spec.ts +++ b/apps/server/src/modules/tool/external-tool/mapper/external-tool-request.mapper.spec.ts @@ -85,6 +85,7 @@ describe('ExternalToolRequestMapper', () => { externalToolCreateParams.isDeactivated = true; externalToolCreateParams.description = 'description'; externalToolCreateParams.thumbnailUrl = 'mockThumbnailUrl'; + externalToolCreateParams.isPreferred = false; const customParameterDO: CustomParameter = customParameterFactory.build({ name: 'mockName', @@ -158,6 +159,7 @@ describe('ExternalToolRequestMapper', () => { medium: externalToolDOCreate.medium, restrictToContexts: externalToolDOCreate.restrictToContexts, thumbnailUrl: externalToolCreateParams.thumbnailUrl, + isPreferred: false, }); }); }); @@ -206,6 +208,7 @@ describe('ExternalToolRequestMapper', () => { externalToolCreateParams.config = lti11ConfigParams; externalToolCreateParams.isDeactivated = false; externalToolCreateParams.description = 'description'; + externalToolCreateParams.isPreferred = false; const customParameterDO: CustomParameter = customParameterFactory.build({ name: 'mockName', @@ -279,6 +282,7 @@ describe('ExternalToolRequestMapper', () => { isDeactivated: externalToolDOCreate.isDeactivated, medium: externalToolDOCreate.medium, restrictToContexts: externalToolDOCreate.restrictToContexts, + isPreferred: false, }); }); }); @@ -331,6 +335,7 @@ describe('ExternalToolRequestMapper', () => { externalToolCreateParams.config = oauth2ConfigParams; externalToolCreateParams.isDeactivated = false; externalToolCreateParams.description = 'description'; + externalToolCreateParams.isPreferred = false; const customParameterDO: CustomParameter = customParameterFactory.build({ name: 'mockName', @@ -406,6 +411,7 @@ describe('ExternalToolRequestMapper', () => { isDeactivated: externalToolDOCreate.isDeactivated, medium: externalToolDOCreate.medium, restrictToContexts: externalToolDOCreate.restrictToContexts, + isPreferred: false, }); }); }); @@ -442,6 +448,7 @@ describe('ExternalToolRequestMapper', () => { externalToolUpdateParams.config = basicConfigParams; externalToolUpdateParams.isDeactivated = false; externalToolUpdateParams.description = 'description'; + externalToolUpdateParams.isPreferred = false; const customParameterDO: CustomParameter = customParameterFactory.build({ name: 'mockName', @@ -518,6 +525,7 @@ describe('ExternalToolRequestMapper', () => { isDeactivated: externalToolDOUpdate.isDeactivated, medium: externalToolDOUpdate.medium, restrictToContexts: externalToolDOUpdate.restrictToContexts, + isPreferred: false, }); }); }); @@ -567,6 +575,7 @@ describe('ExternalToolRequestMapper', () => { externalToolUpdateParams.config = lti11ConfigParams; externalToolUpdateParams.isDeactivated = false; externalToolUpdateParams.description = 'description'; + externalToolUpdateParams.isPreferred = false; const customParameterDO: CustomParameter = customParameterFactory.build({ name: 'mockName', @@ -644,6 +653,7 @@ describe('ExternalToolRequestMapper', () => { isDeactivated: externalToolDOUpdate.isDeactivated, medium: externalToolDOUpdate.medium, restrictToContexts: externalToolDOUpdate.restrictToContexts, + isPreferred: false, }); }); }); @@ -697,6 +707,7 @@ describe('ExternalToolRequestMapper', () => { externalToolUpdateParams.config = oauth2ConfigParams; externalToolUpdateParams.isDeactivated = false; externalToolUpdateParams.description = 'description'; + externalToolUpdateParams.isPreferred = false; const customParameterDO: CustomParameter = customParameterFactory.build({ name: 'mockName', @@ -776,6 +787,7 @@ describe('ExternalToolRequestMapper', () => { isDeactivated: externalToolDOUpdate.isDeactivated, medium: externalToolDOUpdate.medium, restrictToContexts: externalToolDOUpdate.restrictToContexts, + isPreferred: false, }); }); }); diff --git a/apps/server/src/modules/tool/external-tool/mapper/external-tool-request.mapper.ts b/apps/server/src/modules/tool/external-tool/mapper/external-tool-request.mapper.ts index 5805ee736e6..361fe1123ca 100644 --- a/apps/server/src/modules/tool/external-tool/mapper/external-tool-request.mapper.ts +++ b/apps/server/src/modules/tool/external-tool/mapper/external-tool-request.mapper.ts @@ -90,6 +90,8 @@ export class ExternalToolRequestMapper { openNewTab: externalToolUpdateParams.openNewTab, restrictToContexts: externalToolUpdateParams.restrictToContexts, medium: this.mapRequestToExternalToolMedium(externalToolUpdateParams.medium), + isPreferred: externalToolUpdateParams.isPreferred, + iconName: externalToolUpdateParams.iconName, }; } @@ -120,6 +122,8 @@ export class ExternalToolRequestMapper { restrictToContexts: externalToolCreateParams.restrictToContexts, medium: this.mapRequestToExternalToolMedium(externalToolCreateParams.medium), description: externalToolCreateParams.description, + isPreferred: externalToolCreateParams.isPreferred, + iconName: externalToolCreateParams.iconName, }; } diff --git a/apps/server/src/modules/tool/external-tool/mapper/external-tool-response.mapper.spec.ts b/apps/server/src/modules/tool/external-tool/mapper/external-tool-response.mapper.spec.ts index 31dba051a5b..e1dee797acc 100644 --- a/apps/server/src/modules/tool/external-tool/mapper/external-tool-response.mapper.spec.ts +++ b/apps/server/src/modules/tool/external-tool/mapper/external-tool-response.mapper.spec.ts @@ -95,6 +95,7 @@ describe(ExternalToolResponseMapper.name, () => { isDeactivated: true, description: externalTool.description, thumbnailUrl: externalTool.thumbnail?.uploadUrl, + isPreferred: false, }); return { @@ -187,6 +188,7 @@ describe(ExternalToolResponseMapper.name, () => { config: oauth2ToolConfigResponse, isDeactivated: false, description: externalTool.description, + isPreferred: false, }); return { @@ -274,6 +276,7 @@ describe(ExternalToolResponseMapper.name, () => { config: lti11ToolConfigResponse, isDeactivated: false, description: externalTool.description, + isPreferred: false, }); return { diff --git a/apps/server/src/modules/tool/external-tool/mapper/external-tool-response.mapper.ts b/apps/server/src/modules/tool/external-tool/mapper/external-tool-response.mapper.ts index 13a68d0e834..4a7087ea70e 100644 --- a/apps/server/src/modules/tool/external-tool/mapper/external-tool-response.mapper.ts +++ b/apps/server/src/modules/tool/external-tool/mapper/external-tool-response.mapper.ts @@ -75,6 +75,8 @@ export class ExternalToolResponseMapper { openNewTab: externalTool.openNewTab, restrictToContexts: externalTool.restrictToContexts, medium: this.mapMediumToResponse(externalTool.medium), + isPreferred: externalTool.isPreferred, + iconName: externalTool.iconName, }); } diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.spec.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.spec.ts index ae618ae084a..6ada828de21 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.spec.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.spec.ts @@ -2,6 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { ValidationError } from '@shared/common'; +import { Page } from '@shared/domain/domainobject'; import { ToolConfig } from '../../tool-config'; import { ExternalTool } from '../domain'; import { externalToolFactory } from '../testing'; @@ -201,6 +202,163 @@ describe(ExternalToolValidationService.name, () => { expect(logoService.validateLogoSize).toHaveBeenCalledWith(externalTool); }); }); + + describe('when the external tool is set to be a preferred tool', () => { + describe('when the preferred tool has an icon name', () => { + const setup = () => { + const externalTool: ExternalTool = externalToolFactory.build({ + isPreferred: true, + iconName: 'mdiAlert', + }); + const mockedPreferredToolsPage: Page = new Page([], 0); + + externalToolService.findExternalTools.mockResolvedValue(mockedPreferredToolsPage); + configService.get.mockReturnValue(10); + + return { + externalTool, + }; + }; + + it('should not throw an validation error', async () => { + const { externalTool } = setup(); + + const result: Promise = service.validateCreate(externalTool); + + await expect(result).resolves.not.toThrow(); + }); + }); + + describe('when the preferred tool has undefined icon name', () => { + const setup = () => { + const externalTool: ExternalTool = externalToolFactory.build({ + isPreferred: true, + iconName: undefined, + }); + + const mockedPreferredToolsPage: Page = new Page([], 0); + + externalToolService.findExternalTools.mockResolvedValue(mockedPreferredToolsPage); + configService.get.mockReturnValue(10); + + const expectedError = new ValidationError( + `tool_preferred_tools_missing_icon_name: The icon name of the preferred tool ${externalTool.name} is missing.` + ); + + return { + externalTool, + expectedError, + }; + }; + + it('should throw an validation error', async () => { + const { externalTool, expectedError } = setup(); + + const result: Promise = service.validateCreate(externalTool); + + await expect(result).rejects.toThrow(expectedError); + }); + }); + + describe('when the preferred tool has a blank icon name', () => { + const setup = () => { + const externalTool: ExternalTool = externalToolFactory.build({ + isPreferred: true, + iconName: '', + }); + + const mockedPreferredToolsPage: Page = new Page([], 0); + + externalToolService.findExternalTools.mockResolvedValue(mockedPreferredToolsPage); + configService.get.mockReturnValue(10); + + const expectedError = new ValidationError( + `tool_preferred_tools_missing_icon_name: The icon name of the preferred tool ${externalTool.name} is missing.` + ); + + return { + externalTool, + expectedError, + }; + }; + + it('should throw an validation error', async () => { + const { externalTool, expectedError } = setup(); + + const result: Promise = service.validateCreate(externalTool); + + await expect(result).rejects.toThrow(expectedError); + }); + }); + + describe('when the preferred tool limits had already been reached', () => { + const setup = () => { + const externalTool: ExternalTool = externalToolFactory.build({ + isPreferred: true, + iconName: 'mdiAlert', + }); + + const mockedPreferredToolsPage: Page = new Page( + externalToolFactory.buildList(3), + 3 + ); + + externalToolService.findExternalTools.mockResolvedValue(mockedPreferredToolsPage); + configService.get.mockReturnValue(3); + + const expectedError = new ValidationError( + `tool_preferred_tools_limit_reached: Unable to add a new preferred tool, the total limit had been reached.` + ); + + return { + externalTool, + expectedError, + }; + }; + + it('should throw an validation error', async () => { + const { externalTool, expectedError } = setup(); + + const result: Promise = service.validateCreate(externalTool); + + await expect(result).rejects.toThrow(expectedError); + }); + }); + + describe('when the preferred tool limits had already been exceeded', () => { + const setup = () => { + const externalTool: ExternalTool = externalToolFactory.build({ + isPreferred: true, + iconName: 'mdiAlert', + }); + + const mockedPreferredToolsPage: Page = new Page( + externalToolFactory.buildList(5), + 3 + ); + + externalToolService.findExternalTools.mockResolvedValue(mockedPreferredToolsPage); + configService.get.mockReturnValue(3); + + const expectedError = new ValidationError( + `tool_preferred_tools_limit_reached: Unable to add a new preferred tool, the total limit had been reached.` + ); + + return { + externalTool, + expectedError, + }; + }; + + it('should throw an validation error', async () => { + const { externalTool, expectedError } = setup(); + + const result: Promise = service.validateCreate(externalTool); + + await expect(result).rejects.toThrow(expectedError); + }); + }); + }); }); describe('validateUpdate', () => { @@ -374,5 +532,299 @@ describe(ExternalToolValidationService.name, () => { expect(logoService.validateLogoSize).toHaveBeenCalledWith(externalTool); }); }); + + describe('when the external tool is not an preferred tool', () => { + describe('when the external tool is set to be a preferred tool', () => { + describe('when the preferred tool has an icon name', () => { + const setup = () => { + const existingToolToUpdate = externalToolFactory.buildWithId({ + isPreferred: false, + }); + + const toolWithNewParams = externalToolFactory.build({ + ...existingToolToUpdate.getProps(), + id: existingToolToUpdate.id, + isPreferred: true, + iconName: 'mdiAlert', + }); + + const existingPreferredTools: Page = new Page([], 0); + + externalToolService.findExternalTools.mockResolvedValue(existingPreferredTools); + configService.get.mockReturnValue(10); + + return { + existingToolToUpdateId: existingToolToUpdate.id, + toolWithNewParams, + }; + }; + + it('should not throw an validation error', async () => { + const { existingToolToUpdateId, toolWithNewParams } = setup(); + + const result: Promise = service.validateUpdate(existingToolToUpdateId, toolWithNewParams); + + await expect(result).resolves.not.toThrow(); + }); + }); + + describe('when the preferred tool has undefined icon name', () => { + const setup = () => { + const existingToolToUpdate = externalToolFactory.buildWithId({ + isPreferred: false, + }); + + const toolWithNewParams = externalToolFactory.build({ + ...existingToolToUpdate.getProps(), + id: existingToolToUpdate.id, + isPreferred: true, + iconName: undefined, + }); + + const existingPreferredTools: Page = new Page([], 0); + + externalToolService.findExternalTools.mockResolvedValue(existingPreferredTools); + configService.get.mockReturnValue(10); + + const expectedError = new ValidationError( + `tool_preferred_tools_missing_icon_name: The icon name of the preferred tool ${toolWithNewParams.name} is missing.` + ); + + return { + existingToolToUpdateId: existingToolToUpdate.id, + toolWithNewParams, + expectedError, + }; + }; + + it('should throw an validation error', async () => { + const { existingToolToUpdateId, toolWithNewParams, expectedError } = setup(); + + const result: Promise = service.validateUpdate(existingToolToUpdateId, toolWithNewParams); + + await expect(result).rejects.toThrow(expectedError); + }); + }); + + describe('when the preferred tool has a blank icon name', () => { + const setup = () => { + const existingToolToUpdate = externalToolFactory.buildWithId({ + isPreferred: false, + }); + + const toolWithNewParams = externalToolFactory.build({ + ...existingToolToUpdate.getProps(), + id: existingToolToUpdate.id, + isPreferred: true, + iconName: '', + }); + + const existingPreferredTools: Page = new Page([], 0); + + externalToolService.findExternalTools.mockResolvedValue(existingPreferredTools); + configService.get.mockReturnValue(10); + + const expectedError = new ValidationError( + `tool_preferred_tools_missing_icon_name: The icon name of the preferred tool ${toolWithNewParams.name} is missing.` + ); + + return { + existingToolToUpdateId: existingToolToUpdate.id, + toolWithNewParams, + expectedError, + }; + }; + + it('should throw an validation error', async () => { + const { existingToolToUpdateId, toolWithNewParams, expectedError } = setup(); + + const result: Promise = service.validateUpdate(existingToolToUpdateId, toolWithNewParams); + + await expect(result).rejects.toThrow(expectedError); + }); + }); + + describe('when the preferred tool limits had already been reached', () => { + const setup = () => { + const existingToolToUpdate = externalToolFactory.buildWithId({ + isPreferred: false, + }); + + const toolWithNewParams = externalToolFactory.build({ + ...existingToolToUpdate.getProps(), + id: existingToolToUpdate.id, + isPreferred: true, + iconName: 'mdiAlert', + }); + + const existingPreferredTools: Page = new Page( + externalToolFactory.buildListWithId(3, { + isPreferred: true, + iconName: 'mdiFlag', + }), + 3 + ); + + externalToolService.findExternalTools.mockResolvedValue(existingPreferredTools); + configService.get.mockReturnValue(3); + + const expectedError = new ValidationError( + `tool_preferred_tools_limit_reached: Unable to add a new preferred tool, the total limit had been reached.` + ); + + return { + existingToolToUpdateId: existingToolToUpdate.id, + toolWithNewParams, + expectedError, + }; + }; + + it('should throw an validation error', async () => { + const { existingToolToUpdateId, toolWithNewParams, expectedError } = setup(); + + const result: Promise = service.validateUpdate(existingToolToUpdateId, toolWithNewParams); + + await expect(result).rejects.toThrow(expectedError); + expect(externalToolService.findExternalTools).toBeCalledWith({ isPreferred: true }); + }); + }); + + describe('when the preferred tool limits had already been exceeded', () => { + const setup = () => { + const existingToolToUpdate = externalToolFactory.buildWithId({ + isPreferred: false, + }); + + const toolWithNewParams = externalToolFactory.build({ + ...existingToolToUpdate.getProps(), + id: existingToolToUpdate.id, + isPreferred: true, + iconName: 'mdiAlert', + }); + + const existingPreferredTools: Page = new Page( + externalToolFactory.buildListWithId(3, { + isPreferred: true, + iconName: 'mdiFlag', + }), + 5 + ); + + externalToolService.findExternalTools.mockResolvedValue(existingPreferredTools); + configService.get.mockReturnValue(3); + + const expectedError = new ValidationError( + `tool_preferred_tools_limit_reached: Unable to add a new preferred tool, the total limit had been reached.` + ); + + return { + existingToolToUpdateId: existingToolToUpdate.id, + toolWithNewParams, + expectedError, + }; + }; + + it('should throw an validation error', async () => { + const { existingToolToUpdateId, toolWithNewParams, expectedError } = setup(); + + const result: Promise = service.validateUpdate(existingToolToUpdateId, toolWithNewParams); + + await expect(result).rejects.toThrow(expectedError); + expect(externalToolService.findExternalTools).toBeCalledWith({ isPreferred: true }); + }); + }); + }); + }); + + describe('when the external tool is already an preferred tool', () => { + describe('when the preferred tool limit is reached', () => { + const setup = () => { + const existingToolToUpdate = externalToolFactory.buildWithId({ + isPreferred: true, + iconName: 'mdiAlert', + }); + + const toolWithNewParams = externalToolFactory.build({ + ...existingToolToUpdate.getProps(), + id: existingToolToUpdate.id, + name: 'new-name-tool', + }); + + const existingOtherPreferredTools = externalToolFactory.buildListWithId(3, { + isPreferred: true, + iconName: 'mdiFlag', + }); + + const existingTools: Page = new Page( + [existingToolToUpdate, ...existingOtherPreferredTools], + 4 + ); + + externalToolService.findExternalTools.mockResolvedValue(existingTools); + configService.get.mockReturnValue(4); + + return { + existingToolToUpdateId: existingToolToUpdate.id, + toolWithNewParams, + }; + }; + + it('should not throw any error', async () => { + const { existingToolToUpdateId, toolWithNewParams } = setup(); + + const result: Promise = service.validateUpdate(existingToolToUpdateId, toolWithNewParams); + + await expect(result).resolves.not.toThrow(); + expect(externalToolService.findExternalTools).toBeCalledWith({ isPreferred: true }); + }); + }); + + describe('when the preferred tool limits had already been exceeded', () => { + const setup = () => { + const existingToolToUpdate = externalToolFactory.buildWithId({ + isPreferred: true, + iconName: 'mdiAlert', + }); + + const toolWithNewParams = externalToolFactory.build({ + ...existingToolToUpdate.getProps(), + id: existingToolToUpdate.id, + name: 'new-name-tool', + }); + + const existingOtherPreferredTools = externalToolFactory.buildListWithId(3, { + isPreferred: true, + iconName: 'mdiFlag', + }); + + const existingTools: Page = new Page( + [existingToolToUpdate, ...existingOtherPreferredTools], + 4 + ); + + externalToolService.findExternalTools.mockResolvedValue(existingTools); + configService.get.mockReturnValue(2); + + const expectedError = new ValidationError( + `tool_preferred_tools_limit_reached: Unable to add a new preferred tool, the total limit had been reached.` + ); + + return { + existingToolToUpdateId: existingToolToUpdate.id, + toolWithNewParams, + expectedError, + }; + }; + + it('should not throw any error', async () => { + const { existingToolToUpdateId, toolWithNewParams } = setup(); + + const result: Promise = service.validateUpdate(existingToolToUpdateId, toolWithNewParams); + + await expect(result).resolves.not.toThrow(); + expect(externalToolService.findExternalTools).toBeCalledWith({ isPreferred: true }); + }); + }); + }); }); }); diff --git a/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.ts b/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.ts index 53db997369f..281f551873a 100644 --- a/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.ts +++ b/apps/server/src/modules/tool/external-tool/service/external-tool-validation.service.ts @@ -1,5 +1,8 @@ import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; import { ValidationError } from '@shared/common'; +import { Page } from '@shared/domain/domainobject'; +import { ToolConfig } from '../../tool-config'; import { ExternalTool } from '../domain'; import { ExternalToolLogoService } from './external-tool-logo.service'; import { ExternalToolParameterValidationService } from './external-tool-parameter-validation.service'; @@ -10,7 +13,8 @@ export class ExternalToolValidationService { constructor( private readonly externalToolService: ExternalToolService, private readonly externalToolParameterValidationService: ExternalToolParameterValidationService, - private readonly externalToolLogoService: ExternalToolLogoService + private readonly externalToolLogoService: ExternalToolLogoService, + private readonly configService: ConfigService ) {} async validateCreate(externalTool: ExternalTool): Promise { @@ -21,6 +25,10 @@ export class ExternalToolValidationService { this.validateLti11Config(externalTool); this.externalToolLogoService.validateLogoSize(externalTool); + + if (externalTool.isPreferred) { + await this.validatePreferredTool(externalTool); + } } async validateUpdate(toolId: string, externalTool: ExternalTool): Promise { @@ -53,6 +61,10 @@ export class ExternalToolValidationService { } this.externalToolLogoService.validateLogoSize(externalTool); + + if (externalTool.isPreferred) { + await this.validatePreferredTool(externalTool); + } } private async validateOauth2Config(externalTool: ExternalTool): Promise { @@ -88,4 +100,29 @@ export class ExternalToolValidationService { } return duplicate == null || duplicate.id === externalTool.id; } + + private async validatePreferredTool(toolToValidate: ExternalTool): Promise { + if (!toolToValidate.iconName) { + throw new ValidationError( + `tool_preferred_tools_missing_icon_name: The icon name of the preferred tool ${toolToValidate.name} is missing.` + ); + } + + const preferredTools: Page = await this.externalToolService.findExternalTools({ + isPreferred: true, + }); + + const isToolToValidateAlreadyPreferred: boolean = preferredTools.data.some( + (existingPreferredTool: ExternalTool) => existingPreferredTool.id === toolToValidate.id + ); + if (isToolToValidateAlreadyPreferred) { + return; + } + + if (preferredTools.total >= this.configService.get('CTL_TOOLS__PREFERRED_TOOLS_LIMIT')) { + throw new ValidationError( + `tool_preferred_tools_limit_reached: Unable to add a new preferred tool, the total limit had been reached.` + ); + } + } } diff --git a/apps/server/src/modules/tool/external-tool/testing/external-tool-entity.factory.ts b/apps/server/src/modules/tool/external-tool/testing/external-tool-entity.factory.ts index 1c8b4df07a4..0b820bf21d1 100644 --- a/apps/server/src/modules/tool/external-tool/testing/external-tool-entity.factory.ts +++ b/apps/server/src/modules/tool/external-tool/testing/external-tool-entity.factory.ts @@ -1,4 +1,7 @@ +import { DeepPartial } from 'fishery'; import { ObjectId } from '@mikro-orm/mongodb'; +import { fileRecordFactory } from '@shared/testing'; +import { BaseFactory } from '@shared/testing/factory/base.factory'; import { CustomParameterLocation, CustomParameterScope, @@ -6,7 +9,7 @@ import { LtiMessageType, LtiPrivacyPermission, ToolConfigType, -} from '@modules/tool/common/enum'; +} from '../../common/enum'; import { BasicToolConfigEntity, CustomParameterEntity, @@ -15,10 +18,7 @@ import { ExternalToolMediumEntity, Lti11ToolConfigEntity, Oauth2ToolConfigEntity, -} from '@modules/tool/external-tool/entity'; -import { fileRecordFactory } from '@shared/testing'; -import { BaseFactory } from '@shared/testing/factory/base.factory'; -import { DeepPartial } from 'fishery'; +} from '../entity'; export class ExternalToolEntityFactory extends BaseFactory { withName(name: string): this { @@ -126,6 +126,7 @@ export const externalToolEntityFactory = ExternalToolEntityFactory.define( uploadUrl: 'https://uploadurl.com', fileRecord: fileRecordFactory.build(), }, + isPreferred: false, }; } ); diff --git a/apps/server/src/modules/tool/external-tool/testing/external-tool.factory.ts b/apps/server/src/modules/tool/external-tool/testing/external-tool.factory.ts index aa0653fdd5a..e3c43a5f6d6 100644 --- a/apps/server/src/modules/tool/external-tool/testing/external-tool.factory.ts +++ b/apps/server/src/modules/tool/external-tool/testing/external-tool.factory.ts @@ -1,5 +1,7 @@ +import { DeepPartial } from 'fishery'; import { ObjectId } from '@mikro-orm/mongodb'; -import { CustomParameter } from '@modules/tool/common/domain'; +import { DoBaseFactory } from '@shared/testing/factory/domainobject/do-base.factory'; +import { CustomParameter } from '../../common/domain'; import { CustomParameterLocation, CustomParameterScope, @@ -8,7 +10,7 @@ import { LtiPrivacyPermission, TokenEndpointAuthMethod, ToolConfigType, -} from '@modules/tool/common/enum'; +} from '../../common/enum'; import { BasicToolConfig, ExternalTool, @@ -16,9 +18,7 @@ import { ExternalToolProps, Lti11ToolConfig, Oauth2ToolConfig, -} from '@modules/tool/external-tool/domain'; -import { DoBaseFactory } from '@shared/testing/factory/domainobject/do-base.factory'; -import { DeepPartial } from 'fishery'; +} from '../domain'; import { fileRecordRefFactory } from './file-record-ref.factory'; export const basicToolConfigFactory = DoBaseFactory.define(BasicToolConfig, () => { @@ -149,5 +149,6 @@ export const externalToolFactory = ExternalToolFactory.define(ExternalTool, ({ s isDeactivated: false, openNewTab: false, createdAt: new Date(2020, 1, 1), + isPreferred: false, }; }); diff --git a/apps/server/src/modules/tool/external-tool/uc/dto/external-tool.types.ts b/apps/server/src/modules/tool/external-tool/uc/dto/external-tool.types.ts index ffa611966e0..72ddb7f66c7 100644 --- a/apps/server/src/modules/tool/external-tool/uc/dto/external-tool.types.ts +++ b/apps/server/src/modules/tool/external-tool/uc/dto/external-tool.types.ts @@ -44,6 +44,10 @@ export type ExternalToolDto = { restrictToContexts?: ToolContextType[]; medium?: ExternalToolMediumDto; + + isPreferred: boolean; + + iconName?: string; }; export type ExternalToolCreate = ExternalToolDto; diff --git a/apps/server/src/modules/tool/tool-config.ts b/apps/server/src/modules/tool/tool-config.ts index d9eaee0dc9e..8e9755b553d 100644 --- a/apps/server/src/modules/tool/tool-config.ts +++ b/apps/server/src/modules/tool/tool-config.ts @@ -6,4 +6,5 @@ export interface ToolConfig { FEATURE_CTL_TOOLS_COPY_ENABLED: boolean; CTL_TOOLS_RELOAD_TIME_MS: number; FILES_STORAGE__SERVICE_BASE_URL: string; + CTL_TOOLS__PREFERRED_TOOLS_LIMIT: number; } diff --git a/apps/server/src/shared/repo/externaltool/external-tool.repo.mapper.ts b/apps/server/src/shared/repo/externaltool/external-tool.repo.mapper.ts index eed1a34be20..841d2adc61d 100644 --- a/apps/server/src/shared/repo/externaltool/external-tool.repo.mapper.ts +++ b/apps/server/src/shared/repo/externaltool/external-tool.repo.mapper.ts @@ -64,6 +64,8 @@ export class ExternalToolRepoMapper { restrictToContexts: entity.restrictToContexts, medium: this.mapExternalToolMediumEntityToDO(entity.medium), createdAt: entity.createdAt, + isPreferred: entity.isPreferred, + iconName: entity.iconName, }); } @@ -144,6 +146,8 @@ export class ExternalToolRepoMapper { openNewTab: domainObject.openNewTab, restrictToContexts: domainObject.restrictToContexts, medium: this.mapExternalToolMediumDOToEntity(domainObject.medium), + isPreferred: domainObject.isPreferred, + iconName: domainObject.iconName, }; } diff --git a/apps/server/src/shared/repo/externaltool/external-tool.repo.ts b/apps/server/src/shared/repo/externaltool/external-tool.repo.ts index a5c2d783152..751e96da0f5 100644 --- a/apps/server/src/shared/repo/externaltool/external-tool.repo.ts +++ b/apps/server/src/shared/repo/externaltool/external-tool.repo.ts @@ -124,6 +124,7 @@ export class ExternalToolRepo { .byClientId(query.clientId) .byHidden(query.isHidden) .byIds(query.ids) + .byPreferred(query.isPreferred) .allowEmptyQuery(true); if (order._id == null) { diff --git a/apps/server/src/shared/repo/externaltool/external-tool.scope.ts b/apps/server/src/shared/repo/externaltool/external-tool.scope.ts index 7cb65b090a8..5f157647b63 100644 --- a/apps/server/src/shared/repo/externaltool/external-tool.scope.ts +++ b/apps/server/src/shared/repo/externaltool/external-tool.scope.ts @@ -30,4 +30,11 @@ export class ExternalToolScope extends Scope { } return this; } + + byPreferred(isPreferred?: boolean): this { + if (isPreferred !== undefined) { + this.addQuery({ isPreferred }); + } + return this; + } } diff --git a/config/default.schema.json b/config/default.schema.json index 1d9f3f14fca..387e7331b1a 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -1404,6 +1404,11 @@ "type": "integer", "default": 300000, "description": "Maximum size of the logo of an external tool in bytes" + }, + "PREFERRED_TOOLS_LIMIT": { + "type": "integer", + "default": 5, + "description": "Maximum number of preferred tools that can be set" } } }, From 2eba077f59f20a7adb9bbe7c27a2d9ea68774dcf Mon Sep 17 00:00:00 2001 From: Igor Richter <93926487+IgorCapCoder@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:52:32 +0200 Subject: [PATCH 3/8] N21-2203 handle policies-info error responses (#5262) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * handle policies-info error responses * fix everything --------- Co-authored-by: Marvin Öhlerking --- .../src/infra/schulconnex-client/index.ts | 2 - .../response/policies-info/index.ts | 4 + ...x-policies-info-access-control-response.ts | 13 +++ ...olicies-info-error-description-response.ts | 9 ++ ...chulconnex-policies-info-error-response.ts | 10 ++ ...ulconnex-policies-info-license-response.ts | 16 ++++ ...onnex-policies-info-permission-response.ts | 3 +- .../schulconnex-policies-info-response.ts | 25 ++--- .../response/schulconnex-person-response.ts | 1 + .../schulconnex-api.interface.ts | 2 +- .../schulconnex-rest-client.spec.ts | 16 +++- .../schulconnex-rest-client.ts | 22 +++-- .../infra/schulconnex-client/testing/index.ts | 6 +- ...ulconnex-policies-info-response-factory.ts | 16 ---- ...ulconnex-policies-info-response.factory.ts | 41 ++++++++ .../modules/idp-console/idp-console.module.ts | 2 +- .../idp-console/uc/synchronization.uc.spec.ts | 3 +- apps/server/src/modules/oauth/index.ts | 1 - .../modules/provisioning/loggable/index.ts | 1 + ...icies-info-error-response-loggable.spec.ts | 31 ++++++ .../policies-info-error-response-loggable.ts | 17 ++++ .../provisioning/provisioning.module.ts | 2 +- .../provisioning/strategy/loggable/index.ts | 1 - .../provisioning/strategy/sanis/index.ts | 2 +- .../strategy/sanis/sanis.strategy.spec.ts | 94 ++++++++++++++++--- .../strategy/sanis/sanis.strategy.ts | 25 +++-- .../sanis/schulconnex-response-mapper.spec.ts | 13 +-- .../sanis/schulconnex-response-mapper.ts | 8 +- .../provisioning/strategy/tsp/tsp.strategy.ts | 2 +- .../src/modules/server/server.module.ts | 4 +- .../api-test/import-user-populate.api.spec.ts | 3 +- ...lconnex-fetch-import-users.service.spec.ts | 3 +- .../modules/user-import/user-import.module.ts | 6 +- .../api-test/user-login-migration.api.spec.ts | 5 +- .../user-login-migration-api.module.ts | 2 +- .../shared/controller/transformer/index.ts | 1 + .../polymorphic-array.transformer.spec.ts | 57 +++++++++++ .../polymorphic-array.transformer.ts | 24 +++++ 38 files changed, 403 insertions(+), 90 deletions(-) create mode 100644 apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-access-control-response.ts create mode 100644 apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-error-description-response.ts create mode 100644 apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-error-response.ts create mode 100644 apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-license-response.ts delete mode 100644 apps/server/src/infra/schulconnex-client/testing/schulconnex-policies-info-response-factory.ts create mode 100644 apps/server/src/infra/schulconnex-client/testing/schulconnex-policies-info-response.factory.ts create mode 100644 apps/server/src/modules/provisioning/loggable/policies-info-error-response-loggable.spec.ts create mode 100644 apps/server/src/modules/provisioning/loggable/policies-info-error-response-loggable.ts delete mode 100644 apps/server/src/modules/provisioning/strategy/loggable/index.ts create mode 100644 apps/server/src/shared/controller/transformer/polymorphic-array.transformer.spec.ts create mode 100644 apps/server/src/shared/controller/transformer/polymorphic-array.transformer.ts diff --git a/apps/server/src/infra/schulconnex-client/index.ts b/apps/server/src/infra/schulconnex-client/index.ts index e31b5de998f..ef1cc753c41 100644 --- a/apps/server/src/infra/schulconnex-client/index.ts +++ b/apps/server/src/infra/schulconnex-client/index.ts @@ -1,6 +1,4 @@ export { SchulconnexRestClientOptions } from './schulconnex-rest-client-options'; -export { SchulconnexClientModule } from './schulconnex-client.module'; export { SchulconnexRestClient } from './schulconnex-rest-client'; export * from './response'; -export { schulconnexResponseFactory, schulconnexPoliciesInfoResponseFactory } from './testing'; export { SchulconnexClientConfig } from './schulconnex-client-config'; diff --git a/apps/server/src/infra/schulconnex-client/response/policies-info/index.ts b/apps/server/src/infra/schulconnex-client/response/policies-info/index.ts index d2d85b0601f..4cdc0c9a497 100644 --- a/apps/server/src/infra/schulconnex-client/response/policies-info/index.ts +++ b/apps/server/src/infra/schulconnex-client/response/policies-info/index.ts @@ -2,3 +2,7 @@ export { SchulconnexPoliciesInfoTargetResponse } from './schulconnex-policies-in export { SchulconnexPoliciesInfoResponse } from './schulconnex-policies-info-response'; export { SchulconnexPoliciesInfoActionType } from './schulconnex-policies-info-action-type'; export { SchulconnexPoliciesInfoPermissionResponse } from './schulconnex-policies-info-permission-response'; +export { SchulconnexPoliciesInfoAccessControlResponse } from './schulconnex-policies-info-access-control-response'; +export { SchulconnexPoliciesInfoErrorDescriptionResponse } from './schulconnex-policies-info-error-description-response'; +export { SchulconnexPoliciesInfoLicenseResponse } from './schulconnex-policies-info-license-response'; +export { SchulconnexPoliciesInfoErrorResponse } from './schulconnex-policies-info-error-response'; diff --git a/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-access-control-response.ts b/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-access-control-response.ts new file mode 100644 index 00000000000..41d99231c15 --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-access-control-response.ts @@ -0,0 +1,13 @@ +import { Type } from 'class-transformer'; +import { IsObject, IsString, ValidateNested } from 'class-validator'; +import { SchulconnexPoliciesInfoErrorDescriptionResponse } from './schulconnex-policies-info-error-description-response'; + +export class SchulconnexPoliciesInfoAccessControlResponse { + @IsString() + '@type'!: string; + + @IsObject() + @ValidateNested() + @Type(() => SchulconnexPoliciesInfoErrorDescriptionResponse) + error!: SchulconnexPoliciesInfoErrorDescriptionResponse; +} diff --git a/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-error-description-response.ts b/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-error-description-response.ts new file mode 100644 index 00000000000..c5237b3e6fd --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-error-description-response.ts @@ -0,0 +1,9 @@ +import { IsString } from 'class-validator'; + +export class SchulconnexPoliciesInfoErrorDescriptionResponse { + @IsString() + code!: string; + + @IsString() + value!: string; +} diff --git a/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-error-response.ts b/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-error-response.ts new file mode 100644 index 00000000000..bc7250a77bb --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-error-response.ts @@ -0,0 +1,10 @@ +import { Type } from 'class-transformer'; +import { IsObject, ValidateNested } from 'class-validator'; +import { SchulconnexPoliciesInfoAccessControlResponse } from './schulconnex-policies-info-access-control-response'; + +export class SchulconnexPoliciesInfoErrorResponse { + @IsObject() + @ValidateNested() + @Type(() => SchulconnexPoliciesInfoAccessControlResponse) + access_control!: SchulconnexPoliciesInfoAccessControlResponse; +} diff --git a/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-license-response.ts b/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-license-response.ts new file mode 100644 index 00000000000..8d34dd7dbd9 --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-license-response.ts @@ -0,0 +1,16 @@ +import { Type } from 'class-transformer'; +import { IsArray, IsObject, ValidateNested } from 'class-validator'; +import { SchulconnexPoliciesInfoPermissionResponse } from './schulconnex-policies-info-permission-response'; +import { SchulconnexPoliciesInfoTargetResponse } from './schulconnex-policies-info-target-response'; + +export class SchulconnexPoliciesInfoLicenseResponse { + @IsObject() + @ValidateNested() + @Type(() => SchulconnexPoliciesInfoTargetResponse) + target!: SchulconnexPoliciesInfoTargetResponse; + + @IsArray() + @ValidateNested({ each: true }) + @Type(() => SchulconnexPoliciesInfoPermissionResponse) + permission!: SchulconnexPoliciesInfoPermissionResponse[]; +} diff --git a/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-permission-response.ts b/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-permission-response.ts index 7e929d22957..68aa696bcd4 100644 --- a/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-permission-response.ts +++ b/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-permission-response.ts @@ -1,7 +1,8 @@ -import { IsArray } from 'class-validator'; +import { IsArray, IsString } from 'class-validator'; import { SchulconnexPoliciesInfoActionType } from './schulconnex-policies-info-action-type'; export class SchulconnexPoliciesInfoPermissionResponse { @IsArray() + @IsString({ each: true }) action!: SchulconnexPoliciesInfoActionType[]; } diff --git a/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-response.ts b/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-response.ts index 304d444efd5..454fda96a22 100644 --- a/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-response.ts +++ b/apps/server/src/infra/schulconnex-client/response/policies-info/schulconnex-policies-info-response.ts @@ -1,16 +1,19 @@ -import { Type } from 'class-transformer'; -import { IsArray, IsObject, ValidateNested } from 'class-validator'; -import { SchulconnexPoliciesInfoPermissionResponse } from './schulconnex-policies-info-permission-response'; -import { SchulconnexPoliciesInfoTargetResponse } from './schulconnex-policies-info-target-response'; +import { PolymorphicArrayTransform } from '@shared/controller'; +import { ClassConstructor } from 'class-transformer'; +import { IsArray, ValidateNested } from 'class-validator'; +import { SchulconnexPoliciesInfoErrorResponse } from './schulconnex-policies-info-error-response'; +import { SchulconnexPoliciesInfoLicenseResponse } from './schulconnex-policies-info-license-response'; -export class SchulconnexPoliciesInfoResponse { - @IsObject() - @ValidateNested() - @Type(() => SchulconnexPoliciesInfoTargetResponse) - target!: SchulconnexPoliciesInfoTargetResponse; +const policiesInfoDiscriminator = ( + obj: unknown +): ClassConstructor => + typeof obj === 'object' && obj !== null && 'target' in obj && 'permission' in obj + ? SchulconnexPoliciesInfoLicenseResponse + : SchulconnexPoliciesInfoErrorResponse; +export class SchulconnexPoliciesInfoResponse { @IsArray() @ValidateNested({ each: true }) - @Type(() => SchulconnexPoliciesInfoPermissionResponse) - permission!: SchulconnexPoliciesInfoPermissionResponse[]; + @PolymorphicArrayTransform(policiesInfoDiscriminator) + data!: (SchulconnexPoliciesInfoLicenseResponse | SchulconnexPoliciesInfoErrorResponse)[]; } diff --git a/apps/server/src/infra/schulconnex-client/response/schulconnex-person-response.ts b/apps/server/src/infra/schulconnex-client/response/schulconnex-person-response.ts index 2bbf3057ce6..922d80b5106 100644 --- a/apps/server/src/infra/schulconnex-client/response/schulconnex-person-response.ts +++ b/apps/server/src/infra/schulconnex-client/response/schulconnex-person-response.ts @@ -2,6 +2,7 @@ import { Type } from 'class-transformer'; import { IsObject, IsOptional, ValidateNested } from 'class-validator'; import { SchulconnexGeburtResponse } from './schulconnex-geburt-response'; import { SchulconnexNameResponse } from './schulconnex-name-response'; +import 'reflect-metadata'; export class SchulconnexPersonResponse { @IsObject() diff --git a/apps/server/src/infra/schulconnex-client/schulconnex-api.interface.ts b/apps/server/src/infra/schulconnex-client/schulconnex-api.interface.ts index 45d90ff3a17..dfc859546e8 100644 --- a/apps/server/src/infra/schulconnex-client/schulconnex-api.interface.ts +++ b/apps/server/src/infra/schulconnex-client/schulconnex-api.interface.ts @@ -6,5 +6,5 @@ export interface SchulconnexApiInterface { getPersonenInfo(params: SchulconnexPersonenInfoParams): Promise; - getPoliciesInfo(accessToken: string, options?: { overrideUrl: string }): Promise; + getPoliciesInfo(accessToken: string, options?: { overrideUrl: string }): Promise; } diff --git a/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.spec.ts b/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.spec.ts index 89d5b2f6711..7d802924304 100644 --- a/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.spec.ts +++ b/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.spec.ts @@ -5,10 +5,14 @@ import { axiosResponseFactory } from '@shared/testing'; import { Logger } from '@src/core/logger'; import { of } from 'rxjs'; import { SchulconnexConfigurationMissingLoggable } from './loggable'; -import { SchulconnexPoliciesInfoResponse, SchulconnexResponse } from './response'; +import { + SchulconnexPoliciesInfoLicenseResponse, + SchulconnexPoliciesInfoResponse, + SchulconnexResponse, +} from './response'; import { SchulconnexRestClient } from './schulconnex-rest-client'; import { SchulconnexRestClientOptions } from './schulconnex-rest-client-options'; -import { schulconnexPoliciesInfoResponseFactory, schulconnexResponseFactory } from './testing'; +import { schulconnexPoliciesInfoLicenseResponseFactory, schulconnexResponseFactory } from './testing'; describe(SchulconnexRestClient.name, () => { let client: SchulconnexRestClient; @@ -199,7 +203,8 @@ describe(SchulconnexRestClient.name, () => { describe('when requesting policies-info', () => { const setup = () => { const accessToken = 'accessToken'; - const response: SchulconnexPoliciesInfoResponse[] = schulconnexPoliciesInfoResponseFactory.buildList(1); + const response: SchulconnexPoliciesInfoLicenseResponse[] = + schulconnexPoliciesInfoLicenseResponseFactory.buildList(1); httpService.get.mockReturnValueOnce(of(axiosResponseFactory.build({ data: response }))); @@ -224,7 +229,7 @@ describe(SchulconnexRestClient.name, () => { it('should return the response', async () => { const { accessToken } = setup(); - const result: SchulconnexPoliciesInfoResponse[] = await client.getPoliciesInfo(accessToken); + const result: SchulconnexPoliciesInfoResponse = await client.getPoliciesInfo(accessToken); expect(result).toBeDefined(); }); @@ -234,7 +239,8 @@ describe(SchulconnexRestClient.name, () => { const setup = () => { const accessToken = 'accessToken'; const customUrl = 'https://override.url/policies-info'; - const response: SchulconnexPoliciesInfoResponse[] = schulconnexPoliciesInfoResponseFactory.buildList(1); + const response: SchulconnexPoliciesInfoLicenseResponse[] = + schulconnexPoliciesInfoLicenseResponseFactory.buildList(1); httpService.get.mockReturnValueOnce(of(axiosResponseFactory.build({ data: response }))); diff --git a/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.ts b/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.ts index 5bc86efd21e..317bd1eca38 100644 --- a/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.ts +++ b/apps/server/src/infra/schulconnex-client/schulconnex-rest-client.ts @@ -8,7 +8,12 @@ import QueryString from 'qs'; import { lastValueFrom, Observable } from 'rxjs'; import { SchulconnexConfigurationMissingLoggable } from './loggable'; import { SchulconnexPersonenInfoParams } from './request'; -import { SchulconnexPoliciesInfoResponse, SchulconnexResponse } from './response'; +import { + SchulconnexPoliciesInfoErrorResponse, + SchulconnexPoliciesInfoLicenseResponse, + SchulconnexPoliciesInfoResponse, + SchulconnexResponse, +} from './response'; import { SchulconnexApiInterface } from './schulconnex-api.interface'; import { SchulconnexRestClientOptions } from './schulconnex-rest-client-options'; @@ -51,15 +56,18 @@ export class SchulconnexRestClient implements SchulconnexApiInterface { public async getPoliciesInfo( accessToken: string, options?: { overrideUrl: string } - ): Promise { + ): Promise { const url: URL = new URL(options?.overrideUrl ?? `${this.SCHULCONNEX_API_BASE_URL}/policies-info`); - const response: Promise = this.getRequest( - url, - accessToken - ); + const response: (SchulconnexPoliciesInfoLicenseResponse | SchulconnexPoliciesInfoErrorResponse)[] = + await this.getRequest<(SchulconnexPoliciesInfoLicenseResponse | SchulconnexPoliciesInfoErrorResponse)[]>( + url, + accessToken + ); - return response; + const responseObject: SchulconnexPoliciesInfoResponse = { data: response }; + + return responseObject; } private checkOptions(): boolean { diff --git a/apps/server/src/infra/schulconnex-client/testing/index.ts b/apps/server/src/infra/schulconnex-client/testing/index.ts index 7ab3afe2ff1..d60a7b3488e 100644 --- a/apps/server/src/infra/schulconnex-client/testing/index.ts +++ b/apps/server/src/infra/schulconnex-client/testing/index.ts @@ -1,2 +1,6 @@ export { schulconnexResponseFactory } from './schulconnex-response-factory'; -export { schulconnexPoliciesInfoResponseFactory } from './schulconnex-policies-info-response-factory'; +export { + schulconnexPoliciesInfoLicenseResponseFactory, + schulconnexPoliciesInfoErrorResponseFactory, + schulconnexPoliciesInfoResponseFactory, +} from './schulconnex-policies-info-response.factory'; diff --git a/apps/server/src/infra/schulconnex-client/testing/schulconnex-policies-info-response-factory.ts b/apps/server/src/infra/schulconnex-client/testing/schulconnex-policies-info-response-factory.ts deleted file mode 100644 index 38541e9e4cf..00000000000 --- a/apps/server/src/infra/schulconnex-client/testing/schulconnex-policies-info-response-factory.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { Factory } from 'fishery'; -import { SchulconnexPoliciesInfoActionType, SchulconnexPoliciesInfoResponse } from '../response'; - -export const schulconnexPoliciesInfoResponseFactory = Factory.define(() => { - return { - target: { - uid: 'bildungscloud', - partOf: '', - }, - permission: [ - { - action: [SchulconnexPoliciesInfoActionType.EXECUTE], - }, - ], - }; -}); diff --git a/apps/server/src/infra/schulconnex-client/testing/schulconnex-policies-info-response.factory.ts b/apps/server/src/infra/schulconnex-client/testing/schulconnex-policies-info-response.factory.ts new file mode 100644 index 00000000000..92963e99972 --- /dev/null +++ b/apps/server/src/infra/schulconnex-client/testing/schulconnex-policies-info-response.factory.ts @@ -0,0 +1,41 @@ +import { Factory } from 'fishery'; +import { + SchulconnexPoliciesInfoActionType, + SchulconnexPoliciesInfoErrorResponse, + SchulconnexPoliciesInfoLicenseResponse, + SchulconnexPoliciesInfoResponse, +} from '../response'; + +export const schulconnexPoliciesInfoLicenseResponseFactory = Factory.define( + () => { + return { + target: { + uid: 'bildungscloud', + partOf: '', + }, + permission: [ + { + action: [SchulconnexPoliciesInfoActionType.EXECUTE], + }, + ], + }; + } +); + +export const schulconnexPoliciesInfoErrorResponseFactory = Factory.define(() => { + return { + access_control: { + '@type': 'bilo error mock', + error: { + code: '500', + value: 'something went wrong', + }, + }, + }; +}); + +export const schulconnexPoliciesInfoResponseFactory = Factory.define(() => { + return { + data: [schulconnexPoliciesInfoLicenseResponseFactory.build(), schulconnexPoliciesInfoErrorResponseFactory.build()], + }; +}); diff --git a/apps/server/src/modules/idp-console/idp-console.module.ts b/apps/server/src/modules/idp-console/idp-console.module.ts index 4b36404e7e5..84568dcce3d 100644 --- a/apps/server/src/modules/idp-console/idp-console.module.ts +++ b/apps/server/src/modules/idp-console/idp-console.module.ts @@ -1,6 +1,6 @@ import { ConsoleWriterModule } from '@infra/console'; import { RabbitMQWrapperModule } from '@infra/rabbitmq'; -import { SchulconnexClientModule } from '@infra/schulconnex-client'; +import { SchulconnexClientModule } from '@infra/schulconnex-client/schulconnex-client.module'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { defaultMikroOrmOptions } from '@modules/server'; import { SynchronizationEntity, SynchronizationModule } from '@modules/synchronization'; diff --git a/apps/server/src/modules/idp-console/uc/synchronization.uc.spec.ts b/apps/server/src/modules/idp-console/uc/synchronization.uc.spec.ts index 892708bf090..feaf80daf3b 100644 --- a/apps/server/src/modules/idp-console/uc/synchronization.uc.spec.ts +++ b/apps/server/src/modules/idp-console/uc/synchronization.uc.spec.ts @@ -1,5 +1,6 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { SchulconnexResponse, schulconnexResponseFactory, SchulconnexRestClient } from '@infra/schulconnex-client'; +import { SchulconnexResponse, SchulconnexRestClient } from '@infra/schulconnex-client'; +import { schulconnexResponseFactory } from '@infra/schulconnex-client/testing'; import { Synchronization, synchronizationFactory, diff --git a/apps/server/src/modules/oauth/index.ts b/apps/server/src/modules/oauth/index.ts index 67f5f98b440..9cd1d26e142 100644 --- a/apps/server/src/modules/oauth/index.ts +++ b/apps/server/src/modules/oauth/index.ts @@ -1,3 +1,2 @@ -export * from './oauth.module'; export * from './interface'; export * from './service'; diff --git a/apps/server/src/modules/provisioning/loggable/index.ts b/apps/server/src/modules/provisioning/loggable/index.ts index 9b9f2196e88..00068126737 100644 --- a/apps/server/src/modules/provisioning/loggable/index.ts +++ b/apps/server/src/modules/provisioning/loggable/index.ts @@ -5,3 +5,4 @@ export * from './school-name-required-loggable-exception'; export * from './group-role-unknown.loggable'; export { SchoolExternalToolCreatedLoggable } from './school-external-tool-created.loggable'; export { FetchingPoliciesInfoFailedLoggable } from './fetching-policies-info-failed.loggable'; +export { PoliciesInfoErrorResponseLoggable } from './policies-info-error-response-loggable'; diff --git a/apps/server/src/modules/provisioning/loggable/policies-info-error-response-loggable.spec.ts b/apps/server/src/modules/provisioning/loggable/policies-info-error-response-loggable.spec.ts new file mode 100644 index 00000000000..4ba118020c2 --- /dev/null +++ b/apps/server/src/modules/provisioning/loggable/policies-info-error-response-loggable.spec.ts @@ -0,0 +1,31 @@ +import { SchulconnexPoliciesInfoErrorResponse } from '@infra/schulconnex-client'; +import { schulconnexPoliciesInfoErrorResponseFactory } from '@infra/schulconnex-client/testing'; +import { PoliciesInfoErrorResponseLoggable } from './policies-info-error-response-loggable'; + +describe(PoliciesInfoErrorResponseLoggable.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const errorResponse: SchulconnexPoliciesInfoErrorResponse = schulconnexPoliciesInfoErrorResponseFactory.build(); + + const loggable: PoliciesInfoErrorResponseLoggable = new PoliciesInfoErrorResponseLoggable(errorResponse); + + return { + loggable, + errorResponse, + }; + }; + + it('should return the correct log message', () => { + const { loggable, errorResponse } = setup(); + + expect(loggable.getLogMessage()).toEqual({ + message: 'The /policies-info endpoint returned an error for a media source.', + data: { + type: errorResponse.access_control['@type'], + code: errorResponse.access_control.error.code, + value: errorResponse.access_control.error.value, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/provisioning/loggable/policies-info-error-response-loggable.ts b/apps/server/src/modules/provisioning/loggable/policies-info-error-response-loggable.ts new file mode 100644 index 00000000000..b867a29002d --- /dev/null +++ b/apps/server/src/modules/provisioning/loggable/policies-info-error-response-loggable.ts @@ -0,0 +1,17 @@ +import { SchulconnexPoliciesInfoErrorResponse } from '@infra/schulconnex-client'; +import { Loggable, LoggableMessage } from '@shared/common/loggable/interfaces'; + +export class PoliciesInfoErrorResponseLoggable implements Loggable { + constructor(private readonly item: SchulconnexPoliciesInfoErrorResponse) {} + + getLogMessage(): LoggableMessage { + return { + message: 'The /policies-info endpoint returned an error for a media source.', + data: { + type: this.item.access_control['@type'], + code: this.item.access_control.error.code, + value: this.item.access_control.error.value, + }, + }; + } +} diff --git a/apps/server/src/modules/provisioning/provisioning.module.ts b/apps/server/src/modules/provisioning/provisioning.module.ts index 6e14f961752..6474efc61ce 100644 --- a/apps/server/src/modules/provisioning/provisioning.module.ts +++ b/apps/server/src/modules/provisioning/provisioning.module.ts @@ -10,7 +10,7 @@ import { SchoolExternalToolModule } from '@modules/tool/school-external-tool'; import { UserModule } from '@modules/user'; import { Module } from '@nestjs/common'; import { LoggerModule } from '@src/core/logger'; -import { SchulconnexClientModule } from '@src/infra/schulconnex-client'; +import { SchulconnexClientModule } from '@src/infra/schulconnex-client/schulconnex-client.module'; import { ClassModule } from '../class'; import { UserLicenseModule } from '../user-license'; import { ProvisioningService } from './service/provisioning.service'; diff --git a/apps/server/src/modules/provisioning/strategy/loggable/index.ts b/apps/server/src/modules/provisioning/strategy/loggable/index.ts deleted file mode 100644 index 102d04f9628..00000000000 --- a/apps/server/src/modules/provisioning/strategy/loggable/index.ts +++ /dev/null @@ -1 +0,0 @@ -export * from '../../loggable/bad-data.loggable-exception'; diff --git a/apps/server/src/modules/provisioning/strategy/sanis/index.ts b/apps/server/src/modules/provisioning/strategy/sanis/index.ts index 33928785efd..03132b1fcd6 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/index.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/index.ts @@ -1,2 +1,2 @@ -export * from './sanis.strategy'; +export { SanisProvisioningStrategy } from './sanis.strategy'; export { SchulconnexResponseMapper } from './schulconnex-response-mapper'; diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts index 377080049bf..cdbcf05532d 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.spec.ts @@ -1,13 +1,18 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { SchulconnexGruppenResponse, + SchulconnexPoliciesInfoLicenseResponse, + SchulconnexPoliciesInfoResponse, SchulconnexResponse, - schulconnexResponseFactory, SchulconnexResponseValidationGroups, SchulconnexRestClient, } from '@infra/schulconnex-client'; -import { SchulconnexPoliciesInfoResponse } from '@infra/schulconnex-client/response'; -import { schulconnexPoliciesInfoResponseFactory } from '@infra/schulconnex-client/testing/schulconnex-policies-info-response-factory'; +import { + schulconnexPoliciesInfoErrorResponseFactory, + schulconnexPoliciesInfoLicenseResponseFactory, + schulconnexPoliciesInfoResponseFactory, + schulconnexResponseFactory, +} from '@infra/schulconnex-client/testing'; import { GroupService } from '@modules/group'; import { GroupTypes } from '@modules/group/domain'; import { InternalServerErrorException } from '@nestjs/common'; @@ -27,6 +32,7 @@ import { OauthDataStrategyInputDto, ProvisioningSystemDto, } from '../../dto'; +import { PoliciesInfoErrorResponseLoggable } from '../../loggable'; import { ProvisioningConfig } from '../../provisioning.config'; import { SchulconnexCourseSyncService, @@ -170,12 +176,13 @@ describe(SanisProvisioningStrategy.name, () => { }, }), ]; - const schulconnexLizenzInfoResponses: SchulconnexPoliciesInfoResponse[] = - schulconnexPoliciesInfoResponseFactory.buildList(1); - const schulconnexLizenzInfoResponse = schulconnexLizenzInfoResponses[0]; + const schulconnexPoliciesInfoLicenseResponse: SchulconnexPoliciesInfoLicenseResponse = + schulconnexPoliciesInfoLicenseResponseFactory.build(); const licenses: ExternalLicenseDto[] = SchulconnexResponseMapper.mapToExternalLicenses([ - schulconnexLizenzInfoResponse, + schulconnexPoliciesInfoLicenseResponse, ]); + const schulconnexPoliciesInfoResponse: SchulconnexPoliciesInfoResponse = + schulconnexPoliciesInfoResponseFactory.build({ data: [schulconnexPoliciesInfoLicenseResponse] }); config.FEATURE_SANIS_GROUP_PROVISIONING_ENABLED = true; config.FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED = true; @@ -185,7 +192,7 @@ describe(SanisProvisioningStrategy.name, () => { mapper.mapToExternalGroupDtos.mockReturnValue(groups); validationFunction.mockResolvedValueOnce([]); validationFunction.mockResolvedValueOnce([]); - schulconnexRestClient.getPoliciesInfo.mockResolvedValueOnce(schulconnexLizenzInfoResponses); + schulconnexRestClient.getPoliciesInfo.mockResolvedValueOnce(schulconnexPoliciesInfoResponse); validationFunction.mockResolvedValueOnce([]); return { @@ -196,7 +203,7 @@ describe(SanisProvisioningStrategy.name, () => { groups, licenses, schulconnexResponse, - schulconnexLizenzInfoResponse, + schulconnexPoliciesInfoResponse, }; }; @@ -235,11 +242,11 @@ describe(SanisProvisioningStrategy.name, () => { }); it('should validate the response for licenses', async () => { - const { input, schulconnexLizenzInfoResponse } = setup(); + const { input, schulconnexPoliciesInfoResponse } = setup(); await strategy.getData(input); - expect(validationFunction).toHaveBeenCalledWith(schulconnexLizenzInfoResponse, { + expect(validationFunction).toHaveBeenCalledWith(schulconnexPoliciesInfoResponse, { always: true, forbidUnknownValues: false, }); @@ -400,7 +407,7 @@ describe(SanisProvisioningStrategy.name, () => { }; }; - it('should not call getLizenzInfo', async () => { + it('should not call getPoliciesInfo', async () => { const { input } = setup(); await strategy.getData(input); @@ -515,6 +522,69 @@ describe(SanisProvisioningStrategy.name, () => { }); }); + describe('when policies-info returns at least one error response', () => { + const setup = () => { + const provisioningUrl = 'sanisProvisioningUrl'; + const input: OauthDataStrategyInputDto = new OauthDataStrategyInputDto({ + system: new ProvisioningSystemDto({ + systemId: 'systemId', + provisioningStrategy: SystemProvisioningStrategy.SANIS, + provisioningUrl, + }), + idToken: 'sanisIdToken', + accessToken: 'sanisAccessToken', + }); + const schulconnexResponse: SchulconnexResponse = setupSchulconnexResponse(); + const user: ExternalUserDto = new ExternalUserDto({ + externalId: 'externalUserId', + }); + const school: ExternalSchoolDto = new ExternalSchoolDto({ + externalId: 'externalSchoolId', + name: 'schoolName', + }); + + const schulconnexPoliciesInfoLicenseResponse = schulconnexPoliciesInfoLicenseResponseFactory.build(); + const schulconnexPoliciesInfoErrorResponse = schulconnexPoliciesInfoErrorResponseFactory.build(); + + const schulconnexPoliciesInfoResponse: SchulconnexPoliciesInfoResponse = + schulconnexPoliciesInfoResponseFactory.build({ + data: [schulconnexPoliciesInfoLicenseResponse, schulconnexPoliciesInfoErrorResponse], + }); + + const licenses: ExternalLicenseDto[] = SchulconnexResponseMapper.mapToExternalLicenses([ + schulconnexPoliciesInfoLicenseResponse, + ]); + + config.FEATURE_SCHULCONNEX_MEDIA_LICENSE_ENABLED = true; + schulconnexRestClient.getPersonInfo.mockResolvedValueOnce(schulconnexResponse); + mapper.mapToExternalUserDto.mockReturnValue(user); + mapper.mapToExternalSchoolDto.mockReturnValue(school); + schulconnexRestClient.getPoliciesInfo.mockResolvedValueOnce(schulconnexPoliciesInfoResponse); + validationFunction.mockResolvedValue([]); + + return { schulconnexPoliciesInfoErrorResponse, input, licenses }; + }; + + it('should log the error response', async () => { + const { input, schulconnexPoliciesInfoErrorResponse } = setup(); + + await strategy.getData(input); + + expect(logger.warning).toHaveBeenCalledWith( + new PoliciesInfoErrorResponseLoggable(schulconnexPoliciesInfoErrorResponse) + ); + }); + + it('should return the correct licenses', async () => { + const { input, licenses } = setup(); + + const result: OauthDataDto = await strategy.getData(input); + + expect(result.externalLicenses).toEqual(licenses); + expect(result.externalLicenses).toHaveLength(1); + }); + }); + describe('when the validation of the response fails', () => { const setup = () => { const provisioningUrl = 'sanisProvisioningUrl'; diff --git a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts index 6c118efcd1c..590dd214240 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/sanis.strategy.ts @@ -1,9 +1,10 @@ import { + SchulconnexPoliciesInfoLicenseResponse, SchulconnexPoliciesInfoResponse, SchulconnexResponse, SchulconnexResponseValidationGroups, -} from '@infra/schulconnex-client/response'; -import { SchulconnexRestClient } from '@infra/schulconnex-client/schulconnex-rest-client'; + SchulconnexRestClient, +} from '@infra/schulconnex-client'; import { GroupService } from '@modules/group/service/group.service'; import { Injectable, InternalServerErrorException } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; @@ -21,7 +22,7 @@ import { OauthDataDto, OauthDataStrategyInputDto, } from '../../dto'; -import { FetchingPoliciesInfoFailedLoggable } from '../../loggable'; +import { FetchingPoliciesInfoFailedLoggable, PoliciesInfoErrorResponseLoggable } from '../../loggable'; import { ProvisioningConfig } from '../../provisioning.config'; import { SchulconnexProvisioningStrategy } from '../oidc'; import { @@ -109,12 +110,24 @@ export class SanisProvisioningStrategy extends SchulconnexProvisioningStrategy { } ); - const schulconnexLizenzInfoResponses = plainToClass( + const schulconnexPoliciesInfoResponse = plainToClass( SchulconnexPoliciesInfoResponse, schulconnexPoliciesInfoAxiosResponse ); - await this.checkResponseValidation(schulconnexLizenzInfoResponses); - externalLicenses = SchulconnexResponseMapper.mapToExternalLicenses(schulconnexLizenzInfoResponses); + + await this.checkResponseValidation(schulconnexPoliciesInfoResponse); + + const schulconnexPoliciesInfoLicenceResponses: SchulconnexPoliciesInfoLicenseResponse[] = + schulconnexPoliciesInfoResponse.data.filter((item): item is SchulconnexPoliciesInfoLicenseResponse => { + if (item instanceof SchulconnexPoliciesInfoLicenseResponse) { + return true; + } + + this.logger.warning(new PoliciesInfoErrorResponseLoggable(item)); + return false; + }); + + externalLicenses = SchulconnexResponseMapper.mapToExternalLicenses(schulconnexPoliciesInfoLicenceResponses); } catch (error) { this.logger.warning(new FetchingPoliciesInfoFailedLoggable(externalUser, policiesInfoUrl)); } diff --git a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.spec.ts b/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.spec.ts index 663d53fe2da..36ad4321943 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.spec.ts @@ -4,12 +4,13 @@ import { SchulconnexGroupType, SchulconnexGruppenResponse, SchulconnexPersonenkontextResponse, - SchulconnexPoliciesInfoResponse, - schulconnexPoliciesInfoResponseFactory, SchulconnexResponse, - schulconnexResponseFactory, SchulconnexSonstigeGruppenzugehoerigeResponse, } from '@infra/schulconnex-client'; +import { + schulconnexPoliciesInfoLicenseResponseFactory, + schulconnexResponseFactory, +} from '@infra/schulconnex-client/testing'; import { GroupTypes } from '@modules/group'; import { ConfigService } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; @@ -561,7 +562,7 @@ describe(SchulconnexResponseMapper.name, () => { describe('mapToExternalLicenses', () => { describe('when a license response has a medium id and no media source', () => { const setup = () => { - const licenseResponse: SchulconnexPoliciesInfoResponse[] = schulconnexPoliciesInfoResponseFactory.buildList(1, { + const licenseResponse = schulconnexPoliciesInfoLicenseResponseFactory.buildList(1, { target: { uid: 'bildungscloud', partOf: '' }, }); @@ -587,7 +588,7 @@ describe(SchulconnexResponseMapper.name, () => { describe('when a license response has a medium id and a media source', () => { const setup = () => { - const licenseResponse: SchulconnexPoliciesInfoResponse[] = schulconnexPoliciesInfoResponseFactory.buildList(1, { + const licenseResponse = schulconnexPoliciesInfoLicenseResponseFactory.buildList(1, { target: { uid: 'bildungscloud', partOf: 'bildungscloud-source' }, }); @@ -613,7 +614,7 @@ describe(SchulconnexResponseMapper.name, () => { describe('when a license response has no medium id', () => { const setup = () => { - const licenseResponse: SchulconnexPoliciesInfoResponse[] = schulconnexPoliciesInfoResponseFactory.buildList(1, { + const licenseResponse = schulconnexPoliciesInfoLicenseResponseFactory.buildList(1, { target: { uid: '', partOf: 'bildungscloud-source' }, }); diff --git a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts b/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts index b76db0ea58e..5961ae19917 100644 --- a/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts +++ b/apps/server/src/modules/provisioning/strategy/sanis/schulconnex-response-mapper.ts @@ -6,11 +6,11 @@ import { SchulconnexGroupType, SchulconnexGruppenResponse, SchulconnexLaufzeitResponse, - SchulconnexPoliciesInfoResponse, + SchulconnexPoliciesInfoLicenseResponse, SchulconnexResponse, SchulconnexRole, SchulconnexSonstigeGruppenzugehoerigeResponse, -} from '@infra/schulconnex-client/response'; +} from '@infra/schulconnex-client'; import { GroupTypes } from '@modules/group'; import { Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; @@ -240,9 +240,9 @@ export class SchulconnexResponseMapper { }; } - public static mapToExternalLicenses(licenseInfos: SchulconnexPoliciesInfoResponse[]): ExternalLicenseDto[] { + public static mapToExternalLicenses(licenseInfos: SchulconnexPoliciesInfoLicenseResponse[]): ExternalLicenseDto[] { const externalLicenseDtos: ExternalLicenseDto[] = licenseInfos - .map((license: SchulconnexPoliciesInfoResponse) => { + .map((license: SchulconnexPoliciesInfoLicenseResponse) => { if (license.target.partOf === '') { license.target.partOf = undefined; } diff --git a/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts b/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts index 3ed54b80762..c9a0d7e3610 100644 --- a/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts @@ -12,9 +12,9 @@ import { OauthDataStrategyInputDto, ProvisioningDto, } from '../../dto'; +import { BadDataLoggableException } from '../../loggable'; import { TspProvisioningService } from '../../service/tsp-provisioning.service'; import { ProvisioningStrategy } from '../base.strategy'; -import { BadDataLoggableException } from '../loggable'; import { TspJwtPayload } from './tsp.jwt.payload'; @Injectable() diff --git a/apps/server/src/modules/server/server.module.ts b/apps/server/src/modules/server/server.module.ts index f1ff7d226be..af457631d66 100644 --- a/apps/server/src/modules/server/server.module.ts +++ b/apps/server/src/modules/server/server.module.ts @@ -3,7 +3,7 @@ import { AuthGuardModule } from '@infra/auth-guard'; import { MongoDatabaseModuleOptions, MongoMemoryDatabaseModule } from '@infra/database'; import { MailModule } from '@infra/mail'; import { RabbitMQWrapperModule, RabbitMQWrapperTestModule } from '@infra/rabbitmq'; -import { SchulconnexClientModule } from '@infra/schulconnex-client'; +import { SchulconnexClientModule } from '@infra/schulconnex-client/schulconnex-client.module'; import { Dictionary, IPrimaryKey } from '@mikro-orm/core'; import { MikroOrmModule, MikroOrmModuleSyncOptions } from '@mikro-orm/nestjs'; import { AccountApiModule } from '@modules/account/account-api.module'; @@ -30,6 +30,7 @@ import { RoomApiModule } from '@modules/room/room-api.module'; import { RosterModule } from '@modules/roster/roster.module'; import { SchoolApiModule } from '@modules/school/school-api.module'; import { SharingApiModule } from '@modules/sharing/sharing.module'; +import { ShdApiModule } from '@modules/shd/shd.api.module'; import { SystemApiModule } from '@modules/system/system-api.module'; import { TaskApiModule } from '@modules/task/task-api.module'; import { TeamsApiModule } from '@modules/teams/teams-api.module'; @@ -46,7 +47,6 @@ import { ALL_ENTITIES } from '@shared/domain/entity'; import { createConfigModuleOptions, DB_PASSWORD, DB_URL, DB_USERNAME } from '@src/config'; import { CoreModule } from '@src/core'; import { LoggerModule } from '@src/core/logger'; -import { ShdApiModule } from '@modules/shd/shd.api.module'; import { ServerConfigController, ServerController, ServerUc } from './api'; import { SERVER_CONFIG_TOKEN, serverConfig } from './server.config'; diff --git a/apps/server/src/modules/user-import/controller/api-test/import-user-populate.api.spec.ts b/apps/server/src/modules/user-import/controller/api-test/import-user-populate.api.spec.ts index 0ff4a8c1320..47eee9ccb07 100644 --- a/apps/server/src/modules/user-import/controller/api-test/import-user-populate.api.spec.ts +++ b/apps/server/src/modules/user-import/controller/api-test/import-user-populate.api.spec.ts @@ -1,4 +1,5 @@ -import { SchulconnexResponse, schulconnexResponseFactory } from '@infra/schulconnex-client'; +import { SchulconnexResponse } from '@infra/schulconnex-client'; +import { schulconnexResponseFactory } from '@infra/schulconnex-client/testing'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { OauthTokenResponse } from '@modules/oauth/service/dto'; import { serverConfig, ServerConfig } from '@modules/server'; diff --git a/apps/server/src/modules/user-import/service/schulconnex-fetch-import-users.service.spec.ts b/apps/server/src/modules/user-import/service/schulconnex-fetch-import-users.service.spec.ts index 3283f45a51b..c764d2b1793 100644 --- a/apps/server/src/modules/user-import/service/schulconnex-fetch-import-users.service.spec.ts +++ b/apps/server/src/modules/user-import/service/schulconnex-fetch-import-users.service.spec.ts @@ -1,6 +1,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { MongoMemoryDatabaseModule } from '@infra/database'; -import { SchulconnexResponse, schulconnexResponseFactory, SchulconnexRestClient } from '@infra/schulconnex-client'; +import { SchulconnexResponse, SchulconnexRestClient } from '@infra/schulconnex-client'; +import { schulconnexResponseFactory } from '@infra/schulconnex-client/testing'; import type { System } from '@modules/system'; import { SystemEntity } from '@modules/system/entity'; import { systemFactory } from '@modules/system/testing'; diff --git a/apps/server/src/modules/user-import/user-import.module.ts b/apps/server/src/modules/user-import/user-import.module.ts index 55dadecb912..6b7aa195767 100644 --- a/apps/server/src/modules/user-import/user-import.module.ts +++ b/apps/server/src/modules/user-import/user-import.module.ts @@ -1,8 +1,8 @@ -import { SchulconnexClientModule } from '@infra/schulconnex-client'; +import { SchulconnexClientModule } from '@infra/schulconnex-client/schulconnex-client.module'; import { AccountModule } from '@modules/account'; import { AuthorizationModule } from '@modules/authorization'; import { LegacySchoolModule } from '@modules/legacy-school'; -import { OauthModule } from '@modules/oauth'; +import { OauthModule } from '@modules/oauth/oauth.module'; import { SystemModule } from '@modules/system'; import { UserModule } from '@modules/user'; import { UserLoginMigrationModule } from '@modules/user-login-migration'; @@ -11,9 +11,9 @@ import { Module } from '@nestjs/common'; import { LegacySchoolRepo, UserRepo } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; import { ImportUserController } from './controller/import-user.controller'; +import { ImportUserRepo } from './repo'; import { SchulconnexFetchImportUsersService, UserImportService } from './service'; import { UserImportFetchUc, UserImportUc } from './uc'; -import { ImportUserRepo } from './repo'; @Module({ imports: [ diff --git a/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts b/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts index 6f9b35499eb..acfb9c4c0b9 100644 --- a/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts +++ b/apps/server/src/modules/user-login-migration/controller/api-test/user-login-migration.api.spec.ts @@ -1,6 +1,5 @@ import { Configuration } from '@hpi-schul-cloud/commons/lib'; -import { SchulconnexResponse, SchulconnexRole } from '@infra/schulconnex-client'; -import { SchulconnexPoliciesInfoResponse } from '@infra/schulconnex-client/response'; +import { SchulconnexPoliciesInfoResponse, SchulconnexResponse, SchulconnexRole } from '@infra/schulconnex-client'; import { EntityManager, ObjectId } from '@mikro-orm/mongodb'; import { OauthTokenResponse } from '@modules/oauth/service/dto'; import { ServerTestModule } from '@modules/server'; @@ -27,8 +26,8 @@ import { ErrorResponse } from '@src/core/error/dto'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { UUID } from 'bson'; -import { Response } from 'supertest'; import { DeepPartial } from 'fishery'; +import { Response } from 'supertest'; import { UserLoginMigrationUc } from '../../uc'; import { ForceMigrationParams, Oauth2MigrationParams, UserLoginMigrationResponse } from '../dto'; diff --git a/apps/server/src/modules/user-login-migration/user-login-migration-api.module.ts b/apps/server/src/modules/user-login-migration/user-login-migration-api.module.ts index 4a55101f4b7..af4e7ec5d90 100644 --- a/apps/server/src/modules/user-login-migration/user-login-migration-api.module.ts +++ b/apps/server/src/modules/user-login-migration/user-login-migration-api.module.ts @@ -1,7 +1,7 @@ import { AuthenticationModule } from '@modules/authentication'; import { AuthorizationModule } from '@modules/authorization'; import { LegacySchoolModule } from '@modules/legacy-school'; -import { OauthModule } from '@modules/oauth'; +import { OauthModule } from '@modules/oauth/oauth.module'; import { ProvisioningModule } from '@modules/provisioning'; import { UserModule } from '@modules/user'; import { ImportUserModule } from '@modules/user-import'; diff --git a/apps/server/src/shared/controller/transformer/index.ts b/apps/server/src/shared/controller/transformer/index.ts index 346d44c5b7b..26b5d83fa86 100644 --- a/apps/server/src/shared/controller/transformer/index.ts +++ b/apps/server/src/shared/controller/transformer/index.ts @@ -2,3 +2,4 @@ export * from './string-to-boolean.transformer'; export * from './decode-html-entities.transformer'; export * from './single-value-to-array.transformer'; export * from './sanitize-html.transformer'; +export { PolymorphicArrayTransform } from './polymorphic-array.transformer'; diff --git a/apps/server/src/shared/controller/transformer/polymorphic-array.transformer.spec.ts b/apps/server/src/shared/controller/transformer/polymorphic-array.transformer.spec.ts new file mode 100644 index 00000000000..5337752e7cf --- /dev/null +++ b/apps/server/src/shared/controller/transformer/polymorphic-array.transformer.spec.ts @@ -0,0 +1,57 @@ +import { PolymorphicArrayTransform } from '@shared/controller'; +import { ClassConstructor, plainToClass } from 'class-transformer'; + +describe(PolymorphicArrayTransform.name, () => { + class Str { + str!: string; + } + + class Num { + num!: number; + } + + class PolymorphicArrayDto { + @PolymorphicArrayTransform( + (obj: unknown): ClassConstructor => + typeof obj === 'object' && obj !== null && 'str' in obj ? Str : Num + ) + input!: (Str | Num)[]; + } + + describe('when input is not an array', () => { + const setup = () => { + const data = { + input: { num: 1, str: 'string' }, + }; + + return { data }; + }; + it('should not transform input', () => { + const { data } = setup(); + + const result = plainToClass(PolymorphicArrayDto, data); + + expect(result).toEqual(data); + }); + }); + + describe('when input is an array', () => { + const setup = () => { + const data = { + input: [{ num: 1 }, { str: 'string' }], + }; + + return { data }; + }; + + it('should transform input', () => { + const { data } = setup(); + + const result = plainToClass(PolymorphicArrayDto, data); + + expect(result.input).toHaveLength(2); + expect(result.input[0]).toBeInstanceOf(Num); + expect(result.input[1]).toBeInstanceOf(Str); + }); + }); +}); diff --git a/apps/server/src/shared/controller/transformer/polymorphic-array.transformer.ts b/apps/server/src/shared/controller/transformer/polymorphic-array.transformer.ts new file mode 100644 index 00000000000..1143ac7e0bb --- /dev/null +++ b/apps/server/src/shared/controller/transformer/polymorphic-array.transformer.ts @@ -0,0 +1,24 @@ +import { ClassConstructor, plainToClass, Transform, TransformFnParams } from 'class-transformer'; + +export function PolymorphicArrayTransform( + constructorDiscriminatorFn: (obj: unknown) => ClassConstructor +): PropertyDecorator { + return Transform( + (params: TransformFnParams): unknown => { + if (!Array.isArray(params.value)) { + return params.value; + } + + const transformedArray: T[] = params.value.map((value: unknown) => { + const constructor: ClassConstructor = constructorDiscriminatorFn(value); + + const obj: T = plainToClass(constructor, value); + + return obj; + }); + + return transformedArray; + }, + { toClassOnly: true } + ); +} From a82af02e99ac3683e6a7fbaeac6ab1fbb8205cab Mon Sep 17 00:00:00 2001 From: MBergCap <111343628+MBergCap@users.noreply.github.com> Date: Wed, 2 Oct 2024 18:37:51 +0200 Subject: [PATCH 4/8] N21-2188 show external tool context restriction (#5270) --- .../tool/external-tool/testing/external-tool.factory.ts | 1 + .../controller/dto/school-external-tool.response.ts | 7 ++++++- .../school-external-tool/domain/school-external-tool.do.ts | 7 +++++++ .../mapper/school-external-tool-response.mapper.ts | 1 + .../service/school-external-tool.service.spec.ts | 6 +++++- .../service/school-external-tool.service.ts | 1 + .../testing/school-external-tool.factory.ts | 1 + 7 files changed, 22 insertions(+), 2 deletions(-) diff --git a/apps/server/src/modules/tool/external-tool/testing/external-tool.factory.ts b/apps/server/src/modules/tool/external-tool/testing/external-tool.factory.ts index e3c43a5f6d6..d1bff9ba0ef 100644 --- a/apps/server/src/modules/tool/external-tool/testing/external-tool.factory.ts +++ b/apps/server/src/modules/tool/external-tool/testing/external-tool.factory.ts @@ -149,6 +149,7 @@ export const externalToolFactory = ExternalToolFactory.define(ExternalTool, ({ s isDeactivated: false, openNewTab: false, createdAt: new Date(2020, 1, 1), + restrictToContexts: undefined, isPreferred: false, }; }); diff --git a/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool.response.ts b/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool.response.ts index 76e4affd524..7a57d039746 100644 --- a/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool.response.ts +++ b/apps/server/src/modules/tool/school-external-tool/controller/dto/school-external-tool.response.ts @@ -1,4 +1,5 @@ -import { ApiProperty } from '@nestjs/swagger'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { ToolContextType } from '@modules/tool/common/enum'; import { CustomParameterEntryResponse } from './custom-parameter-entry.response'; import { SchoolExternalToolConfigurationStatusResponse } from './school-external-tool-configuration.response'; @@ -24,6 +25,9 @@ export class SchoolExternalToolResponse { @ApiProperty({ type: SchoolExternalToolConfigurationStatusResponse }) status: SchoolExternalToolConfigurationStatusResponse; + @ApiPropertyOptional({ enum: ToolContextType, enumName: 'ToolContextType', isArray: true }) + restrictToContexts?: ToolContextType[]; + constructor(response: SchoolExternalToolResponse) { this.id = response.id; this.name = response.name; @@ -32,5 +36,6 @@ export class SchoolExternalToolResponse { this.isDeactivated = response.isDeactivated; this.parameters = response.parameters; this.status = response.status; + this.restrictToContexts = response.restrictToContexts; } } diff --git a/apps/server/src/modules/tool/school-external-tool/domain/school-external-tool.do.ts b/apps/server/src/modules/tool/school-external-tool/domain/school-external-tool.do.ts index 4d8ed9b8a9f..2480442b86a 100644 --- a/apps/server/src/modules/tool/school-external-tool/domain/school-external-tool.do.ts +++ b/apps/server/src/modules/tool/school-external-tool/domain/school-external-tool.do.ts @@ -1,4 +1,5 @@ import { AuthorizableObject, DomainObject } from '@shared/domain/domain-object'; +import { ToolContextType } from '@modules/tool/common/enum'; import { CustomParameterEntry } from '../../common/domain'; import { SchoolExternalToolConfigurationStatus } from './school-external-tool-configuration-status'; @@ -16,6 +17,8 @@ export interface SchoolExternalToolProps extends AuthorizableObject { isDeactivated: boolean; status?: SchoolExternalToolConfigurationStatus; + + restrictToContexts?: ToolContextType[]; } export class SchoolExternalTool extends DomainObject { @@ -50,4 +53,8 @@ export class SchoolExternalTool extends DomainObject { set status(value: SchoolExternalToolConfigurationStatus) { this.props.status = value; } + + get restrictToContexts(): ToolContextType[] | undefined { + return this.props.restrictToContexts; + } } diff --git a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.ts b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.ts index 50e75ecd37d..3381119843e 100644 --- a/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.ts +++ b/apps/server/src/modules/tool/school-external-tool/mapper/school-external-tool-response.mapper.ts @@ -28,6 +28,7 @@ export class SchoolExternalToolResponseMapper { isOutdatedOnScopeSchool: schoolExternalTool.status.isOutdatedOnScopeSchool, isGloballyDeactivated: schoolExternalTool.status.isGloballyDeactivated, }), + restrictToContexts: schoolExternalTool.restrictToContexts, }); return response; diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.spec.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.spec.ts index 09433388abd..fe91b251c53 100644 --- a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.spec.ts +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.spec.ts @@ -2,6 +2,7 @@ import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { ValidationError } from '@shared/common'; import { SchoolExternalToolRepo } from '@shared/repo'; +import { ToolContextType } from '@modules/tool/common/enum'; import { CommonToolDeleteService, CommonToolValidationService } from '../../common/service'; import { ExternalToolService } from '../../external-tool'; import { type ExternalTool } from '../../external-tool/domain'; @@ -53,7 +54,9 @@ describe(SchoolExternalToolService.name, () => { describe('findSchoolExternalTools', () => { describe('when called with query', () => { const setup = () => { - const externalTool: ExternalTool = externalToolFactory.build(); + const externalTool: ExternalTool = externalToolFactory.build({ + restrictToContexts: [ToolContextType.COURSE], + }); const schoolExternalTool: SchoolExternalTool = schoolExternalToolFactory.build({ name: undefined, status: undefined, @@ -101,6 +104,7 @@ describe(SchoolExternalToolService.name, () => { isGloballyDeactivated: externalTool.isDeactivated, isOutdatedOnScopeSchool: true, }), + restrictToContexts: externalTool.restrictToContexts, }), ]); }); diff --git a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.ts b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.ts index 809cbd94f3c..d25c6d4e762 100644 --- a/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.ts +++ b/apps/server/src/modules/tool/school-external-tool/service/school-external-tool.service.ts @@ -55,6 +55,7 @@ export class SchoolExternalToolService { ...tool.getProps(), name: externalTool.name, status, + restrictToContexts: externalTool.restrictToContexts, }); return schoolExternalTool; diff --git a/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool.factory.ts b/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool.factory.ts index 3d979b90331..a3e8fd641e7 100644 --- a/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool.factory.ts +++ b/apps/server/src/modules/tool/school-external-tool/testing/school-external-tool.factory.ts @@ -28,5 +28,6 @@ export const schoolExternalToolFactory = SchoolExternalToolFactory.define(School toolId: 'toolId', isDeactivated: false, status: schoolExternalToolConfigurationStatusFactory.build(), + restrictToContexts: undefined, }; }); 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 5/8] 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": "iVBORw0KGgoAAAANSUhEUgAAAIAAAACACAYAAADDPmHLAAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAAYdAAAGHQBd4HF4AAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBoAAAAOdEVYdFRpdGxlAE9TTSBMb2dvM6v3AwAAAAt0RVh0QXV0aG9yAEhpbmTQ2CnUAAAAUnRFWHRDb3B5cmlnaHQAQ0MgQXR0cmlidXRpb24tU2hhcmVBbGlrZSBodHRwOi8vY3JlYXRpdmVjb21tb25zLm9yZy9saWNlbnNlcy9ieS1zYS8zLjAvXoNavAAATk9JREFUeNrtvWd0XNl5JSrJ7y3Psv3We8+2JPt5pLHGsuSxrZEltULnVnez2cw5gkQGCBA555xzzjnnnHMGCjknJjCTTbY6s1uxv7fPqaqLW1W3CgAJeSRN/9iLhcAigb3PF/b5zrlfIqIvfYHtYWJ8/K9mp6cvLczNxS4vLlatra4Orq+vL60sL99cWlx8vDA//+nM9PRvp6emPpybnb23uLCwjq9Nr66sdOH7i+bn5kKnJiYO/iH9TF+6kHH8MvBvwJf/VIjCz/IXwBvAEeCvn+W9QNj/WJyfT726vn4VZP56eHCQWpqaqLqiggrz8ykrPZ0yUlMpPTmZUhMTKSUhQQD7OA3ITkujkoICqquqoo7WVhofG3sP7zk8NzPjOT05+f/u4Of6r4AeYLibAngXIMWf9YAj8BzwZ39khL8JBAPDwK8UPxPD58AcEA8c3Y4gZqamTq0uL7dcWVt7ODsz83lHWxsV5OVxosUESyI+XgXJYsTFCUjF91aVldFQf/+v8O8tLy0s5E6Mjf2HBOEXgGzgmuhn2thNAVSJ3liMj4EOwAt4GfjzPzDC9wAhCsJ/rf7/Pxb6Nmn5ubQKYn52NgAr/b2xkRGqq66m7IwMSktK2jbZugiXRGysgDz8W11tbZ8PDPVshFX6tasRLoX/tlsCsFK+qXutMRnlnNL2D/4SGFSssreAv/pPJPwvRYSPSBHOYJR7kuwq9MinxYh8qi9Q7aIv1S4HUGyvHdmWXtQqiOSm2HtLKwtPWHgvyM3VTvouka2OJIaYGAG1lRVU311NriU2dDHzBLlUGVHqkDPVrwSL/98GuyWAf1W+aeu1UFr4MJU6N8Ipe9yN/JrMyTTvtLZf3G+BSSBGkWv/ZpcJZyILBUa1EW5ReI58Gk3Jp9mIovovUdKYlYCOG8HUuRBC60+y5Pg0i6beTaJqCCKmhwniAoVU+dLUwgSxFV+MfK5C/C6ubl1kayA6WkBDTTX+f5107YNSuvHLPA6HCgPlz5+3KwJQ/MIfsjdNHXGixY9SFUjjWMDrntuRlD/tScFtlnSp4Kyu0LoEpAJngX/YIeF7tyKc/duh7ZepZNabhu/H0eqTTCpedFMhnqHleiCt4WudC8H8TzmyOJgYVh6V05VrczQhk1FpYSEv4P6XkK1GuAaioigZfzbV1dDscjtd+biAEvsdd7UOUBJQrkwBjHB1LKph4G40FYOEoPgzZJlyQleeus6UChgB3xYR/lcKwsOAMeA32ggPAeHs3xq6H0srn2SoYO6DFEqRWauQX7fuS6ufZHJxDN2I5t+3Koggk5ZutdLi/CxVlJRQhoj4/+zVrY1whkQJsPdvb6uitsUk8e/om7slAAv2hgZZJ2nq/USa/yiFQ0oMSszdiqXBPW/S0LF9NAJBlC/4UFS3DdmWXdAliPuKtCFJuDkIZ1GmaMaLBu/F0PLH6Zv4RBUrQNtGsAr5Vate+NqmQEZvx9LorRgIIkOO68MkQ7jPy8r6g1jd2sjmiIyURFlhLgWVeSt/Z/q7JYDvKkloWA+iuQ8hAG1QiGMi05oGX3+dJtKtNMQhexSPXOtPcX125FhpwAsZ9t566cfoQrqI8PyzFATCC2c8qR8iWvo4TQvSVcAEMfwwmlLHN1d/6ZI7/16xQNj3tc0H0fIHubSOkN/V0UFZ6MmftVALCwggB1tbsjAzI4OLF+nMqVN0+OBB2vPmm/Taq6/S22+9RUcPH6ZzZ86QsYEBWV26RC729hQTErJjwjkiIlSQnZJCiZVR7HeYuysCUIiArU5KGnKAAJLVkKICJoThUwcQAd6g2Y0YSYGII8jkLxKpbi2QQhsukVelMeoJD+q9EyWZXsT1x+LHm1CKgX29+3YopY3bCuTnzDiieE0TxCFG13QSXbmyTA21tZSOIu9pVnciVqyPuzsZgcy39uyhF154gQ4eOkTnz+uRiYkpWVlbk7OLC/n5+1F8UgylZydRZEwYeXh4kI2tHZmbX6KLF/XpyJGjdATCsDQ1pWAfn20TniBGeDhHKv6P+VUZnw729PzlbgmghAnApdqQZj9I3sSHqmCCmFkO46t/xPq0VoFIYfb9JGqa9dtWilGvP6Z/kUCtNwIpY8pereizpuF3YiQjx8LtelpdWaTy4mJKZeF+h+Hc39ubzp4+Ta+8/DK9/MordBqvbe3sKC4hhmpaimh4toFqhq5QcfcNqh5/n+5/NgqM0YPPZPTglwzjdOvDEbrycIAWb/TQxFI7NXSUkV+gL8Rznvbv20fG+voUGRgoTbaIcCkkAjXlpVchgu/uhgDMmAD0Ea5l78bTzAdJAmZVkEwTZY5cAONZ1hrikMamOOomfXWmlxl8/xDCe8fNYKpZ96HCBRdKn7DVqPSVKF50lYwe87fqaW56mgqQ71NEq307uTvY15dOnThBzz//PJ05c5bc3T0oKz+N2vrLaeFmB935ZIgGpqtp4c4gNcz/lmpnfk2Nk3fo+rsDNLbYQL3jVTS60Ei3Qf6DzybooYBJfCzH6t1BGhhvQsSIpBPHj9N5iCsKQtBKeFiYVhRlZz8Y6Or6xrMK4NvK3Fy96kfTIgGoYyzMmAtgctBXUiDaIggTQ8/VMK3iaMEKT1Kr6rdCy40gjagx/6CUV/r5IH8nlTnL7edABCP+xImT5I+Pc4tQV9ztorufjnBsfDBAQ3O11D5cQU3TdyCAzzn6Vq5AFLW0dKeLbnwwSGMrTdQ7WUM3P8LfeyITiJdjiuPme2M0OtdCq7cHqa61mI5DdEYXLvBaQRvZ8UqEhqogIyFhbW15+b88tQAUIrjDBBDXb0fT6AYk8UEijSWYywvALi+tItEWPYZR7HWvh0oKZOr9BMm+Xhcar/mrRJC5x9kI+wtUAu8+WYp8iYqcFWFGCMUstx8+coS8kJ9zyuupY/461Q6MUM/8JC0+WqeF+/PU0NdK7VOILK1zVDX2HjUt/JZ6rtyhzqkqahgqppFbTeg8mmlkAxtGPQXUt1pPY7dbaflxn0D8w8+mOeav99DSRj9ez3Ks3xuhlMw4nh6cUFckaCFbAxAMQ3pCQtezCqCQCYBV7owMORI1IMu14QIYizDREMcmtIuidtJHq0B4nQAjJ3XcZlsCKJhzoRn8HR5F3k+ntSsgBj0+C/vb6buj8Ys7goLu5z//Obkh1CekF1LF0COqm/uc6oGinttU2HWDr/L62V9T3dSnIP1zKmxboOaZWRre6OCkN42VUv1wMY3ebOYCGL3VQo0jZdSzWEeyW20ku91G95+A/E9B/qczIHyGple76MqDEUEADz/F+8220drdUcouTCZjdBjayNaGnNTUqGcRADNs0LYd54XV5HsJNPn+JpSimJgL4v0/E8Gw8TFEhEs03uFBk3djuEB0RQ+G6vGtI4fs3TgqX/HYlggypux4JzCzPEyN2MRJQ8G3nb47ECv9zTfeoANo4aKio6ioVUY5zcuc+Mb533HSywbuU2HHFU56M8Pi59S5do9q+kuoZ7mGRm42cdKbx8upfrCYxmA0MTDSW8crqXu+jsZvd9DM/R66z8j/bEYgfHypg248YulhDpjH5+ZJttCF1DCFaDBGPaP1dNnSnKKRiiQJDw6mODXg85+X5eefe1oBfEtZB5Qv+dDEe/EckwISBEyshtKw6XEuAgH4ZY6GGdHUozit0YOhcz2YJh7HaxWIOHp03w1DNLDdUgQ90zXU2dJCGdh/39JoQavlir78pZdeovN6epSSmkST6wjts+uU17oM8j+H/z5Htz6epPErA9Q3N0ALD2/Q7P1bNHVvHKQ3IzUUQQC1fKUzwtuRAppGyvlKH7/dzklvG6+mvqVG/N0+uvfJFF/hDHLCYUUvd9H1R+OceDkWuABufzBDV+7LaOH6EG08nkZKcqEQLy9JwlUQFMQRGxT0S4jguzsWgEIEN5kAonttBAHoxPVwkjU402io0WZUMD+xKRaJCCJ7FEstSwGS4pCKHM3XA3SSnzPiQ1PjMspiBsk2TBZLGDgs31vb2FBpVT61DZXRjfcHaeTqIuW1LPKVPnHnHoo+GU1fbafZ6x0o5Mbp+gfDQnivHypB4VcvhPduFIaNw+U0fqudplA0Ljzsp/6ZJlp/OEwPQPp9RvyncwrMc8xc6aOrD2WceIYHT5gAevif1x9N0ezVIQhmme59tEhpOXHk5eioQbYk0E1E+fs/xJ9feRoBMO+e7Msv0vgv4jQB0pVQF8P4RgRPCUwEsmEfrdGDoVLmqSIOsUDU64+2W8E6BTAxNwiLtHBrRw1wQR//4osvkpe3DzV3V9Daw17qQdt2+5MxVO8yKmieosbZz6ht+de08dE0DaHXX73fzwWw8LBHCO+Nw2XUv9rIVzojfOZWN3XL6kA4Wj/k+LsfTfF8fuvDKZCvIB6rfBMLaAVH+Sp/+Nkix43HU6gLBjjpdz9coMnlAbr/8RK98+kKRLFCucUZFOnnJ0m4ErEi4GctfBoB6CvTQP+DSJKBdHWM68BYo4vcJPLT1xk5KmUe/Pu1CUQcPZpuaI8A1bIkGuju5nl/K0ctwNOTXgL5Ts4uMHKK4WJ2UM9ENS3f66E7aNXuPBmjThR2Ra0LVDPyADl9hmRLLZz8u08m0F72COG9eaSCBteaESk6aePDcZA8Q1Mo6vommmjlLvYcljqx8TSEz89zyFf9goCHny7SvY/nQXIfRDCCwm8cr/tp412WJlY4Vm5N0Nz6CF17MEML18bw8SQ1d5aqkB6rDtQLSoR6eX1+YP/+53YqgG8qBVA870lj78bS2C/kkGlAUxyyhzE0uBebRKf264wgHagD+u9EbJlimDiKFlwlyU+R2dDsnIzyMU2zlYXK3LY3IEwjY2Oqby2j2RsdPLyvv9PPV/9tLgAZ+vZpap58QNVDt6lh/DGNXn+HVh9fAclYsY8HOeEM/ctNNHYD4f5ON117X0b3EOLvfoLKfr2XFjcG6drjcbr3REn+ggjy1f7g0yWOO3jftbsTtHpngm6/t6AgfxWrfpX/eeOdOVq/O41aAF97sk6PPrtC+QXJPA2IyZaEvz9ZGBou70gAChGwbVwK77ai0Xdj5CLQBTWBDJ3cT4P79mwZPRoX/CQFMvaLeOq5F06tG4E09E4U5c85Sxd+s9XUhKo/BUWfLguV9dJss+bo0aNUUllAiyBteB6O3UQtyVZa6eaHY3D3xhWYoLVfrFLLzIdU1rlEnSu/oy6G1d/RwoNl6l9qotaxGmoZrabehSZ0Pr209M4QBDBHG79A0bjUDXOnE/kdwvpwVoX4+09A/JMlORDmGa69M03TawM0MtcO8aAA/FgGMQ7TzSeDQB/QD8exnfqnankUuv9kBgXlAsVEBGmQLYVQRL29e/a8vlMBsCFEssHWLhPAVlAXxNDFw3Kf4FGMIA6pCFI+5r6t9NJ4zU+D/NQRexqXDfOdMV1+OYMBKv030KGkZ8EivtmNX2YdrTzoR84fpfHVNnQ0bVj9Exx3n0zS2v1B6pT1UlX3PHWD+B6Gtd9Rx9w71CarxcrvBLqoY6KBBlfaaeZeP936aIYmVnoQ/sfQ20/Q8q1RmgKxjHSGe0/m5HXGJ920/hHqio/K4B8kUniuMdrKEBq+E00p1Zcpv8NRxRyrg2cSlH6RootMVRzU4m5MbLk4aRAeowRqhQSkinTURV7Ozrd3KgA9ZRpgbdjI42gFYmjkXTm0CuJRNA2+vYcGT+zTKhAlqiY9aQTfL51eNlNM2bK7pgUsK6JGjFmz0K/LMw9G+8TavfDICIT9bhqcaaDJtXaQIV/xGx+MUf9kA6IA8vwncPxu9dMw7Fn259BMO8L+NZq4/QvqXYcvMHydhlaxUu/1cdJHr3ZR12QT2sNBmtkYoJl1EI6Vvn4f4RxtX11vJjyCHBCWIWmO5bbbU2GPs/Bx341wCgTZU9j4YsZYEb4WmW9Mpf2uFF1oquKexhSbUUalGyXjZ2Zks3oglW0VxydQUXoGVWLEraaomKMiL59tTZ/diQD+QSmAgll3Ggb5SozoRAwNz/jz1T/kcm7LyNF5I4Ra1wO3TC+9DyKo7io2huZdBAGMTw5QNiZ6tvLMT2OjxdrGllqGq3m71jiAlbfaAmt2AKF+iK68N0J9U420hjB+86MJuvJ4BJFhHB+P0OAs0sNHk7T+7goNXP0Yhd84jWNXb+beACd9CiLpxIbO7L0hHgl6Zstp9cNaapmP5N1LfMUluJ7eWlvcpGpLOIs+gv8x+V4i+afo0RC2ypkgeq+H0RQ+17aMtq7QREU8LCoMsVG9PBhmuXkgukhOeGERVRcUasDfw+PRtgWgEMEVJoDQTksaehzFMayCaGlU2cu9gLRLkgJRjyBNK/47Si/FS25UMBZE/Z2dlILwpmuDxM/NjV7FgEZ+cR5aN3n1Xt9fRn0LiALYtGFgRRzL6cPr7cjnPWjpejnGrndR21g9X+mMcIYOWSMnfe7+EAQwTDPY0ctvTKbCyRByzDxF3oXnaQKrd+BOJMbmIim13opKBl21trcRWN1KP0QpiuBMfepaD1ERSisGWSMLjYUIMvYwlgtFhj+HbyRTPc4XiMmuyi/QQAEGYdD+ntiJADKYAKxKztPQoyjtUBPHULihPAJ0uEoKRF0UpaOu204v7HuypiGwmWa2BbrlBskxDF8ws6cLIZ5V7ozwNhmKt/lGTraS8Db076PXOnloV4Z3ttI7xhv5SmdgpHdONNPUzQEupIYVTBZheEbP63WKH7KEAE6QU/ZJasDm1BiMrrppb0qqsaDKMQ+t7S0jlRXCYnEEpF2gXqQCsTnGRBJZYCwIQvZOHPkln6eR+9E8chSXJFM1SK5EqNcKRIkL587d2IkAzirTADNiBh9FaoGqIAaNj8oFcCVYUiDq0aMK42C9d8O3kWJiqGrdC62fLY2O9lIG+n5dGyTMNWNDHEUVhcJKZ4R3zTRS+0Q9J56RPXW3j9pG62jyVq+w0hnhExt91AXCZ+8NC8hpjqe0Pk8+hMLSUHi7CRkGvsVfuxWcJru0Y5Q46kRuDb8g85QFOuVdTvYFixTY/phCu+6jq7qrAqPQInLOHxQ+Duu8TkaYRehE3SA2x5QCEEcQLpQNuVD6N+KprrSEKkAyR06uJLwxtfSzn/3sO9sVAN8XOBG5nxJ7bKgXbdnAO5FyPJJDQwxtzvI9AZOjkuKQQhfGu6pnvbZML+23gymZDX6Ox8uLP9be6dggOY5tXStrG/j4rcJKn7zdQ71zzdTQW0UjV9DP3+wh2XX5SlcSz8M7yJ5AUdc92QrzZ5S6r5VTzpQn2aYcJZfcU0Id4l50lqziD/PXwU2GXAyJI5cpvCefDOIWaa91PlkXfUSO1SSJC5HTdNKvR/jYNHWDDjpVkVv9L6npWq1gkDUvBlAEBCCOIInVFlQ24sYFMXQvitLSovkqL8/O0URWNkc6Fg0E4LBdAezlEQBDnMXTblQic6WiUReEPl+qx0ZR770wiCFCAQjifgQN6h3iAhjod5cWiJbo0bjit2WKyZ+XewE9snrKxxk9XTtibPeMDWeW1pTw0C5Dy8aqd0b6+EYPNQ/WUg/SSP98G7UM1tEgXDvZtV4e3uewJz93fwS5Hvm9OZlyp7wEwtmKv+j7Jtnjd8LCPXsd2WUqfN0i+iCZRewjz5JzdNYzhAwjmsir6WNyB6HOtb/VEIBt6ae0364EIuimC1EztM+2iMwy7vCv+ba8L5hhTYv+FF5gJHw8AAOtoNuRXCJPUmiuIYXnG5GNHwpOjL6VITUqUYqBGHXADxnZrgDYzD6Z5Z+hvnfCqV+Bvodh1LTuT6XjckHUQxB1ixCEj5589budEwlDJBC16CEWSPGIs06B9NwPE8Ju/2ArpWGwQ9dumAuGKcwxidspg1V7UzW0M8iu99LIGkSB3biJG/2ccEb8FBy88asD1DJXTLGtDpzoxFHV9jOi05ScEQVc80/z1+KvJQxbcvIds05QdJsZlcvct3RQ+26HU0GfE+V1O1DzSgRPFUqBdN4u5D5Iyyqs8FpLaod7WjnuQc1L/jT8AGNzSLN5EEJMvQVdTDxBjThSpkJ4ZiaVwCUVw9LU7LeIAv/XdgTADmzg2JUpSA/Xit4HYdQcqEdlGKMqPvQaNUx6UB0iRA8iRD/IV8eABGoXvKnlSoDW9NJ6K1D4Jfd3d1IS+l1dO2J6GOsKQ4pgFbu4iGOV+yQmcEZWu6lvtgMhvo0GF7s4+QsPxng9UDYfzv+dwHp9OuP6KvngrKFvpR5f/YxwJghW9LGVHlCnL+lQ8tSQcJgye223dE+rkVqiSkwoIF2P0hqtqAhRyab4YzLPvEux7fVUgT2T7A47Cs7SlzTJhu5HkXHQQToZdYDKK3J/o056MY6vc6ALYAjFJhgEcGIrH+CvlIc3UsccqBerXhPhHP0Zl+Qr//g+6lv044JoWvdDynChQhw1q1v2odolbwgiVBFFNEXBxFI85iIpDoaO2/LdwPyRQGqG9RsvRb5oR+x1TPdUNVYKpLOczlc5MLrWQz1TaPluQBh3Rmh4CdFguRerqQhzBw783/GrvkgG/ns4xKs7FIdO3bDyz7q9RqecXuZEiyMEy/+sGGSdARNAeJMJ9d2L0NrVtF8JoiAQWzXhASF4kU/yObIM8+Hk25X9kgrn+iEAd/JNPU9h+YYa0aPreghFFpvQfuuXuACyKhOvVOBwq5hwhiKkTCVycO4RM4+5Wwlgr7IDqL3qSz0gVQUP5egvlx8OGTz4FvWNe0oKREoQNQte1HUnZDOSQBj1Kz4aAum+G8qjScWsB1+JLSN5cLnStW5/Mvg4OdHBg4eob7JLIH3+/qiA/tlOTvjCAxktPhgn2UYXuSQYU6zicCkjlRVzboVnVATAEN1rTvp+b5JzzkkyDt5Lrnno/cvPk1fZeYroMEFU2M/BWkImABYpqhY8NFrcnluh1HEtmPwzL1BQ9kWqm0cKRV6vnEuhY06p5FAprxdSWzwoJMeA8nocKTTPUCWCtF8NIt+085Tfi27H9CdcALE1IXF1qAMY0YVKwCpXB6agHiMKfFmXAIT83/0gVAU9CvRO4/jWm2/wnb/eXlcNcfTqQCMEUYQVnzfkSFWznpz8/EFH6kS72bjmSxUzHlwsrM5ouRbAhdF2G47hQj1lQcHatj8ZLLHbd9kKMwLXhjjhLLTLIeMYnOumCQxaMPLb1vMpFW0lIzq01Vie40EkX+34WF0AcYMWKPrM+GvTcNUUkIDV71WK08rlemQOMkxwPwH73qgmM+pmhF8PppIRF466OW9qR+vmlBJJ3mWtFNjxDrnU/oYcqn5He61y0Tl8iFrgLndJh2GVM4GE5hmoRI++O1hct8L4BPdLpj/mAjDIOvVvHXV1HxTAIdUKHI45cuAASwP/rEsAPP97I4R13Q+hrgchID9ERQj9Tqf56u8ttpIUiCrCdAqECSK+zpyycI6/+aq/SgQR1xsDI3WUjp0/XdufhpiojUmMEQhXrnSGuTsyGprvhTj6qWwhUiDPKPgwOZekkW/rOAV2tlLsYJCkAJRIn7Qjt9TTFNdhgTMLdhpfZ4Xg5fhDPGVYxR2m2jlPpIIwoaUdglEU2nWHt3xm6bdVOoN9dsXkWLJOrTcLBP+DCSBETQBKxA/acQGgXX/MuOtubLzGIkA+iM7HeJwU2BkECOA1bfMAQv5PHrOXC0ANPd3yfn/A8DB13QuWFIgUenQIJG/QYcsI0tVZxzc/dG1/nsVsfVNng0D60sMJYJKWgZmNUWofr6PIhs2zB1H98SCiFHn3nkCCU82vybM6S6sAGJySTlDljDv1P4ygzCk7DQGwFMBesxTR9yBcpbWtu1LO/51DLjVkmnaTnGt+R/5tj5Eylsk82I0aVyNUPJBaRIyQPH1Jg8y1xpAL4GjI3kbGX29z81QxBJCHSClGLjwAJUwxAg8BnNMmACH/V13xps77wRrojTTgAuiptpEUiAa2IZCaJS+t4mi/G0ehPRPU3lpLCWwQQkF2HISQhD2ADLSFeVB8MareC2fP0sBEn0D68sMpWn5nilaw7946W0ZOaad5nuc5fSCCnKo/pbdtCjQMG8uch6TnfVarAFgKiGk35+S23w6iFAkBxA5YUGyfBS9kxV1Nw/Ui/m8c82ol2+wxfD2GC6MPZptHwmnujIod1No5LwqGANTNsf6HkfxENxPAfu/X3Rh/A62tnSUo/HIxFq+OHAVszc2ZAJy0CUDI/x33goBgDfRflk8Dd817SQqkE6QrsR1xtCO/F4+7aBVHyXIB/4W1t7ZTJvthEMYK09KpLCdHA+yk7ujckED6sgJNa9nI2QY8T1vEHOREeTTepEvZD7gZw/KvWAAWOe/QYadonQJgNQBb/R13g0GqH+XOOvL7CpQCCGkwpHoU0er+xwCGXPzbHglOYOlyIxdGJQ7NBuZc1HBQa+CUBufqa5hj5SvyY+JMAM8b/uhlxt9QZ2dZCQplRnQuDsZwYIEw5Cjghv0RCCBamwCE/N8OAWjgbhAN7t9DA4f3ahVIh6QoNMWhFEj5rBuOpftqFUj+nBvs0cc0NCDD3nYelYJoDgm3i52zG1/B/Nw7M8AsJ79yMZY7dlHdZrwyNwp6i0yjTOli9BzPucbJ1zRcOiYA9rWEEWedAuDDKTjE0rwRwDuXPpYSUMtElhtT4bCzVv+j5kqp4ATqBTdSRpczeaWepRqsdnVDrBqFclDuRQ2DLHbAlgvgBcPnPvzSl770dcbfaE9PSikTACMb9ZI6srGD6uvgwARQInUu4C+V+T9x1I7a7gbKcS9QEEAHqnS2+vvNj0kKpEMDWwskB4WMlEDY9xYtymcA4oa9aaBvmMpBfAmIFsCMDwWKgePHjtHMNczYPZqjlYczIF++ilkbySp19pq1fGbJxXQ+fJxHACmf3qbkE9KPXaDowUhJAfDWT+QEJmPlN27482K1Dr4H62BYJ9MvuKia/kfMwCo3fVgk8MjP5ja7lIPaei2Qi0ndIHOult8XtMfplQ4IgB8TnxgYCC+DALIZ2RiWYchSg7+zMxNAr5QAhPxfjp23VqUAxNiQD3z0GxyWFMhWkBJHJXpldYE0bvih2lY9EDLY28497hKF28VyfpEazp09Rwu3piGAGdxaEqlCmPI1W7knvYu49858eLvyX2368yVP6FRAH1/9h11qybMyXoN8bU4gcwv9Cy6QR/IZCinQp2pW/etwUVtvp5JpyiodcWugA/YFFFVlp+KgshY4tsacvFJwVU6BgYpAmD2uD8v5XPIR+te3vpOljACygYEcFgGUZGfCNeVA8ayEpzwFrEkJIFSZ/1vvBmhBIA2c3Mcvh2hDDmqTEokYW4ij+ZY/5Y85agikHX+38YafsAnE0NvVyslnZpAYhSh6lDA2NMTR8ClYuhGbDh3cOmbn8sKvx5wbPeaZ1/kqZwIQ78id9O2ic6GjuOrmMzJMWKUTrpiv69u8gYz9fbbpw9w+sQCYKIxD9sIHOM+vqgnIgYWceV6rg8oE0LDqS6YhAbwDYb2/XWIkJTdZyi12fG94sSFldqLQvoMo2WfPUwQzx5g4SlE0n447RK9efp7+9lt/7S1KAXXFMHuUZGewIlkNDvIi8IaUANgNXdjBwqTKHX8gQBLdcfKhj17n0/Dp/VXEoY6tBFIy60K1a946RZI5I78Uoqejia/6QrHTpQYr3MaR3uLPB0eEVg+5X0ki2861x166ONTvtcrj+ZiRwDoC+4pfC18/4dNKLgU+Gk4gM3rEAvCtukDm2Drn6YABk1SeKWeoCqP12trbBNyYEt/khLZTXoA6l10j98TT1H0vlKcRbxDei5WujBjB+fpUhJqEvQ7E9XgvGD1H+zx//h7IP68UwHBHx2AB2j4x4ens5LAIFrjpBAIYVb8fQMj/CSO21AwBSIEJo3XFi/r1DnIR9F06Su2wI1VEoiN6qIsjs992ywhStiYfCu3sruE+dwEULkDkcuUDrMcNKzLnUaRy3VMj/9smH4XN66xhvrCij3nwh5yrVb52PnyCzgUPUMxghk4nkFnH9hgIUX7sX6VPQYX62L9w1Op/hJca8Tohsm+d/1usE7kcas7NsGKZM4UWG2gIJq3DmgvAtkSPziYeZum6CuSfUwpgsK1tIQ8FoJLsFHb7CBsaxXBsFEbkwjEocwqnoSGAWnUBvKXM/6Wr7tR0208Ef2pSE0LrvAf16x8SDob2nztA3cF61FFrTa0Igdqih7pASudcdQqEoea6N/+ltvYV8y1O5nLlSQHtod0lC/JKNOCRow1IQYUuzv+Xog+QR2m0CsnMkWOGjFHiGh31aFb52sWYeUSBTnKr+4xKYVlnKEwfdQGwrWNnxbAISwdMKG7Zpymj21pre+ubdZ7KMGtRtlwp/Ht63uFUveBJWbjMMqrSVMVBTW234iLovBfC87+CLyuxAPqamm4lg/Ro3GkUjn2RMFT86ji0dy8TQKq6AIT83wjSlWjShVt+1F55mfrMj8r3BYRTwogMFseoM9sM5ou7RgRRppeGDV/KHbXfMnoULsrrgPrBTN7qKclWd7sY2CiYbcAZIXpkgTAWAcRDG864ak1FAI4VPA8bJV2ho+6NqgKInuVFIXtddTWLdyqsQLVLOEpBDQbC+ypnApW7ifGIFmxaKKPXRmt7659znkomXfFxGLnW/VJuDjknUd2qD2xxW4pAKyl2UJOaLTmKFtzF1+v9m0gAX+6srf0oytVVkvgwnIhmeAscQQC+6gLg+d+z0ZgabvmCfCn4aUAphmZ0De31UHuIHvWf2a9ybLzP+DBMHM20UjjtRJWrHloFwtAIkaUoLouoksVRJdpATjYsTXUwtysRTqGR/VEhguTh3gDmyCmJsk4+TebJm0WfY9XnPO971a9Scl8yWUYawR3cNIVYQagXOSUXwHq2UKT6ZmMnbgwbVitu/Lo65Uyg0v5lf7KdwYxBG60GWQhCfM6AC3yOJi4AVnsctEvBDGYgJp+dKLjwokrEiK42pWzc3hLZZ6Uk/xG75l8pgOHOzmO18Eki2MpXkC2FV3BOAgIwE98T+JfK61njhq25ALbCVgJpQd/aFW9I/RcOClGhM8NEJYKk4QeRSi/sa5VXPKlo2YWTz1C65oY7jKOoHtud2mzObLheWeh/9QyOYrWm4O8XUUxbGGXjqrgkWSp+tlqySOunY54tAsHWeRuoxF2o5TYi0nVfsgg7QLbFj4Svs5rAJPUGf12xniMUpr7Z56hw3Im/br2DQxkD1mQS/DafCWARh3kNbGQsd9JBq/+R1GZLxpFlKjOBZ7wyuTgarviSa8JJaoHBxD7uhNvomXoGW+PuEK2+UgBVnESFAMb7+qozMSQrJjuUASeilQjATinIZzgkFoCQ/4tX3aj+lo9WNHDsTCBtdVY0APeQtY5NSx6CSAomHVUEwVZ7AcJ9iuICyGQZroBZcMb7+W3WHnUY0ATRjOxsFDvqYAK4hFbQOl9OImvlxCGdrbIDDuV0wrsF1X0hOcafxf0CdkLd4Vdqia+XcRPoqEcT9+uVf7dsNVeoS3wggIJxR5VuJbDwArkknyTfIj0yC9tHbrhou/aaj1b/I2GwU2Um8LBjJqX2xwkCia0zJe+Ms3AWbcg/Vw8+gQnEFqSS/8UCGGpru5mAq+3EhKvjMo7JKQTwY7EA5PkfYazuprcIPnLoEMR2BdIZrc8jQQeiABNF3Q1vyhmzUxFK/sJmz589a0+1+B71uqO5pYCveEa0BmBzMvMjGAWQnm8dr6qNEtcFAt3qP6WQ7iuYcqqiuBZ7isNt6MUzLir1R4qsENbwVR762d8Vm0SlK3mCUFj3Ur3iqdKxNN/0p+BifYquNaGCCSfMMARo9z9wqse1/jPuBF6MnqRQ3FxeidwuFgiz3XMG7Sm+0ZzyRh34TETBvJtK/lcI4Htednb/jALw8yi4fALhuM1UHSfkswC/Us4FquR/j0YjqgXxYtTpxPYF0p5mJBcA/mSCyMfqL1t2EwRSc92L26m5cw5UedVDa3ppHkXrhwJQSXamBNLR+54wdqCA1mnYrW0QVineP1aIIrnY5vbNw01cCccppEyf6m/4CPVHRN8AJ5sVhfvtS1WiR9l6FqV2Y/Aj8wy5YqXHNZvL22KFKMpAjm3kEXLGgCaLBrVrXlrb29SJXv6eTGwm4THkmnSSoqpNqOWWpmiYUeaTeZaLI7zHUsj/X/7Kl38E8k8CZ+tKS0vL2cyfBOkccP9CMCz7sjz/d4qfFyDk/5hhK6rZ8OKovakO7x2KQyGQDR9qyzKhwbfe5J1CE6aAmCCSey01RMK+f6vU0notnErS0wSLM0OBdLheAiAA44vnKbUH18jd9FWJIJW4eMol+QQXQdUarsCHAELKDQRxBHctkknKde4N7LcrIpe6D/EAivswlpooD3+HkV++4MbJ9kYaSOm6LHQ07mmnKADE11/3odhGM/LKOivZ3jbfDuPR6HLeu+hAShEJA6gGrXNQ8UVM+JqqOKhZ2PBxTjhB3hAA+9ihUv7gi30eP28H8YeA/wf4u/7W1jtJOFQiJpyTLoIjUqMi/NuKBSDk/4JlZ0EAW2E7AmkE2T2W8pNCA2+/Sa1FZoI4MoZtdpxilOmlsjKNMhREM6SpAwZIBM7Fe6XpaUSQ2GYzCqsyFATBRGAfcwS7c17847g2C1jFyN8FemTgJx8ISZ2w4XWIPz6X3mctiCUHG2YeGaf566whW/KAAEpmXbkgmMXNIoF6imGCSJb189V/JniI7DIKBWGUL7qTU/xxRAF/LoJ4RBjP9DOU2ApvAwJowuf1FRdv//u+72aC+H/k5k9Ly5H2igoe/kPUSA9RrHyGczgqpxDAt8QC4PnfFNOu1RueW2I74qi74kGdAed45c8tY9PD1CBzEoRRdc0T1bH9U6eXhtE4vt2ZhoqXkZ0qBbSDTk5GGhEkpBzmTaeFSnpxTDhGRbPOXCAluI2EdR/qI2F5CzjylXoSV9k7C+ZY+Yo72cce5X5IYrsFeSI68DpFIRC//POUMcDEE0JpE50U3rtMnk0fbNrM3q2U3O0tiKMF7+kQcxRzfp5cKCzSsM+xeoIJIBd3IioX61f/+197QAD/N98A6u1dScfPq064Ol7FxVggf1n9kTENJ+GOGcMiLVzAkOYNDwU8VbAdcXCBXPekbocTcnfw6F5qKTSjmhuqAsnBHF3xkrPO6KErxTRgl6wwOYmTrESKEhgRZ2BuWCSiQFTNJbSUHkIE8c49S2n9qq2uW9pJysJqFncv2fg/mofuo7RJG2Hk+3L4AYxq4/+NtMaIrrnqRXbRR6galnNEjRFSwGkV95Sll+gmR/Jruy+55WyGEfB8jKKL/Q8WNdj+iNgcy8dovgOE5oELO84kHGI7gL/5yv/xFRMI4L80Fxcf68dj7CJhgEkSj7aPwQPDst///veZACLUBeDLFGWI1iIT9/El9FyibGZ39ltgJ04sCHVIC6Qj8Jx81RscpJplV8nokdht8czppawiTk48iE6WAhsXw9iYmyuGTUdtKXPEhrLRddhEY1y7w1wlorjgqSd5E/YqoiicceKRQflxPWoJO6SKUlxOrRRJ1boH2eL96lBERuBpK66pp1TMsQDkdIu0IRXSWe4P7V6jnLkq8kDEYLWI2ANxjDtGpfOuGFp1pUyYSCVY9eF4aBSLDHmIUoyr8zij+PXv/G0ABPAXM8PDN/nqF5GtRLAIx3DN/Q9+8AMmgJfUBfCKMqxkzztQxXV3qgTK1vEfgAET322uIohKkC+Giiiue/B+fwDnBGqgYm0RJAlzck+bXkoQntOG4A90e/EiMIkRrSBbHWwTJAabICkdm+nGB5do+hagQOxHWIdJkwFxWEUepLIVVf+jAA6iA+xecf3Bisf8KUdBFMUgxynxOH8djgjgkXlaJbXYxDth6vcWJ967+V2YWWkqAuE1BaIRK1QZ6axoNfd/m5K7LEG8K/dFmChYlGC1BvvYAHYz4+pf3vh2WU9Tk18LrsWNgNkTrEZ48OXLArxxL+K/fPe79KMf/ehdCODP1AXw58Bn/FIokMwEIIVSCCJDphAEG9TggnBQEUPtqPxyiC7b49ICAZiwcqcdtp1eilGYpg5exulciLDHgrLGbDGs4s7FkJ4YxElWRwKugI1D+I+BHxCJI9FejpZC5EjosiDvvLOCINKHrMnUby/vSvLx/8octaGKNQ+8VhUAQ3AFC+mmwsfxGAr1h+nDX+MsIJsUrt+QF6pVSAsnXKKEYdOCpVxBGA2obYqRSjyzzpB7ximekpiYsoZtyS39lIb3kSeDANLl6cWpWu4C7rN6fXpyYOD9RBg/YrKlcBLPJfj2t79NP/7xj720PTKmlz8sArtb5dfdtEC7ILIgiESs6iIolwmg2+ooyHdXQDVipI9aw91zVBFINWzeGgx+sNdFS06UMsAIt6eE7ktYoTYQjZukOMomfHgaUJIdBbK5D458KACbIGwLNDHHhYum6poHOSUfIx88nyi2zZTs44/CxrUSBMK8/XTY4aEYt7II3oe0YYttZXculsJZJ7KLPQLTxpAi6o3JLu4IlWBkjQkgpe8yFxaLAomdeERM6gkyiB7k5Ls3vIci04mysM2eK7OnVFjghSgmK9CSsggSiGcaxrWaw5U8Rrnj9hr+Ry4E4J5+mr8O75X7AN2dDb8rxp4Ia/VUCLe0VIG7kRF95zvfoe9973uPsPr/QpsAeB1gjO3MsmtuktAuDDlKFIIoeu1Fqn3zFUpBns+bc9CIJLGdZjzFcHHgfeu8TlL5Ky9SI57KkX/6DSqIPEMlq66S0UOq/oiJcZNveyrI1gYmkMJeby6CMtjdkdj08sVzB7NwMkiq/ihdcaXA8guCIJJgwOQhQiSA3DCII6RSn3In5KmldsObR5BqdDcJHZfwNQMKKDXDUe/bfB/BPreD1xTM2VTvaMrx/pEQU2DJBSrA5piU91GGa3HC8CQXJohcLJTyxmxqKy+Xh341wsUIAvbjnOQPf/hD+ulPf2qt66FRQh2QMWdPpddcBZRpQLc42rAVy6JAY9g5SoezF4t98WTsj6dh5edisicUo9kpQ5e5WLKt91MNbu7sOojjZXoHhN3DDs9TXBxSEUS9/ijH3kKMl6fO7U8lYjzxfni6WQQcTzuc2HFOxZW4CUcof9ZBo9aIbTfjK1z980wYrIg08HyDopuNeUrKnXLgqSUfdUMyIoFtzGGyw0aY8oxf5MCkZnuraHHDcLDDMfEYuaITYVGjFIWzukhikHZYauCW+mQWoe+naIhenewgCwsVuMgnf/j4F/B/6hKAUAeE9plT6VUXkO+iIgRtUBdIxRwmYVEEDmDjp2rYThBIyToe/cLO3uEpn0WraKU6rORzA6fexjUxjjzF1GA6uPe8fCu5w+OUZC0iRA+ROBIz3CiCRQAt25/iHbFoX3cUc6dRW7jgvdAmNpuQe/YpIa2UX3GjYOy2WUcf4iJRL1BZumC1gQ2+ziIDI4ytflanFGEvg6UWm5hD5JlnJlT+Ef3Tku1tBv6+G2oA1qbWolWOqDPiNYVSHOzzrO5ggnJLP0mtS7hat7+PEhDNpAhXAe5HOIynlykEoL+dR8bwOsCp9iKVQAC6sJVAGrPkM4Od1kc1BBLTYUplV3CY9KJ8xdehTxdHkEocrug9KxdBQ7mZUH+UaylOGWpwjVq6aBtU124Ys0kTIlCkXfERilKrSPT3i07847A6Q/IpOgdCMT4mEgBDAbogp5RjlD5iTS7pJygN1jn7fCXqiuQ+S/JCDRCOvx+LgtAzz1gQQHjfrGR7G1RxkX+vUhglEJN11CFsZXtwgbA0E4RLu1MRVWILnGkWF2OmoruRJB+Ei2GFE1IK8peBr2xHAL7yBzCfouKrzmpw4SjZJkrXnalHX05wA+62EYsjEcMZNfHy20Q67Y5JppcaRXToP/IWjyi6ao+a9QBamBmjfGwPayNcCvkpOHO3GMwjiB3SAPNA2OuyK278z2wcAGUCENceFVfdOdms9nBJO4EHOlsJ4mAFK/s6ex3XgX0AnOdXCiC0d16ypfXMOUMp/ZdVPsciT+G8ExcIc0yZMJoG02hyoJ/S0O6GoNBTJ1sFmPh1wVzki3LXj+H4dh8aJdQBqTO2VHTFWUCxEle1QVMg1a2W8jExENmYfBE9sDP/fFHPZap67SUawLHyShgz2qJIa/BZeSpwP6m1/qheC6D5mVEqwPYwSwFad8K0bJCkhwVR3TjmA7GqkwcsVVIM62xsIABt9YdcAJcFcST2XhLEEcsFcFEQQEjPkmQX44huhHVFYgGwuoM5pcqPR6fqqb+xkeIx0Cm10hnhgSJ4w/H7OW5HU5BfsJOnhgl1QHCvGRVeceIo0oCzBrSJo7renPrZIAizhQ/soQ6s+P5Db/GKvynugs70Uob+v/eU/AEUtTgcoS6QqjU8LGoa5GPlR6Dg24psbRskCTCKMvKCueklTiuZeFSdTewhrfWHMwTAilmlIIrw/y2Cvc1ex0AA3jCblAII6lqTdE9dM07y2kEsCh4BsO9Qe8WPZqb7qLGggKIhbiXZ6oSLEYAHUu7HncgK8seAP9/pI2N4HWBbeYky56MEEWhD0TYEUg7HrcPxuCAEFhXSHQ+IRKM9vdQ0yq+f6ToBUbbNkk/bTQrrG0MxWUtzUzIe9gXyt0m4FCLxHg31mbi9JERILZnYAbSGALTVH05pOD4/aCl8zFJHAjwRJg5W53gXnsXNYPLzBX7tG5IdjDfzIiAWZRRhhelluJJt03j8HcRdgjnHMPzftREeCIdPjNPyYQ+Gu8DfPc0zg3gdoJ/FDk58Tn4dq5SLqZeCdUcNFK477Uggxaswf9phcWLSJw6dRu6i/ZYRhEcRW/ldBJlu8hPCvrVPaHoczwTEbmA4Cr6dkq3LMy/BtHEXRs8r8dzjDEQEq5hDWmsPJwyAMgGIxcHIZH/GtJuSF8h1a5C7gN4tDyQ7GGaiueF4d9lVed2R1+pPE7gEs7+hgZKZw4diTxvZ6jDGVfgK8j8Fnnvah0a9qqwDbMs3+H/eo+kdysERsHwJEWgDF4cOgWTP22Js2nxb6SWwYpV69x6kvrf2UXjRBic/T538pyBbyjNnCMXfqc7JpDY8888l8ZzW+sMxFRPAA5YqoojDCaSyq7jZG2cEvQrOYOv3sfx32PieZPfCogYTQEimJY0OtdNwWxsv9ELx/9JJOEK9GJa4FOPHzz2nFMC5Z3lsnFAHWJVVCTkssAsrbt1BAo4q2IlAkscst4wg2UthPBKlelfS4MmTND8ySHmY/QtHdf9UhOvyzNVcNBZ66wqyqX+0ihpnEvgqFdcgCYhiOTN2KgLJX3SEU+fAhcEiiE+r/NJH17pPNSJIJY7DdUxl0/Q0bivt6qJs1sZqI15EdoAazBXkw+lj5Ic+04MjFSLoYwKwg93pWv+hIIL48QLKW3OQRP6aw5YCURdBeJuh7giC1R+AJ3awf9u/+kOaH5Qg/2nJliBcm6vG3rcQtUZXXRUN4IbRzskcnGfwQHo4yFtasSiicVxcz+Pn5Ij0YIlLm2zyp+Vn/nABFO9aFkPwlJJSmpnso4neHmrEnf5JKEJVVrza6lYnPMDEhMMfOI+noChWPUOhrn5/JwLw434ADkAmTGYKAnCpe0KZS/Ci1+wF5KnAYUcCiekzoawFW0mBFGNDKA15MKpgjYKq36Np2TTlYQg0HMXedkP505Cty1VjCMbrVOw2NhUVUldjDQ30NtDwKIDLp7vGcF6hLIKKWiLwvMAiqm3rpcbGPohmlkYGR2liqJsGMLhRioHWGNi4ISy/b7G6lWSrwxcbPIexv68k/8DlPSxt//CZHh4tVQckTMJ06BkTRBDQNUM5qyB/1V5FCNqgSyAZczY4PGGmKg68bmn0pol0XIicCtsTZs14YhS1RQRSBMjf7dWti2xdRouyMGMfhyESsU2ZZKzkHMwjMoKLUKNksssr2e4k61DY/wnfu93VLQn09wzueJzsntde48S/9MoLdNzrgHJMzHG3BCDUAf7dxpQJe9Kt4X1BBIlTyRCBnQ5sXyCJo5c2BbKKW8JKg2lKSX4yIx+PrYsLo7EY3EQW5EWRVrtDthumY7dF+BZ991aV+bOSrQ5L3H+kONZFb+x7BRtQl3H24YJSAI27IgBxHWBfrUdZK7YUK8sRBODR9ACfc6BskK2OnG1hUyAhLfpyMeDjzmI8/CAjUZL80ahAGg33o05/bPpcfrbVfWr/fv4LPIEhCU/8UnW5ajsi/GnJ1kG4vwJO587RfvmBTo7D596kzEm5ixqEzkMhgPeBr+yWAHgdYIg6IGvFhovAp33ziFX4YBP/nBLZSqyKsbVAgloxfdtbSU3ZBdrJj5STPxLqQ8NBHtTu5chFsJPcrYSS/BdefJ5MPI/Sa6+/THoYlfYFCU9L9m6tbiXZ/sjvSngi3J96+216XkH8iy+/QDZhpzG+7ySYZSlTNuKTQj/cLQEIdUDspAVlQgTJc4G4yUJ55/3vKHYiXSEOGxUxqENdHBE4oOnXsYii8hPczvEx1SQVKciPpQlOfoQm+SFy8gcD3GjA15la3W0p3HJnuVtJ/vMvPI/dPn1eh6Tjl2fqAyG8+jKZ4HYxRt5/9uoWE66EL1KUAYT50uaGDl2wPUhpYzYa+y/MRFPOCT5NHaBNAEId4NtpSBnL1hxhgw2iCdfPKai3j1Jx1TsTiBTUBRIjy1P5+/UZVZz8qbS4Lcj3FMjv93akPg8cNXeypPBL5tvK3Zvkv4iTwK6UtWSrUqDG91vS2cv76XVsolzAL94VK+/3vbqlYH/mDJ1GanoFQzJK4g+deYMicN+wrv0X+yq9p64DdD0vgNcBtlXnKR3kpytEENA9pjbf/jvy7ZhHREjB120BkL+sKYaMZXtyFYrJzzEzUConP3Ub5Purkt/rZk09zpbUaGcmF4GOvC0m3yC0B7eDhuCYuJlkURrSYEyHkF/Z97NK2wCPnXHH1urvhXCscgYX3G18Af3869ggE/X0tOfgq+SO28QL1rfefwnEHYhPWwfoEoBQB6QvWcmxLEf4cAUGR36lcdDBreEx+XVOIzL0IlrUUdRoPsVN4vk2M2H4uF4gvxkPMFIhP0FOvgzkj0VpId+LkW8nkN/tiHuALutT2fkjFGJqIpm7T6uR71TzG6QjR4ofMddZoEZ1mdM550B6de8x/vffxlzdRdyrcxlVuOuFC5zcna5uRrYfRrRYQXcJ6eY8Nm72Kto5JV594yVem4TjAqiCte3vvyRNWT91HaBLAEIdED1uTmkQgBgpeIhj2GA1uTc+1PpQJE2wsF8tkD+ZEqOFfG/VsO/lICff1YqT32mNAclzB6nq0CtUdRBPBju2h0JMjHWSzzdlWq/zOiS4VX/LAtVXUfSaJeKwqPdxen3v5gp9AU9HYRHiGEau9CEMMxBqhgsqTZXAxoyJAuzr7PvexDOM8KAGFcKVvbye3QEKKDfExpv9lg6qFJh/8rR1gC4B/BN7w6O4Ds2++CQFdVyguAkzSl26LCCNAzdvTsTzlc9W2NbkJyjIl1j5YbrJ77TGWb+zB6j60KsC+VUHX6aqAy9T0dE3KcTYiOdtdfLZcKZdyTT5N8LYar5I7sWnKKjlIgXjdVSfMaUvWGl0L76iridrxRmRA9NFuD7PHIdSj1lG097TOGr9+n4NQnXhZUxMH7mwh4zcD2M0/SyFYho4c852h/b6pkAysakW02dKYa2GdCr8bdKDBQ3O4nZLAAaCIzh7iRKmL1HEoBEuWNAnv6bzFNR+gWLHVQWRCjEk4dh03FQYRcvSKWyogoL6OmAojVJrXhonf1Ib+YqVPyRBfqeNmPhXFcRvkl914CWq2v8S5SMSiMk/4VyESx6WyLOhnuJw20iGojYJbNYTCtQkdDkJ45d4VGCiYIJIm7NSaXuzVpxEAsEodl8/hfT3UPwkHh6JOf5gEOmerScHLoj0wG0e7oB1zAnyzL9Afjh+zm4OYyt8Z+6pvYZ7Gt1rAv/EgO+lpE1dhoDwMy3ailPA8d0SQB57Q/Oi05S8aMmRIkLCtDmucNen8AFD8oUgAiGIGJkZvgYhKKEQRVdJAE2n74B8PxdOfqeNMdXjwqnqw69tQf6LHJX7XiCLV5/nrd553yZ+AVRAl0zRxdhwpM3j3uExM62dS+LkJYobMcXdQR382jh2d2AqZhi20+Kq+x/KVLNTg0yJtFlcctlrzA2zSLiyiTILiMBaQxzh2FpXkP858De7JYAN9qbOjXqUvGAhx6ISlhpImMGBCAgiYhDFTvM5PAZFLohONfInFCaP9pXvwsN9o95hOfEC+RLEq5HPEPfm82Qa6CSs3qixfKGDYYjoNaKECXPhY7E4xN2LT/uK/BIpHO2KGriEEzlILy0XcEOXESLE5W35HyEQQNaSzbYNstSZyxTZY8xTUzQ2y2KHzLgIBHFosdjdGoVLoxZ2ywf4b8qQEthvREkgXxdUBaIQxKwFwr6visMnST4cviGRydNmcYFqjr6uIP9V7eTv1yS/8u3nqWDPT+ko+mmlABKmwoXuhcG/SU9oa8XIUINP+7LwHulLjoJAWIQI7zHELeImgiBSIQgp/yNu1JwSJyy0CiRl2pIieoxQlyCdDpvivYwRbax2bLGb45ZSBV+JuyUAIf/HzuCHWLikAd2iQKVe4i84fNshn636epbnRat+S/L3qZLPkPjyD+il778gFJ4pCw4q3Ytf43nhtXp7K4dcEL4dC4IA0nBzWYaESBiYIEI6DCh6CMZRs6YgIvCx8nXStAWeEWzIaxBGOCOfrfqnSS9KpM1bP3X+1yUAIf8nzF/SQKISC1Kw4ORPqZE/riRfYfIMK8lHvm81P0fVwqp/bctiT77qFeS/vUl+5d6fkc3//Gd65bk35MOYXWMqRWoyhBk1bCx0L7rg2zErCCAFZ/XVBaItesgFgVPEQyacaIecIzzqMIEw8lNmLLXWH1Lu6VYWe+igqTj//+1uCYDnf6cGPYqfNxeQIAmxOETkp8VpJ19h8vS4XKa60/uo+sjPd0i+6qpnxDOkvvoDeu5rf0sHT2CaqKePF6RihPYYYGdTvXPRBBOIX+ekIIAk3PuvLhDp6KEpEJ/6c1qjh7b6YycCcWm8qBTA4m7NAwj536/PgOLm8JgVJURi0MSlbZPP3L0W4zNy4jXI316xp05+xqs/pFf+v6/Tz779DZzFs9coUlnn4tNwVqWTkWNTIOLuxa9rfHMGYtZb0v/QBSaO1IXLvF3WFT3SdYpja4GYFZ5WCiBptwQg5P/oKVMugK0QPwdbtlgZ9tXJDxGR703dThZUe/ItVeKfotiTIv8n3/w6eZee1FqgekMAUh1MigR82jdrgCScCdRsb7VHD6UIYsfwSJkZC60C0VZ/bFccSbOW4vx/YrcEwPO/WdEpip01pdg5KZhxyAWgJH9zkmciQUx+gJx8hPwWE+lV/zTFnhT5XqUntBaoiXMY30KLup0OhsGj+ab8GYLY80jRIhL1FKMuEP/m8/LP7zB6bFcgQf0mz5T/tQmA53/HhnMUM2sCmKogVgwIoL3Yb5P8FE3yR0A+a+/qzx/SEvKfrthTJ9+z5ITO4jSo+wI8AeNtdDByuNS/r9jgeqTT/0jWKg5L8m08p1Uc24kgW4nDueHCM+V/qXMBQv736UUlCwGIEaMCU93ko9UbCfenHrR3tSfeekbyFcS//TMF+T8VyP8xJ//4lgWqZ93pLbqXTUTKEoTw79W2tCP/QwkWcfwQAXQJRFf02I5AnjX/SwlAyP/hE0YUNWPMEc1hIgcXw3bIx8MkrAxA+uu7WuyJyX/uv36DbFKSKXrSU2eBysTABKC7gwFgs4YN52BaafMpouFjKTvwPzYROWpM8VNmWgWyVfTYSiDxqC2eNf9LCSBXkf9/F6kgXxMm1MbIV7N3xeSzYq/J4MQm8Ud2p9irfEtM/jdxyfKY4lm/vyXfrgEUra4SBSr+nMUWcc8FrQKJGg9F1d+PAxxPVHYwvdoWeWu7Pf9DVSCs4Eyc37mDul2BBMA1FOX/r+6WAG7waWD0rpHTRhQ5ow5jTv60ZLUvd/iGAt2pHnv1Gqv+8LMVe2Lyf/yNv6fLibko0H6pQhjbjvbpHKKQ4TSKmnTnBSoTgX+XHkWMGQmiiEX1HDYaD9H0Idf/QnL72rN1Bd9rtU3/Q1McXvWnn8JB1SYOTYE41gtjYEtPS776/QDfVIYUL6yWiGlchQYRbMKYWnWQz0a32S5e3em3tef77ZD/tjT56aKc71Z0lNch0bBRA/oqybnuY0kSWWRgBJuldZBn2xywQB4taxrCUcIZg6q+Xb0UMR60A/9DUxxxMzge3nR2B+7pzgViWnBKKYDk3RKAvjKkhExgvxkC2ISRbvLR5/d52u9CsfeCRrEnJ/8HnPyf/v3XKNRrr0ZxGj2Nlqu3FgR+JEmsYfyK1kEVJgav9mkKHs6AqCxF7e12/A9pgUSMon6aMNYRPcx3aK+riiN2ylyc/0/ulgB4/jcpPPnLsCkDUiJ8amvye1zwnAFM5Ogmf+fFnpj8n339q5SPE7B9B96k+AEDyQI1Cj57yEgs+fdVg9QJcm++hrz+CHv6N1VId228Rz4I/yEjifh7Flpa3K38D+3wqGMeyvYiyHbEoS4Qv15DsQC+ulsC4Pnfrh6jSiA+lAvAEOT7CqPbUuR32puKKv3dK/bE5P/kH75G3gkHqCJA/uyBWpfDWgpUVTBx+HWdp3AZVuS0NXAZn7+0RXtrsg3/Q7dA3CGAp40e2xGIQ935Xcn/4nsChfzv0X2eQif1AQNqKfLdnOFTs3dHYfLI27zdL/Y4+a/8QMj5zoVHeB0SLTOkLtjITAQp2GlTL1DFUIrApea4ToGotrfS2IlAoqeMcSHEKa3iiH3G9MKwW/lfLAAh/wfJLlAII79YjfyEbZC/C8WeJvmHFQWpHEUxx7kAimJPqBWpRvLORQmFMJxrjmkIJGqb2I5A1IURNmrAsfMIsj1xRE6aiMP/qd0SAM//xgUnPwye0Af5PiLyYyTIN/y9FHsVb/1ElfyCw6hBDDcBAaTUyp9HUG9zQChSIzQgF0QYiln3plOa4tDS4u6GQNzqTnKvZCfRYycC8enRFwvga7slAJ7/bWvP/FJ15cdoOHwdGM3ekvynKPbUyXcqOETiYlQoSsf1qX/vG9SL+4XDJ1XFoQ6f7rMUItPXKpCtosfTCESZcn4f6YXBvu6ckvzlZyVf+bwAnv8vZpyghjyfHZK/O8WeOvmO+QfldciUEgYqaDaW3x8Yiw0eKZEo4Vh1TKdApKPH0whELgwmSNf6k7uWXqQEYrKZ/1N2SwD6jPyKFK/P2fSuQL6awyeQf3h3K/2KPWLyv0YOID8E5GsDE0atg/xxtIl1ZyUFwshnXoZdxRGdAlFPL9sTiJFWgYSM4LDJ0IXfW3oJGzfa1fyvFEBOWbIH6SKfzefv5h6+NvLt8w5S8ORFDYSoAOcBfQ9zAaRhN0wpjGCkBq9ePbKHB29ecor0Uo/SkeC9ZFp0kmwhFI9uPQoa1xcEshVUxLFNgTjXHuO+ic7oscP6I2zckLy6L5IdQr9J/sldzf9cAJFJFg+Fs3oK8sXTu92Ol3ZxD38z5MvJ/w8R+QcoaOKCCCBfHQpBFEQepV5cRx+F61rtG89wwi9mHhf/cuRI1/wcE4Rd3Rne7gaNX9QaQXYkEIUYHKuPPVUEEYsjFIR7IrXZQbQmBSc1f6Zd6v8FAYwnRV1VXfnhAvm97ja6p3UPSJ/O2arYUyffDuQHgnQlgiQQiPbUo/cc2TWcISv8EgzSjmv75bwL1AA2gDFQANzS8r2KCKEQhOyiwgPRXn9oE0QIoosTIsBO00uIzIA8sVllW6uTcIZVIA04s1urnwsAJ3SfaJAPh2/Ax4lqjr+59R7+Dos9FfK/8TWyzdtPAeN6AgIVCBjD+ToF4WYlJ6VXuCbh3wO+rGXS+VuA0bYEUb8pCPX6Q5tAAob1yG/g/JaRI2hMn9x3Tvjf7RbhmhEgPvy+6vRuAA3hoAbf2NnlYk+dfJu8feQ/fh7Enyd/2Xly79kW4Y+3Q/hW2JYgMhUpQ4sgxHCoOYrCU1MggQrCbWrPbEX4CpAKnAa+/vsiXEMAmNypF8/wDQd7qRzI3K1iT5186xw8n7fnLNk2nN4O4dXPSvgOBJEP3NQlCBYh3CGIQO6ayotT+8rDXAgBYxfJrfP8dghfZq3cfzbhGgIY9HP9xkCo72/5ACdO6zTqHdmVgU1xyK/Y82NKf1lO/o/+4at0LmjPdgi3Bv7990X4NgTxj4DhdgRhXY3ZvJT9zEndDuGndjOH74oTGHn6SPQgxrhajE4+8+kc9WJPlfyv0X6bl/4gCX8WQbDn+Z6OOyT+mdiY1hLbrGH79X9IhGvdDrb8yX+0lh989dkGNt/STv4P/+6rvzrq/BrLcw//GAh/CkGICf/qH8vPIbz4wVf/5iuG//rtjjJG+C4Ue2Ly8d4fAy/9MRL9pw6VD5gIDP71nzrKGOHPUOx9Qf4fqQCUItD/H//UWbbvxR0Uez8ViK94k5H//S/I/2MVgCCCf/nvnWVvv7CjYu8L8v9EBKAUwcV/+VZn6d7nt1XsfUH+n5gAlCK48F2I4K2f6cz3X5D/JyqATRH8Y1fJnp9+Qf7/jgJQiuDUt7/Zzo5mCeS/ych/jqKf/3eQ/7UvyP9TFoBCBH/2wt9/Ldvgu/9I7j/4LjniMqZz3/4mI57hvS/I/xMXgEgIbwNtwPvABlAM/P0Xv8w/Tvz/VqoD+jC7JVsAAAAASUVORK5CYII=", + "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 6/8] 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 7/8] 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 8/8] 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,