From ef6e61d3773b15694f29a241210dd3b5150cb4cb Mon Sep 17 00:00:00 2001 From: Simone Radtke <94017602+SimoneRadtke-Cap@users.noreply.github.com> Date: Fri, 27 Sep 2024 15:05:58 +0200 Subject: [PATCH 1/3] EW-995 Add new tsp rest client (#5236) --- apps/server/src/infra/tsp-client/README.md | 40 ++ .../src/infra/tsp-client/generated/.gitignore | 4 + .../src/infra/tsp-client/generated/.npmignore | 1 + .../generated/.openapi-generator-ignore | 23 + .../generated/.openapi-generator/FILES | 18 + .../src/infra/tsp-client/generated/api.ts | 18 + .../tsp-client/generated/api/export-api.ts | 615 ++++++++++++++++++ .../src/infra/tsp-client/generated/base.ts | 86 +++ .../src/infra/tsp-client/generated/common.ts | 150 +++++ .../tsp-client/generated/configuration.ts | 110 ++++ .../infra/tsp-client/generated/git_push.sh | 57 ++ .../src/infra/tsp-client/generated/index.ts | 18 + .../tsp-client/generated/models/index.ts | 7 + .../generated/models/robj-export-klasse.ts | 60 ++ .../models/robj-export-lehrer-migration.ts | 36 + .../generated/models/robj-export-lehrer.ts | 54 ++ .../models/robj-export-schueler-migration.ts | 36 + .../generated/models/robj-export-schueler.ts | 54 ++ .../generated/models/robj-export-schule.ts | 36 + .../generated/models/version-response.ts | 30 + apps/server/src/infra/tsp-client/index.ts | 3 + .../src/infra/tsp-client/tsp-client-config.ts | 9 + .../tsp-client/tsp-client-factory.spec.ts | 105 +++ .../infra/tsp-client/tsp-client-factory.ts | 73 +++ .../src/infra/tsp-client/tsp-client.module.ts | 8 + .../src/modules/server/server.config.ts | 7 + config/default.schema.json | 5 + openapitools.json | 28 +- package.json | 6 +- sonar-project.properties | 4 +- 30 files changed, 1696 insertions(+), 5 deletions(-) create mode 100644 apps/server/src/infra/tsp-client/README.md create mode 100644 apps/server/src/infra/tsp-client/generated/.gitignore create mode 100644 apps/server/src/infra/tsp-client/generated/.npmignore create mode 100644 apps/server/src/infra/tsp-client/generated/.openapi-generator-ignore create mode 100644 apps/server/src/infra/tsp-client/generated/.openapi-generator/FILES create mode 100644 apps/server/src/infra/tsp-client/generated/api.ts create mode 100644 apps/server/src/infra/tsp-client/generated/api/export-api.ts create mode 100644 apps/server/src/infra/tsp-client/generated/base.ts create mode 100644 apps/server/src/infra/tsp-client/generated/common.ts create mode 100644 apps/server/src/infra/tsp-client/generated/configuration.ts create mode 100644 apps/server/src/infra/tsp-client/generated/git_push.sh create mode 100644 apps/server/src/infra/tsp-client/generated/index.ts create mode 100644 apps/server/src/infra/tsp-client/generated/models/index.ts create mode 100644 apps/server/src/infra/tsp-client/generated/models/robj-export-klasse.ts create mode 100644 apps/server/src/infra/tsp-client/generated/models/robj-export-lehrer-migration.ts create mode 100644 apps/server/src/infra/tsp-client/generated/models/robj-export-lehrer.ts create mode 100644 apps/server/src/infra/tsp-client/generated/models/robj-export-schueler-migration.ts create mode 100644 apps/server/src/infra/tsp-client/generated/models/robj-export-schueler.ts create mode 100644 apps/server/src/infra/tsp-client/generated/models/robj-export-schule.ts create mode 100644 apps/server/src/infra/tsp-client/generated/models/version-response.ts create mode 100644 apps/server/src/infra/tsp-client/index.ts create mode 100644 apps/server/src/infra/tsp-client/tsp-client-config.ts create mode 100644 apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts create mode 100644 apps/server/src/infra/tsp-client/tsp-client-factory.ts create mode 100644 apps/server/src/infra/tsp-client/tsp-client.module.ts diff --git a/apps/server/src/infra/tsp-client/README.md b/apps/server/src/infra/tsp-client/README.md new file mode 100644 index 00000000000..5f4e66829b7 --- /dev/null +++ b/apps/server/src/infra/tsp-client/README.md @@ -0,0 +1,40 @@ +# TSP API CLIENT + +> A short introduction how this module can be used and the api client is generated. + +## How to use the api client + +The clients for the different Tsp endpoints should be created through TspClientFactory. +Through the create methods of the factory the basic configuration will be set. Currently the +factory sets the base url and generates the JWT used for the requests. You can use the client +like this: + +```typescript +export class MyNewService { + // inject the factory into the constructor + constructor(private readonly tspClientFactory: TspClientFactory) {} + + public async doSomeStuff(): Promise { + // this will create a fully initialized client + const exportClient = tspClientFactory.createExportClient(); + + // calling the api + const versionResponse = await exportClient.version(); + + + // do other stuff... + } +} +``` + +## How the code generation works + +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. + +The client generation is done with the npm command `npm run generate-client:tsp-api`. This +will delete the old and create new files. We are using the `tsp-api` generator configuration +from the `openapitools.json` found in the repository root. You can add new endpoints by +extending the `FILTER` list in the `openapiNormalizer` section with new `operationId` entries. +New models must be added to the list of `models` in the `globalProperty` section. diff --git a/apps/server/src/infra/tsp-client/generated/.gitignore b/apps/server/src/infra/tsp-client/generated/.gitignore new file mode 100644 index 00000000000..149b5765472 --- /dev/null +++ b/apps/server/src/infra/tsp-client/generated/.gitignore @@ -0,0 +1,4 @@ +wwwroot/*.js +node_modules +typings +dist diff --git a/apps/server/src/infra/tsp-client/generated/.npmignore b/apps/server/src/infra/tsp-client/generated/.npmignore new file mode 100644 index 00000000000..999d88df693 --- /dev/null +++ b/apps/server/src/infra/tsp-client/generated/.npmignore @@ -0,0 +1 @@ +# empty npmignore to ensure all required files (e.g., in the dist folder) are published by npm \ No newline at end of file diff --git a/apps/server/src/infra/tsp-client/generated/.openapi-generator-ignore b/apps/server/src/infra/tsp-client/generated/.openapi-generator-ignore new file mode 100644 index 00000000000..7484ee590a3 --- /dev/null +++ b/apps/server/src/infra/tsp-client/generated/.openapi-generator-ignore @@ -0,0 +1,23 @@ +# OpenAPI Generator Ignore +# Generated by openapi-generator https://github.com/openapitools/openapi-generator + +# Use this file to prevent files from being overwritten by the generator. +# The patterns follow closely to .gitignore or .dockerignore. + +# As an example, the C# client generator defines ApiClient.cs. +# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line: +#ApiClient.cs + +# You can match any string of characters against a directory, file or extension with a single asterisk (*): +#foo/*/qux +# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux + +# You can recursively match patterns against a directory, file or extension with a double asterisk (**): +#foo/**/qux +# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux + +# You can also negate patterns with an exclamation (!). +# For example, you can ignore all files in a docs folder with the file extension .md: +#docs/*.md +# Then explicitly reverse the ignore rule for a single file: +#!docs/README.md diff --git a/apps/server/src/infra/tsp-client/generated/.openapi-generator/FILES b/apps/server/src/infra/tsp-client/generated/.openapi-generator/FILES new file mode 100644 index 00000000000..f5006ca3a0b --- /dev/null +++ b/apps/server/src/infra/tsp-client/generated/.openapi-generator/FILES @@ -0,0 +1,18 @@ +.gitignore +.npmignore +.openapi-generator-ignore +api.ts +api/export-api.ts +base.ts +common.ts +configuration.ts +git_push.sh +index.ts +models/index.ts +models/robj-export-klasse.ts +models/robj-export-lehrer-migration.ts +models/robj-export-lehrer.ts +models/robj-export-schueler-migration.ts +models/robj-export-schueler.ts +models/robj-export-schule.ts +models/version-response.ts diff --git a/apps/server/src/infra/tsp-client/generated/api.ts b/apps/server/src/infra/tsp-client/generated/api.ts new file mode 100644 index 00000000000..d214c8a28d1 --- /dev/null +++ b/apps/server/src/infra/tsp-client/generated/api.ts @@ -0,0 +1,18 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * + * TIP-Rest Api v1 + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +export * from './api/export-api'; + 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 new file mode 100644 index 00000000000..c1b2f2b1579 --- /dev/null +++ b/apps/server/src/infra/tsp-client/generated/api/export-api.ts @@ -0,0 +1,615 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * + * TIP-Rest Api v1 + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from '../configuration'; +import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; +// Some imports not used depending on template conditions +// @ts-ignore +import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common'; +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; +// @ts-ignore +import type { RobjExportKlasse } from '../models'; +// @ts-ignore +import type { RobjExportLehrer } from '../models'; +// @ts-ignore +import type { RobjExportLehrerMigration } from '../models'; +// @ts-ignore +import type { RobjExportSchueler } from '../models'; +// @ts-ignore +import type { RobjExportSchuelerMigration } from '../models'; +// @ts-ignore +import type { RobjExportSchule } from '../models'; +// @ts-ignore +import type { VersionResponse } from '../models'; +/** + * ExportApi - axios parameter creator + * @export + */ +export const ExportApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary liefert eine Liste von allen geändert oder erstellten Klassen seit dem gegebenen Datum + * @param {string} [dtLetzteAenderung] Datum der letzten Änderung eingeben + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + exportKlasseList: async (dtLetzteAenderung?: string, options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/schulverwaltung_export_klasse`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + if (dtLetzteAenderung !== undefined) { + localVarQueryParameter['dtLetzteAenderung'] = dtLetzteAenderung; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary liefert eine Liste von allen geändert oder erstellten Lehrer seit dem gegebenen Datum + * @param {string} [dtLetzteAenderung] Datum der letzten Änderung eingeben + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + exportLehrerList: async (dtLetzteAenderung?: string, options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/schulverwaltung_export_lehrer`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + if (dtLetzteAenderung !== undefined) { + localVarQueryParameter['dtLetzteAenderung'] = dtLetzteAenderung; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary liefert eine Liste von allen Lehrern. Zu einem Lehrer wird die alte und die neue uid geliefert. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + exportLehrerListMigration: async (options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/schulverwaltung_export_lehrer_migration`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary liefert eine Liste von allen geändert oder erstellten Schüler seit dem gegebenen Datum + * @param {string} [dtLetzteAenderung] Datum der letzten Änderung eingeben + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + exportSchuelerList: async (dtLetzteAenderung?: string, options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/schulverwaltung_export_schueler`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + if (dtLetzteAenderung !== undefined) { + localVarQueryParameter['dtLetzteAenderung'] = dtLetzteAenderung; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary liefert eine Liste von allen Lehrern. Zu einem Schüler wird die alte und die neue uid geliefert. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + exportSchuelerListMigration: async (options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/schulverwaltung_export_schueler_migration`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary liefert eine Liste von allen geändert oder erstellten Schulen seit dem gegebenen Datum + * @param {string} [dtLetzteAenderung] Datum der letzten Änderung eingeben + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + exportSchuleList: async (dtLetzteAenderung?: string, options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/schulverwaltung_export_schule`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + if (dtLetzteAenderung !== undefined) { + localVarQueryParameter['dtLetzteAenderung'] = dtLetzteAenderung; + } + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary liefert die aktuelle Version zurück + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + version: async (options: RawAxiosRequestConfig = {}): Promise => { + const localVarPath = `/schulverwaltung_export_version`; + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * ExportApi - functional programming interface + * @export + */ +export const ExportApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = ExportApiAxiosParamCreator(configuration) + return { + /** + * + * @summary liefert eine Liste von allen geändert oder erstellten Klassen seit dem gegebenen Datum + * @param {string} [dtLetzteAenderung] Datum der letzten Änderung eingeben + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async exportKlasseList(dtLetzteAenderung?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.exportKlasseList(dtLetzteAenderung, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ExportApi.exportKlasseList']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary liefert eine Liste von allen geändert oder erstellten Lehrer seit dem gegebenen Datum + * @param {string} [dtLetzteAenderung] Datum der letzten Änderung eingeben + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async exportLehrerList(dtLetzteAenderung?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.exportLehrerList(dtLetzteAenderung, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ExportApi.exportLehrerList']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary liefert eine Liste von allen Lehrern. Zu einem Lehrer wird die alte und die neue uid geliefert. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async exportLehrerListMigration(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.exportLehrerListMigration(options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ExportApi.exportLehrerListMigration']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary liefert eine Liste von allen geändert oder erstellten Schüler seit dem gegebenen Datum + * @param {string} [dtLetzteAenderung] Datum der letzten Änderung eingeben + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async exportSchuelerList(dtLetzteAenderung?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.exportSchuelerList(dtLetzteAenderung, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ExportApi.exportSchuelerList']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary liefert eine Liste von allen Lehrern. Zu einem Schüler wird die alte und die neue uid geliefert. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async exportSchuelerListMigration(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.exportSchuelerListMigration(options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ExportApi.exportSchuelerListMigration']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary liefert eine Liste von allen geändert oder erstellten Schulen seit dem gegebenen Datum + * @param {string} [dtLetzteAenderung] Datum der letzten Änderung eingeben + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async exportSchuleList(dtLetzteAenderung?: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise>> { + const localVarAxiosArgs = await localVarAxiosParamCreator.exportSchuleList(dtLetzteAenderung, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ExportApi.exportSchuleList']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary liefert die aktuelle Version zurück + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async version(options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.version(options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['ExportApi.version']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * ExportApi - factory interface + * @export + */ +export const ExportApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = ExportApiFp(configuration) + return { + /** + * + * @summary liefert eine Liste von allen geändert oder erstellten Klassen seit dem gegebenen Datum + * @param {string} [dtLetzteAenderung] Datum der letzten Änderung eingeben + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + exportKlasseList(dtLetzteAenderung?: string, options?: any): AxiosPromise> { + return localVarFp.exportKlasseList(dtLetzteAenderung, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary liefert eine Liste von allen geändert oder erstellten Lehrer seit dem gegebenen Datum + * @param {string} [dtLetzteAenderung] Datum der letzten Änderung eingeben + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + exportLehrerList(dtLetzteAenderung?: string, options?: any): AxiosPromise> { + return localVarFp.exportLehrerList(dtLetzteAenderung, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary liefert eine Liste von allen Lehrern. Zu einem Lehrer wird die alte und die neue uid geliefert. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + exportLehrerListMigration(options?: any): AxiosPromise> { + return localVarFp.exportLehrerListMigration(options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary liefert eine Liste von allen geändert oder erstellten Schüler seit dem gegebenen Datum + * @param {string} [dtLetzteAenderung] Datum der letzten Änderung eingeben + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + exportSchuelerList(dtLetzteAenderung?: string, options?: any): AxiosPromise> { + return localVarFp.exportSchuelerList(dtLetzteAenderung, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary liefert eine Liste von allen Lehrern. Zu einem Schüler wird die alte und die neue uid geliefert. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + exportSchuelerListMigration(options?: any): AxiosPromise> { + return localVarFp.exportSchuelerListMigration(options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary liefert eine Liste von allen geändert oder erstellten Schulen seit dem gegebenen Datum + * @param {string} [dtLetzteAenderung] Datum der letzten Änderung eingeben + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + exportSchuleList(dtLetzteAenderung?: string, options?: any): AxiosPromise> { + return localVarFp.exportSchuleList(dtLetzteAenderung, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary liefert die aktuelle Version zurück + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + version(options?: any): AxiosPromise { + return localVarFp.version(options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * ExportApi - interface + * @export + * @interface ExportApi + */ +export interface ExportApiInterface { + /** + * + * @summary liefert eine Liste von allen geändert oder erstellten Klassen seit dem gegebenen Datum + * @param {string} [dtLetzteAenderung] Datum der letzten Änderung eingeben + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ExportApiInterface + */ + exportKlasseList(dtLetzteAenderung?: string, options?: RawAxiosRequestConfig): AxiosPromise>; + + /** + * + * @summary liefert eine Liste von allen geändert oder erstellten Lehrer seit dem gegebenen Datum + * @param {string} [dtLetzteAenderung] Datum der letzten Änderung eingeben + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ExportApiInterface + */ + exportLehrerList(dtLetzteAenderung?: string, options?: RawAxiosRequestConfig): AxiosPromise>; + + /** + * + * @summary liefert eine Liste von allen Lehrern. Zu einem Lehrer wird die alte und die neue uid geliefert. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ExportApiInterface + */ + exportLehrerListMigration(options?: RawAxiosRequestConfig): AxiosPromise>; + + /** + * + * @summary liefert eine Liste von allen geändert oder erstellten Schüler seit dem gegebenen Datum + * @param {string} [dtLetzteAenderung] Datum der letzten Änderung eingeben + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ExportApiInterface + */ + exportSchuelerList(dtLetzteAenderung?: string, options?: RawAxiosRequestConfig): AxiosPromise>; + + /** + * + * @summary liefert eine Liste von allen Lehrern. Zu einem Schüler wird die alte und die neue uid geliefert. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ExportApiInterface + */ + exportSchuelerListMigration(options?: RawAxiosRequestConfig): AxiosPromise>; + + /** + * + * @summary liefert eine Liste von allen geändert oder erstellten Schulen seit dem gegebenen Datum + * @param {string} [dtLetzteAenderung] Datum der letzten Änderung eingeben + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ExportApiInterface + */ + exportSchuleList(dtLetzteAenderung?: string, options?: RawAxiosRequestConfig): AxiosPromise>; + + /** + * + * @summary liefert die aktuelle Version zurück + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ExportApiInterface + */ + version(options?: RawAxiosRequestConfig): AxiosPromise; + +} + +/** + * ExportApi - object-oriented interface + * @export + * @class ExportApi + * @extends {BaseAPI} + */ +export class ExportApi extends BaseAPI implements ExportApiInterface { + /** + * + * @summary liefert eine Liste von allen geändert oder erstellten Klassen seit dem gegebenen Datum + * @param {string} [dtLetzteAenderung] Datum der letzten Änderung eingeben + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ExportApi + */ + public exportKlasseList(dtLetzteAenderung?: string, options?: RawAxiosRequestConfig) { + return ExportApiFp(this.configuration).exportKlasseList(dtLetzteAenderung, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary liefert eine Liste von allen geändert oder erstellten Lehrer seit dem gegebenen Datum + * @param {string} [dtLetzteAenderung] Datum der letzten Änderung eingeben + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ExportApi + */ + public exportLehrerList(dtLetzteAenderung?: string, options?: RawAxiosRequestConfig) { + return ExportApiFp(this.configuration).exportLehrerList(dtLetzteAenderung, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary liefert eine Liste von allen Lehrern. Zu einem Lehrer wird die alte und die neue uid geliefert. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ExportApi + */ + public exportLehrerListMigration(options?: RawAxiosRequestConfig) { + return ExportApiFp(this.configuration).exportLehrerListMigration(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary liefert eine Liste von allen geändert oder erstellten Schüler seit dem gegebenen Datum + * @param {string} [dtLetzteAenderung] Datum der letzten Änderung eingeben + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ExportApi + */ + public exportSchuelerList(dtLetzteAenderung?: string, options?: RawAxiosRequestConfig) { + return ExportApiFp(this.configuration).exportSchuelerList(dtLetzteAenderung, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary liefert eine Liste von allen Lehrern. Zu einem Schüler wird die alte und die neue uid geliefert. + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ExportApi + */ + public exportSchuelerListMigration(options?: RawAxiosRequestConfig) { + return ExportApiFp(this.configuration).exportSchuelerListMigration(options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary liefert eine Liste von allen geändert oder erstellten Schulen seit dem gegebenen Datum + * @param {string} [dtLetzteAenderung] Datum der letzten Änderung eingeben + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ExportApi + */ + public exportSchuleList(dtLetzteAenderung?: string, options?: RawAxiosRequestConfig) { + return ExportApiFp(this.configuration).exportSchuleList(dtLetzteAenderung, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary liefert die aktuelle Version zurück + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof ExportApi + */ + public version(options?: RawAxiosRequestConfig) { + return ExportApiFp(this.configuration).version(options).then((request) => request(this.axios, this.basePath)); + } +} + diff --git a/apps/server/src/infra/tsp-client/generated/base.ts b/apps/server/src/infra/tsp-client/generated/base.ts new file mode 100644 index 00000000000..71bce5185c6 --- /dev/null +++ b/apps/server/src/infra/tsp-client/generated/base.ts @@ -0,0 +1,86 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * + * TIP-Rest Api v1 + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from './configuration'; +// Some imports not used depending on template conditions +// @ts-ignore +import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; + +export const BASE_PATH = "http://localhost/tip-ms/api".replace(/\/+$/, ""); + +/** + * + * @export + */ +export const COLLECTION_FORMATS = { + csv: ",", + ssv: " ", + tsv: "\t", + pipes: "|", +}; + +/** + * + * @export + * @interface RequestArgs + */ +export interface RequestArgs { + url: string; + options: RawAxiosRequestConfig; +} + +/** + * + * @export + * @class BaseAPI + */ +export class BaseAPI { + protected configuration: Configuration | undefined; + + constructor(configuration?: Configuration, protected basePath: string = BASE_PATH, protected axios: AxiosInstance = globalAxios) { + if (configuration) { + this.configuration = configuration; + this.basePath = configuration.basePath ?? basePath; + } + } +}; + +/** + * + * @export + * @class RequiredError + * @extends {Error} + */ +export class RequiredError extends Error { + constructor(public field: string, msg?: string) { + super(msg); + this.name = "RequiredError" + } +} + +interface ServerMap { + [key: string]: { + url: string, + description: string, + }[]; +} + +/** + * + * @export + */ +export const operationServerMap: ServerMap = { +} diff --git a/apps/server/src/infra/tsp-client/generated/common.ts b/apps/server/src/infra/tsp-client/generated/common.ts new file mode 100644 index 00000000000..7f61c675c86 --- /dev/null +++ b/apps/server/src/infra/tsp-client/generated/common.ts @@ -0,0 +1,150 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * + * TIP-Rest Api v1 + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from "./configuration"; +import type { RequestArgs } from "./base"; +import type { AxiosInstance, AxiosResponse } from 'axios'; +import { RequiredError } from "./base"; + +/** + * + * @export + */ +export const DUMMY_BASE_URL = 'https://example.com' + +/** + * + * @throws {RequiredError} + * @export + */ +export const assertParamExists = function (functionName: string, paramName: string, paramValue: unknown) { + if (paramValue === null || paramValue === undefined) { + throw new RequiredError(paramName, `Required parameter ${paramName} was null or undefined when calling ${functionName}.`); + } +} + +/** + * + * @export + */ +export const setApiKeyToObject = async function (object: any, keyParamName: string, configuration?: Configuration) { + if (configuration && configuration.apiKey) { + const localVarApiKeyValue = typeof configuration.apiKey === 'function' + ? await configuration.apiKey(keyParamName) + : await configuration.apiKey; + object[keyParamName] = localVarApiKeyValue; + } +} + +/** + * + * @export + */ +export const setBasicAuthToObject = function (object: any, configuration?: Configuration) { + if (configuration && (configuration.username || configuration.password)) { + object["auth"] = { username: configuration.username, password: configuration.password }; + } +} + +/** + * + * @export + */ +export const setBearerAuthToObject = async function (object: any, configuration?: Configuration) { + if (configuration && configuration.accessToken) { + const accessToken = typeof configuration.accessToken === 'function' + ? await configuration.accessToken() + : await configuration.accessToken; + object["Authorization"] = "Bearer " + accessToken; + } +} + +/** + * + * @export + */ +export const setOAuthToObject = async function (object: any, name: string, scopes: string[], configuration?: Configuration) { + if (configuration && configuration.accessToken) { + const localVarAccessTokenValue = typeof configuration.accessToken === 'function' + ? await configuration.accessToken(name, scopes) + : await configuration.accessToken; + object["Authorization"] = "Bearer " + localVarAccessTokenValue; + } +} + +function setFlattenedQueryParams(urlSearchParams: URLSearchParams, parameter: any, key: string = ""): void { + if (parameter == null) return; + if (typeof parameter === "object") { + if (Array.isArray(parameter)) { + (parameter as any[]).forEach(item => setFlattenedQueryParams(urlSearchParams, item, key)); + } + else { + Object.keys(parameter).forEach(currentKey => + setFlattenedQueryParams(urlSearchParams, parameter[currentKey], `${key}${key !== '' ? '.' : ''}${currentKey}`) + ); + } + } + else { + if (urlSearchParams.has(key)) { + urlSearchParams.append(key, parameter); + } + else { + urlSearchParams.set(key, parameter); + } + } +} + +/** + * + * @export + */ +export const setSearchParams = function (url: URL, ...objects: any[]) { + const searchParams = new URLSearchParams(url.search); + setFlattenedQueryParams(searchParams, objects); + url.search = searchParams.toString(); +} + +/** + * + * @export + */ +export const serializeDataIfNeeded = function (value: any, requestOptions: any, configuration?: Configuration) { + const nonString = typeof value !== 'string'; + const needsSerialization = nonString && configuration && configuration.isJsonMime + ? configuration.isJsonMime(requestOptions.headers['Content-Type']) + : nonString; + return needsSerialization + ? JSON.stringify(value !== undefined ? value : {}) + : (value || ""); +} + +/** + * + * @export + */ +export const toPathString = function (url: URL) { + return url.pathname + url.search + url.hash +} + +/** + * + * @export + */ +export const createRequestFunction = function (axiosArgs: RequestArgs, globalAxios: AxiosInstance, BASE_PATH: string, configuration?: Configuration) { + return >(axios: AxiosInstance = globalAxios, basePath: string = BASE_PATH) => { + const axiosRequestArgs = {...axiosArgs.options, url: (axios.defaults.baseURL ? '' : configuration?.basePath ?? basePath) + axiosArgs.url}; + return axios.request(axiosRequestArgs); + }; +} diff --git a/apps/server/src/infra/tsp-client/generated/configuration.ts b/apps/server/src/infra/tsp-client/generated/configuration.ts new file mode 100644 index 00000000000..141f3554a7d --- /dev/null +++ b/apps/server/src/infra/tsp-client/generated/configuration.ts @@ -0,0 +1,110 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * + * TIP-Rest Api v1 + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export interface ConfigurationParameters { + apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); + username?: string; + password?: string; + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); + basePath?: string; + serverIndex?: number; + baseOptions?: any; + formDataCtor?: new () => any; +} + +export class Configuration { + /** + * parameter for apiKey security + * @param name security name + * @memberof Configuration + */ + apiKey?: string | Promise | ((name: string) => string) | ((name: string) => Promise); + /** + * parameter for basic security + * + * @type {string} + * @memberof Configuration + */ + username?: string; + /** + * parameter for basic security + * + * @type {string} + * @memberof Configuration + */ + password?: string; + /** + * parameter for oauth2 security + * @param name security name + * @param scopes oauth2 scope + * @memberof Configuration + */ + accessToken?: string | Promise | ((name?: string, scopes?: string[]) => string) | ((name?: string, scopes?: string[]) => Promise); + /** + * override base path + * + * @type {string} + * @memberof Configuration + */ + basePath?: string; + /** + * override server index + * + * @type {number} + * @memberof Configuration + */ + serverIndex?: number; + /** + * base options for axios calls + * + * @type {any} + * @memberof Configuration + */ + baseOptions?: any; + /** + * The FormData constructor that will be used to create multipart form data + * requests. You can inject this here so that execution environments that + * do not support the FormData class can still run the generated client. + * + * @type {new () => FormData} + */ + formDataCtor?: new () => any; + + constructor(param: ConfigurationParameters = {}) { + this.apiKey = param.apiKey; + this.username = param.username; + this.password = param.password; + this.accessToken = param.accessToken; + this.basePath = param.basePath; + this.serverIndex = param.serverIndex; + this.baseOptions = param.baseOptions; + this.formDataCtor = param.formDataCtor; + } + + /** + * Check if the given MIME is a JSON MIME. + * JSON MIME examples: + * application/json + * application/json; charset=UTF8 + * APPLICATION/JSON + * application/vnd.company+json + * @param mime - MIME (Multipurpose Internet Mail Extensions) + * @return True if the given MIME is JSON, false otherwise. + */ + public isJsonMime(mime: string): boolean { + const jsonMime: RegExp = new RegExp('^(application\/json|[^;/ \t]+\/[^;/ \t]+[+]json)[ \t]*(;.*)?$', 'i'); + return mime !== null && (jsonMime.test(mime) || mime.toLowerCase() === 'application/json-patch+json'); + } +} diff --git a/apps/server/src/infra/tsp-client/generated/git_push.sh b/apps/server/src/infra/tsp-client/generated/git_push.sh new file mode 100644 index 00000000000..f53a75d4fab --- /dev/null +++ b/apps/server/src/infra/tsp-client/generated/git_push.sh @@ -0,0 +1,57 @@ +#!/bin/sh +# ref: https://help.github.com/articles/adding-an-existing-project-to-github-using-the-command-line/ +# +# Usage example: /bin/sh ./git_push.sh wing328 openapi-petstore-perl "minor update" "gitlab.com" + +git_user_id=$1 +git_repo_id=$2 +release_note=$3 +git_host=$4 + +if [ "$git_host" = "" ]; then + git_host="github.com" + echo "[INFO] No command line input provided. Set \$git_host to $git_host" +fi + +if [ "$git_user_id" = "" ]; then + git_user_id="GIT_USER_ID" + echo "[INFO] No command line input provided. Set \$git_user_id to $git_user_id" +fi + +if [ "$git_repo_id" = "" ]; then + git_repo_id="GIT_REPO_ID" + echo "[INFO] No command line input provided. Set \$git_repo_id to $git_repo_id" +fi + +if [ "$release_note" = "" ]; then + release_note="Minor update" + echo "[INFO] No command line input provided. Set \$release_note to $release_note" +fi + +# Initialize the local directory as a Git repository +git init + +# Adds the files in the local repository and stages them for commit. +git add . + +# Commits the tracked changes and prepares them to be pushed to a remote repository. +git commit -m "$release_note" + +# Sets the new remote +git_remote=$(git remote) +if [ "$git_remote" = "" ]; then # git remote not defined + + if [ "$GIT_TOKEN" = "" ]; then + echo "[INFO] \$GIT_TOKEN (environment variable) is not set. Using the git credential in your environment." + git remote add origin https://${git_host}/${git_user_id}/${git_repo_id}.git + else + git remote add origin https://${git_user_id}:"${GIT_TOKEN}"@${git_host}/${git_user_id}/${git_repo_id}.git + fi + +fi + +git pull origin master + +# Pushes (Forces) the changes in the local repository up to the remote repository +echo "Git pushing to https://${git_host}/${git_user_id}/${git_repo_id}.git" +git push origin master 2>&1 | grep -v 'To https' diff --git a/apps/server/src/infra/tsp-client/generated/index.ts b/apps/server/src/infra/tsp-client/generated/index.ts new file mode 100644 index 00000000000..ad35bdeb9b5 --- /dev/null +++ b/apps/server/src/infra/tsp-client/generated/index.ts @@ -0,0 +1,18 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * + * TIP-Rest Api v1 + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +export * from "./api"; +export * from "./configuration"; +export * from "./models"; diff --git a/apps/server/src/infra/tsp-client/generated/models/index.ts b/apps/server/src/infra/tsp-client/generated/models/index.ts new file mode 100644 index 00000000000..06742e128ad --- /dev/null +++ b/apps/server/src/infra/tsp-client/generated/models/index.ts @@ -0,0 +1,7 @@ +export * from './robj-export-klasse'; +export * from './robj-export-lehrer'; +export * from './robj-export-lehrer-migration'; +export * from './robj-export-schueler'; +export * from './robj-export-schueler-migration'; +export * from './robj-export-schule'; +export * from './version-response'; diff --git a/apps/server/src/infra/tsp-client/generated/models/robj-export-klasse.ts b/apps/server/src/infra/tsp-client/generated/models/robj-export-klasse.ts new file mode 100644 index 00000000000..76b4075bf2f --- /dev/null +++ b/apps/server/src/infra/tsp-client/generated/models/robj-export-klasse.ts @@ -0,0 +1,60 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * + * TIP-Rest Api v1 + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface RobjExportKlasse + */ +export interface RobjExportKlasse { + /** + * + * @type {string} + * @memberof RobjExportKlasse + */ + 'id'?: string; + /** + * + * @type {string} + * @memberof RobjExportKlasse + */ + 'version'?: string; + /** + * + * @type {string} + * @memberof RobjExportKlasse + */ + 'klasseId'?: string; + /** + * + * @type {string} + * @memberof RobjExportKlasse + */ + 'klasseName'?: string; + /** + * + * @type {string} + * @memberof RobjExportKlasse + */ + 'schuleNummer'?: string; + /** + * + * @type {string} + * @memberof RobjExportKlasse + */ + 'lehrerUid'?: string; +} + diff --git a/apps/server/src/infra/tsp-client/generated/models/robj-export-lehrer-migration.ts b/apps/server/src/infra/tsp-client/generated/models/robj-export-lehrer-migration.ts new file mode 100644 index 00000000000..18ecaa90428 --- /dev/null +++ b/apps/server/src/infra/tsp-client/generated/models/robj-export-lehrer-migration.ts @@ -0,0 +1,36 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * + * TIP-Rest Api v1 + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface RobjExportLehrerMigration + */ +export interface RobjExportLehrerMigration { + /** + * + * @type {string} + * @memberof RobjExportLehrerMigration + */ + 'lehrerUidAlt'?: string; + /** + * + * @type {string} + * @memberof RobjExportLehrerMigration + */ + 'lehrerUidNeu'?: string; +} + diff --git a/apps/server/src/infra/tsp-client/generated/models/robj-export-lehrer.ts b/apps/server/src/infra/tsp-client/generated/models/robj-export-lehrer.ts new file mode 100644 index 00000000000..9fe048f7598 --- /dev/null +++ b/apps/server/src/infra/tsp-client/generated/models/robj-export-lehrer.ts @@ -0,0 +1,54 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * + * TIP-Rest Api v1 + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface RobjExportLehrer + */ +export interface RobjExportLehrer { + /** + * + * @type {string} + * @memberof RobjExportLehrer + */ + 'lehrerUid'?: string; + /** + * + * @type {string} + * @memberof RobjExportLehrer + */ + 'lehrerTitel'?: string; + /** + * + * @type {string} + * @memberof RobjExportLehrer + */ + 'lehrerVorname'?: string; + /** + * + * @type {string} + * @memberof RobjExportLehrer + */ + 'lehrerNachname'?: string; + /** + * + * @type {string} + * @memberof RobjExportLehrer + */ + 'schuleNummer'?: string; +} + diff --git a/apps/server/src/infra/tsp-client/generated/models/robj-export-schueler-migration.ts b/apps/server/src/infra/tsp-client/generated/models/robj-export-schueler-migration.ts new file mode 100644 index 00000000000..9adc7801628 --- /dev/null +++ b/apps/server/src/infra/tsp-client/generated/models/robj-export-schueler-migration.ts @@ -0,0 +1,36 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * + * TIP-Rest Api v1 + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface RobjExportSchuelerMigration + */ +export interface RobjExportSchuelerMigration { + /** + * + * @type {string} + * @memberof RobjExportSchuelerMigration + */ + 'schuelerUidAlt'?: string; + /** + * + * @type {string} + * @memberof RobjExportSchuelerMigration + */ + 'schuelerUidNeu'?: string; +} + diff --git a/apps/server/src/infra/tsp-client/generated/models/robj-export-schueler.ts b/apps/server/src/infra/tsp-client/generated/models/robj-export-schueler.ts new file mode 100644 index 00000000000..f10a37b4bc1 --- /dev/null +++ b/apps/server/src/infra/tsp-client/generated/models/robj-export-schueler.ts @@ -0,0 +1,54 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * + * TIP-Rest Api v1 + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface RobjExportSchueler + */ +export interface RobjExportSchueler { + /** + * + * @type {string} + * @memberof RobjExportSchueler + */ + 'schuelerUid'?: string; + /** + * + * @type {string} + * @memberof RobjExportSchueler + */ + 'schuelerVorname'?: string; + /** + * + * @type {string} + * @memberof RobjExportSchueler + */ + 'schuelerNachname'?: string; + /** + * + * @type {string} + * @memberof RobjExportSchueler + */ + 'schuleNummer'?: string; + /** + * + * @type {string} + * @memberof RobjExportSchueler + */ + 'klasseId'?: string; +} + diff --git a/apps/server/src/infra/tsp-client/generated/models/robj-export-schule.ts b/apps/server/src/infra/tsp-client/generated/models/robj-export-schule.ts new file mode 100644 index 00000000000..e56f8a55429 --- /dev/null +++ b/apps/server/src/infra/tsp-client/generated/models/robj-export-schule.ts @@ -0,0 +1,36 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * + * TIP-Rest Api v1 + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface RobjExportSchule + */ +export interface RobjExportSchule { + /** + * + * @type {string} + * @memberof RobjExportSchule + */ + 'schuleNummer'?: string; + /** + * + * @type {string} + * @memberof RobjExportSchule + */ + 'schuleName'?: string; +} + diff --git a/apps/server/src/infra/tsp-client/generated/models/version-response.ts b/apps/server/src/infra/tsp-client/generated/models/version-response.ts new file mode 100644 index 00000000000..e8167411b1a --- /dev/null +++ b/apps/server/src/infra/tsp-client/generated/models/version-response.ts @@ -0,0 +1,30 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * + * TIP-Rest Api v1 + * + * The version of the OpenAPI document: 1.0.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface VersionResponse + */ +export interface VersionResponse { + /** + * + * @type {string} + * @memberof VersionResponse + */ + 'version'?: string; +} + diff --git a/apps/server/src/infra/tsp-client/index.ts b/apps/server/src/infra/tsp-client/index.ts new file mode 100644 index 00000000000..66f5a6ebced --- /dev/null +++ b/apps/server/src/infra/tsp-client/index.ts @@ -0,0 +1,3 @@ +export * from './generated/api'; +export * from './generated/models'; +export { TspClientFactory } from './tsp-client-factory'; diff --git a/apps/server/src/infra/tsp-client/tsp-client-config.ts b/apps/server/src/infra/tsp-client/tsp-client-config.ts new file mode 100644 index 00000000000..95c1da4131b --- /dev/null +++ b/apps/server/src/infra/tsp-client/tsp-client-config.ts @@ -0,0 +1,9 @@ +export interface TspRestClientConfig { + SC_DOMAIN: string; + HOST: string; + 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.spec.ts b/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts new file mode 100644 index 00000000000..45c7c13072a --- /dev/null +++ b/apps/server/src/infra/tsp-client/tsp-client-factory.spec.ts @@ -0,0 +1,105 @@ +import { faker } from '@faker-js/faker'; +import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { ConfigService } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ServerConfig } from '@src/modules/server'; +import { TspClientFactory } from './tsp-client-factory'; + +describe('TspClientFactory', () => { + let module: TestingModule; + let sut: TspClientFactory; + let configServiceMock: DeepMocked>; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + TspClientFactory, + { + 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(); + case 'TSP_API_TOKEN_LIFETIME_MS': + return faker.number.int(); + default: + throw new Error(`Unknown key: ${key}`); + } + }, + }), + }, + ], + }).compile(); + + sut = module.get(TspClientFactory); + configServiceMock = module.get(ConfigService); + }); + + afterAll(async () => { + await module.close(); + }); + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should be defined', () => { + expect(sut).toBeDefined(); + }); + + describe('createExportClient', () => { + describe('when createExportClient is called', () => { + it('should return ExportApiInterface', () => { + const result = sut.createExportClient(); + + expect(result).toBeDefined(); + expect(configServiceMock.getOrThrow).toHaveBeenCalledTimes(0); + }); + }); + + describe('when token is cached', () => { + const setup = () => { + Reflect.set(sut, 'cachedToken', faker.string.alpha()); + const client = sut.createExportClient(); + + return client; + }; + + it('should return ExportApiInterface', () => { + const result = setup(); + + expect(result).toBeDefined(); + expect(configServiceMock.getOrThrow).toHaveBeenCalledTimes(0); + }); + }); + }); + + // TODO: add a working integration test + describe.skip('when using the created client', () => { + const setup = () => { + const client = sut.createExportClient(); + + return client; + }; + + it('should return the migration version', async () => { + const client = setup(); + + const result = await client.version(); + + expect(result.status).toBe(200); + expect(result.data.version).toBeDefined(); + }); + }); +}); diff --git a/apps/server/src/infra/tsp-client/tsp-client-factory.ts b/apps/server/src/infra/tsp-client/tsp-client-factory.ts new file mode 100644 index 00000000000..835cd8e333d --- /dev/null +++ b/apps/server/src/infra/tsp-client/tsp-client-factory.ts @@ -0,0 +1,73 @@ +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { randomUUID } from 'crypto'; +import * as jwt from 'jsonwebtoken'; +import { Configuration, ExportApiFactory, ExportApiInterface } from './generated'; +import { TspRestClientConfig } from './tsp-client-config'; + +@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'); + 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 { + const factory = ExportApiFactory( + new Configuration({ + accessToken: this.createJwt(), + basePath: this.baseUrl, + }) + ); + + return factory; + } + + private createJwt(): string { + const now = Date.now(); + + if (this.cachedToken && this.tokenExpiresAt && this.tokenExpiresAt > now) { + return this.cachedToken; + } + + this.tokenExpiresAt = now + this.tokenLifetime; + + 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 = jwt.sign(payload, this.signingKey); + + return this.cachedToken; + } +} diff --git a/apps/server/src/infra/tsp-client/tsp-client.module.ts b/apps/server/src/infra/tsp-client/tsp-client.module.ts new file mode 100644 index 00000000000..c5b459df3f8 --- /dev/null +++ b/apps/server/src/infra/tsp-client/tsp-client.module.ts @@ -0,0 +1,8 @@ +import { Module } from '@nestjs/common'; +import { TspClientFactory } from './tsp-client-factory'; + +@Module({ + providers: [TspClientFactory], + exports: [TspClientFactory], +}) +export class TspClientModule {} diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index 738a696bc61..fa875ec0c60 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -27,6 +27,7 @@ import type { VideoConferenceConfig } from '@modules/video-conference'; 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'; @@ -68,6 +69,7 @@ export interface ServerConfig UserImportConfig, VideoConferenceConfig, BbbConfig, + TspRestClientConfig, AlertConfig, ShdConfig { NODE_ENV: NodeEnvType; @@ -306,6 +308,11 @@ const config: ServerConfig = { FEATURE_SANIS_GROUP_PROVISIONING_ENABLED: Configuration.get('FEATURE_SANIS_GROUP_PROVISIONING_ENABLED') as boolean, 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/config/default.schema.json b/config/default.schema.json index 6e92fd9f451..1d9f3f14fca 100644 --- a/config/default.schema.json +++ b/config/default.schema.json @@ -174,6 +174,11 @@ "default": "", "description": "The key used to sign/verify TSP request tokens." }, + "TSP_API_TOKEN_LIFETIME_MS": { + "type": "number", + "default": "30000", + "description": "The TSP token lifetime in milliseconds." + }, "FEATURE_TSP_ENABLED": { "type": "boolean", "default": false, diff --git a/openapitools.json b/openapitools.json index 97d49682b51..01614e969dc 100644 --- a/openapitools.json +++ b/openapitools.json @@ -2,6 +2,32 @@ "$schema": "./node_modules/@openapitools/openapi-generator-cli/config.schema.json", "spaces": 2, "generator-cli": { - "version": "7.6.0" + "version": "7.6.0", + "generators": { + "tsp-api": { + "generatorName": "typescript-axios", + "inputSpec": "https://test2.schulportal-thueringen.de/tip-ms/api/swagger.json", + "output": "./apps/server/src/infra/tsp-client/generated", + "skipValidateSpec": true, + "enablePostProcessFile": true, + "openapiNormalizer": { + "FILTER": "operationId:exportKlasseList|exportLehrerListMigration|exportLehrerList|exportSchuelerListMigration|exportSchuelerList|exportSchuleList|version" + }, + "globalProperty": { + "models": "RobjExportKlasse:RobjExportLehrerMigration:RobjExportLehrer:RobjExportSchuelerMigration:RobjExportSchueler:RobjExportSchule:VersionResponse", + "apis": "", + "supportingFiles": "" + }, + "additionalProperties": { + "apiPackage": "api", + "enumNameSuffix": "", + "enumPropertyNaming": "UPPERCASE", + "modelPackage": "models", + "supportsES6": true, + "withInterfaces": true, + "withSeparateModelsAndApi": true + } + } + } } } diff --git a/package.json b/package.json index c71342f0d6d..37f044e9f3e 100644 --- a/package.json +++ b/package.json @@ -105,7 +105,7 @@ "nest:start:common-cartridge": "node dist/apps/server/apps/common-cartridge.app", "nest:start:common-cartridge:dev": "nest start common-cartridge --watch --", "nest:start:common-cartridge:debug": "nest start common-cartridge --debug --watch --", - "nest:start:sync":"npm run nest:start:console -- sync run", + "nest:start:sync": "npm run nest:start:console -- sync run", "nest:test": "npm run nest:test:cov && npm run nest:lint", "nest:test:all": "jest \"^((?!(\\.load)\\.spec\\.ts).)*\"", "nest:test:unit": "jest \"^((?!(\\.api|\\.load)\\.spec\\.ts).)*\\.spec\\.ts$\"", @@ -121,7 +121,9 @@ "schoolExport": "node ./scripts/schoolExport.js", "schoolImport": "node ./scripts/schoolImport.js", "generate-client:authorization": "node ./scripts/generate-client.js -u 'http://localhost:3030/api/v3/docs-json/' -p 'apps/server/src/infra/authorization-client/authorization-api-client' -c 'openapitools-config.json' -f 'operationId:AuthorizationReferenceController_authorizeByReference'", - "generate-client:etherpad": "node ./scripts/generate-client.js -u 'http://localhost:9001/api/openapi.json' -p 'apps/server/src/infra/etherpad-client/etherpad-api-client' -c 'openapitools-config.json'" + "generate-client:etherpad": "node ./scripts/generate-client.js -u 'http://localhost:9001/api/openapi.json' -p 'apps/server/src/infra/etherpad-client/etherpad-api-client' -c 'openapitools-config.json'", + "pregenerate-client:tsp-api": "rimraf ./apps/server/src/infra/tsp-client/generated", + "generate-client:tsp-api": "openapi-generator-cli generate -c ./openapitools.json --generator-key tsp-api" }, "dependencies": { "@aws-sdk/lib-storage": "^3.617.0", diff --git a/sonar-project.properties b/sonar-project.properties index 496fca1be03..f85109f473b 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,8 +3,8 @@ sonar.projectKey=hpi-schul-cloud_schulcloud-server sonar.sources=. sonar.tests=. sonar.test.inclusions=**/*.spec.ts -sonar.exclusions=**/*.js,jest.config.ts,globalSetup.ts,globalTeardown.ts,**/*.app.ts,**/seed-data/*.ts,**/migrations/mikro-orm/*.ts,**/etherpad-api-client/**/*.ts,**/authorization-api-client/**/*.ts, **/course-api-client/**/*.ts,**/board-api-client/**/*.ts -sonar.coverage.exclusions=**/board-management.uc.ts,**/*.module.ts,**/*.factory.ts,**/migrations/mikro-orm/*.ts,**/globalSetup.ts,**/globalTeardown.ts,**/etherpad-api-client/**/*.ts,**/authorization-api-client/**/*.ts, **/course-api-client/**/*.ts,**/board-api-client/**/*.ts +sonar.exclusions=**/*.js,jest.config.ts,globalSetup.ts,globalTeardown.ts,**/*.app.ts,**/seed-data/*.ts,**/migrations/mikro-orm/*.ts,**/etherpad-api-client/**/*.ts,**/authorization-api-client/**/*.ts, **/course-api-client/**/*.ts,**/board-api-client/**/*.ts,**/generated/**/*.ts +sonar.coverage.exclusions=**/board-management.uc.ts,**/*.module.ts,**/*.factory.ts,**/migrations/mikro-orm/*.ts,**/globalSetup.ts,**/globalTeardown.ts,**/etherpad-api-client/**/*.ts,**/authorization-api-client/**/*.ts, **/course-api-client/**/*.ts,**/board-api-client/**/*.ts,**/generated/**/*.ts sonar.cpd.exclusions=**/controller/dto/**/*.ts,**/api/dto/**/*.ts,**/shared/testing/factory/*.factory.ts sonar.javascript.lcov.reportPaths=merged-lcov.info sonar.typescript.tsconfigPaths=tsconfig.json,src/apps/server/tsconfig.app.json From dded4fef95526f60e2ed976e0df9e3e4a7ac62cb Mon Sep 17 00:00:00 2001 From: mamutmk5 <3045922+mamutmk5@users.noreply.github.com> Date: Mon, 30 Sep 2024 10:47:37 +0200 Subject: [PATCH 2/3] BC-5112 - add Annotations to deployment for Restart (#5265) --- ansible/roles/common-cartridge/templates/deployment.yml.j2 | 6 ++++++ .../templates/admin-api-server-deployment.yml.j2 | 6 ++++++ .../templates/amqp-files-deployment.yml.j2 | 6 ++++++ .../templates/api-files-deployment.yml.j2 | 6 ++++++ .../templates/api-fwu-deployment.yml.j2 | 6 ++++++ .../templates/board-collaboration-deployment.yml.j2 | 6 ++++++ .../schulcloud-server-core/templates/deployment.yml.j2 | 6 ++++++ .../templates/preview-generator-deployment.yml.j2 | 6 ++++++ .../templates/tldraw-deployment.yml.j2 | 6 ++++++ .../templates/api-h5p-deployment.yml.j2 | 6 ++++++ .../templates/management-deployment.yml.j2 | 6 ++++++ .../templates/api-ldap-worker-deployment.yml.j2 | 6 ++++++ .../templates/deployment.yml.j2 | 6 ++++++ .../templates/api-tsp-sync-deployment.yml.j2 | 6 ++++++ 14 files changed, 84 insertions(+) diff --git a/ansible/roles/common-cartridge/templates/deployment.yml.j2 b/ansible/roles/common-cartridge/templates/deployment.yml.j2 index b09366736cb..ff840ebd95a 100644 --- a/ansible/roles/common-cartridge/templates/deployment.yml.j2 +++ b/ansible/roles/common-cartridge/templates/deployment.yml.j2 @@ -3,6 +3,12 @@ kind: Deployment metadata: name: common-cartridge-deployment namespace: {{ NAMESPACE }} +{% if ANNOTATIONS is defined and ANNOTATIONS|bool %} + annotations: +{% if RELOADER is defined and RELOADER|bool %} + reloader.stakater.com/auto: "true" +{% endif %} +{% endif %} labels: app: common-cartridge app.kubernetes.io/part-of: schulcloud-verbund diff --git a/ansible/roles/schulcloud-server-core/templates/admin-api-server-deployment.yml.j2 b/ansible/roles/schulcloud-server-core/templates/admin-api-server-deployment.yml.j2 index 4a5e7751dca..ae0ebe2fb6c 100644 --- a/ansible/roles/schulcloud-server-core/templates/admin-api-server-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/admin-api-server-deployment.yml.j2 @@ -3,6 +3,12 @@ kind: Deployment metadata: name: admin-api-deployment namespace: {{ NAMESPACE }} +{% if ANNOTATIONS is defined and ANNOTATIONS|bool %} + annotations: +{% if RELOADER is defined and RELOADER|bool %} + reloader.stakater.com/auto: "true" +{% endif %} +{% endif %} labels: app: api-admin app.kubernetes.io/part-of: schulcloud-verbund diff --git a/ansible/roles/schulcloud-server-core/templates/amqp-files-deployment.yml.j2 b/ansible/roles/schulcloud-server-core/templates/amqp-files-deployment.yml.j2 index 8902d144c40..89bfe8d61c9 100644 --- a/ansible/roles/schulcloud-server-core/templates/amqp-files-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/amqp-files-deployment.yml.j2 @@ -3,6 +3,12 @@ kind: Deployment metadata: name: amqp-files-deployment namespace: {{ NAMESPACE }} +{% if ANNOTATIONS is defined and ANNOTATIONS|bool %} + annotations: +{% if RELOADER is defined and RELOADER|bool %} + reloader.stakater.com/auto: "true" +{% endif %} +{% endif %} labels: app: amqp-files app.kubernetes.io/part-of: schulcloud-verbund diff --git a/ansible/roles/schulcloud-server-core/templates/api-files-deployment.yml.j2 b/ansible/roles/schulcloud-server-core/templates/api-files-deployment.yml.j2 index f7c263afc85..d42d0af45b6 100644 --- a/ansible/roles/schulcloud-server-core/templates/api-files-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/api-files-deployment.yml.j2 @@ -3,6 +3,12 @@ kind: Deployment metadata: name: api-files-deployment namespace: {{ NAMESPACE }} +{% if ANNOTATIONS is defined and ANNOTATIONS|bool %} + annotations: +{% if RELOADER is defined and RELOADER|bool %} + reloader.stakater.com/auto: "true" +{% endif %} +{% endif %} labels: app: api-files app.kubernetes.io/part-of: schulcloud-verbund diff --git a/ansible/roles/schulcloud-server-core/templates/api-fwu-deployment.yml.j2 b/ansible/roles/schulcloud-server-core/templates/api-fwu-deployment.yml.j2 index 0c4d5d6f394..340c939d0fd 100644 --- a/ansible/roles/schulcloud-server-core/templates/api-fwu-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/api-fwu-deployment.yml.j2 @@ -3,6 +3,12 @@ kind: Deployment metadata: name: api-fwu-deployment namespace: {{ NAMESPACE }} +{% if ANNOTATIONS is defined and ANNOTATIONS|bool %} + annotations: +{% if RELOADER is defined and RELOADER|bool %} + reloader.stakater.com/auto: "true" +{% endif %} +{% endif %} labels: app: api-fwu app.kubernetes.io/part-of: schulcloud-verbund diff --git a/ansible/roles/schulcloud-server-core/templates/board-collaboration-deployment.yml.j2 b/ansible/roles/schulcloud-server-core/templates/board-collaboration-deployment.yml.j2 index 60ebcc839c8..dc0e0483c15 100644 --- a/ansible/roles/schulcloud-server-core/templates/board-collaboration-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/board-collaboration-deployment.yml.j2 @@ -3,6 +3,12 @@ kind: Deployment metadata: name: board-collaboration-deployment namespace: {{ NAMESPACE }} +{% if ANNOTATIONS is defined and ANNOTATIONS|bool %} + annotations: +{% if RELOADER is defined and RELOADER|bool %} + reloader.stakater.com/auto: "true" +{% endif %} +{% endif %} labels: app: board-collaboration app.kubernetes.io/part-of: schulcloud-verbund diff --git a/ansible/roles/schulcloud-server-core/templates/deployment.yml.j2 b/ansible/roles/schulcloud-server-core/templates/deployment.yml.j2 index 83fe41b0adb..4aa30ff41b4 100644 --- a/ansible/roles/schulcloud-server-core/templates/deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/deployment.yml.j2 @@ -3,6 +3,12 @@ kind: Deployment metadata: name: api-deployment namespace: {{ NAMESPACE }} +{% if ANNOTATIONS is defined and ANNOTATIONS|bool %} + annotations: +{% if RELOADER is defined and RELOADER|bool %} + reloader.stakater.com/auto: "true" +{% endif %} +{% endif %} labels: app: api app.kubernetes.io/part-of: schulcloud-verbund diff --git a/ansible/roles/schulcloud-server-core/templates/preview-generator-deployment.yml.j2 b/ansible/roles/schulcloud-server-core/templates/preview-generator-deployment.yml.j2 index ba72d275c75..4fdb651aa02 100644 --- a/ansible/roles/schulcloud-server-core/templates/preview-generator-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/preview-generator-deployment.yml.j2 @@ -3,6 +3,12 @@ kind: Deployment metadata: name: preview-generator-deployment namespace: {{ NAMESPACE }} +{% if ANNOTATIONS is defined and ANNOTATIONS|bool %} + annotations: +{% if RELOADER is defined and RELOADER|bool %} + reloader.stakater.com/auto: "true" +{% endif %} +{% endif %} labels: app: preview-generator app.kubernetes.io/part-of: schulcloud-verbund diff --git a/ansible/roles/schulcloud-server-core/templates/tldraw-deployment.yml.j2 b/ansible/roles/schulcloud-server-core/templates/tldraw-deployment.yml.j2 index 56cf6d540c8..ed052650dad 100644 --- a/ansible/roles/schulcloud-server-core/templates/tldraw-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-core/templates/tldraw-deployment.yml.j2 @@ -3,6 +3,12 @@ kind: Deployment metadata: name: tldraw-deployment namespace: {{ NAMESPACE }} +{% if ANNOTATIONS is defined and ANNOTATIONS|bool %} + annotations: +{% if RELOADER is defined and RELOADER|bool %} + reloader.stakater.com/auto: "true" +{% endif %} +{% endif %} labels: app: tldraw-server app.kubernetes.io/part-of: schulcloud-verbund diff --git a/ansible/roles/schulcloud-server-h5p/templates/api-h5p-deployment.yml.j2 b/ansible/roles/schulcloud-server-h5p/templates/api-h5p-deployment.yml.j2 index 5bed585e511..0e52c547a1c 100644 --- a/ansible/roles/schulcloud-server-h5p/templates/api-h5p-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-h5p/templates/api-h5p-deployment.yml.j2 @@ -3,6 +3,12 @@ kind: Deployment metadata: name: api-h5p-deployment namespace: {{ NAMESPACE }} +{% if ANNOTATIONS is defined and ANNOTATIONS|bool %} + annotations: +{% if RELOADER is defined and RELOADER|bool %} + reloader.stakater.com/auto: "true" +{% endif %} +{% endif %} labels: app: api-h5p app.kubernetes.io/part-of: schulcloud-verbund diff --git a/ansible/roles/schulcloud-server-init/templates/management-deployment.yml.j2 b/ansible/roles/schulcloud-server-init/templates/management-deployment.yml.j2 index 1fc3b3f0af2..3f02564879b 100644 --- a/ansible/roles/schulcloud-server-init/templates/management-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-init/templates/management-deployment.yml.j2 @@ -3,6 +3,12 @@ kind: Deployment metadata: name: management-deployment namespace: {{ NAMESPACE }} +{% if ANNOTATIONS is defined and ANNOTATIONS|bool %} + annotations: +{% if RELOADER is defined and RELOADER|bool %} + reloader.stakater.com/auto: "true" +{% endif %} +{% endif %} labels: app: management-deployment app.kubernetes.io/part-of: schulcloud-verbund diff --git a/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-worker-deployment.yml.j2 b/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-worker-deployment.yml.j2 index bfcca9e9dd2..027ce4e0c74 100644 --- a/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-worker-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-ldapsync/templates/api-ldap-worker-deployment.yml.j2 @@ -3,6 +3,12 @@ kind: Deployment metadata: name: api-worker-deployment namespace: {{ NAMESPACE }} +{% if ANNOTATIONS is defined and ANNOTATIONS|bool %} + annotations: +{% if RELOADER is defined and RELOADER|bool %} + reloader.stakater.com/auto: "true" +{% endif %} +{% endif %} labels: app: api-worker app.kubernetes.io/part-of: schulcloud-verbund diff --git a/ansible/roles/schulcloud-server-migration-system/templates/deployment.yml.j2 b/ansible/roles/schulcloud-server-migration-system/templates/deployment.yml.j2 index 91859fc969c..ea0b242ee2c 100644 --- a/ansible/roles/schulcloud-server-migration-system/templates/deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-migration-system/templates/deployment.yml.j2 @@ -3,6 +3,12 @@ kind: Deployment metadata: name: api-migration-systems-deployment namespace: {{ NAMESPACE }} +{% if ANNOTATIONS is defined and ANNOTATIONS|bool %} + annotations: +{% if RELOADER is defined and RELOADER|bool %} + reloader.stakater.com/auto: "true" +{% endif %} +{% endif %} labels: app: api-migration-systems app.kubernetes.io/part-of: schulcloud-verbund diff --git a/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-deployment.yml.j2 b/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-deployment.yml.j2 index c38da100fa4..38a13ef7629 100644 --- a/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-deployment.yml.j2 +++ b/ansible/roles/schulcloud-server-tspsync/templates/api-tsp-sync-deployment.yml.j2 @@ -3,6 +3,12 @@ kind: Deployment metadata: name: api-tsp-sync-deployment namespace: {{ NAMESPACE }} +{% if ANNOTATIONS is defined and ANNOTATIONS|bool %} + annotations: +{% if RELOADER is defined and RELOADER|bool %} + reloader.stakater.com/auto: "true" +{% endif %} +{% endif %} labels: app: api-tsp-sync app.kubernetes.io/part-of: schulcloud-verbund From 873e20c590772264a7e7808fc74daf2ece3b11c1 Mon Sep 17 00:00:00 2001 From: Majed Mak <132336669+MajedAlaitwniCap@users.noreply.github.com> Date: Mon, 30 Sep 2024 11:53:43 +0200 Subject: [PATCH 3/3] EW-1004 implement getData method for TSP Strategy (#5253) --- .../provisioning/dto/external-class.dto.ts | 2 +- .../provisioning/dto/external-school.dto.ts | 2 +- .../bad-data.loggable-exception.spec.ts | 0 .../loggable/bad-data.loggable-exception.ts | 0 .../modules/provisioning/loggable/index.ts | 2 + ...l-name-required-loggable-exception.spec.ts | 28 +++++ ...school-name-required-loggable-exception.ts | 27 +++++ .../service/tsp-provisioning.service.spec.ts | 2 +- .../service/tsp-provisioning.service.ts | 6 +- .../provisioning/strategy/loggable/index.ts | 2 +- ...connex-school-provisioning.service.spec.ts | 43 +++++++ ...schulconnex-school-provisioning.service.ts | 5 + .../strategy/tsp/tsp.jwt.payload.ts | 35 ++++++ .../strategy/tsp/tsp.strategy.spec.ts | 113 +++++++++++++++++- .../provisioning/strategy/tsp/tsp.strategy.ts | 63 +++++++++- 15 files changed, 316 insertions(+), 14 deletions(-) rename apps/server/src/modules/provisioning/{strategy => }/loggable/bad-data.loggable-exception.spec.ts (100%) rename apps/server/src/modules/provisioning/{strategy => }/loggable/bad-data.loggable-exception.ts (100%) create mode 100644 apps/server/src/modules/provisioning/loggable/school-name-required-loggable-exception.spec.ts create mode 100644 apps/server/src/modules/provisioning/loggable/school-name-required-loggable-exception.ts create mode 100644 apps/server/src/modules/provisioning/strategy/tsp/tsp.jwt.payload.ts diff --git a/apps/server/src/modules/provisioning/dto/external-class.dto.ts b/apps/server/src/modules/provisioning/dto/external-class.dto.ts index a832922981c..a2641bd0707 100644 --- a/apps/server/src/modules/provisioning/dto/external-class.dto.ts +++ b/apps/server/src/modules/provisioning/dto/external-class.dto.ts @@ -1,7 +1,7 @@ export class ExternalClassDto { public readonly externalId: string; - public readonly name: string; + public readonly name?: string; constructor(props: Readonly) { this.externalId = props.externalId; diff --git a/apps/server/src/modules/provisioning/dto/external-school.dto.ts b/apps/server/src/modules/provisioning/dto/external-school.dto.ts index 701ee63f931..67aa2f15186 100644 --- a/apps/server/src/modules/provisioning/dto/external-school.dto.ts +++ b/apps/server/src/modules/provisioning/dto/external-school.dto.ts @@ -1,7 +1,7 @@ export class ExternalSchoolDto { externalId: string; - name: string; + name?: string; officialSchoolNumber?: string; diff --git a/apps/server/src/modules/provisioning/strategy/loggable/bad-data.loggable-exception.spec.ts b/apps/server/src/modules/provisioning/loggable/bad-data.loggable-exception.spec.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/loggable/bad-data.loggable-exception.spec.ts rename to apps/server/src/modules/provisioning/loggable/bad-data.loggable-exception.spec.ts diff --git a/apps/server/src/modules/provisioning/strategy/loggable/bad-data.loggable-exception.ts b/apps/server/src/modules/provisioning/loggable/bad-data.loggable-exception.ts similarity index 100% rename from apps/server/src/modules/provisioning/strategy/loggable/bad-data.loggable-exception.ts rename to apps/server/src/modules/provisioning/loggable/bad-data.loggable-exception.ts diff --git a/apps/server/src/modules/provisioning/loggable/index.ts b/apps/server/src/modules/provisioning/loggable/index.ts index ee2c20e74f0..9b9f2196e88 100644 --- a/apps/server/src/modules/provisioning/loggable/index.ts +++ b/apps/server/src/modules/provisioning/loggable/index.ts @@ -1,5 +1,7 @@ export * from './user-for-group-not-found.loggable'; export * from './school-for-group-not-found.loggable'; +export * from './bad-data.loggable-exception'; +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'; diff --git a/apps/server/src/modules/provisioning/loggable/school-name-required-loggable-exception.spec.ts b/apps/server/src/modules/provisioning/loggable/school-name-required-loggable-exception.spec.ts new file mode 100644 index 00000000000..2eea53e1481 --- /dev/null +++ b/apps/server/src/modules/provisioning/loggable/school-name-required-loggable-exception.spec.ts @@ -0,0 +1,28 @@ +import { SchoolNameRequiredLoggableException } from './school-name-required-loggable-exception'; + +describe(SchoolNameRequiredLoggableException.name, () => { + describe('getLogMessage', () => { + const setup = () => { + const fieldName = 'id_token'; + + const exception = new SchoolNameRequiredLoggableException(fieldName); + + return { exception, fieldName }; + }; + + it('should return a LogMessage', () => { + const { exception, fieldName } = setup(); + + const logMessage = exception.getLogMessage(); + + expect(logMessage).toEqual({ + type: 'SCHOOL_NAME_REQUIRED', + message: 'External school name is required', + stack: exception.stack, + data: { + fieldName, + }, + }); + }); + }); +}); diff --git a/apps/server/src/modules/provisioning/loggable/school-name-required-loggable-exception.ts b/apps/server/src/modules/provisioning/loggable/school-name-required-loggable-exception.ts new file mode 100644 index 00000000000..47986d51e65 --- /dev/null +++ b/apps/server/src/modules/provisioning/loggable/school-name-required-loggable-exception.ts @@ -0,0 +1,27 @@ +import { HttpStatus } from '@nestjs/common'; +import { BusinessError } from '@shared/common'; +import { ErrorLogMessage, Loggable, LogMessage, ValidationErrorLogMessage } from '@src/core/logger'; + +export class SchoolNameRequiredLoggableException extends BusinessError implements Loggable { + constructor(private readonly fieldName: string) { + super( + { + type: 'SCHOOL_NAME_REQUIRED', + title: 'School name is required', + defaultMessage: 'External school name is required', + }, + HttpStatus.INTERNAL_SERVER_ERROR + ); + } + + getLogMessage(): LogMessage | ErrorLogMessage | ValidationErrorLogMessage { + return { + type: this.type, + message: this.message, + stack: this.stack, + data: { + fieldName: this.fieldName, + }, + }; + } +} diff --git a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts index ecc7c932584..6746bb7bcd2 100644 --- a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.spec.ts @@ -13,7 +13,7 @@ import { SchoolService } from '@src/modules/school'; import { schoolFactory } from '@src/modules/school/testing'; import { UserService } from '@src/modules/user'; import { ExternalClassDto, ExternalSchoolDto, ExternalUserDto, OauthDataDto, ProvisioningSystemDto } from '../dto'; -import { BadDataLoggableException } from '../strategy/loggable'; +import { BadDataLoggableException } from '../loggable'; import { TspProvisioningService } from './tsp-provisioning.service'; describe('TspProvisioningService', () => { diff --git a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.ts b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.ts index e9d2f6ec6b6..643e684dc40 100644 --- a/apps/server/src/modules/provisioning/service/tsp-provisioning.service.ts +++ b/apps/server/src/modules/provisioning/service/tsp-provisioning.service.ts @@ -8,7 +8,7 @@ import { RoleName } from '@shared/domain/interface'; import { School, SchoolService } from '@src/modules/school'; import { UserService } from '@src/modules/user'; import { ExternalClassDto, ExternalSchoolDto, ExternalUserDto, OauthDataDto, ProvisioningSystemDto } from '../dto'; -import { BadDataLoggableException } from '../strategy/loggable'; +import { BadDataLoggableException } from '../loggable'; @Injectable() export class TspProvisioningService { @@ -47,7 +47,9 @@ export class TspProvisioningService { if (currentClass) { // Case: Class exists -> update class currentClass.schoolId = school.id; - currentClass.name = clazz.name; + if (clazz.name) { + currentClass.name = clazz.name; + } currentClass.year = school.currentYear?.id; currentClass.source = this.ENTITY_SOURCE; currentClass.sourceOptions = new ClassSourceOptions({ tspUid: clazz.externalId }); diff --git a/apps/server/src/modules/provisioning/strategy/loggable/index.ts b/apps/server/src/modules/provisioning/strategy/loggable/index.ts index 213a83976d8..102d04f9628 100644 --- a/apps/server/src/modules/provisioning/strategy/loggable/index.ts +++ b/apps/server/src/modules/provisioning/strategy/loggable/index.ts @@ -1 +1 @@ -export * from './bad-data.loggable-exception'; +export * from '../../loggable/bad-data.loggable-exception'; diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-school-provisioning.service.spec.ts b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-school-provisioning.service.spec.ts index 31c41b8ee1a..42b2e72bb74 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-school-provisioning.service.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-school-provisioning.service.spec.ts @@ -7,6 +7,7 @@ import { SchoolFeature } from '@shared/domain/types'; import { federalStateFactory, legacySchoolDoFactory, schoolYearFactory } from '@shared/testing'; import { ExternalSchoolDto } from '../../../dto'; import { SchulconnexSchoolProvisioningService } from './schulconnex-school-provisioning.service'; +import { SchoolNameRequiredLoggableException } from '../../../loggable'; describe(SchulconnexSchoolProvisioningService.name, () => { let module: TestingModule; @@ -146,6 +147,48 @@ describe(SchulconnexSchoolProvisioningService.name, () => { }); }); + describe('when the external system does not provide a Name for the school', () => { + const setup = () => { + const systemId = new ObjectId().toHexString(); + const externalSchoolDto: ExternalSchoolDto = new ExternalSchoolDto({ + externalId: 'externalId', + officialSchoolNumber: 'officialSchoolNumber', + }); + + const schoolYear = schoolYearFactory.build(); + const federalState = federalStateFactory.build(); + const savedSchoolDO = new LegacySchoolDo({ + id: 'schoolId', + externalId: 'externalId', + name: 'name', + officialSchoolNumber: 'officialSchoolNumber', + systems: [systemId], + features: [SchoolFeature.OAUTH_PROVISIONING_ENABLED], + schoolYear, + federalState, + }); + + schoolService.save.mockResolvedValue(savedSchoolDO); + schoolService.getSchoolByExternalId.mockResolvedValue(null); + schoolYearService.getCurrentSchoolYear.mockResolvedValue(schoolYear); + federalStateService.findFederalStateByName.mockResolvedValue(federalState); + + return { + systemId, + externalSchoolDto, + savedSchoolDO, + }; + }; + + it('should throw an error', async () => { + const { systemId, externalSchoolDto } = setup(); + + await expect(service.provisionExternalSchool(externalSchoolDto, systemId)).rejects.toThrowError( + new SchoolNameRequiredLoggableException('ExternalSchool.name') + ); + }); + }); + describe('when the external system does not provide a location for the school', () => { const setup = () => { const systemId = new ObjectId().toHexString(); diff --git a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-school-provisioning.service.ts b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-school-provisioning.service.ts index 4483b324b78..10aaaca1be0 100644 --- a/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-school-provisioning.service.ts +++ b/apps/server/src/modules/provisioning/strategy/oidc/service/schulconnex-school-provisioning.service.ts @@ -5,6 +5,7 @@ import { LegacySchoolDo } from '@shared/domain/domainobject'; import { FederalStateEntity, SchoolYearEntity } from '@shared/domain/entity'; import { EntityId, SchoolFeature } from '@shared/domain/types'; import { ExternalSchoolDto } from '../../../dto'; +import { SchoolNameRequiredLoggableException } from '../../../loggable'; @Injectable() export class SchulconnexSchoolProvisioningService { @@ -53,6 +54,10 @@ export class SchulconnexSchoolProvisioningService { } private getSchoolName(externalSchool: ExternalSchoolDto): string { + if (!externalSchool.name) { + throw new SchoolNameRequiredLoggableException('ExternalSchool.name'); + } + const schoolName: string = externalSchool.location ? `${externalSchool.name} (${externalSchool.location})` : externalSchool.name; diff --git a/apps/server/src/modules/provisioning/strategy/tsp/tsp.jwt.payload.ts b/apps/server/src/modules/provisioning/strategy/tsp/tsp.jwt.payload.ts new file mode 100644 index 00000000000..87df93adbce --- /dev/null +++ b/apps/server/src/modules/provisioning/strategy/tsp/tsp.jwt.payload.ts @@ -0,0 +1,35 @@ +import { IsString, IsOptional, IsArray } from 'class-validator'; +import { JwtPayload } from 'jsonwebtoken'; + +export class TspJwtPayload implements JwtPayload { + @IsString() + public sub!: string; + + @IsOptional() + @IsString() + public sid: string | undefined; + + @IsOptional() + @IsString() + public ptscListRolle: string | undefined; + + @IsOptional() + @IsString() + public personVorname: string | undefined; + + @IsOptional() + @IsString() + public personNachname: string | undefined; + + @IsOptional() + @IsString() + public ptscSchuleNummer: string | undefined; + + @IsOptional() + @IsArray() + public ptscListKlasseId: [] | undefined; + + constructor(data: Partial) { + Object.assign(this, data); + } +} diff --git a/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.spec.ts b/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.spec.ts index 784296ea7c3..4ce648fe27d 100644 --- a/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.spec.ts +++ b/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.spec.ts @@ -5,7 +5,10 @@ import { Test, TestingModule } from '@nestjs/testing'; import { RoleName } from '@shared/domain/interface'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; import { userDoFactory } from '@shared/testing'; +import { IdTokenExtractionFailureLoggableException } from '@src/modules/oauth/loggable'; +import jwt from 'jsonwebtoken'; import { + ExternalClassDto, ExternalSchoolDto, ExternalUserDto, OauthDataDto, @@ -55,8 +58,114 @@ describe('TspProvisioningStrategy', () => { describe('getData', () => { describe('When called', () => { - it('should throw', () => { - expect(() => sut.getData({} as OauthDataStrategyInputDto)).toThrow(); + const setup = () => { + const input: OauthDataStrategyInputDto = new OauthDataStrategyInputDto({ + system: new ProvisioningSystemDto({ + systemId: 'externalSchoolId', + provisioningStrategy: SystemProvisioningStrategy.TSP, + }), + idToken: 'tspIdToken', + accessToken: 'tspAccessToken', + }); + + jest.spyOn(jwt, 'decode').mockImplementation(() => { + return { + sub: 'externalUserId', + sid: 'externalSchoolId', + ptscListRolle: 'schueler,lehrer,admin', + personVorname: 'firstName', + personNachname: 'lastName', + ptscSchuleNummer: 'externalSchoolId', + ptscListKlasseId: ['externalClassId1', 'externalClassId2'], + }; + }); + + const user: ExternalUserDto = new ExternalUserDto({ + externalId: 'externalUserId', + roles: [RoleName.STUDENT, RoleName.TEACHER, RoleName.ADMINISTRATOR], + firstName: 'firstName', + lastName: 'lastName', + }); + + const school: ExternalSchoolDto = new ExternalSchoolDto({ + externalId: 'externalSchoolId', + }); + + const externalClass1 = new ExternalClassDto({ externalId: 'externalClassId1' }); + const externalClass2 = new ExternalClassDto({ externalId: 'externalClassId2' }); + const externalClasses = [externalClass1, externalClass2]; + + return { input, user, school, externalClasses }; + }; + + it('should return mapped oauthDataDto if input is valid', async () => { + const { input, user, school, externalClasses } = setup(); + const result = await sut.getData(input); + + expect(result).toEqual({ + system: input.system, + externalUser: user, + externalSchool: school, + externalGroups: undefined, + externalLicenses: undefined, + externalClasses, + }); + }); + }); + + describe('When idToken is invalid', () => { + const setup = () => { + const input: OauthDataStrategyInputDto = new OauthDataStrategyInputDto({ + system: new ProvisioningSystemDto({ + systemId: 'externalSchoolId', + provisioningStrategy: SystemProvisioningStrategy.TSP, + }), + idToken: 'invalidIdToken', + accessToken: 'tspAccessToken', + }); + + jest.spyOn(jwt, 'decode').mockImplementation(() => null); + + return { input }; + }; + + it('should throw IdTokenExtractionFailure', async () => { + const { input } = setup(); + + await expect(sut.getData(input)).rejects.toThrow(new IdTokenExtractionFailureLoggableException('sub')); + }); + }); + + describe('When payload is invalid', () => { + const setup = () => { + const input: OauthDataStrategyInputDto = new OauthDataStrategyInputDto({ + system: new ProvisioningSystemDto({ + systemId: 'externalSchoolId', + provisioningStrategy: SystemProvisioningStrategy.TSP, + }), + idToken: 'tspIdToken', + accessToken: 'tspAccessToken', + }); + + jest.spyOn(jwt, 'decode').mockImplementation(() => { + return { + sub: 'externalUserId', + sid: 1000, + ptscListRolle: 'teacher', + personVorname: 'firstName', + personNachname: 'lastName', + ptscSchuleNummer: 'externalSchoolId', + ptscListKlasseId: ['externalClassId1', 'externalClassId2'], + }; + }); + + return { input }; + }; + + it('should throw IdTokenExtractionFailure', async () => { + const { input } = setup(); + + await expect(sut.getData(input)).rejects.toThrow(new IdTokenExtractionFailureLoggableException('sub')); }); }); }); 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 8f05262f977..3ed54b80762 100644 --- a/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts +++ b/apps/server/src/modules/provisioning/strategy/tsp/tsp.strategy.ts @@ -1,12 +1,30 @@ -import { Injectable, NotImplementedException } from '@nestjs/common'; +import { Injectable } from '@nestjs/common'; +import { RoleName } from '@shared/domain/interface'; import { SystemProvisioningStrategy } from '@shared/domain/interface/system-provisioning.strategy'; -import { OauthDataDto, OauthDataStrategyInputDto, ProvisioningDto } from '../../dto'; +import { IdTokenExtractionFailureLoggableException } from '@src/modules/oauth/loggable'; +import { validate } from 'class-validator'; +import jwt, { JwtPayload } from 'jsonwebtoken'; +import { + ExternalClassDto, + ExternalSchoolDto, + ExternalUserDto, + OauthDataDto, + OauthDataStrategyInputDto, + ProvisioningDto, +} from '../../dto'; import { TspProvisioningService } from '../../service/tsp-provisioning.service'; import { ProvisioningStrategy } from '../base.strategy'; import { BadDataLoggableException } from '../loggable'; +import { TspJwtPayload } from './tsp.jwt.payload'; @Injectable() export class TspProvisioningStrategy extends ProvisioningStrategy { + RoleMapping: Record = { + lehrer: RoleName.TEACHER, + schueler: RoleName.STUDENT, + admin: RoleName.ADMINISTRATOR, + }; + constructor(private readonly provisioningService: TspProvisioningService) { super(); } @@ -15,10 +33,43 @@ export class TspProvisioningStrategy extends ProvisioningStrategy { return SystemProvisioningStrategy.TSP; } - // eslint-disable-next-line @typescript-eslint/no-unused-vars - override getData(input: OauthDataStrategyInputDto): Promise { - // TODO EW-1004 - throw new NotImplementedException(); + override async getData(input: OauthDataStrategyInputDto): Promise { + const decodedAccessToken: JwtPayload | null = jwt.decode(input.accessToken, { json: true }); + + if (!decodedAccessToken) { + throw new IdTokenExtractionFailureLoggableException('sub'); + } + + const payload = new TspJwtPayload(decodedAccessToken); + const errors = await validate(payload); + + if (errors.length > 0) { + throw new IdTokenExtractionFailureLoggableException(errors.map((error) => error.property).join(', ')); + } + + const externalUserDto = new ExternalUserDto({ + externalId: payload.sub, + firstName: payload.personVorname, + lastName: payload.personNachname, + roles: (payload.ptscListRolle ?? '').split(',').map((tspRole) => this.RoleMapping[tspRole]), + }); + + const externalSchoolDto = new ExternalSchoolDto({ + externalId: payload.ptscSchuleNummer || '', + }); + + const externalClassDtoList = (payload.ptscListKlasseId ?? []).map( + (classId: string) => new ExternalClassDto({ externalId: classId }) + ); + + const oauthDataDto = new OauthDataDto({ + system: input.system, + externalUser: externalUserDto, + externalSchool: externalSchoolDto, + externalClasses: externalClassDtoList, + }); + + return oauthDataDto; } override async apply(data: OauthDataDto): Promise {