diff --git a/ansible/roles/common-cartridge/tasks/main.yml b/ansible/roles/common-cartridge/tasks/main.yml index a4d6e99575e..6771d9f73f8 100644 --- a/ansible/roles/common-cartridge/tasks/main.yml +++ b/ansible/roles/common-cartridge/tasks/main.yml @@ -52,11 +52,11 @@ - service # This is a testing route and will not be deployed -# - name: Ingress -# kubernetes.core.k8s: -# kubeconfig: ~/.kube/config -# namespace: "{{ NAMESPACE }}" -# template: ingress.yml.j2 -# when: WITH_COMMON_CARTRIDGE is defined and WITH_COMMON_CARTRIDGE|bool -# tags: -# - ingress +- name: Ingress + kubernetes.core.k8s: + kubeconfig: ~/.kube/config + namespace: "{{ NAMESPACE }}" + template: ingress.yml.j2 + when: WITH_COMMON_CARTRIDGE is defined and WITH_COMMON_CARTRIDGE|bool + tags: + - ingress diff --git a/apps/server/src/infra/courses-client/courses-client.adapter.spec.ts b/apps/server/src/infra/courses-client/courses-client.adapter.spec.ts new file mode 100644 index 00000000000..da3149d8874 --- /dev/null +++ b/apps/server/src/infra/courses-client/courses-client.adapter.spec.ts @@ -0,0 +1,72 @@ +import { faker } from '@faker-js/faker'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { CoursesClientAdapter } from './courses-client.adapter'; +import { CoursesApi, CreateCourseBodyParams } from './generated'; + +describe(CoursesClientAdapter.name, () => { + let module: TestingModule; + let sut: CoursesClientAdapter; + let coursesApiMock: DeepMocked; + + beforeAll(async () => { + module = await Test.createTestingModule({ + providers: [ + CoursesClientAdapter, + { + provide: CoursesApi, + useValue: createMock(), + }, + ], + }).compile(); + + sut = module.get(CoursesClientAdapter); + coursesApiMock = module.get(CoursesApi); + }); + + afterAll(async () => { + await module.close(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(sut).toBeDefined(); + }); + + describe('getCourseCommonCartridgeMetadata', () => { + const setup = () => { + const courseId = faker.string.uuid(); + + return { courseId }; + }; + + it('should call courseControllerGetCourseCcMetadataById with the correct courseId', async () => { + const { courseId } = setup(); + + await sut.getCourseCommonCartridgeMetadata(courseId); + + expect(coursesApiMock.courseControllerGetCourseCcMetadataById).toHaveBeenCalledWith(courseId); + }); + }); + + describe('createCourse', () => { + const setup = () => { + const params: CreateCourseBodyParams = { + title: faker.word.noun(), + }; + + return { params }; + }; + + it('should call courseControllerCreateCourse with the correct params', async () => { + const { params } = setup(); + + await sut.createCourse(params); + + expect(coursesApiMock.courseControllerCreateCourse).toHaveBeenCalledWith(params); + }); + }); +}); diff --git a/apps/server/src/infra/courses-client/courses-client.adapter.ts b/apps/server/src/infra/courses-client/courses-client.adapter.ts new file mode 100644 index 00000000000..4628d93f2df --- /dev/null +++ b/apps/server/src/infra/courses-client/courses-client.adapter.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@nestjs/common'; +import { CourseCommonCartridgeMetadataResponse, CoursesApi, CreateCourseBodyParams } from './generated'; + +@Injectable() +export class CoursesClientAdapter { + constructor(private readonly coursesApi: CoursesApi) {} + + public async getCourseCommonCartridgeMetadata(courseId: string): Promise { + const response = await this.coursesApi.courseControllerGetCourseCcMetadataById(courseId); + + return response.data; + } + + public async createCourse(params: CreateCourseBodyParams): Promise { + await this.coursesApi.courseControllerCreateCourse(params); + } +} diff --git a/apps/server/src/infra/courses-client/courses-client.config.ts b/apps/server/src/infra/courses-client/courses-client.config.ts new file mode 100644 index 00000000000..a31383b2e02 --- /dev/null +++ b/apps/server/src/infra/courses-client/courses-client.config.ts @@ -0,0 +1,3 @@ +export interface CoursesClientConfig { + API_HOST: string; +} diff --git a/apps/server/src/infra/courses-client/courses-client.module.spec.ts b/apps/server/src/infra/courses-client/courses-client.module.spec.ts new file mode 100644 index 00000000000..e2332fc3c87 --- /dev/null +++ b/apps/server/src/infra/courses-client/courses-client.module.spec.ts @@ -0,0 +1,57 @@ +import { faker } from '@faker-js/faker'; +import { createMock } from '@golevelup/ts-jest'; +import { ConfigModule, ConfigService } from '@nestjs/config'; +import { REQUEST } from '@nestjs/core'; +import { Test, TestingModule } from '@nestjs/testing'; +import { Request } from 'express'; +import { CoursesClientAdapter } from './courses-client.adapter'; +import { CoursesClientModule } from './courses-client.module'; +import { CoursesApi } from './generated'; + +describe(CoursesClientModule.name, () => { + let module: TestingModule; + let sut: CoursesClientModule; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [CoursesClientModule, ConfigModule.forRoot({ isGlobal: true })], + }) + .overrideProvider(ConfigService) + .useValue( + createMock({ + getOrThrow: () => faker.internet.url(), + }) + ) + .overrideProvider(REQUEST) + .useValue({ headers: { authorization: `Bearer ${faker.string.alphanumeric(42)}` } } as Request) + .compile(); + + sut = module.get(CoursesClientModule); + }); + + afterAll(async () => { + await module.close(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(sut).toBeDefined(); + }); + + describe('when requesting dependencies', () => { + it('should resolve CoursesApi', async () => { + const dependency = await module.resolve(CoursesApi); + + expect(dependency).toBeInstanceOf(CoursesApi); + }); + + it('should resolve CoursesClientAdapter', async () => { + const dependency = await module.resolve(CoursesClientAdapter); + + expect(dependency).toBeInstanceOf(CoursesClientAdapter); + }); + }); +}); diff --git a/apps/server/src/infra/courses-client/courses-client.module.ts b/apps/server/src/infra/courses-client/courses-client.module.ts new file mode 100644 index 00000000000..a03fd839933 --- /dev/null +++ b/apps/server/src/infra/courses-client/courses-client.module.ts @@ -0,0 +1,31 @@ +import { Module, Scope } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { REQUEST } from '@nestjs/core'; +import { JwtExtractor } from '@shared/common/utils'; +import { Request } from 'express'; +import { CoursesClientAdapter } from './courses-client.adapter'; +import { CoursesClientConfig } from './courses-client.config'; +import { Configuration, CoursesApi } from './generated'; + +@Module({ + providers: [ + CoursesClientAdapter, + { + provide: CoursesApi, + scope: Scope.REQUEST, + useFactory: (configService: ConfigService, request: Request): CoursesApi => { + const basePath = configService.getOrThrow('API_HOST'); + const accessToken = JwtExtractor.extractJwtFromRequest(request); + const configuration = new Configuration({ + basePath: `${basePath}/v3`, + accessToken, + }); + + return new CoursesApi(configuration); + }, + inject: [ConfigService, REQUEST], + }, + ], + exports: [CoursesClientAdapter], +}) +export class CoursesClientModule {} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/.gitignore b/apps/server/src/infra/courses-client/generated/.gitignore similarity index 100% rename from apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/.gitignore rename to apps/server/src/infra/courses-client/generated/.gitignore diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/.npmignore b/apps/server/src/infra/courses-client/generated/.npmignore similarity index 100% rename from apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/.npmignore rename to apps/server/src/infra/courses-client/generated/.npmignore diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/.openapi-generator-ignore b/apps/server/src/infra/courses-client/generated/.openapi-generator-ignore similarity index 100% rename from apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/.openapi-generator-ignore rename to apps/server/src/infra/courses-client/generated/.openapi-generator-ignore diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/.openapi-generator/FILES b/apps/server/src/infra/courses-client/generated/.openapi-generator/FILES similarity index 64% rename from apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/.openapi-generator/FILES rename to apps/server/src/infra/courses-client/generated/.openapi-generator/FILES index ca8e5c1e70f..e10dec0e56c 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/.openapi-generator/FILES +++ b/apps/server/src/infra/courses-client/generated/.openapi-generator/FILES @@ -1,15 +1,13 @@ -.gitignore -.npmignore -.openapi-generator-ignore -api.ts -api/courses-api.ts -base.ts -common.ts -configuration.ts -git_push.sh -index.ts -models/course-common-cartridge-metadata-response.ts -models/course-export-body-params.ts -models/course-metadata-list-response.ts -models/course-metadata-response.ts -models/index.ts +.gitignore +.npmignore +.openapi-generator-ignore +api.ts +api/courses-api.ts +base.ts +common.ts +configuration.ts +git_push.sh +index.ts +models/course-common-cartridge-metadata-response.ts +models/create-course-body-params.ts +models/index.ts diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/api.ts b/apps/server/src/infra/courses-client/generated/api.ts similarity index 100% rename from apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/api.ts rename to apps/server/src/infra/courses-client/generated/api.ts diff --git a/apps/server/src/infra/courses-client/generated/api/courses-api.ts b/apps/server/src/infra/courses-client/generated/api/courses-api.ts new file mode 100644 index 00000000000..9be58d522be --- /dev/null +++ b/apps/server/src/infra/courses-client/generated/api/courses-api.ts @@ -0,0 +1,240 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + +import type { Configuration } from '../configuration'; +import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios'; +import globalAxios from 'axios'; +// Some imports not used depending on template conditions +// @ts-ignore +import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common'; +// @ts-ignore +import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; +// @ts-ignore +import type { CourseCommonCartridgeMetadataResponse } from '../models'; +// @ts-ignore +import type { CreateCourseBodyParams } from '../models'; +/** + * CoursesApi - axios parameter creator + * @export + */ +export const CoursesApiAxiosParamCreator = function (configuration?: Configuration) { + return { + /** + * + * @summary Create a new course. + * @param {CreateCourseBodyParams} createCourseBodyParams + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + courseControllerCreateCourse: async (createCourseBodyParams: CreateCourseBodyParams, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'createCourseBodyParams' is not null or undefined + assertParamExists('courseControllerCreateCourse', 'createCourseBodyParams', createCourseBodyParams) + const localVarPath = `/courses`; + // 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: 'POST', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + localVarHeaderParameter['Content-Type'] = 'application/json'; + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + localVarRequestOptions.data = serializeDataIfNeeded(createCourseBodyParams, localVarRequestOptions, configuration) + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + /** + * + * @summary Get common cartridge metadata of a course by Id. + * @param {string} courseId The id of the course + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + courseControllerGetCourseCcMetadataById: async (courseId: string, options: RawAxiosRequestConfig = {}): Promise => { + // verify required parameter 'courseId' is not null or undefined + assertParamExists('courseControllerGetCourseCcMetadataById', 'courseId', courseId) + const localVarPath = `/courses/{courseId}/cc-metadata` + .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))); + // use dummy base URL string because the URL constructor only accepts absolute URLs. + const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); + let baseOptions; + if (configuration) { + baseOptions = configuration.baseOptions; + } + + const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; + const localVarHeaderParameter = {} as any; + const localVarQueryParameter = {} as any; + + // authentication bearer required + // http bearer authentication required + await setBearerAuthToObject(localVarHeaderParameter, configuration) + + + + setSearchParams(localVarUrlObj, localVarQueryParameter); + let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; + localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; + + return { + url: toPathString(localVarUrlObj), + options: localVarRequestOptions, + }; + }, + } +}; + +/** + * CoursesApi - functional programming interface + * @export + */ +export const CoursesApiFp = function(configuration?: Configuration) { + const localVarAxiosParamCreator = CoursesApiAxiosParamCreator(configuration) + return { + /** + * + * @summary Create a new course. + * @param {CreateCourseBodyParams} createCourseBodyParams + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async courseControllerCreateCourse(createCourseBodyParams: CreateCourseBodyParams, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.courseControllerCreateCourse(createCourseBodyParams, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['CoursesApi.courseControllerCreateCourse']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + /** + * + * @summary Get common cartridge metadata of a course by Id. + * @param {string} courseId The id of the course + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + async courseControllerGetCourseCcMetadataById(courseId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { + const localVarAxiosArgs = await localVarAxiosParamCreator.courseControllerGetCourseCcMetadataById(courseId, options); + const localVarOperationServerIndex = configuration?.serverIndex ?? 0; + const localVarOperationServerBasePath = operationServerMap['CoursesApi.courseControllerGetCourseCcMetadataById']?.[localVarOperationServerIndex]?.url; + return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); + }, + } +}; + +/** + * CoursesApi - factory interface + * @export + */ +export const CoursesApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { + const localVarFp = CoursesApiFp(configuration) + return { + /** + * + * @summary Create a new course. + * @param {CreateCourseBodyParams} createCourseBodyParams + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + courseControllerCreateCourse(createCourseBodyParams: CreateCourseBodyParams, options?: any): AxiosPromise { + return localVarFp.courseControllerCreateCourse(createCourseBodyParams, options).then((request) => request(axios, basePath)); + }, + /** + * + * @summary Get common cartridge metadata of a course by Id. + * @param {string} courseId The id of the course + * @param {*} [options] Override http request option. + * @throws {RequiredError} + */ + courseControllerGetCourseCcMetadataById(courseId: string, options?: any): AxiosPromise { + return localVarFp.courseControllerGetCourseCcMetadataById(courseId, options).then((request) => request(axios, basePath)); + }, + }; +}; + +/** + * CoursesApi - interface + * @export + * @interface CoursesApi + */ +export interface CoursesApiInterface { + /** + * + * @summary Create a new course. + * @param {CreateCourseBodyParams} createCourseBodyParams + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof CoursesApiInterface + */ + courseControllerCreateCourse(createCourseBodyParams: CreateCourseBodyParams, options?: RawAxiosRequestConfig): AxiosPromise; + + /** + * + * @summary Get common cartridge metadata of a course by Id. + * @param {string} courseId The id of the course + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof CoursesApiInterface + */ + courseControllerGetCourseCcMetadataById(courseId: string, options?: RawAxiosRequestConfig): AxiosPromise; + +} + +/** + * CoursesApi - object-oriented interface + * @export + * @class CoursesApi + * @extends {BaseAPI} + */ +export class CoursesApi extends BaseAPI implements CoursesApiInterface { + /** + * + * @summary Create a new course. + * @param {CreateCourseBodyParams} createCourseBodyParams + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof CoursesApi + */ + public courseControllerCreateCourse(createCourseBodyParams: CreateCourseBodyParams, options?: RawAxiosRequestConfig) { + return CoursesApiFp(this.configuration).courseControllerCreateCourse(createCourseBodyParams, options).then((request) => request(this.axios, this.basePath)); + } + + /** + * + * @summary Get common cartridge metadata of a course by Id. + * @param {string} courseId The id of the course + * @param {*} [options] Override http request option. + * @throws {RequiredError} + * @memberof CoursesApi + */ + public courseControllerGetCourseCcMetadataById(courseId: string, options?: RawAxiosRequestConfig) { + return CoursesApiFp(this.configuration).courseControllerGetCourseCcMetadataById(courseId, options).then((request) => request(this.axios, this.basePath)); + } +} + diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/base.ts b/apps/server/src/infra/courses-client/generated/base.ts similarity index 95% rename from apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/base.ts rename to apps/server/src/infra/courses-client/generated/base.ts index 82686c7b81b..5bcf014a72f 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/base.ts +++ b/apps/server/src/infra/courses-client/generated/base.ts @@ -19,7 +19,7 @@ import type { Configuration } from './configuration'; import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios'; import globalAxios from 'axios'; -export const BASE_PATH = "http://localhost/api/v3".replace(/\/+$/, ""); +export const BASE_PATH = "http://localhost:3030/api/v3".replace(/\/+$/, ""); /** * diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/common.ts b/apps/server/src/infra/courses-client/generated/common.ts similarity index 100% rename from apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/common.ts rename to apps/server/src/infra/courses-client/generated/common.ts diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/configuration.ts b/apps/server/src/infra/courses-client/generated/configuration.ts similarity index 100% rename from apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/configuration.ts rename to apps/server/src/infra/courses-client/generated/configuration.ts diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/git_push.sh b/apps/server/src/infra/courses-client/generated/git_push.sh similarity index 100% rename from apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/git_push.sh rename to apps/server/src/infra/courses-client/generated/git_push.sh diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/index.ts b/apps/server/src/infra/courses-client/generated/index.ts similarity index 100% rename from apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/index.ts rename to apps/server/src/infra/courses-client/generated/index.ts diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/models/course-common-cartridge-metadata-response.ts b/apps/server/src/infra/courses-client/generated/models/course-common-cartridge-metadata-response.ts similarity index 100% rename from apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/models/course-common-cartridge-metadata-response.ts rename to apps/server/src/infra/courses-client/generated/models/course-common-cartridge-metadata-response.ts diff --git a/apps/server/src/infra/courses-client/generated/models/create-course-body-params.ts b/apps/server/src/infra/courses-client/generated/models/create-course-body-params.ts new file mode 100644 index 00000000000..7c27ac50eae --- /dev/null +++ b/apps/server/src/infra/courses-client/generated/models/create-course-body-params.ts @@ -0,0 +1,30 @@ +/* tslint:disable */ +/* eslint-disable */ +/** + * Schulcloud-Verbund-Software Server API + * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. + * + * The version of the OpenAPI document: 3.0 + * + * + * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). + * https://openapi-generator.tech + * Do not edit the class manually. + */ + + + +/** + * + * @export + * @interface CreateCourseBodyParams + */ +export interface CreateCourseBodyParams { + /** + * The title of the course + * @type {string} + * @memberof CreateCourseBodyParams + */ + 'title': string; +} + diff --git a/apps/server/src/infra/courses-client/generated/models/index.ts b/apps/server/src/infra/courses-client/generated/models/index.ts new file mode 100644 index 00000000000..3a4d2661a7c --- /dev/null +++ b/apps/server/src/infra/courses-client/generated/models/index.ts @@ -0,0 +1,2 @@ +export * from './course-common-cartridge-metadata-response'; +export * from './create-course-body-params'; diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/index.ts b/apps/server/src/infra/courses-client/index.ts similarity index 66% rename from apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/index.ts rename to apps/server/src/infra/courses-client/index.ts index 501ca215cc8..d49321c824e 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/index.ts +++ b/apps/server/src/infra/courses-client/index.ts @@ -1,4 +1,4 @@ -export { CoursesClientModule } from './courses-client.module'; -export { CoursesClientAdapter } from './courses-client.adapter'; -export { CoursesClientConfig } from './courses-client.config'; -export { CourseCommonCartridgeMetadataDto } from './dto/course-common-cartridge-metadata.dto'; +export { CoursesClientAdapter } from './courses-client.adapter'; +export { CoursesClientConfig } from './courses-client.config'; +export { CoursesClientModule } from './courses-client.module'; +export type * from './generated/models'; diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-api.module.ts b/apps/server/src/modules/common-cartridge/common-cartridge-api.module.ts index 317f705ec7a..11dab56459a 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge-api.module.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge-api.module.ts @@ -1,14 +1,24 @@ +import { AuthGuardModule, AuthGuardOptions } from '@infra/auth-guard'; +import { AuthorizationClientModule } from '@infra/authorization-client'; import { HttpModule } from '@nestjs/axios'; import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { createConfigModuleOptions } from '@src/config'; import { CoreModule } from '@src/core'; +import { authorizationClientConfig } from '../files-storage/files-storage.config'; import { config } from './common-cartridge.config'; import { CommonCartridgeModule } from './common-cartridge.module'; import { CommonCartridgeController } from './controller/common-cartridge.controller'; @Module({ - imports: [CoreModule, HttpModule, ConfigModule.forRoot(createConfigModuleOptions(config)), CommonCartridgeModule], + imports: [ + CoreModule, + HttpModule, + AuthorizationClientModule.register(authorizationClientConfig), + AuthGuardModule.register([AuthGuardOptions.JWT]), + ConfigModule.forRoot(createConfigModuleOptions(config)), + CommonCartridgeModule, + ], controllers: [CommonCartridgeController], }) export class CommonCartridgeApiModule {} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/api/courses-api.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/api/courses-api.ts deleted file mode 100644 index 5cb342b5acc..00000000000 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/api/courses-api.ts +++ /dev/null @@ -1,611 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Schulcloud-Verbund-Software Server API - * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. - * - * The version of the OpenAPI document: 3.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - -import type { Configuration } from '../configuration'; -import type { AxiosPromise, AxiosInstance, RawAxiosRequestConfig } from 'axios'; -import globalAxios from 'axios'; -// Some imports not used depending on template conditions -// @ts-ignore -import { DUMMY_BASE_URL, assertParamExists, setApiKeyToObject, setBasicAuthToObject, setBearerAuthToObject, setOAuthToObject, setSearchParams, serializeDataIfNeeded, toPathString, createRequestFunction } from '../common'; -// @ts-ignore -import { BASE_PATH, COLLECTION_FORMATS, type RequestArgs, BaseAPI, RequiredError, operationServerMap } from '../base'; -// @ts-ignore -import type { CourseCommonCartridgeMetadataResponse } from '../models'; -// @ts-ignore -import type { CourseExportBodyParams } from '../models'; -// @ts-ignore -import type { CourseMetadataListResponse } from '../models'; -/** - * CoursesApi - axios parameter creator - * @export - */ -export const CoursesApiAxiosParamCreator = function (configuration?: Configuration) { - return { - /** - * - * @param {string} courseId The id of the course - * @param {CourseControllerExportCourseVersion} version The version of CC export - * @param {CourseExportBodyParams} courseExportBodyParams - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - courseControllerExportCourse: async (courseId: string, version: CourseControllerExportCourseVersion, courseExportBodyParams: CourseExportBodyParams, options: RawAxiosRequestConfig = {}): Promise => { - // verify required parameter 'courseId' is not null or undefined - assertParamExists('courseControllerExportCourse', 'courseId', courseId) - // verify required parameter 'version' is not null or undefined - assertParamExists('courseControllerExportCourse', 'version', version) - // verify required parameter 'courseExportBodyParams' is not null or undefined - assertParamExists('courseControllerExportCourse', 'courseExportBodyParams', courseExportBodyParams) - const localVarPath = `/courses/{courseId}/export` - .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))); - // 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: 'POST', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - if (version !== undefined) { - localVarQueryParameter['version'] = version; - } - - - - localVarHeaderParameter['Content-Type'] = 'application/json'; - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = serializeDataIfNeeded(courseExportBodyParams, localVarRequestOptions, configuration) - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, - /** - * - * @param {number} [skip] Number of elements (not pages) to be skipped - * @param {number} [limit] Page limit, defaults to 10. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - courseControllerFindForUser: async (skip?: number, limit?: number, options: RawAxiosRequestConfig = {}): Promise => { - const localVarPath = `/courses`; - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - if (skip !== undefined) { - localVarQueryParameter['skip'] = skip; - } - - if (limit !== undefined) { - localVarQueryParameter['limit'] = limit; - } - - - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, - /** - * - * @summary Get common cartridge metadata of a course by Id. - * @param {string} courseId The id of the course - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - courseControllerGetCourseCcMetadataById: async (courseId: string, options: RawAxiosRequestConfig = {}): Promise => { - // verify required parameter 'courseId' is not null or undefined - assertParamExists('courseControllerGetCourseCcMetadataById', 'courseId', courseId) - const localVarPath = `/courses/{courseId}/cc-metadata` - .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))); - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, - /** - * - * @summary Get permissions for a user in a course. - * @param {string} courseId The id of the course - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - courseControllerGetUserPermissions: async (courseId: string, options: RawAxiosRequestConfig = {}): Promise => { - // verify required parameter 'courseId' is not null or undefined - assertParamExists('courseControllerGetUserPermissions', 'courseId', courseId) - const localVarPath = `/courses/{courseId}/user-permissions` - .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))); - // use dummy base URL string because the URL constructor only accepts absolute URLs. - const localVarUrlObj = new URL(localVarPath, DUMMY_BASE_URL); - let baseOptions; - if (configuration) { - baseOptions = configuration.baseOptions; - } - - const localVarRequestOptions = { method: 'GET', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, - /** - * - * @summary Imports a course from a Common Cartridge file. - * @param {File} file The Common Cartridge file to import. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - courseControllerImportCourse: async (file: File, options: RawAxiosRequestConfig = {}): Promise => { - // verify required parameter 'file' is not null or undefined - assertParamExists('courseControllerImportCourse', 'file', file) - const localVarPath = `/courses/import`; - // 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: 'POST', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - const localVarFormParams = new ((configuration && configuration.formDataCtor) || FormData)(); - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - - if (file !== undefined) { - localVarFormParams.append('file', file as any); - } - - - localVarHeaderParameter['Content-Type'] = 'multipart/form-data'; - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - localVarRequestOptions.data = localVarFormParams; - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, - /** - * - * @summary Stop the synchronization of a course with a group. - * @param {string} courseId The id of the course - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - courseControllerStopSynchronization: async (courseId: string, options: RawAxiosRequestConfig = {}): Promise => { - // verify required parameter 'courseId' is not null or undefined - assertParamExists('courseControllerStopSynchronization', 'courseId', courseId) - const localVarPath = `/courses/{courseId}/stop-sync` - .replace(`{${"courseId"}}`, encodeURIComponent(String(courseId))); - // 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: 'POST', ...baseOptions, ...options}; - const localVarHeaderParameter = {} as any; - const localVarQueryParameter = {} as any; - - // authentication bearer required - // http bearer authentication required - await setBearerAuthToObject(localVarHeaderParameter, configuration) - - - - setSearchParams(localVarUrlObj, localVarQueryParameter); - let headersFromBaseOptions = baseOptions && baseOptions.headers ? baseOptions.headers : {}; - localVarRequestOptions.headers = {...localVarHeaderParameter, ...headersFromBaseOptions, ...options.headers}; - - return { - url: toPathString(localVarUrlObj), - options: localVarRequestOptions, - }; - }, - } -}; - -/** - * CoursesApi - functional programming interface - * @export - */ -export const CoursesApiFp = function(configuration?: Configuration) { - const localVarAxiosParamCreator = CoursesApiAxiosParamCreator(configuration) - return { - /** - * - * @param {string} courseId The id of the course - * @param {CourseControllerExportCourseVersion} version The version of CC export - * @param {CourseExportBodyParams} courseExportBodyParams - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async courseControllerExportCourse(courseId: string, version: CourseControllerExportCourseVersion, courseExportBodyParams: CourseExportBodyParams, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.courseControllerExportCourse(courseId, version, courseExportBodyParams, options); - const localVarOperationServerIndex = configuration?.serverIndex ?? 0; - const localVarOperationServerBasePath = operationServerMap['CoursesApi.courseControllerExportCourse']?.[localVarOperationServerIndex]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); - }, - /** - * - * @param {number} [skip] Number of elements (not pages) to be skipped - * @param {number} [limit] Page limit, defaults to 10. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async courseControllerFindForUser(skip?: number, limit?: number, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.courseControllerFindForUser(skip, limit, options); - const localVarOperationServerIndex = configuration?.serverIndex ?? 0; - const localVarOperationServerBasePath = operationServerMap['CoursesApi.courseControllerFindForUser']?.[localVarOperationServerIndex]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); - }, - /** - * - * @summary Get common cartridge metadata of a course by Id. - * @param {string} courseId The id of the course - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async courseControllerGetCourseCcMetadataById(courseId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.courseControllerGetCourseCcMetadataById(courseId, options); - const localVarOperationServerIndex = configuration?.serverIndex ?? 0; - const localVarOperationServerBasePath = operationServerMap['CoursesApi.courseControllerGetCourseCcMetadataById']?.[localVarOperationServerIndex]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); - }, - /** - * - * @summary Get permissions for a user in a course. - * @param {string} courseId The id of the course - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async courseControllerGetUserPermissions(courseId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.courseControllerGetUserPermissions(courseId, options); - const localVarOperationServerIndex = configuration?.serverIndex ?? 0; - const localVarOperationServerBasePath = operationServerMap['CoursesApi.courseControllerGetUserPermissions']?.[localVarOperationServerIndex]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); - }, - /** - * - * @summary Imports a course from a Common Cartridge file. - * @param {File} file The Common Cartridge file to import. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async courseControllerImportCourse(file: File, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.courseControllerImportCourse(file, options); - const localVarOperationServerIndex = configuration?.serverIndex ?? 0; - const localVarOperationServerBasePath = operationServerMap['CoursesApi.courseControllerImportCourse']?.[localVarOperationServerIndex]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); - }, - /** - * - * @summary Stop the synchronization of a course with a group. - * @param {string} courseId The id of the course - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - async courseControllerStopSynchronization(courseId: string, options?: RawAxiosRequestConfig): Promise<(axios?: AxiosInstance, basePath?: string) => AxiosPromise> { - const localVarAxiosArgs = await localVarAxiosParamCreator.courseControllerStopSynchronization(courseId, options); - const localVarOperationServerIndex = configuration?.serverIndex ?? 0; - const localVarOperationServerBasePath = operationServerMap['CoursesApi.courseControllerStopSynchronization']?.[localVarOperationServerIndex]?.url; - return (axios, basePath) => createRequestFunction(localVarAxiosArgs, globalAxios, BASE_PATH, configuration)(axios, localVarOperationServerBasePath || basePath); - }, - } -}; - -/** - * CoursesApi - factory interface - * @export - */ -export const CoursesApiFactory = function (configuration?: Configuration, basePath?: string, axios?: AxiosInstance) { - const localVarFp = CoursesApiFp(configuration) - return { - /** - * - * @param {string} courseId The id of the course - * @param {CourseControllerExportCourseVersion} version The version of CC export - * @param {CourseExportBodyParams} courseExportBodyParams - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - courseControllerExportCourse(courseId: string, version: CourseControllerExportCourseVersion, courseExportBodyParams: CourseExportBodyParams, options?: any): AxiosPromise { - return localVarFp.courseControllerExportCourse(courseId, version, courseExportBodyParams, options).then((request) => request(axios, basePath)); - }, - /** - * - * @param {number} [skip] Number of elements (not pages) to be skipped - * @param {number} [limit] Page limit, defaults to 10. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - courseControllerFindForUser(skip?: number, limit?: number, options?: any): AxiosPromise { - return localVarFp.courseControllerFindForUser(skip, limit, options).then((request) => request(axios, basePath)); - }, - /** - * - * @summary Get common cartridge metadata of a course by Id. - * @param {string} courseId The id of the course - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - courseControllerGetCourseCcMetadataById(courseId: string, options?: any): AxiosPromise { - return localVarFp.courseControllerGetCourseCcMetadataById(courseId, options).then((request) => request(axios, basePath)); - }, - /** - * - * @summary Get permissions for a user in a course. - * @param {string} courseId The id of the course - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - courseControllerGetUserPermissions(courseId: string, options?: any): AxiosPromise { - return localVarFp.courseControllerGetUserPermissions(courseId, options).then((request) => request(axios, basePath)); - }, - /** - * - * @summary Imports a course from a Common Cartridge file. - * @param {File} file The Common Cartridge file to import. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - courseControllerImportCourse(file: File, options?: any): AxiosPromise { - return localVarFp.courseControllerImportCourse(file, options).then((request) => request(axios, basePath)); - }, - /** - * - * @summary Stop the synchronization of a course with a group. - * @param {string} courseId The id of the course - * @param {*} [options] Override http request option. - * @throws {RequiredError} - */ - courseControllerStopSynchronization(courseId: string, options?: any): AxiosPromise { - return localVarFp.courseControllerStopSynchronization(courseId, options).then((request) => request(axios, basePath)); - }, - }; -}; - -/** - * CoursesApi - interface - * @export - * @interface CoursesApi - */ -export interface CoursesApiInterface { - /** - * - * @param {string} courseId The id of the course - * @param {CourseControllerExportCourseVersion} version The version of CC export - * @param {CourseExportBodyParams} courseExportBodyParams - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof CoursesApiInterface - */ - courseControllerExportCourse(courseId: string, version: CourseControllerExportCourseVersion, courseExportBodyParams: CourseExportBodyParams, options?: RawAxiosRequestConfig): AxiosPromise; - - /** - * - * @param {number} [skip] Number of elements (not pages) to be skipped - * @param {number} [limit] Page limit, defaults to 10. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof CoursesApiInterface - */ - courseControllerFindForUser(skip?: number, limit?: number, options?: RawAxiosRequestConfig): AxiosPromise; - - /** - * - * @summary Get common cartridge metadata of a course by Id. - * @param {string} courseId The id of the course - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof CoursesApiInterface - */ - courseControllerGetCourseCcMetadataById(courseId: string, options?: RawAxiosRequestConfig): AxiosPromise; - - /** - * - * @summary Get permissions for a user in a course. - * @param {string} courseId The id of the course - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof CoursesApiInterface - */ - courseControllerGetUserPermissions(courseId: string, options?: RawAxiosRequestConfig): AxiosPromise; - - /** - * - * @summary Imports a course from a Common Cartridge file. - * @param {File} file The Common Cartridge file to import. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof CoursesApiInterface - */ - courseControllerImportCourse(file: File, options?: RawAxiosRequestConfig): AxiosPromise; - - /** - * - * @summary Stop the synchronization of a course with a group. - * @param {string} courseId The id of the course - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof CoursesApiInterface - */ - courseControllerStopSynchronization(courseId: string, options?: RawAxiosRequestConfig): AxiosPromise; - -} - -/** - * CoursesApi - object-oriented interface - * @export - * @class CoursesApi - * @extends {BaseAPI} - */ -export class CoursesApi extends BaseAPI implements CoursesApiInterface { - /** - * - * @param {string} courseId The id of the course - * @param {CourseControllerExportCourseVersion} version The version of CC export - * @param {CourseExportBodyParams} courseExportBodyParams - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof CoursesApi - */ - public courseControllerExportCourse(courseId: string, version: CourseControllerExportCourseVersion, courseExportBodyParams: CourseExportBodyParams, options?: RawAxiosRequestConfig) { - return CoursesApiFp(this.configuration).courseControllerExportCourse(courseId, version, courseExportBodyParams, options).then((request) => request(this.axios, this.basePath)); - } - - /** - * - * @param {number} [skip] Number of elements (not pages) to be skipped - * @param {number} [limit] Page limit, defaults to 10. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof CoursesApi - */ - public courseControllerFindForUser(skip?: number, limit?: number, options?: RawAxiosRequestConfig) { - return CoursesApiFp(this.configuration).courseControllerFindForUser(skip, limit, options).then((request) => request(this.axios, this.basePath)); - } - - /** - * - * @summary Get common cartridge metadata of a course by Id. - * @param {string} courseId The id of the course - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof CoursesApi - */ - public courseControllerGetCourseCcMetadataById(courseId: string, options?: RawAxiosRequestConfig) { - return CoursesApiFp(this.configuration).courseControllerGetCourseCcMetadataById(courseId, options).then((request) => request(this.axios, this.basePath)); - } - - /** - * - * @summary Get permissions for a user in a course. - * @param {string} courseId The id of the course - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof CoursesApi - */ - public courseControllerGetUserPermissions(courseId: string, options?: RawAxiosRequestConfig) { - return CoursesApiFp(this.configuration).courseControllerGetUserPermissions(courseId, options).then((request) => request(this.axios, this.basePath)); - } - - /** - * - * @summary Imports a course from a Common Cartridge file. - * @param {File} file The Common Cartridge file to import. - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof CoursesApi - */ - public courseControllerImportCourse(file: File, options?: RawAxiosRequestConfig) { - return CoursesApiFp(this.configuration).courseControllerImportCourse(file, options).then((request) => request(this.axios, this.basePath)); - } - - /** - * - * @summary Stop the synchronization of a course with a group. - * @param {string} courseId The id of the course - * @param {*} [options] Override http request option. - * @throws {RequiredError} - * @memberof CoursesApi - */ - public courseControllerStopSynchronization(courseId: string, options?: RawAxiosRequestConfig) { - return CoursesApiFp(this.configuration).courseControllerStopSynchronization(courseId, options).then((request) => request(this.axios, this.basePath)); - } -} - -/** - * @export - */ -export const CourseControllerExportCourseVersion = { - _0_0: '1.0.0', - _1_0: '1.1.0', - _2_0: '1.2.0', - _3_0: '1.3.0', - _4_0: '1.4.0' -} as const; -export type CourseControllerExportCourseVersion = typeof CourseControllerExportCourseVersion[keyof typeof CourseControllerExportCourseVersion]; diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/models/course-export-body-params.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/models/course-export-body-params.ts deleted file mode 100644 index 7fe921f621c..00000000000 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/models/course-export-body-params.ts +++ /dev/null @@ -1,42 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Schulcloud-Verbund-Software Server API - * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. - * - * The version of the OpenAPI document: 3.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - - -/** - * - * @export - * @interface CourseExportBodyParams - */ -export interface CourseExportBodyParams { - /** - * The list of ids of topics which should be exported. If empty no topics are exported. - * @type {Array} - * @memberof CourseExportBodyParams - */ - 'topics': Array; - /** - * The list of ids of tasks which should be exported. If empty no tasks are exported. - * @type {Array} - * @memberof CourseExportBodyParams - */ - 'tasks': Array; - /** - * The list of ids of column boards which should be exported. If empty no column boards are exported. - * @type {Array} - * @memberof CourseExportBodyParams - */ - 'columnBoards': Array; -} - diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/models/course-metadata-list-response.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/models/course-metadata-list-response.ts deleted file mode 100644 index 489f5be83ea..00000000000 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/models/course-metadata-list-response.ts +++ /dev/null @@ -1,51 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Schulcloud-Verbund-Software Server API - * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. - * - * The version of the OpenAPI document: 3.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - -// May contain unused imports in some cases -// @ts-ignore -import type { CourseMetadataResponse } from './course-metadata-response'; - -/** - * - * @export - * @interface CourseMetadataListResponse - */ -export interface CourseMetadataListResponse { - /** - * The items for the current page. - * @type {Array} - * @memberof CourseMetadataListResponse - */ - 'data': Array; - /** - * The total amount of items. - * @type {number} - * @memberof CourseMetadataListResponse - */ - 'total': number; - /** - * The amount of items skipped from the start. - * @type {number} - * @memberof CourseMetadataListResponse - */ - 'skip': number; - /** - * The page size of the response. - * @type {number} - * @memberof CourseMetadataListResponse - */ - 'limit': number; -} - diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/models/course-metadata-response.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/models/course-metadata-response.ts deleted file mode 100644 index 0ba6eacd5a7..00000000000 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/models/course-metadata-response.ts +++ /dev/null @@ -1,66 +0,0 @@ -/* tslint:disable */ -/* eslint-disable */ -/** - * Schulcloud-Verbund-Software Server API - * This is v3 of Schulcloud-Verbund-Software Server. Checkout /docs for v1. - * - * The version of the OpenAPI document: 3.0 - * - * - * NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech). - * https://openapi-generator.tech - * Do not edit the class manually. - */ - - - -/** - * - * @export - * @interface CourseMetadataResponse - */ -export interface CourseMetadataResponse { - /** - * The id of the Grid element - * @type {string} - * @memberof CourseMetadataResponse - */ - 'id': string; - /** - * Title of the Grid element - * @type {string} - * @memberof CourseMetadataResponse - */ - 'title': string; - /** - * Short title of the Grid element - * @type {string} - * @memberof CourseMetadataResponse - */ - 'shortTitle': string; - /** - * Color of the Grid element - * @type {string} - * @memberof CourseMetadataResponse - */ - 'displayColor': string; - /** - * Start date of the course - * @type {string} - * @memberof CourseMetadataResponse - */ - 'startDate'?: string; - /** - * End date of the course. After this the course counts as archived - * @type {string} - * @memberof CourseMetadataResponse - */ - 'untilDate'?: string; - /** - * Start of the copying process if it is still ongoing - otherwise property is not set. - * @type {string} - * @memberof CourseMetadataResponse - */ - 'copyingSince'?: string; -} - diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/models/index.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/models/index.ts deleted file mode 100644 index 2e4b620054f..00000000000 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/course-api-client/models/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export * from './course-common-cartridge-metadata-response'; -export * from './course-export-body-params'; -export * from './course-metadata-list-response'; -export * from './course-metadata-response'; diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.adapter.spec.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.adapter.spec.ts deleted file mode 100644 index 058a7517f98..00000000000 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.adapter.spec.ts +++ /dev/null @@ -1,103 +0,0 @@ -import { Test, TestingModule } from '@nestjs/testing'; -import { createMock, DeepMocked } from '@golevelup/ts-jest'; -import { REQUEST } from '@nestjs/core'; -import { Request } from 'express'; -import { faker } from '@faker-js/faker'; -import { AxiosResponse } from 'axios'; -import { CoursesClientAdapter } from './courses-client.adapter'; -import { CourseCommonCartridgeMetadataResponse, CoursesApi } from './course-api-client'; - -const jwtToken = 'dummyJwtToken'; - -describe(CoursesClientAdapter.name, () => { - let module: TestingModule; - let service: CoursesClientAdapter; - let coursesApi: DeepMocked; - - beforeAll(async () => { - module = await Test.createTestingModule({ - providers: [ - CoursesClientAdapter, - { - provide: CoursesApi, - useValue: createMock(), - }, - { - provide: REQUEST, - useValue: createMock({ - headers: { - authorization: `Bearer ${jwtToken}`, - }, - }), - }, - ], - }).compile(); - - service = module.get(CoursesClientAdapter); - coursesApi = module.get(CoursesApi); - }); - - afterAll(async () => { - await module.close(); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should be defined', () => { - expect(service).toBeDefined(); - }); - - describe('getCourseCommonCartridgeMetadata', () => { - describe('when getCourseCommonCartridgeMetadata is called', () => { - const setup = () => { - const courseId = faker.string.uuid(); - const response = createMock>({ - data: { - id: faker.string.uuid(), - title: faker.lorem.word(), - creationDate: faker.date.recent().toString(), - copyRightOwners: [faker.lorem.word(), faker.lorem.word()], - }, - }); - - coursesApi.courseControllerGetCourseCcMetadataById.mockResolvedValueOnce(response); - return { courseId }; - }; - it('should return course common cartridge metadata', async () => { - const { courseId } = setup(); - - const expectedOptions = { headers: { authorization: `Bearer ${jwtToken}` } }; - - const result = await service.getCourseCommonCartridgeMetadata(courseId); - - expect(coursesApi.courseControllerGetCourseCcMetadataById).toHaveBeenCalledWith(courseId, expectedOptions); - expect(result.id).toBeDefined(); - expect(result.title).toBeDefined(); - expect(result.creationDate).toBeDefined(); - expect(result.copyRightOwners).toBeDefined(); - }); - }); - }); - - describe('when no JWT token is found', () => { - const setup = () => { - const courseId = faker.string.uuid(); - const error = new Error('Authentication is required.'); - const request = createMock({ - headers: {}, - }); - - const adapter: CoursesClientAdapter = new CoursesClientAdapter(coursesApi, request); - - return { error, courseId, adapter }; - }; - - it('should throw an Error', async () => { - const { error, courseId, adapter } = setup(); - - await expect(adapter.getCourseCommonCartridgeMetadata(courseId)).rejects.toThrowError(error); - }); - }); -}); diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.adapter.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.adapter.ts deleted file mode 100644 index 324f907f06f..00000000000 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.adapter.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { Inject, Injectable, UnauthorizedException } from '@nestjs/common'; -import { REQUEST } from '@nestjs/core'; -import { extractJwtFromHeader } from '@shared/common'; -import { RawAxiosRequestConfig } from 'axios'; -import { Request } from 'express'; -import { CourseCommonCartridgeMetadataDto } from './dto/course-common-cartridge-metadata.dto'; -import { CoursesApi } from './course-api-client'; - -@Injectable() -export class CoursesClientAdapter { - constructor(private readonly coursesApi: CoursesApi, @Inject(REQUEST) private request: Request) {} - - public async getCourseCommonCartridgeMetadata(courseId: string): Promise { - const options = this.createOptionParams(); - const response = await this.coursesApi.courseControllerGetCourseCcMetadataById(courseId, options); - const courseCommonCartridgeMetadata: CourseCommonCartridgeMetadataDto = new CourseCommonCartridgeMetadataDto({ - id: response.data.id, - title: response.data.title, - creationDate: response.data.creationDate, - copyRightOwners: response.data.copyRightOwners, - }); - - return courseCommonCartridgeMetadata; - } - - private createOptionParams(): RawAxiosRequestConfig { - const jwt = this.getJwt(); - const options: RawAxiosRequestConfig = { headers: { authorization: `Bearer ${jwt}` } }; - - return options; - } - - private getJwt(): string { - const jwt = extractJwtFromHeader(this.request) || this.request.headers.authorization; - - if (!jwt) { - throw new UnauthorizedException('Authentication is required.'); - } - - return jwt; - } -} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.config.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.config.ts deleted file mode 100644 index 94f86ce2560..00000000000 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.config.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { ConfigurationParameters } from './course-api-client'; - -export interface CoursesClientConfig extends ConfigurationParameters { - basePath?: string; -} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.module.spec.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.module.spec.ts deleted file mode 100644 index 515b77ac509..00000000000 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.module.spec.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { TestingModule, Test } from '@nestjs/testing'; -import { CoursesClientModule } from './courses-client.module'; -import { CoursesClientAdapter } from './courses-client.adapter'; - -describe('CommonCartridgeClientModule', () => { - let module: TestingModule; - - beforeAll(async () => { - module = await Test.createTestingModule({ - imports: [ - CoursesClientModule.register({ - basePath: 'http://localhost:3000', - }), - ], - }).compile(); - }); - - afterAll(async () => { - await module.close(); - }); - - describe('when module is initialized', () => { - it('should be defined', () => { - const coursesClientAdapter = module.get(CoursesClientAdapter); - - expect(coursesClientAdapter).toBeDefined(); - }); - }); -}); diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.module.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.module.ts deleted file mode 100644 index 039d58d11ca..00000000000 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/courses-client.module.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { DynamicModule, Module } from '@nestjs/common'; -import { CoursesClientAdapter } from './courses-client.adapter'; -import { Configuration, CoursesApi } from './course-api-client'; -import { CoursesClientConfig } from './courses-client.config'; - -@Module({}) -export class CoursesClientModule { - static register(config: CoursesClientConfig): DynamicModule { - const providers = [ - CoursesClientAdapter, - { - provide: CoursesApi, - useFactory: () => { - const configuration = new Configuration(config); - return new CoursesApi(configuration); - }, - }, - ]; - - return { - module: CoursesClientModule, - providers, - exports: [CoursesClientAdapter], - }; - } -} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/dto/course-common-cartridge-metadata.dto.ts b/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/dto/course-common-cartridge-metadata.dto.ts deleted file mode 100644 index 117963823ca..00000000000 --- a/apps/server/src/modules/common-cartridge/common-cartridge-client/course-client/dto/course-common-cartridge-metadata.dto.ts +++ /dev/null @@ -1,16 +0,0 @@ -export class CourseCommonCartridgeMetadataDto { - id: string; - - title: string; - - creationDate?: string; - - copyRightOwners: Array; - - constructor(props: CourseCommonCartridgeMetadataDto) { - this.id = props.id; - this.title = props.title; - this.creationDate = props.creationDate; - this.copyRightOwners = props.copyRightOwners; - } -} diff --git a/apps/server/src/modules/common-cartridge/common-cartridge.config.spec.ts b/apps/server/src/modules/common-cartridge/common-cartridge.config.spec.ts index 2e15af59e9e..7946ab03a32 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge.config.spec.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge.config.spec.ts @@ -8,6 +8,9 @@ describe('commonCartridgeConfig', () => { expect(result).toStrictEqual({ NEST_LOG_LEVEL: expect.any(String), INCOMING_REQUEST_TIMEOUT: expect.any(Number), + FEATURE_COMMON_CARTRIDGE_COURSE_IMPORT_MAX_FILE_SIZE: expect.any(Number), + JWT_PUBLIC_KEY: expect.any(String), + JWT_SIGNING_ALGORITHM: expect.any(String) as unknown as Algorithm, }); }); }); diff --git a/apps/server/src/modules/common-cartridge/common-cartridge.config.ts b/apps/server/src/modules/common-cartridge/common-cartridge.config.ts index 724e2c558e0..bd8783e922f 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge.config.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge.config.ts @@ -3,11 +3,19 @@ import { Configuration } from '@hpi-schul-cloud/commons'; export interface CommonCartridgeConfig { NEST_LOG_LEVEL: string; INCOMING_REQUEST_TIMEOUT: number; + FEATURE_COMMON_CARTRIDGE_COURSE_IMPORT_MAX_FILE_SIZE: number; + JWT_PUBLIC_KEY: string; + JWT_SIGNING_ALGORITHM: Algorithm; } const commonCartridgeConfig: CommonCartridgeConfig = { NEST_LOG_LEVEL: Configuration.get('NEST_LOG_LEVEL') as string, INCOMING_REQUEST_TIMEOUT: Configuration.get('FILES_STORAGE__INCOMING_REQUEST_TIMEOUT') as number, + JWT_PUBLIC_KEY: (Configuration.get('JWT_PUBLIC_KEY') as string).replace(/\\n/g, '\n'), + JWT_SIGNING_ALGORITHM: Configuration.get('JWT_SIGNING_ALGORITHM') as Algorithm, + FEATURE_COMMON_CARTRIDGE_COURSE_IMPORT_MAX_FILE_SIZE: Configuration.get( + 'FEATURE_COMMON_CARTRIDGE_COURSE_IMPORT_MAX_FILE_SIZE' + ) as number, }; export function config(): CommonCartridgeConfig { diff --git a/apps/server/src/modules/common-cartridge/common-cartridge.module.ts b/apps/server/src/modules/common-cartridge/common-cartridge.module.ts index c5beb84df62..8463e2dc3e8 100644 --- a/apps/server/src/modules/common-cartridge/common-cartridge.module.ts +++ b/apps/server/src/modules/common-cartridge/common-cartridge.module.ts @@ -1,23 +1,24 @@ import { Configuration } from '@hpi-schul-cloud/commons'; +import { CoursesClientModule } from '@infra/courses-client'; import { MikroOrmModule } from '@mikro-orm/nestjs'; import { FilesStorageClientModule } from '@modules/files-storage-client'; import { Module } from '@nestjs/common'; +import { defaultMikroOrmOptions } from '@shared/common/defaultMikroOrmOptions'; import { ALL_ENTITIES } from '@shared/domain/entity'; import { DB_PASSWORD, DB_URL, DB_USERNAME } from '@src/config'; import { RabbitMQWrapperModule } from '@src/infra/rabbitmq'; -import { defaultMikroOrmOptions } from '@shared/common/defaultMikroOrmOptions'; import { BoardClientModule } from './common-cartridge-client/board-client'; -import { CoursesClientModule } from './common-cartridge-client/course-client'; -import { CommonCartridgeExportService } from './service/common-cartridge-export.service'; -import { CommonCartridgeUc } from './uc/common-cartridge.uc'; -import { CourseRoomsModule } from './common-cartridge-client/room-client'; import { CardClientModule } from './common-cartridge-client/card-client/card-client.module'; import { LessonClientModule } from './common-cartridge-client/lesson-client/lesson-client.module'; +import { CourseRoomsModule } from './common-cartridge-client/room-client'; +import { CommonCartridgeExportService, CommonCartridgeImportService } from './service'; +import { CommonCartridgeUc } from './uc/common-cartridge.uc'; @Module({ imports: [ RabbitMQWrapperModule, FilesStorageClientModule, + CoursesClientModule, MikroOrmModule.forRoot({ ...defaultMikroOrmOptions, type: 'mongo', @@ -32,18 +33,14 @@ import { LessonClientModule } from './common-cartridge-client/lesson-client/less CourseRoomsModule.register({ basePath: `${Configuration.get('API_HOST') as string}/v3/`, }), - CardClientModule.register({ basePath: `${Configuration.get('API_HOST') as string}/v3/`, }), - CoursesClientModule.register({ - basePath: `${Configuration.get('API_HOST') as string}/v3/`, - }), LessonClientModule.register({ basePath: `${Configuration.get('API_HOST') as string}/v3/`, }), ], - providers: [CommonCartridgeUc, CommonCartridgeExportService], + providers: [CommonCartridgeUc, CommonCartridgeExportService, CommonCartridgeImportService], exports: [CommonCartridgeUc], }) export class CommonCartridgeModule {} diff --git a/apps/server/src/modules/common-cartridge/controller/common-cartridge.controller.api.spec.ts b/apps/server/src/modules/common-cartridge/controller/common-cartridge.controller.api.spec.ts new file mode 100644 index 00000000000..bb2fa34132d --- /dev/null +++ b/apps/server/src/modules/common-cartridge/controller/common-cartridge.controller.api.spec.ts @@ -0,0 +1,98 @@ +import { faker } from '@faker-js/faker'; +import { createMock } from '@golevelup/ts-jest'; +import { ICurrentUser } from '@infra/auth-guard'; +import { INestApplication } from '@nestjs/common'; +import { ConfigModule } from '@nestjs/config'; +import { Test, TestingModule } from '@nestjs/testing'; +import { axiosResponseFactory } from '@shared/testing'; +import { CoursesApi } from '@src/infra/courses-client/generated'; +import supertest from 'supertest'; +import { CommonCartridgeApiModule } from '../common-cartridge-api.module'; +import { CommonCartridgeFileBuilder } from '../export/builders/common-cartridge-file-builder'; +import { CommonCartridgeElementType, CommonCartridgeVersion } from '../export/common-cartridge.enums'; + +jest.mock('../../../infra/courses-client/generated/api/courses-api', () => { + const coursesApiMock = createMock(); + + coursesApiMock.courseControllerCreateCourse.mockResolvedValue(axiosResponseFactory.build({ status: 201 })); + + return { + CoursesApi: jest.fn(() => coursesApiMock), + }; +}); +jest.mock('../../../infra/auth-guard/decorator/jwt-auth.decorator', () => { + return { + CurrentUser: () => jest.fn(() => createMock()), + JwtAuthentication: () => jest.fn(), + }; +}); + +describe('CommonCartridgeController (API)', () => { + let module: TestingModule; + let app: INestApplication; + + beforeAll(async () => { + module = await Test.createTestingModule({ + imports: [ + CommonCartridgeApiModule, + ConfigModule.forRoot({ + isGlobal: true, + load: [ + () => { + return { + SC_DOMAIN: faker.internet.url(), + API_HOST: faker.internet.url(), + JWT_PUBLIC_KEY: faker.string.alphanumeric(42), + JWT_SIGNING_ALGORITHM: 'RS256', + INCOMING_REQUEST_TIMEOUT: 10_000, + NEST_LOG_LEVEL: 'error', + FEATURE_COMMON_CARTRIDGE_COURSE_IMPORT_MAX_FILE_SIZE: 10_000, + }; + }, + ], + }), + ], + }).compile(); + app = module.createNestApplication(); + + await app.init(); + }); + + afterAll(async () => { + await module.close(); + }); + + describe('importCourse', () => { + const setup = () => { + const ccBuilder = new CommonCartridgeFileBuilder({ + identifier: 'course-1', + version: CommonCartridgeVersion.V_1_1_0, + }); + + ccBuilder.addMetadata({ + type: CommonCartridgeElementType.METADATA, + title: 'Course 1', + creationDate: new Date(), + copyrightOwners: ['Teacher 1'], + }); + + const ccFile = ccBuilder.build(); + + return { + ccFile, + }; + }; + + it('should import a course from a Common Cartridge file', async () => { + const { ccFile } = setup(); + + const response = await supertest(app.getHttpServer()) + .post('/common-cartridge/import') + .set('Authorization', `Bearer ${faker.string.alphanumeric(42)}`) + .set('Content-Type', 'application/octet-stream') + .attach('file', ccFile, 'course-1.zip'); + + expect(response.status).toBe(201); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/controller/common-cartridge.controller.spec.ts b/apps/server/src/modules/common-cartridge/controller/common-cartridge.controller.spec.ts index a274eff5f7c..b8c235083dc 100644 --- a/apps/server/src/modules/common-cartridge/controller/common-cartridge.controller.spec.ts +++ b/apps/server/src/modules/common-cartridge/controller/common-cartridge.controller.spec.ts @@ -1,5 +1,6 @@ import { faker } from '@faker-js/faker'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { ConfigModule } from '@nestjs/config'; import { Test, TestingModule } from '@nestjs/testing'; import { CommonCartridgeUc } from '../uc/common-cartridge.uc'; import { CommonCartridgeController } from './common-cartridge.controller'; @@ -13,6 +14,16 @@ describe('CommonCartridgeController', () => { beforeAll(async () => { module = await Test.createTestingModule({ + imports: [ + ConfigModule.forRoot({ + isGlobal: true, + load: [ + () => { + return { FEATURE_COMMON_CARTRIDGE_COURSE_IMPORT_MAX_FILE_SIZE: 10_000 }; + }, + ], + }), + ], controllers: [CommonCartridgeController], providers: [ { @@ -44,6 +55,7 @@ describe('CommonCartridgeController', () => { id: courseId, title: faker.lorem.sentence(), copyRightOwners: [faker.lorem.words()], + creationDate: faker.date.recent().toISOString(), }, }); diff --git a/apps/server/src/modules/common-cartridge/controller/common-cartridge.controller.ts b/apps/server/src/modules/common-cartridge/controller/common-cartridge.controller.ts index bd609c4ff88..344cc6c9cc1 100644 --- a/apps/server/src/modules/common-cartridge/controller/common-cartridge.controller.ts +++ b/apps/server/src/modules/common-cartridge/controller/common-cartridge.controller.ts @@ -1,9 +1,23 @@ -import { Controller, Get, Param } from '@nestjs/common'; -import { ApiTags } from '@nestjs/swagger'; +import { CurrentUser, ICurrentUser, JwtAuthentication } from '@infra/auth-guard'; +import { Controller, Get, Param, Post, UploadedFile, UseInterceptors } from '@nestjs/common'; +import { FileInterceptor } from '@nestjs/platform-express'; +import { + ApiBadRequestResponse, + ApiBody, + ApiConsumes, + ApiCreatedResponse, + ApiInternalServerErrorResponse, + ApiOperation, + ApiProduces, + ApiTags, + ApiUnauthorizedResponse, +} from '@nestjs/swagger'; import { CommonCartridgeUc } from '../uc/common-cartridge.uc'; -import { ExportCourseParams } from './dto'; +import { CommonCartridgeImportBodyParams, ExportCourseParams } from './dto'; import { CourseExportBodyResponse } from './dto/course-export-body.response'; +import { CommonCartridgeFileValidatorPipe } from './utils'; +@JwtAuthentication() @ApiTags('common-cartridge') @Controller('common-cartridge') export class CommonCartridgeController { @@ -11,6 +25,26 @@ export class CommonCartridgeController { @Get('export/:parentId') public async exportCourse(@Param() exportCourseParams: ExportCourseParams): Promise { - return this.commonCartridgeUC.exportCourse(exportCourseParams.parentId); + const response = await this.commonCartridgeUC.exportCourse(exportCourseParams.parentId); + + return response; + } + + @Post('import') + @UseInterceptors(FileInterceptor('file')) + @ApiOperation({ summary: 'Imports a course from a Common Cartridge file.' }) + @ApiConsumes('application/octet-stream') + @ApiProduces('application/json') + @ApiBody({ type: CommonCartridgeImportBodyParams, required: true }) + @ApiCreatedResponse({ description: 'Course was successfully imported.' }) + @ApiUnauthorizedResponse({ description: 'Request is unauthorized.' }) + @ApiBadRequestResponse({ description: 'Request data has invalid format.' }) + @ApiInternalServerErrorResponse({ description: 'Internal server error.' }) + public async importCourse( + @CurrentUser() currentUser: ICurrentUser, + @UploadedFile(CommonCartridgeFileValidatorPipe) + file: Express.Multer.File + ): Promise { + await this.commonCartridgeUC.importCourse(file.buffer); } } diff --git a/apps/server/src/modules/common-cartridge/controller/dto/common-cartridge-import-body.params.ts b/apps/server/src/modules/common-cartridge/controller/dto/common-cartridge-import-body.params.ts new file mode 100644 index 00000000000..e49c1edc129 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/controller/dto/common-cartridge-import-body.params.ts @@ -0,0 +1,11 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class CommonCartridgeImportBodyParams { + @ApiProperty({ + type: String, + format: 'binary', + required: true, + description: 'The Common Cartridge file to import.', + }) + public file!: Express.Multer.File; +} diff --git a/apps/server/src/modules/common-cartridge/controller/dto/course-export-body.response.ts b/apps/server/src/modules/common-cartridge/controller/dto/course-export-body.response.ts index 9709cbff7d1..82a51725034 100644 --- a/apps/server/src/modules/common-cartridge/controller/dto/course-export-body.response.ts +++ b/apps/server/src/modules/common-cartridge/controller/dto/course-export-body.response.ts @@ -1,12 +1,12 @@ -import { CourseCommonCartridgeMetadataDto } from '../../common-cartridge-client/course-client'; +import { CourseCommonCartridgeMetadataResponse } from '@infra/courses-client'; import { CourseFileIdsResponse } from './common-cartridge.response'; export class CourseExportBodyResponse { - courseFileIds?: CourseFileIdsResponse; + public courseFileIds?: CourseFileIdsResponse; - courseCommonCartridgeMetadata: CourseCommonCartridgeMetadataDto; + public courseCommonCartridgeMetadata: CourseCommonCartridgeMetadataResponse; - constructor(courseExportBodyResponse: CourseExportBodyResponse) { + constructor(courseExportBodyResponse: Readonly) { this.courseFileIds = courseExportBodyResponse.courseFileIds; this.courseCommonCartridgeMetadata = courseExportBodyResponse.courseCommonCartridgeMetadata; } diff --git a/apps/server/src/modules/common-cartridge/controller/dto/index.ts b/apps/server/src/modules/common-cartridge/controller/dto/index.ts index e93173f89f7..f6440953265 100644 --- a/apps/server/src/modules/common-cartridge/controller/dto/index.ts +++ b/apps/server/src/modules/common-cartridge/controller/dto/index.ts @@ -1,2 +1,3 @@ +export { CommonCartridgeImportBodyParams } from './common-cartridge-import-body.params'; export * from './common-cartridge.params'; export * from './common-cartridge.response'; diff --git a/apps/server/src/modules/learnroom/utils/common-cartridge-file-validator.pipe.spec.ts b/apps/server/src/modules/common-cartridge/controller/utils/common-cartridge-file-validator.pipe.spec.ts similarity index 100% rename from apps/server/src/modules/learnroom/utils/common-cartridge-file-validator.pipe.spec.ts rename to apps/server/src/modules/common-cartridge/controller/utils/common-cartridge-file-validator.pipe.spec.ts diff --git a/apps/server/src/modules/learnroom/utils/common-cartridge-file-validator.pipe.ts b/apps/server/src/modules/common-cartridge/controller/utils/common-cartridge-file-validator.pipe.ts similarity index 84% rename from apps/server/src/modules/learnroom/utils/common-cartridge-file-validator.pipe.ts rename to apps/server/src/modules/common-cartridge/controller/utils/common-cartridge-file-validator.pipe.ts index b72d27f0932..18557abb151 100644 --- a/apps/server/src/modules/learnroom/utils/common-cartridge-file-validator.pipe.ts +++ b/apps/server/src/modules/common-cartridge/controller/utils/common-cartridge-file-validator.pipe.ts @@ -1,12 +1,12 @@ import { BadRequestException, Injectable, PipeTransform } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { LearnroomConfig } from '../learnroom.config'; +import { CommonCartridgeConfig } from '../../common-cartridge.config'; @Injectable() export class CommonCartridgeFileValidatorPipe implements PipeTransform { private zipFileMagicNumber = '504b0304'; - constructor(private readonly configService: ConfigService) {} + constructor(private readonly configService: ConfigService) {} public transform(value: Express.Multer.File): Express.Multer.File { this.checkValue(value); diff --git a/apps/server/src/modules/learnroom/utils/index.ts b/apps/server/src/modules/common-cartridge/controller/utils/index.ts similarity index 100% rename from apps/server/src/modules/learnroom/utils/index.ts rename to apps/server/src/modules/common-cartridge/controller/utils/index.ts diff --git a/apps/server/src/modules/common-cartridge/index.ts b/apps/server/src/modules/common-cartridge/index.ts index 2f177a38fb6..1673fae99bb 100644 --- a/apps/server/src/modules/common-cartridge/index.ts +++ b/apps/server/src/modules/common-cartridge/index.ts @@ -1,3 +1,4 @@ +export { CommonCartridgeConfig } from './common-cartridge.config'; export { CommonCartridgeFileBuilder, CommonCartridgeFileBuilderProps, diff --git a/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.spec.ts b/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.spec.ts index f6d1066f5b0..7df015c74b2 100644 --- a/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.spec.ts +++ b/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.spec.ts @@ -1,15 +1,15 @@ import { faker } from '@faker-js/faker'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; +import { CoursesClientAdapter } from '@infra/courses-client'; import { FilesStorageClientAdapterService } from '@modules/files-storage-client'; import { Test, TestingModule } from '@nestjs/testing'; import { BoardClientAdapter } from '../common-cartridge-client/board-client'; -import { CommonCartridgeExportService } from './common-cartridge-export.service'; -import { CoursesClientAdapter } from '../common-cartridge-client/course-client'; -import { CourseRoomsClientAdapter } from '../common-cartridge-client/room-client'; import { CardClientAdapter } from '../common-cartridge-client/card-client/card-client.adapter'; import { CardListResponseDto } from '../common-cartridge-client/card-client/dto/card-list-response.dto'; import { CardResponseDto } from '../common-cartridge-client/card-client/dto/card-response.dto'; import { ContentElementType } from '../common-cartridge-client/card-client/enums/content-element-type.enum'; +import { CourseRoomsClientAdapter } from '../common-cartridge-client/room-client'; +import { CommonCartridgeExportService } from './common-cartridge-export.service'; describe('CommonCartridgeExportService', () => { let module: TestingModule; @@ -87,6 +87,7 @@ describe('CommonCartridgeExportService', () => { id: courseId, title: faker.lorem.sentence(), copyRightOwners: [faker.lorem.word()], + creationDate: faker.date.recent().toString(), }; coursesClientAdapterMock.getCourseCommonCartridgeMetadata.mockResolvedValue(expected); diff --git a/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.ts b/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.ts index 05895643bf1..35b984b7ff9 100644 --- a/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.ts +++ b/apps/server/src/modules/common-cartridge/service/common-cartridge-export.service.ts @@ -1,11 +1,11 @@ +import { CourseCommonCartridgeMetadataResponse, CoursesClientAdapter } from '@infra/courses-client'; import { FileDto, FilesStorageClientAdapterService } from '@modules/files-storage-client'; import { Injectable } from '@nestjs/common'; import { BoardClientAdapter } from '../common-cartridge-client/board-client'; -import { CourseCommonCartridgeMetadataDto, CoursesClientAdapter } from '../common-cartridge-client/course-client'; -import { CourseRoomsClientAdapter } from '../common-cartridge-client/room-client'; -import { RoomBoardDto } from '../common-cartridge-client/room-client/dto/room-board.dto'; import { CardClientAdapter } from '../common-cartridge-client/card-client/card-client.adapter'; import { CardListResponseDto } from '../common-cartridge-client/card-client/dto/card-list-response.dto'; +import { CourseRoomsClientAdapter } from '../common-cartridge-client/room-client'; +import { RoomBoardDto } from '../common-cartridge-client/room-client/dto/room-board.dto'; @Injectable() export class CommonCartridgeExportService { @@ -23,7 +23,7 @@ export class CommonCartridgeExportService { return courseFiles; } - public async findCourseCommonCartridgeMetadata(courseId: string): Promise { + public async findCourseCommonCartridgeMetadata(courseId: string): Promise { const courseCommonCartridgeMetadata = await this.coursesClientAdapter.getCourseCommonCartridgeMetadata(courseId); return courseCommonCartridgeMetadata; diff --git a/apps/server/src/modules/common-cartridge/service/common-cartridge-import.service.spec.ts b/apps/server/src/modules/common-cartridge/service/common-cartridge-import.service.spec.ts new file mode 100644 index 00000000000..fb771065ccb --- /dev/null +++ b/apps/server/src/modules/common-cartridge/service/common-cartridge-import.service.spec.ts @@ -0,0 +1,59 @@ +import { faker } from '@faker-js/faker'; +import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { CoursesClientAdapter } from '@infra/courses-client'; +import { Test, TestingModule } from '@nestjs/testing'; +import type { CommonCartridgeFileParser } from '../import/common-cartridge-file-parser'; +import { CommonCartridgeImportService } from './common-cartridge-import.service'; + +jest.mock('../import/common-cartridge-file-parser', () => { + const fileParserMock = createMock(); + + fileParserMock.getTitle.mockReturnValue(faker.lorem.words()); + + return { + CommonCartridgeFileParser: jest.fn(() => fileParserMock), + }; +}); + +describe(CommonCartridgeImportService.name, () => { + let module: TestingModule; + let sut: CommonCartridgeImportService; + let coursesClientAdapterMock: DeepMocked; + + beforeEach(async () => { + module = await Test.createTestingModule({ + providers: [ + CommonCartridgeImportService, + { + provide: CoursesClientAdapter, + useValue: createMock(), + }, + ], + }).compile(); + + sut = module.get(CommonCartridgeImportService); + coursesClientAdapterMock = module.get(CoursesClientAdapter); + }); + + afterEach(async () => { + await module.close(); + }); + + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(sut).toBeDefined(); + }); + + describe('importFile', () => { + describe('when importing a file', () => { + it('should create a course', async () => { + await sut.importFile(Buffer.from('')); + + expect(coursesClientAdapterMock.createCourse).toHaveBeenCalledWith({ title: expect.any(String) }); + }); + }); + }); +}); diff --git a/apps/server/src/modules/common-cartridge/service/common-cartridge-import.service.ts b/apps/server/src/modules/common-cartridge/service/common-cartridge-import.service.ts new file mode 100644 index 00000000000..be14e00382b --- /dev/null +++ b/apps/server/src/modules/common-cartridge/service/common-cartridge-import.service.ts @@ -0,0 +1,21 @@ +import { CoursesClientAdapter } from '@infra/courses-client'; +import { Injectable } from '@nestjs/common'; +import { CommonCartridgeFileParser } from '../import/common-cartridge-file-parser'; +import { DEFAULT_FILE_PARSER_OPTIONS } from '../import/common-cartridge-import.types'; + +@Injectable() +export class CommonCartridgeImportService { + constructor(private readonly coursesClient: CoursesClientAdapter) {} + + public async importFile(file: Buffer): Promise { + const parser = new CommonCartridgeFileParser(file, DEFAULT_FILE_PARSER_OPTIONS); + + await this.createCourse(parser); + } + + private async createCourse(parser: CommonCartridgeFileParser): Promise { + const courseName = parser.getTitle() || 'Untitled Course'; + + await this.coursesClient.createCourse({ title: courseName }); + } +} diff --git a/apps/server/src/modules/common-cartridge/service/index.ts b/apps/server/src/modules/common-cartridge/service/index.ts new file mode 100644 index 00000000000..a84597c07d4 --- /dev/null +++ b/apps/server/src/modules/common-cartridge/service/index.ts @@ -0,0 +1,2 @@ +export { CommonCartridgeExportService } from './common-cartridge-export.service'; +export { CommonCartridgeImportService } from './common-cartridge-import.service'; diff --git a/apps/server/src/modules/common-cartridge/uc/common-cartridge.uc.spec.ts b/apps/server/src/modules/common-cartridge/uc/common-cartridge.uc.spec.ts index 8fc9bcba92b..170e30ea09f 100644 --- a/apps/server/src/modules/common-cartridge/uc/common-cartridge.uc.spec.ts +++ b/apps/server/src/modules/common-cartridge/uc/common-cartridge.uc.spec.ts @@ -2,14 +2,16 @@ import { faker } from '@faker-js/faker'; import { DeepMocked, createMock } from '@golevelup/ts-jest'; import { Test, TestingModule } from '@nestjs/testing'; import { CourseFileIdsResponse } from '../controller/dto'; +import { CourseExportBodyResponse } from '../controller/dto/course-export-body.response'; import { CommonCartridgeExportService } from '../service/common-cartridge-export.service'; +import { CommonCartridgeImportService } from '../service/common-cartridge-import.service'; import { CommonCartridgeUc } from './common-cartridge.uc'; -import { CourseExportBodyResponse } from '../controller/dto/course-export-body.response'; describe('CommonCartridgeUc', () => { let module: TestingModule; let sut: CommonCartridgeUc; let commonCartridgeExportServiceMock: DeepMocked; + let commonCartridgeImportServiceMock: DeepMocked; beforeAll(async () => { module = await Test.createTestingModule({ @@ -19,11 +21,16 @@ describe('CommonCartridgeUc', () => { provide: CommonCartridgeExportService, useValue: createMock(), }, + { + provide: CommonCartridgeImportService, + useValue: createMock(), + }, ], }).compile(); sut = module.get(CommonCartridgeUc); commonCartridgeExportServiceMock = module.get(CommonCartridgeExportService); + commonCartridgeImportServiceMock = module.get(CommonCartridgeImportService); }); afterAll(async () => { @@ -43,15 +50,14 @@ describe('CommonCartridgeUc', () => { id: courseId, title: faker.lorem.sentence(), copyRightOwners: [], + creationDate: faker.date.recent().toDateString(), }, }); commonCartridgeExportServiceMock.findCourseFileRecords.mockResolvedValue([]); - commonCartridgeExportServiceMock.findCourseCommonCartridgeMetadata.mockResolvedValue({ - id: expected.courseCommonCartridgeMetadata?.id ?? '', - title: expected.courseCommonCartridgeMetadata?.title ?? '', - copyRightOwners: expected.courseCommonCartridgeMetadata?.copyRightOwners ?? [], - }); + commonCartridgeExportServiceMock.findCourseCommonCartridgeMetadata.mockResolvedValue( + expected.courseCommonCartridgeMetadata + ); return { courseId, expected }; }; @@ -64,4 +70,20 @@ describe('CommonCartridgeUc', () => { expect(result).toEqual(expected); }); }); + + describe('importCourse', () => { + const setup = () => { + const file = Buffer.from(faker.lorem.paragraphs()); + + return { file }; + }; + + it('should class the import service', async () => { + const { file } = setup(); + + await sut.importCourse(file); + + expect(commonCartridgeImportServiceMock.importFile).toHaveBeenCalledWith(file); + }); + }); }); diff --git a/apps/server/src/modules/common-cartridge/uc/common-cartridge.uc.ts b/apps/server/src/modules/common-cartridge/uc/common-cartridge.uc.ts index 8caa9381633..56517960c10 100644 --- a/apps/server/src/modules/common-cartridge/uc/common-cartridge.uc.ts +++ b/apps/server/src/modules/common-cartridge/uc/common-cartridge.uc.ts @@ -1,19 +1,21 @@ import { Injectable } from '@nestjs/common'; import { EntityId } from '@shared/domain/types'; import { CourseFileIdsResponse } from '../controller/dto'; -import { CommonCartridgeExportService } from '../service/common-cartridge-export.service'; import { CourseExportBodyResponse } from '../controller/dto/course-export-body.response'; -import { CourseCommonCartridgeMetadataDto } from '../common-cartridge-client/course-client'; +import { CommonCartridgeImportService } from '../service'; +import { CommonCartridgeExportService } from '../service/common-cartridge-export.service'; @Injectable() export class CommonCartridgeUc { - constructor(private readonly exportService: CommonCartridgeExportService) {} + constructor( + private readonly exportService: CommonCartridgeExportService, + private readonly importService: CommonCartridgeImportService + ) {} public async exportCourse(courseId: EntityId): Promise { const files = await this.exportService.findCourseFileRecords(courseId); const courseFileIds = new CourseFileIdsResponse(files.map((file) => file.id)); - const courseCommonCartridgeMetadata: CourseCommonCartridgeMetadataDto = - await this.exportService.findCourseCommonCartridgeMetadata(courseId); + const courseCommonCartridgeMetadata = await this.exportService.findCourseCommonCartridgeMetadata(courseId); const response = new CourseExportBodyResponse({ courseFileIds, @@ -22,4 +24,8 @@ export class CommonCartridgeUc { return response; } + + public async importCourse(file: Buffer): Promise { + await this.importService.importFile(file); + } } diff --git a/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts b/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts index ef10c434469..3069fde5af1 100644 --- a/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts +++ b/apps/server/src/modules/learnroom/controller/api-test/course.api.spec.ts @@ -1,7 +1,7 @@ import { faker } from '@faker-js/faker/locale/af_ZA'; import { EntityManager } from '@mikro-orm/mongodb'; import { ServerTestModule } from '@modules/server/server.module'; -import { HttpStatus, INestApplication, StreamableFile } from '@nestjs/common'; +import { HttpStatus, INestApplication } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { Course as CourseEntity } from '@shared/domain/entity'; import { Permission } from '@shared/domain/interface'; @@ -24,6 +24,7 @@ const createTeacher = () => { const { teacherAccount, teacherUser } = UserAndAccountTestFactory.buildTeacher({}, [ Permission.COURSE_VIEW, Permission.COURSE_EDIT, + Permission.COURSE_CREATE, ]); return { account: teacherAccount, user: teacherUser }; }; @@ -96,42 +97,6 @@ describe('Course Controller (API)', () => { }); }); - describe('[POST] /courses/:id/export', () => { - const setup = async () => { - const student1 = createStudent(); - const student2 = createStudent(); - const teacher = createTeacher(); - const substitutionTeacher = createTeacher(); - const teacherUnknownToCourse = createTeacher(); - const course = courseFactory.build({ - teachers: [teacher.user], - students: [student1.user, student2.user], - }); - - await em.persistAndFlush([teacher.account, teacher.user, course]); - em.clear(); - - const loggedInClient = await testApiClient.login(teacher.account); - - return { course, teacher, teacherUnknownToCourse, substitutionTeacher, student1, loggedInClient }; - }; - - it('should find course export', async () => { - const { course, loggedInClient } = await setup(); - - const body = { topics: [faker.string.uuid()], tasks: [faker.string.uuid()], columnBoards: [faker.string.uuid()] }; - const response = await loggedInClient.post(`${course.id}/export?version=1.1.0`, body); - - expect(response.statusCode).toEqual(201); - const file = response.body as StreamableFile; - expect(file).toBeDefined(); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(response.header['content-type']).toBe('application/zip'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access - expect(response.header['content-disposition']).toBe('attachment;'); - }); - }); - describe('[POST] /courses/import', () => { const setup = async () => { const teacher = createTeacher(); @@ -413,4 +378,26 @@ describe('Course Controller (API)', () => { expect(data.id).toBe(course.id); }); }); + + describe('[POST] /courses', () => { + const setup = async () => { + const teacher = createTeacher(); + const course = courseFactory.build(); + + await em.persistAndFlush([teacher.account, teacher.user]); + em.clear(); + + const loggedInClient = await testApiClient.login(teacher.account); + + return { loggedInClient, course }; + }; + + it('should create course', async () => { + const { loggedInClient } = await setup(); + + const response = await loggedInClient.post().send({ title: faker.lorem.words() }); + + expect(response.statusCode).toEqual(201); + }); + }); }); diff --git a/apps/server/src/modules/learnroom/controller/course.controller.ts b/apps/server/src/modules/learnroom/controller/course.controller.ts index 8b4854622d5..f7cb1379ee9 100644 --- a/apps/server/src/modules/learnroom/controller/course.controller.ts +++ b/apps/server/src/modules/learnroom/controller/course.controller.ts @@ -1,4 +1,5 @@ import { CurrentUser, ICurrentUser, JwtAuthentication } from '@infra/auth-guard'; +import { CommonCartridgeFileValidatorPipe } from '@modules/common-cartridge/controller/utils'; import { Body, Controller, @@ -8,8 +9,6 @@ import { Param, Post, Query, - Res, - StreamableFile, UploadedFile, UseInterceptors, } from '@nestjs/common'; @@ -22,21 +21,19 @@ import { ApiInternalServerErrorResponse, ApiNoContentResponse, ApiOperation, + ApiProduces, ApiTags, ApiUnprocessableEntityResponse, } from '@nestjs/swagger'; import { PaginationParams } from '@shared/controller/'; -import { Response } from 'express'; import { CourseMapper } from '../mapper/course.mapper'; -import { CourseExportUc, CourseImportUc, CourseSyncUc, CourseUc } from '../uc'; -import { CommonCartridgeFileValidatorPipe } from '../utils'; +import { CourseImportUc, CourseSyncUc, CourseUc } from '../uc'; import { - CourseExportBodyParams, CourseImportBodyParams, CourseMetadataListResponse, - CourseQueryParams, CourseSyncBodyParams, CourseUrlParams, + CreateCourseBodyParams, } from './dto'; import { CourseCommonCartridgeMetadataResponse } from './dto/course-cc-metadata.response'; @@ -46,13 +43,12 @@ import { CourseCommonCartridgeMetadataResponse } from './dto/course-cc-metadata. export class CourseController { constructor( private readonly courseUc: CourseUc, - private readonly courseExportUc: CourseExportUc, private readonly courseImportUc: CourseImportUc, private readonly courseSyncUc: CourseSyncUc ) {} @Get() - async findForUser( + public async findForUser( @CurrentUser() currentUser: ICurrentUser, @Query() pagination: PaginationParams ): Promise { @@ -64,29 +60,15 @@ export class CourseController { return result; } - @Post(':courseId/export') - async exportCourse( - @CurrentUser() currentUser: ICurrentUser, - @Param() urlParams: CourseUrlParams, - @Query() queryParams: CourseQueryParams, - @Body() bodyParams: CourseExportBodyParams, - @Res({ passthrough: true }) response: Response - ): Promise { - const result = await this.courseExportUc.exportCourse( - urlParams.courseId, - currentUser.userId, - queryParams.version, - bodyParams.topics, - bodyParams.tasks, - bodyParams.columnBoards - ); - - response.set({ - 'Content-Type': 'application/zip', - 'Content-Disposition': 'attachment;', - }); - - return new StreamableFile(result); + @Post() + @ApiOperation({ summary: 'Create a new course.' }) + @ApiConsumes('application/json') + @ApiProduces('application/json') + @ApiCreatedResponse({ description: 'Course was successfully created.' }) + @ApiBadRequestResponse({ description: 'Request data has invalid format.' }) + @ApiInternalServerErrorResponse({ description: 'Internal server error.' }) + public async createCourse(@CurrentUser() user: ICurrentUser, @Body() body: CreateCourseBodyParams): Promise { + await this.courseUc.createCourse(user, body.title); } @Post('import') diff --git a/apps/server/src/modules/learnroom/controller/dto/create-course-body.params.ts b/apps/server/src/modules/learnroom/controller/dto/create-course-body.params.ts new file mode 100644 index 00000000000..995065c2f86 --- /dev/null +++ b/apps/server/src/modules/learnroom/controller/dto/create-course-body.params.ts @@ -0,0 +1,12 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { IsString } from 'class-validator'; + +export class CreateCourseBodyParams { + @IsString() + @ApiProperty({ + description: 'The title of the course', + required: true, + nullable: false, + }) + public title!: string; +} diff --git a/apps/server/src/modules/learnroom/controller/dto/index.ts b/apps/server/src/modules/learnroom/controller/dto/index.ts index 756a0f26429..5cf8887ab55 100644 --- a/apps/server/src/modules/learnroom/controller/dto/index.ts +++ b/apps/server/src/modules/learnroom/controller/dto/index.ts @@ -1,7 +1,12 @@ +export * from './course-export.body.params'; export * from './course-import.body.params'; export * from './course-metadata.response'; +export * from './course-room-element.url.params'; +export * from './course-room.url.params'; +export * from './course-sync.body.params'; export * from './course.query.params'; export * from './course.url.params'; +export { CreateCourseBodyParams } from './create-course-body.params'; export * from './dashboard.response'; export * from './dashboard.url.params'; export * from './lesson'; @@ -9,8 +14,4 @@ export * from './move-element.body.params'; export * from './patch-group.params'; export * from './patch-order.params'; export * from './patch-visibility.params'; -export * from './course-room-element.url.params'; -export * from './course-room.url.params'; export * from './single-column-board'; -export * from './course-sync.body.params'; -export * from './course-export.body.params'; diff --git a/apps/server/src/modules/learnroom/learnroom.module.ts b/apps/server/src/modules/learnroom/learnroom.module.ts index c06158e4a38..7f2e1437207 100644 --- a/apps/server/src/modules/learnroom/learnroom.module.ts +++ b/apps/server/src/modules/learnroom/learnroom.module.ts @@ -20,6 +20,7 @@ import { UserRepo, } from '@shared/repo'; import { LoggerModule } from '@src/core/logger'; +import { CommonCartridgeFileValidatorPipe } from '../common-cartridge/controller/utils'; import { COURSE_REPO } from './domain'; import { CommonCartridgeExportMapper } from './mapper/common-cartridge-export.mapper'; import { CommonCartridgeImportMapper } from './mapper/common-cartridge-import.mapper'; @@ -38,7 +39,6 @@ import { DashboardService, GroupDeletedHandlerService, } from './service'; -import { CommonCartridgeFileValidatorPipe } from './utils'; /** * @deprecated - the learnroom module is deprecated and will be removed in the future diff --git a/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts b/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts index c57b7300510..b73e90a5e90 100644 --- a/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts +++ b/apps/server/src/modules/learnroom/service/common-cartridge-import.service.ts @@ -156,7 +156,7 @@ export class CommonCartridgeImportService { column: Column, cardProps: CommonCartridgeImportOrganizationProps, organizations: CommonCartridgeImportOrganizationProps[] - ) { + ): Promise { const card = this.boardNodeFactory.buildCard(); const { title, height } = this.mapper.mapOrganizationToCard(cardProps, true); card.title = title; @@ -176,7 +176,7 @@ export class CommonCartridgeImportService { parser: CommonCartridgeFileParser, card: Card, cardElementProps: CommonCartridgeImportOrganizationProps - ) { + ): Promise { if (cardElementProps.isResource) { const resource = parser.getResource(cardElementProps); const contentElementType = this.mapper.mapResourceTypeToContentElementType(resource?.type); diff --git a/apps/server/src/modules/learnroom/uc/course.uc.spec.ts b/apps/server/src/modules/learnroom/uc/course.uc.spec.ts index 363fffb3c97..b4bc0c86188 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.spec.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.spec.ts @@ -1,3 +1,4 @@ +import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; import { ObjectId } from '@mikro-orm/mongodb'; import { AuthorizationService } from '@modules/authorization'; @@ -5,7 +6,7 @@ import { RoleDto, RoleService } from '@modules/role'; import { Test, TestingModule } from '@nestjs/testing'; import { Permission, RoleName, SortOrder } from '@shared/domain/interface'; import { CourseRepo } from '@shared/repo'; -import { courseFactory, setupEntities, UserAndAccountTestFactory } from '@shared/testing'; +import { courseFactory, currentUserFactory, setupEntities, UserAndAccountTestFactory } from '@shared/testing'; import { CourseService } from '../service'; import { CourseUc } from './course.uc'; @@ -52,6 +53,10 @@ describe('CourseUc', () => { await module.close(); }); + beforeEach(() => { + jest.clearAllMocks(); + }); + describe('findAllByUser', () => { const setup = () => { const courses = courseFactory.buildList(5); @@ -121,4 +126,44 @@ describe('CourseUc', () => { expect(courseService.findById).toHaveBeenCalledWith(course.id); }); }); + + describe('createCourse', () => { + describe('when creating a course', () => { + const setup = () => { + const { teacherUser } = UserAndAccountTestFactory.buildTeacher({}, []); + const currentUser = currentUserFactory.build({ userId: teacherUser.id }); + const courseTitle = faker.lorem.words(); + + return { currentUser, teacherUser, courseTitle }; + }; + + it('should create a course', async () => { + const { currentUser, courseTitle } = setup(); + + await expect(uc.createCourse(currentUser, courseTitle)).resolves.not.toThrow(); + expect(courseService.create).toHaveBeenCalled(); + }); + }); + + describe('when user does not have permission to create a course', () => { + const setup = () => { + const { teacherUser } = UserAndAccountTestFactory.buildTeacher({}, []); + const currentUser = currentUserFactory.build({ userId: teacherUser.id }); + const courseTitle = faker.lorem.words(); + + authorizationService.checkAllPermissions.mockImplementation(() => { + throw new Error('User does not have permission'); + }); + + return { currentUser, teacherUser, courseTitle }; + }; + + it('should throw an error', async () => { + const { currentUser, courseTitle } = setup(); + + await expect(uc.createCourse(currentUser, courseTitle)).rejects.toThrow(); + expect(courseService.create).not.toHaveBeenCalled(); + }); + }); + }); }); diff --git a/apps/server/src/modules/learnroom/uc/course.uc.ts b/apps/server/src/modules/learnroom/uc/course.uc.ts index 5f562c229e5..63ee6944304 100644 --- a/apps/server/src/modules/learnroom/uc/course.uc.ts +++ b/apps/server/src/modules/learnroom/uc/course.uc.ts @@ -3,15 +3,16 @@ import { RoleService } from '@modules/role'; import { Injectable } from '@nestjs/common'; import { PaginationParams } from '@shared/controller/'; import { Course } from '@shared/domain/entity'; -import { SortOrder } from '@shared/domain/interface'; +import { Permission, SortOrder } from '@shared/domain/interface'; import { Counted, EntityId } from '@shared/domain/types'; import { CourseRepo } from '@shared/repo'; +import { ICurrentUser } from '@src/infra/auth-guard'; import { RoleNameMapper } from '../mapper/rolename.mapper'; import { CourseService } from '../service'; @Injectable() export class CourseUc { - public constructor( + constructor( private readonly courseRepo: CourseRepo, private readonly courseService: CourseService, private readonly authService: AuthorizationService, @@ -32,6 +33,18 @@ export class CourseUc { } public async findCourseById(courseId: EntityId): Promise { - return this.courseService.findById(courseId); + const course = await this.courseService.findById(courseId); + + return course; + } + + public async createCourse(currentUser: ICurrentUser, name: string): Promise { + const user = await this.authService.getUserWithPermissions(currentUser.userId); + + this.authService.checkAllPermissions(user, [Permission.COURSE_CREATE]); + + const course = new Course({ teachers: [user], school: user.school, name }); + + await this.courseService.create(course); } } diff --git a/apps/server/src/modules/server/server.config.ts b/apps/server/src/modules/server/server.config.ts index 3c8f9d89708..fe858b67219 100644 --- a/apps/server/src/modules/server/server.config.ts +++ b/apps/server/src/modules/server/server.config.ts @@ -1,5 +1,6 @@ import { Configuration } from '@hpi-schul-cloud/commons'; import { JwtAuthGuardConfig } from '@infra/auth-guard'; +import { CoursesClientConfig } from '@infra/courses-client'; import { EncryptionConfig } from '@infra/encryption/encryption.config'; import type { IdentityManagementConfig } from '@infra/identity-management'; import type { MailConfig } from '@infra/mail/interfaces/mail-config'; @@ -75,7 +76,8 @@ export interface ServerConfig AlertConfig, ShdConfig, OauthConfig, - EncryptionConfig { + EncryptionConfig, + CoursesClientConfig { NODE_ENV: NodeEnvType; SC_DOMAIN: string; HOST: string; @@ -130,6 +132,7 @@ export interface ServerConfig } const config: ServerConfig = { + API_HOST: Configuration.get('API_HOST') as string, ACCESSIBILITY_REPORT_EMAIL: Configuration.get('ACCESSIBILITY_REPORT_EMAIL') as string, ADMIN_TABLES_DISPLAY_CONSENT_COLUMN: Configuration.get('ADMIN_TABLES_DISPLAY_CONSENT_COLUMN') as boolean, ALERT_STATUS_URL: diff --git a/apps/server/src/shared/common/utils/jwt.spec.ts b/apps/server/src/shared/common/utils/jwt.spec.ts index f0d9a74f22f..67d026a8d47 100644 --- a/apps/server/src/shared/common/utils/jwt.spec.ts +++ b/apps/server/src/shared/common/utils/jwt.spec.ts @@ -1,10 +1,13 @@ +import { faker } from '@faker-js/faker'; import { createMock, DeepMocked } from '@golevelup/ts-jest'; +import { UnauthorizedException } from '@nestjs/common'; import { Request } from 'express'; import { JwtFromRequestFunction } from 'passport-jwt'; import { JwtExtractor } from './jwt'; describe('JwtExtractor', () => { let request: DeepMocked; + beforeEach(() => { request = createMock(); }); @@ -39,4 +42,19 @@ describe('JwtExtractor', () => { expect(extractor(request)).toEqual(null); }); }); + + describe('extractJwtFromRequest', () => { + describe('when no token is found', () => { + it('should throw an UnauthorizedException', () => { + expect(() => JwtExtractor.extractJwtFromRequest(request)).toThrow(UnauthorizedException); + }); + }); + + it('should return the token if exists in cookie', () => { + const token = faker.string.alphanumeric(42); + request.headers.authorization = `Bearer ${token}`; + + expect(JwtExtractor.extractJwtFromRequest(request)).toEqual(token); + }); + }); }); diff --git a/apps/server/src/shared/common/utils/jwt.ts b/apps/server/src/shared/common/utils/jwt.ts index ebc589236dc..d5e1f8acaed 100644 --- a/apps/server/src/shared/common/utils/jwt.ts +++ b/apps/server/src/shared/common/utils/jwt.ts @@ -1,9 +1,10 @@ +import { UnauthorizedException } from '@nestjs/common'; +import cookie from 'cookie'; import { Request } from 'express'; import { ExtractJwt, JwtFromRequestFunction } from 'passport-jwt'; -import cookie from 'cookie'; export class JwtExtractor { - static fromCookie(name: string): JwtFromRequestFunction { + public static fromCookie(name: string): JwtFromRequestFunction { return (request: Request) => { let token: string | null = null; const cookies = cookie.parse(request.headers.cookie || ''); @@ -13,6 +14,16 @@ export class JwtExtractor { return token; }; } + + public static extractJwtFromRequest(request: Request): string { + const jwt = ExtractJwt.fromAuthHeaderAsBearerToken()(request); + + if (!jwt) { + throw new UnauthorizedException('No JWT token found'); + } + + return jwt; + } } export const extractJwtFromHeader = ExtractJwt.fromExtractors([ diff --git a/openapitools.json b/openapitools.json index bd567305968..39cf304936d 100644 --- a/openapitools.json +++ b/openapitools.json @@ -27,6 +27,30 @@ "withInterfaces": true, "withSeparateModelsAndApi": true } + }, + "courses-api": { + "generatorName": "typescript-axios", + "inputSpec": "http://localhost:3030/api/v3/docs-json", + "output": "./apps/server/src/infra/courses-client/generated", + "skipValidateSpec": true, + "enablePostProcessFile": true, + "openapiNormalizer": { + "FILTER": "operationId:CourseController_getCourseCcMetadataById|CourseController_createCourse" + }, + "globalProperty": { + "models": "CourseCommonCartridgeMetadataResponse:CreateCourseBodyParams", + "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 21409c40dcb..e599445c9fa 100644 --- a/package.json +++ b/package.json @@ -121,7 +121,9 @@ "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'", "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" + "generate-client:tsp-api": "openapi-generator-cli generate -c ./openapitools.json --generator-key tsp-api", + "pregenerate-client:courses-api": "rimraf ./apps/server/src/infra/courses-client/generated", + "generate-client:courses-api": "openapi-generator-cli generate -c ./openapitools.json --generator-key courses-api" }, "dependencies": { "@aws-sdk/lib-storage": "^3.617.0", @@ -339,4 +341,4 @@ "tsconfig-paths": "^4.1.1", "typescript": "^5.5.4" } -} \ No newline at end of file +} diff --git a/sonar-project.properties b/sonar-project.properties index 68a946b5fac..9ec5f4c132b 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -3,8 +3,8 @@ sonar.projectKey=hpi-schul-cloud_schulcloud-server sonar.sources=. sonar.tests=. sonar.test.inclusions=**/*.spec.ts -sonar.exclusions=**/*.js,jest.config.ts,globalSetup.ts,globalTeardown.ts,**/*.app.ts,**/seed-data/*.ts,**/migrations/mikro-orm/*.ts,**/etherpad-api-client/**/*.ts,**/authorization-api-client/**/*.ts, **/course-api-client/**/*.ts,**/board-api-client/**/*.ts,**/generated/**/*.ts,**/room-api-client/**/*.ts,**/cards-api-client/**/*.ts,**/lessons-api-client/**/*.ts -sonar.coverage.exclusions=**/board-management.uc.ts,**/*.module.ts,**/*.factory.ts,**/migrations/mikro-orm/*.ts,**/globalSetup.ts,**/globalTeardown.ts,**/etherpad-api-client/**/*.ts,**/authorization-api-client/**/*.ts, **/course-api-client/**/*.ts,**/board-api-client/**/*.ts,**/generated/**/*.ts,**/room-api-client/**/*.ts,apps/server/src/console/console.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,**/board-api-client/**/*.ts,**/generated/**/*.ts,**/room-api-client/**/*.ts,**/cards-api-client/**/*.ts,**/lessons-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,**/board-api-client/**/*.ts,**/generated/**/*.ts,**/room-api-client/**/*.ts,apps/server/src/console/console.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