diff --git a/.gitbook/assets/image (1).png b/.gitbook/assets/image (1).png new file mode 100644 index 0000000000000000000000000000000000000000..10febd929a561a618dbc7af89b8498b1c1eca5f4 Binary files /dev/null and b/.gitbook/assets/image (1).png differ diff --git a/.gitbook/assets/image (2).png b/.gitbook/assets/image (2).png new file mode 100644 index 0000000000000000000000000000000000000000..55c6fb389a5a050f8d7922e3def1068d1dd536f3 Binary files /dev/null and b/.gitbook/assets/image (2).png differ diff --git a/.gitbook/assets/image (3).png b/.gitbook/assets/image (3).png new file mode 100644 index 0000000000000000000000000000000000000000..e393f019c58c87a67d27afe489bd21e38d4f524b Binary files /dev/null and b/.gitbook/assets/image (3).png differ diff --git a/.gitbook/assets/image.png b/.gitbook/assets/image.png new file mode 100644 index 0000000000000000000000000000000000000000..10febd929a561a618dbc7af89b8498b1c1eca5f4 Binary files /dev/null and b/.gitbook/assets/image.png differ diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 00c4c7468c568c935a3eb46ec1cb6de75b5adbd8..61453a5d3480f43d8e76c02346bca9ff47d780b1 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -1,4 +1,5 @@ stages: + - test - build - release @@ -8,6 +9,25 @@ services: before_script: - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY +test: + image: node:16.14-alpine + stage: test + only: + refs: + - main + - rc + - beta + - develop + # This matches maintenance branches + - /^(([0-9]+)\.)?([0-9]+)\.x/ + # This matches pre-releases + - /^([0-9]+)\.([0-9]+)\.([0-9]+)(?:-([0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*))?(?:\+[0-9A-Za-z-]+)?$/ + before_script: [] + script: + - cd ./api + - npm ci --development + - npm run test + build: image: docker:dind stage: build diff --git a/README.md b/README.md index 356c61c3384347317f77eb0d6670878d6aba9b63..f7fa352e8a0a07e51cb3badd46d8a4899bbd5b9f 100644 --- a/README.md +++ b/README.md @@ -1,18 +1,16 @@ # ðŸ Home -[](https://github.com/semantic-release/semantic-release) -[](https://gitlab.com/sibmip/gateway/-/commits/main) +[](https://github.com/semantic-release/semantic-release) [](https://gitlab.com/sibmip/gateway/-/commits/main) ## Introduction -The MIP Gateway is a middleware layer between the [MIP Frontend](https://github.com/HBPMedical/portal-frontend) and a federate analytic engine (Exareme, Datashield, FATE, MedCo, etc...). +The MIP Gateway is a middleware layer between the [MIP Frontend](https://github.com/HBPMedical/portal-frontend) and a federate analytic engine (Exareme, Datashield, etc...). ## Contact -* [Manuel Spuhler](https://github.com/nicedexter) (<manuel.spuhler@chuv.ch>) -* [Steve Mendes Reis](https://github.com/M4n0x) (<steve.mendes-reis@chuv.ch>) -## Technical documentation - -Technical documentation can be found at [https://mip-front.gitbook.io/mip-gateway-doc/](https://mip-front.gitbook.io/mip-gateway-doc/) +* [Manuel Spuhler](https://github.com/nicedexter) ([manuel.spuhler@chuv.ch](mailto:manuel.spuhler@chuv.ch)) +* [Steve Mendes Reis](https://github.com/M4n0x) ([steve.mendes-reis@chuv.ch](mailto:steve.mendes-reis@chuv.ch)) +## Technical documentation +Technical documentation can be found at [https://mip-front.gitbook.io/mip-gateway-doc/](https://mip-front.gitbook.io/mip-gateway-doc/) diff --git a/SUMMARY.md b/SUMMARY.md index dd911cc8176892961788e6fdd4e25dfb0db02bb6..ace0f7f0958fff00a51dbb5e6bf408bd874c68ce 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -7,7 +7,13 @@ * [Get Started](docs/for-developers/get-started/README.md) * [Introduction](docs/get-started/Introduction.md) * [Setup development environment](docs/get-started/Setup-development-environment.md) -* [Connector](docs/for-developers/connector/README.md) +* [Configuration](for-developers/configuration/README.md) + * [Gateway](for-developers/configuration/gateway.md) +* [Connectors](docs/for-developers/connector/README.md) * [Parsing response with JSONdata](docs/gateway/Connector/Parsing-response-with-JSONata.md) +* [Gateway](for-developers/gateway/README.md) + * [🔑 Authentication](for-developers/gateway/authentication.md) + * [👥 Users](for-developers/gateway/users.md) * [Frontend](docs/for-developers/frontend/README.md) * [Update GraphQL Queries](docs/frontend/Update-queries-GrahpQL-in-the-frontend.md) + * [📊 Visualisations](for-developers/frontend/visualisations.md) diff --git a/api/docker-compose.yml b/api/docker-compose.yml index dee08d3392e474fc1124072cc287d6c2f342d72d..5a4acef400c6e73f17bde060650479a23a6a4318 100644 --- a/api/docker-compose.yml +++ b/api/docker-compose.yml @@ -1,11 +1,11 @@ services: db: - image: postgres + image: postgres:14-alpine restart: always ports: - "5454:5432" environment: - POSTGRES_PASSWORD: pass123 + POSTGRES_PASSWORD: pass123 volumes: - db-volume:/var/lib/postgres networks: diff --git a/api/src/auth/auth.resolver.spec.ts b/api/src/auth/auth.resolver.spec.ts index ca6c620bddc48e94292a126f95d39452caedc3a6..c109dbe8712230015317033214cd8f2870350c7e 100644 --- a/api/src/auth/auth.resolver.spec.ts +++ b/api/src/auth/auth.resolver.spec.ts @@ -2,11 +2,14 @@ import { getMockRes } from '@jest-mock/express'; import { Test, TestingModule } from '@nestjs/testing'; import { MockFunctionMetadata, ModuleMocker } from 'jest-mock'; import LocalService from '../engine/connectors/local/main.connector'; -import { ENGINE_SERVICE } from '../engine/engine.constants'; +import { + ENGINE_MODULE_OPTIONS, + ENGINE_SERVICE, +} from '../engine/engine.constants'; +import { User } from '../users/models/user.model'; import { authConstants } from './auth-constants'; import { AuthResolver } from './auth.resolver'; import { AuthService } from './auth.service'; -import { User } from '../users/models/user.model'; const moduleMocker = new ModuleMocker(global); @@ -40,6 +43,12 @@ describe('AuthResolver', () => { provide: ENGINE_SERVICE, useClass: LocalService, }, + { + provide: ENGINE_MODULE_OPTIONS, + useValue: { + type: 'DummyConnector', + }, + }, AuthResolver, ], }) @@ -71,7 +80,8 @@ describe('AuthResolver', () => { }); it('logout', () => { - resolver.logout(res, user); + const request: any = jest.fn(); + resolver.logout(request, res, user); expect(mockClearCookie.mock.calls[0][0]).toBe(authConstants.cookie.name); }); diff --git a/api/src/auth/auth.resolver.ts b/api/src/auth/auth.resolver.ts index 228943e65bc8582c5a209e5d815523f1716b98ad..cfa8e714c43004d503eaf016e2f00a11503990d7 100644 --- a/api/src/auth/auth.resolver.ts +++ b/api/src/auth/auth.resolver.ts @@ -6,11 +6,15 @@ import { } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import { Args, Mutation, Resolver } from '@nestjs/graphql'; -import { Response } from 'express'; +import { Response, Request } from 'express'; +import { GQLRequest } from '../common/decorators/gql-request.decoractor'; import { GQLResponse } from '../common/decorators/gql-response.decoractor'; import { parseToBoolean } from '../common/utilities'; -import { ENGINE_SERVICE } from '../engine/engine.constants'; -import { IEngineService } from '../engine/engine.interfaces'; +import { + ENGINE_MODULE_OPTIONS, + ENGINE_SERVICE, +} from '../engine/engine.constants'; +import { IEngineOptions, IEngineService } from '../engine/engine.interfaces'; import { User } from '../users/models/user.model'; import { authConstants } from './auth-constants'; import { AuthService } from './auth.service'; @@ -29,6 +33,8 @@ export class AuthResolver { constructor( @Inject(ENGINE_SERVICE) private readonly engineService: IEngineService, + @Inject(ENGINE_MODULE_OPTIONS) + private readonly engineOptions: IEngineOptions, private readonly authService: AuthService, private readonly configService: ConfigService, ) {} @@ -68,11 +74,23 @@ export class AuthResolver { @Mutation(() => Boolean) @UseGuards(JwtAuthGuard) - logout(@GQLResponse() res: Response, @CurrentUser() user: User): boolean { - if (user) this.logger.verbose(`${user.username} logged out`); + logout( + @GQLRequest() req: Request, + @GQLResponse() res: Response, + @CurrentUser() user: User, + ): boolean { + if (user) { + this.logger.verbose(`${user.username} logged out`); + try { + this.engineService.logout?.(req); + } catch (e) { + this.logger.debug( + `Service ${this.engineOptions.type} produce an error when logging out ${user.username}`, + ); + } + } res.clearCookie(authConstants.cookie.name); - this.engineService.logout?.(); return true; } diff --git a/api/src/config/matomo.config.ts b/api/src/config/matomo.config.ts new file mode 100644 index 0000000000000000000000000000000000000000..5e7fa269a4913509846da9d367fc607954c7ee52 --- /dev/null +++ b/api/src/config/matomo.config.ts @@ -0,0 +1,10 @@ +import { registerAs } from '@nestjs/config'; +import { parseToBoolean } from 'src/common/utilities'; + +export default registerAs('matomo', () => { + return { + enabled: parseToBoolean(process.env.MATOMO_ENABLED, false), + urlBase: process.env.MATOMO_URL || undefined, + siteId: process.env.MATOMO_SITE_ID || undefined, + }; +}); diff --git a/api/src/engine/connectors/datashield/main.connector.ts b/api/src/engine/connectors/datashield/main.connector.ts index ecd37104474f98d05c6d23a49dea928af32e296f..03b4e80b139ff0e5791307d6277309a84096ae3a 100644 --- a/api/src/engine/connectors/datashield/main.connector.ts +++ b/api/src/engine/connectors/datashield/main.connector.ts @@ -195,7 +195,33 @@ export default class DataShieldService implements IEngineService { }; } - async getExperiment(): Promise<Experiment> { + async getExperiment(id: string): Promise<Experiment> { + throw new NotImplementedException(); + } + + async removeExperiment(id: string): Promise<PartialExperiment> { + throw new NotImplementedException(); + } + + async logout(request: Request): Promise<void> { + const user = request.user as User; + const cookie = [`sid=${user.extraFields['sid']}`, `user=${user.id}`].join( + ';', + ); + + const path = new URL('/logout', this.options.baseurl).href; + + this.httpService.get(path, { + headers: { + cookie, + }, + }); + } + + async editExperient( + id: string, + expriment: ExperimentEditInput, + ): Promise<Experiment> { throw new NotImplementedException(); } diff --git a/api/src/engine/connectors/exareme/converters.ts b/api/src/engine/connectors/exareme/converters.ts index 5ca4b79db61a06924af99aebb4e0ca44670de630..56f35ddcd2876a5cd6435cd3454c6fb414034923 100644 --- a/api/src/engine/connectors/exareme/converters.ts +++ b/api/src/engine/connectors/exareme/converters.ts @@ -227,6 +227,14 @@ export const dataToExperiment = ( status: 'error', variables: [], domain: data['domain'] ?? '', + results: [ + { + rawdata: { + type: 'text/plain+error', + data: 'Error when parsing experiment data from the Engine', + }, + }, + ], datasets: [], algorithm: { id: 'unknown', diff --git a/api/src/engine/connectors/exareme/main.connector.ts b/api/src/engine/connectors/exareme/main.connector.ts index 8d25f9ffd2b3eba957ee2bcbe73a7f704841b38a..a87e303490c3338cf2b776a725cf28f74f2a8679 100644 --- a/api/src/engine/connectors/exareme/main.connector.ts +++ b/api/src/engine/connectors/exareme/main.connector.ts @@ -5,6 +5,7 @@ import { HttpStatus, Inject, Injectable, + InternalServerErrorException, } from '@nestjs/common'; import { AxiosRequestConfig } from 'axios'; import { Request } from 'express'; @@ -26,6 +27,7 @@ import { ExperimentEditInput } from 'src/engine/models/experiment/input/experime import { ListExperiments } from 'src/engine/models/experiment/list-experiments.model'; import { Group } from 'src/engine/models/group.model'; import { Variable } from 'src/engine/models/variable.model'; +import { UpdateUserInput } from 'src/users/inputs/update-user.input'; import { User } from 'src/users/models/user.model'; import { transformToUser } from '../datashield/transformations'; import { @@ -45,7 +47,6 @@ type Headers = Record<string, string>; @Injectable() export default class ExaremeService implements IEngineService { - headers = {}; constructor( @Inject(ENGINE_MODULE_OPTIONS) private readonly options: IEngineOptions, private readonly httpService: HttpService, @@ -75,9 +76,7 @@ export default class ExaremeService implements IEngineService { this.options.baseurl + `experiments${isTransient ? '/transient' : ''}`; const resultAPI = await firstValueFrom( - this.post<ExperimentData>(request, path, form, { - headers: this.headers, - }), + this.post<ExperimentData>(request, path, form), ); return dataToExperiment(resultAPI.data); @@ -141,11 +140,7 @@ export default class ExaremeService implements IEngineService { const path = this.options.baseurl + `experiments/${id}`; try { - await firstValueFrom( - this.delete(request, path, { - headers: this.headers, - }), - ); + await firstValueFrom(this.delete(request, path)); return { id: id, }; @@ -158,11 +153,7 @@ export default class ExaremeService implements IEngineService { const path = this.options.baseurl + 'pathologies'; try { - const data = await firstValueFrom( - this.get<Pathology[]>(request, path, { - headers: this.headers, - }), - ); + const data = await firstValueFrom(this.get<Pathology[]>(request, path)); return ( data?.data @@ -192,20 +183,24 @@ export default class ExaremeService implements IEngineService { async getActiveUser(request: Request): Promise<User> { const path = this.options.baseurl + 'activeUser'; - const response = await firstValueFrom(this.get<string>(request, path)); - return transformToUser.evaluate(response.data); + try { + return transformToUser.evaluate(response.data); + } catch (e) { + new InternalServerErrorException('Cannot parse user data from Engine', e); + } } - async updateUser(request: Request): Promise<User> { + async updateUser(request: Request): Promise<UpdateUserInput | undefined> { const path = this.options.baseurl + 'activeUser/agreeNDA'; - - this.post<string>(request, path, request.body).pipe( - map((response) => response.data), + await firstValueFrom( + this.post<string>(request, path, { + agreeNDA: true, + }), ); - return this.getActiveUser(request); + return undefined; //we don't want to manage data locally } getAlgorithmsREST(request: Request): Observable<string> { @@ -256,7 +251,7 @@ export default class ExaremeService implements IEngineService { }; private getHeadersFromRequest(request: Request): Headers { - if (!request || request.headers) return {}; + if (!request || !request.headers) return {}; return request.headers as Headers; } diff --git a/api/src/engine/engine.interfaces.ts b/api/src/engine/engine.interfaces.ts index 2b35528f95632da49beff3e7df1ac5d103adbe03..3df6762aba25a3cb3cb5aedc6b8db07478f5f99e 100644 --- a/api/src/engine/engine.interfaces.ts +++ b/api/src/engine/engine.interfaces.ts @@ -65,7 +65,7 @@ export interface IEngineService { req?: Request, userId?: string, data?: UpdateUserInput, - ): Promise<User>; + ): Promise<UpdateUserInput | undefined>; logout?(req?: Request): Promise<void>; diff --git a/api/src/engine/engine.resolver.ts b/api/src/engine/engine.resolver.ts index 2ea02a19025029f551ea684fa756ca7b842e82d6..6260111702f3268881409f80f5db91cc197c1cba 100644 --- a/api/src/engine/engine.resolver.ts +++ b/api/src/engine/engine.resolver.ts @@ -41,18 +41,20 @@ export class EngineResolver { @Public() configuration(): Configuration { const config = this.engineService.getConfiguration?.(); + const matomo = this.configSerivce.get('matomo'); const data = { ...(config ?? {}), - skipAuth: parseToBoolean( - this.configSerivce.get(authConstants.skipAuth), - true, - ), + connectorId: this.engineOptions.type, skipTos: parseToBoolean(this.configSerivce.get(ENGINE_SKIP_TOS)), enableSSO: parseToBoolean( this.configSerivce.get(authConstants.enableSSO), ), - connectorId: this.engineOptions.type, + skipAuth: parseToBoolean( + this.configSerivce.get(authConstants.skipAuth), + true, + ), + matomo, }; const version = Md5.hashStr(JSON.stringify(data)); diff --git a/api/src/engine/models/configuration.model.ts b/api/src/engine/models/configuration.model.ts index d2d390dc67e6bbd120932a382dc324a0d1e1f677..6ffe779cc55962c0dc0c20fd0e3d47c9e4d73348 100644 --- a/api/src/engine/models/configuration.model.ts +++ b/api/src/engine/models/configuration.model.ts @@ -1,4 +1,5 @@ import { Field, ObjectType } from '@nestjs/graphql'; +import { Matomo } from './configuration/matomo.model'; @ObjectType() export class Configuration { @Field() @@ -21,4 +22,7 @@ export class Configuration { @Field({ nullable: true, defaultValue: true }) enableSSO?: boolean; + + @Field(() => Matomo, { nullable: true }) + matomo?: Matomo; } diff --git a/api/src/engine/models/configuration/matomo.model.ts b/api/src/engine/models/configuration/matomo.model.ts new file mode 100644 index 0000000000000000000000000000000000000000..62782ce3f4b63bfdee7cbf8821578dd04d9a6db5 --- /dev/null +++ b/api/src/engine/models/configuration/matomo.model.ts @@ -0,0 +1,13 @@ +import { Field, ObjectType } from '@nestjs/graphql'; + +@ObjectType() +export class Matomo { + @Field({ nullable: true, defaultValue: false }) + enabled?: boolean; + + @Field({ nullable: true }) + siteId?: string; + + @Field({ nullable: true }) + urlBase?: string; +} diff --git a/api/src/files/files.controller.ts b/api/src/files/files.controller.ts index 91f3f8549eaf05822ed51d456b29dc03e49a5a99..8ca4ba83f0632dcf4c8f9150dbd8ace0469450b6 100644 --- a/api/src/files/files.controller.ts +++ b/api/src/files/files.controller.ts @@ -20,9 +20,10 @@ export class FilesController { @Res() response: Response, @Param('name') filename: string, ) { + const proto = request.headers['x-forwarded-proto'] ?? request.protocol; if (filename.endsWith('.md')) { const baseurl = - request.protocol + + proto + '://' + join(request.get('host'), process.env.BASE_URL_CONTEXT ?? '', 'assets'); // not full url, should consider "/services" const text = this.filesService.getMarkdown(filename, baseurl); diff --git a/api/src/main/app.module.ts b/api/src/main/app.module.ts index 4f784efb09a185d903b603ad1ec5c677afd9c703..ebc1be97cb1be3d02a48c3ec4d561280df249b03 100644 --- a/api/src/main/app.module.ts +++ b/api/src/main/app.module.ts @@ -7,6 +7,7 @@ import { GraphQLError } from 'graphql'; import { join } from 'path'; import { AuthModule } from 'src/auth/auth.module'; import dbConfig from 'src/config/db.config'; +import matomoConfig from 'src/config/matomo.config'; import { EngineModule } from 'src/engine/engine.module'; import { FilesModule } from 'src/files/files.module'; import { UsersModule } from 'src/users/users.module'; @@ -18,7 +19,7 @@ import { AppService } from './app.service'; ConfigModule.forRoot({ isGlobal: true, envFilePath: ['.env', '.env.defaults'], - load: [dbConfig], + load: [dbConfig, matomoConfig], }), GraphQLModule.forRoot<ApolloDriverConfig>({ driver: ApolloDriver, diff --git a/api/src/schema.gql b/api/src/schema.gql index fbb51baa74e16140788371071aaf17ac61a8788e..33142a24cc95cbc543605924b31ff76083003968 100644 --- a/api/src/schema.gql +++ b/api/src/schema.gql @@ -14,6 +14,12 @@ type AuthenticationOutput { accessToken: String! } +type Matomo { + enabled: Boolean + siteId: String + urlBase: String +} + type Configuration { connectorId: String! hasGalaxy: Boolean @@ -22,6 +28,7 @@ type Configuration { skipAuth: Boolean skipTos: Boolean enableSSO: Boolean + matomo: Matomo } type Dataset { diff --git a/api/src/users/users.resolver.spec.ts b/api/src/users/users.resolver.spec.ts index e449f71fb0900a50fc53711d934d8be13fdcc4d1..ad7ccb85c21bfd417f33c76cfb55fec94cbdcb9e 100644 --- a/api/src/users/users.resolver.spec.ts +++ b/api/src/users/users.resolver.spec.ts @@ -51,9 +51,14 @@ describe('UsersResolver', () => { const engineService = { getActiveUser, - updateUser: jest.fn().mockResolvedValue({ ...user, ...updateData }), + updateUser: jest + .fn() + .mockReturnValue(undefined) + .mockResolvedValue(undefined), }; + const updateService = jest.fn().mockResolvedValue({ ...user, ...internUser }); + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [UsersResolver], @@ -62,7 +67,7 @@ describe('UsersResolver', () => { if (token == UsersService) { return { findOne, - update: jest.fn().mockResolvedValue({ ...user, ...internUser }), + update: updateService, }; } if (token == ENGINE_SERVICE) { @@ -107,14 +112,18 @@ describe('UsersResolver', () => { }); it('Update user from engine ', async () => { - expect(await resolver.updateUser(req, updateData, user)).toStrictEqual({ - ...user, - ...updateData, - }); + engineService.updateUser.mockClear(); + updateService.mockClear(); + await resolver.updateUser(req, updateData, user); + expect(engineService.updateUser.mock.calls.length > 0); + expect(updateService.mock.calls.length === 0); }); it('Update user from database', async () => { - engineService.updateUser = undefined; + engineService.updateUser = jest + .fn() + .mockReturnValue(undefined) + .mockResolvedValue(undefined); expect(await resolver.updateUser(req, updateData, user)).toStrictEqual({ ...user, ...internUser, diff --git a/api/src/users/users.resolver.ts b/api/src/users/users.resolver.ts index 13ec63ae73dc528d0b9c1dd1a0425f7854045f4e..ad1fcd0e750bf635ab72bbd9c10a660e77658c18 100644 --- a/api/src/users/users.resolver.ts +++ b/api/src/users/users.resolver.ts @@ -75,10 +75,17 @@ export class UsersResolver { @Args('updateUserInput') updateUserInput: UpdateUserInput, @CurrentUser() user?: User, ) { - if (this.engineService.updateUser) - return this.engineService.updateUser(request, user?.id, updateUserInput); + let updateData: UpdateUserInput | undefined = updateUserInput; + if (this.engineService.updateUser) { + updateData = await this.engineService.updateUser( + request, + user?.id, + updateData, + ); + } - await this.usersService.update(user.id, updateUserInput); + if (updateData && Object.keys(updateData).length > 0) + await this.usersService.update(user.id, updateData); return await this.getUser(request, user); } diff --git a/docs/.gitbook/assets/components.drawio (1).png b/docs/.gitbook/assets/components.drawio (1).png new file mode 100644 index 0000000000000000000000000000000000000000..e701f253b8ba82d311f85d2d9561b4e47b26f25f Binary files /dev/null and b/docs/.gitbook/assets/components.drawio (1).png differ diff --git a/docs/.gitbook/assets/image (1).png b/docs/.gitbook/assets/image (1).png new file mode 100644 index 0000000000000000000000000000000000000000..b60b6b6ba86d70f6ef8ab69b9161f3a1139f05e7 Binary files /dev/null and b/docs/.gitbook/assets/image (1).png differ diff --git a/docs/.gitbook/assets/overview (1).png b/docs/.gitbook/assets/overview (1).png new file mode 100644 index 0000000000000000000000000000000000000000..4845f5180a2520fefda99b715ecdce0f90a2c0c3 Binary files /dev/null and b/docs/.gitbook/assets/overview (1).png differ diff --git a/docs/.gitbook/assets/visualizations (1).png b/docs/.gitbook/assets/visualizations (1).png new file mode 100644 index 0000000000000000000000000000000000000000..d47700dd12f2ff2e2689205c5abe36c05465bea7 Binary files /dev/null and b/docs/.gitbook/assets/visualizations (1).png differ diff --git a/docs/frontend/Update-queries-GrahpQL-in-the-frontend.md b/docs/frontend/Update-queries-GrahpQL-in-the-frontend.md index 7a3c232081626ac7ff8afd6a9be31a89720108eb..2682c48664e88bfdeba6bb1409918f8d6b89d45e 100644 --- a/docs/frontend/Update-queries-GrahpQL-in-the-frontend.md +++ b/docs/frontend/Update-queries-GrahpQL-in-the-frontend.md @@ -1,113 +1,20 @@ # Update GraphQL Queries -## New method - -The context of the this page is related to the portal-frontend not the gateway directly. - -In order to update or create new types and operations, you can proceed with the following command : - -```bash -yarn codegen -``` - -This command will generate all the operations and types for you. You should place the graphql's operations in the file queries.ts under the folder `src/components/API/GraphQL`. Types will be all generated in one file under the name `types.generated.ts`, operations will be in `queries.generated.tsx` file and all fragments under the file `fragments.generated.tsx`. - -*** - -## Previous method - -This method is no more needed as the front-portal has been updated and all the commands can be made on the front directly. - -### Context - -_(This guide mainly follow the procedure describe here : https://blog.logrocket.com/build-graphql-react-app-typescript/)_ - -This guide is made to generate types and hooks to query/mutate for the frontend based on the GraphQL API. This tool is used to avoid to do it manually. - -The generation could be made directly from the frontend project but due to the old dependencies in the current frontend project it does not work. - -The guide, that will be describe here, is a workaround to generate the types/hooks outside of the front project. - -### Procedure - -First of all make sur that you have npm and yarn installed on your system. - -The first to do is to create a new react typescript project - -#### Setup dependencies - -Create an empty folder and run after these commands from the newly created folder : - -`yarn add @apollo/client graphql` - -`yarn add -D @graphql-codegen/cli` - -It will setup all the dependencies needed to generated the types and hooks. +This guide is made to generate types and hooks (query/mutate) for the frontend based on the GraphQL API. This tool is used to avoid to do it manually. #### Declare GrahpQL queries -To generate the types and hooks, you need to provide the queries that you'll be using, so you need to put a file named `queries.ts`, an example is provided : +To generate new types and hooks, you need to update the file `queries.ts`. In this file you should describe the query you want to make, the types needed will be automatically deducted from your description. -```ts -import { gql } from '@apollo/client'; +#### Update queries and types -export const QUERY_DOMAINS = gql` - query listDomains { - domains { - id - } - } -`; +To update or create new types and operations, you can run the following command : -export const QUERY2 = gql` - query listVariables { - domains { - variables { - id - } - } - } -`; - -... +```bash +yarn codegen ``` -#### Init codegen configuration - -To init codegen configuration, you can enter the following command - -`npx graphql-codegen init` - -After that you will need to provide some information : - -* What type of application are you building ? - * Choose `Application built with React` -* Where is your schema ? - * Give the url of your graphql's endpoint (default : http://127.0.0.1:8081/graphql) -* Where are your operations and fragments ? - * `./queries.ts` -* Pick plugins - * Let the default 3 plugins : TS, TS Operations, TS React Apollo -* Where to write the output - * `src/generated/graphql.tsx` (default) -* Do you want to generated an introspection file ? - * no -* How to name the config file ? - * `codegen.yml` -* What script in package.json should run the codegen - * `codegen` - -After this process you will need to run `yarn install` in order to install the new dependencies that have been added to the package.json. - -#### Generate and integrate - -Everything is now configured, you just need to run `yarn codegen` it will generated all you need in ./src/generated/graphql.tsx. You can copy the content of the generated replace the previous one if there is one or just create a new file under /src/generated/graphql.tsx - -If an error occurs telling you `Unable to find template plugin matching typescript-operations` you should try to run this command `npm i -D @graphql-codegen/typescript change-case` (see issue in the link section) and retry the previous command. - -**Update queries.ts** - -If you needed to regenerate the GrahpQL types and hooks you can keep your folder that is already setup and just change the content of the queries.ts, re-run `yarn codegen` and that's it. +This command will generate all the operations and types for you. Remeber you should place the graphql's operations in the file `queries.ts` under the folder `src/components/API/GraphQL`. Types will be all generated in one file under the name `types.generated.ts`, operations will be in `queries.generated.tsx` file finally all fragments under the file `fragments.generated.tsx`. ### Links diff --git a/docs/get-started/Introduction.md b/docs/get-started/Introduction.md index be9cf98aa6738e05b0cccf5a918785691fd160ea..8e731eb6b1fa6edd7da4e56ffe8d4aee96d1d0d0 100644 --- a/docs/get-started/Introduction.md +++ b/docs/get-started/Introduction.md @@ -6,7 +6,7 @@ description: Introduction for developers The MIP is mainly composed by 3 components - +.png>) * **Frontend** : user interface (React.js) * **Gateway** : middleware used to abstract calls from an engine (Nest.js and GraphQL) @@ -34,7 +34,7 @@ GraphQL is used to provide some useful features : ### Connectors -In order to be able to communicate with an engine, the gateway need connectors. A connector is a concrete implementation of the interface `IEngineService`. +In order to be able to communicate with an engine, the gateway need a connector. A connector is a concrete implementation of the interface `IEngineService`. ```typescript export default class DatashieldService implements IEngineService { @@ -51,7 +51,7 @@ The code above is an example of a connector. ### Visualizations - +.png>) With the Frontend we will introduce a new way to deal with visualizations. Previously the visualizations were completely manage by the engine. As a part of abstraction from a specific engine we want to be able to delegate this task to the visualization components. diff --git a/docs/get-started/Setup-development-environment.md b/docs/get-started/Setup-development-environment.md index 60fdc19cb4fbbb133ddebe397feac25bf0052e7e..287f73db845ec46cee62b5c768b778e30602e02d 100644 --- a/docs/get-started/Setup-development-environment.md +++ b/docs/get-started/Setup-development-environment.md @@ -13,6 +13,7 @@ As we have seen in the previous chapter we need three component in order to setu * Engine * Frontend * Gateway + * DB (postgres) In this guide we will see how to setup the last two elements. @@ -23,12 +24,13 @@ Make sure to have * [Node.js](https://nodejs.org) * [NPM](https://npmjs.com) * [Yarn](https://yarnpkg.com) +* [Docker](https://docs.docker.com/get-docker/) and [docker-compose](https://docs.docker.com/compose/install/) installed in your computer. ## Setup Gateway -First of all you should clone the repository 'gateway' either from the [Gitlab](https://gitlab.com/sibmip/gateway) or from the [GitHub](https://github.com/HBPMedical/gateway). +First of all you should clone the repository either from the [Gitlab](https://gitlab.com/sibmip/gateway) or from the [GitHub](https://github.com/HBPMedical/gateway). Once the pull is completed, you can make the following commands @@ -40,39 +42,31 @@ git checkout develop npm install ``` -After these steps you should be able to start the gateway in dev mode with the following command +#### Run the DB -```bash -npm run start:dev -``` +The gateway need a DB in order to work. [TypeORM](https://typeorm.io) is used to make the DB calls agnostic from the real implementation.  -### env.defaults +We provide a docker-compose to run a `postgres` DB, you can use it by running the following command -There is an environment file that allows some configuration for the gateway. - -```yaml -ENGINE_TYPE=local -ENGINE_BASE_URL=http://127.0.0.1:8080/services/ -GATEWAY_PORT=8081 +```bash +docker-compose up -d ``` -* ENGINE\_TYPE - * Allows you to choose which connector you want to load, `local` is used for development purpose and is not intended to be used in production. -* ENGINE\_BASE\_URL - * Indicate the endpoint for the engine, this parameter can be retrieve in the connector side. If you are using `local` connector this parameter is not useful. -* GATEWAY\_PORT - * Indicate the port for the Gateway. +For debugging purpose, you can omit the -d (detached) parameter. + +#### Run the Gateway -These parameters can be overwrite by either : +After the other steps have been completed, you should be able to start the gateway in dev mode with the following command -* setting a variable in `.env` file (you can create it if it does not exist) along with the file .env.defaults in the root folder -* or setting an environment variable on your system +```bash +npm run start:dev +``` ### GraphQL Playground Once you have started the Gateway, you can play with the GraphQL playground that is automatically integrated within the gateway, follow this link : [http://127.0.0.1:8081/graphql](http://127.0.0.1:8081/graphql). You should be able to see something like this : - +.png>) This environment is a tool provided by GraphQL to play with queries, mutations, etc... diff --git a/for-developers/authentication.md b/for-developers/authentication.md new file mode 100644 index 0000000000000000000000000000000000000000..bed3b7e54346a681b603aa11ad401be6dce03a65 --- /dev/null +++ b/for-developers/authentication.md @@ -0,0 +1,92 @@ +# Authentication + +The authentication implementation is based on [passport.js](https://www.passportjs.org) it allows a flexible way to implement different strategies inside the gateway.  + +For now the authentication system is quite simple and only use JWT. The real implementation of authorization and authentication is left to the connector.  + +#### How it works ? + +The communication between the frontend and the gateway is handled by JWT token who contains user information such as his username. + +.png>) + +The gateway will handle the authentication process with the frontend in a unique fashion always using a JWT token. This token can contains information specific to some connector. For that purpose the user model contains a field `extraFields` which basically a dictionary.  + +{% code title="user.model.ts" %} +```typescript +import { Field, ObjectType } from '@nestjs/graphql'; +import { Entity, PrimaryColumn, Column } from 'typeorm'; + +@Entity({ name: 'user' }) +@ObjectType() +export class User { + @PrimaryColumn() + @Field() + id: string; + + @Field() + username: string; + + @Field({ nullable: true }) + fullname?: string; + + @Field({ nullable: true }) + email?: string; + + @Column({ nullable: true, default: false }) + @Field({ nullable: true }) + agreeNDA?: boolean; + + extraFields?: Record<string, any>; +} +``` +{% endcode %} + +This field can be used by the connector to store information related to the user as other token for engine API endpoints. + +#### Login  + +The real login system is delegated to the connector by using the `login` method in the interface. + +{% code title="engine.interface.ts" %} +```typescript +export interface IEngineService { + // ... + + /** + * Method that login a user with username and password + * @param username + * @param password + * @returns User object or empty if user not found + */ + login?( + username: string, + password: string, + ): Promise<User | undefined>; + + // ... +} +``` +{% endcode %} + +This method can be optional as the authentication can be made by a 3rd party system under the same domain as this is the case for `exareme`. + +When the login is performed, this function should return a `User` object and can feed the `extraFields` attribute with data needed to perform future request to the engine. + +#### Logout + +The same mechanism is applied to the logout system using the method logout from the engine. + +{% code title="engine.interface.ts" %} +```typescript +export interface IEngineService { + // ... + + logout?(req: Request + ): Promise<void>; + + // ... +} +``` +{% endcode %} + diff --git a/for-developers/configuration/README.md b/for-developers/configuration/README.md new file mode 100644 index 0000000000000000000000000000000000000000..94c18d7ebb66b5602f7227b8fe947cf3a8d0ee36 --- /dev/null +++ b/for-developers/configuration/README.md @@ -0,0 +1,2 @@ +# Configuration + diff --git a/for-developers/configuration/gateway.md b/for-developers/configuration/gateway.md new file mode 100644 index 0000000000000000000000000000000000000000..2b16dacfcbdfbc415f9091c3086a000032787d21 --- /dev/null +++ b/for-developers/configuration/gateway.md @@ -0,0 +1,60 @@ +--- +description: >- + This page description all the possible configuration that can be made in the + Gateway. +--- + +# Gateway + +### :toolbox: Options + +#### General + +| name | type | default | description | +| ------------------ | ------- | ------------------------------- | ------------------------------------------------------------------------------------------- | +| ENGINE\_TYPE | string | exareme | Define the connector that should be used : **`exareme, datashield, csv, local`**. | +| ENGINE\_BASE\_URL | string | http://127.0.0.1:8080/services/ | Specify the endpoint for the data source. The parameter will be provided for the connector. | +| TOS\_SKIP | boolean | false | Allow to skip the `terms of services` (this parameter is provided to the frontend) | +| GATEWAY\_PORT | number | 8081 | Indicate the port that should be used by the gateway | +| NODE\_ENV | string | dev | Value can be `prod` or `dev` | +| BASE\_URL\_CONTEXT | string | null | Define context of the gateway. E.g. `api` if the api is under `http://127.0.0.1/api/` | + +#### Authentication + +| name | type | default | description | +| ----------------------------- | ------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | +| AUTH\_SKIP | boolean | false | Allow to skip authentication. Warn: all routes will be accessible without authentication. | +| AUTH\_JWT\_SECRET | string | N/A | Secret that should be used to generate JWT token | +| AUTH\_JWT\_TOKEN\_EXPIRES\_IN | string | '2d' | <p>JWT token time to live.</p><p>Expressed in seconds or a string describing a time span <a href="https://github.com/vercel/ms">vercel/ms</a></p> | +| AUTH\_COOKIE\_SAME\_SITE | string | 'strict' | Specify the cookie same site option. Value can be `lax`, `strict` or `none` | +| AUTH\_COOKIE\_SECURE | boolean | true | Specify the cookie secure option. Should be set to true if same site is not set to `strict`. | +| AUTH\_ENABLE\_SSO | boolean | false | Enable SSO login process, this variable will be provided to the frontend in order to perform the login. | + +#### Database + +| name | type | default | description | +| ------------ | ------ | --------- | ------------------------------- | +| DB\_HOST | string | localhost | Hostname | +| DB\_PORT | number | 5432 | Port number | +| DB\_USERNAME | string | postgres | Username | +| DB\_PASSWORD | string | pass123 | Password | +| DB\_NAME | string | postgres | Name of the database's instance | + +#### Matomo + +Matomo is an open source alternative to Google Analytics. The gateway provide this configuration in order to be used by any frontend. The real implementation is left to the frontend. + +| name | type | default | description | +| ---------------- | ------------------- | --------- | --------------------------------------------------------------------------------------------------- | +| MATOMO\_ENABLED | boolean | false | Enable or disable Matomo | +| MATOMO\_URL | string \| undefined | undefined | Base url for matomo scripts and data reporting. This parameter is `required` if Matomo is `enabled` | +| MATOMO\_SITE\_ID | string \| undefined | undefined | Matomo Website ID. This parameter is required if `Matomo` is `enabled`. | + +### Overwrite parameters + +These parameters can be overwrite by either + +* setting a variable in `.env` file (you can create it if it does not exist) along with the file `.env.defaults` in the root folder +* or setting an environment variable on your system + +Default variables are stored in the `.env.defaults` file, under the `db.config.ts` file for the database configuration and `matomo.config.ts` for Matomo configuration. diff --git a/for-developers/frontend/visualisations.md b/for-developers/frontend/visualisations.md new file mode 100644 index 0000000000000000000000000000000000000000..56395ce99f2db62122194abc64401a12c204838d --- /dev/null +++ b/for-developers/frontend/visualisations.md @@ -0,0 +1,14 @@ +# 📊 Visualisations + +To see the different possible visualisations in the frontend we have integrated [storybook.js](https://storybook.js.org) directly in the frontend. + +Start the storybook by launching this command in the frontend folder + +```bash +yarn storybook +``` + +This command will give you access to the a website with all the visualisation that are currently implemented. + +.png>) + diff --git a/for-developers/gateway/README.md b/for-developers/gateway/README.md new file mode 100644 index 0000000000000000000000000000000000000000..342b3ae0ea594fa61eaa01233074965ffc14263d --- /dev/null +++ b/for-developers/gateway/README.md @@ -0,0 +1,2 @@ +# Gateway + diff --git a/for-developers/gateway/authentication.md b/for-developers/gateway/authentication.md new file mode 100644 index 0000000000000000000000000000000000000000..6c8a63fa9d5e7bf3a606cca9069b135d6301f6f9 --- /dev/null +++ b/for-developers/gateway/authentication.md @@ -0,0 +1,92 @@ +# 🔑 Authentication + +The authentication implementation is based on [passport.js](https://www.passportjs.org) it allows a flexible way to implement different strategies inside the gateway.  + +For now the authentication system is quite simple and only use JWT. The real implementation of authorization and authentication is left to the connector/engine.  + +#### How it works ? + +The communication between the frontend and the gateway is handled by JWT token who contains user information such as his username. + +.png>) + +The gateway will handle the authentication process with the frontend in a unique fashion always using a JWT token. This token can contains information specific to some connector. For that purpose the user model contains a field `extraFields` which basically a dictionary.  + +{% code title="user.model.ts" %} +```typescript +import { Field, ObjectType } from '@nestjs/graphql'; +import { Entity, PrimaryColumn, Column } from 'typeorm'; + +@Entity({ name: 'user' }) +@ObjectType() +export class User { + @PrimaryColumn() + @Field() + id: string; + + @Field() + username: string; + + @Field({ nullable: true }) + fullname?: string; + + @Field({ nullable: true }) + email?: string; + + @Column({ nullable: true, default: false }) + @Field({ nullable: true }) + agreeNDA?: boolean; + + extraFields?: Record<string, any>; +} +``` +{% endcode %} + +This field can be used by the connector to store information related to the user as other token for engine API endpoints. + +#### Login  + +The real login system is delegated to the connector by using the `login` method in the interface. + +{% code title="engine.interface.ts" %} +```typescript +export interface IEngineService { + // ... + + /** + * Method that login a user with username and password + * @param username + * @param password + * @returns User object or empty if user not found + */ + login?( + username: string, + password: string, + ): Promise<User | undefined>; + + // ... +} +``` +{% endcode %} + +This method can be optional as the authentication can be made by a 3rd party system under the same domain as this is the case for `exareme`. + +When the login is performed, this function should return a `User` object and can feed the `extraFields` attribute with data needed to perform future request to the engine. + +#### Logout + +The same mechanism is applied to the logout system using the method logout from the engine. + +{% code title="engine.interface.ts" %} +```typescript +export interface IEngineService { + // ... + + logout?(req: Request + ): Promise<void>; + + // ... +} +``` +{% endcode %} + diff --git a/for-developers/gateway/users.md b/for-developers/gateway/users.md new file mode 100644 index 0000000000000000000000000000000000000000..40dd3a1cd50388ab5dec4d11b2eee1a139d6a81a --- /dev/null +++ b/for-developers/gateway/users.md @@ -0,0 +1,68 @@ +# 👥 Users + +This page describe how users are managed in the gateway. There is mainly two functions in the users module  + +* `GetUser` which retrieve the current user logged in (active user) +* `UpdateUser` which allow the active user to modify is own profile (mainly for `agreeNDA`) + +The gateway is not meant to manage users directly. This is the engine's role to provide the user and a way to modify them. Thus the gateway provide support for some specific user's attribute that are closely related to the MIP usage. For now the Gateway can only manage the `agreeNDA` property for each user but this can be easily extended. + +### How it works ? + +Let's say we want to retrieve the current user, the gateway will ask through the connector for the user's data in the same time the gateway will look in his own database if it has some data for this user. Then both data are merged to fit the User model. Data from the engine have precedence over the gateway data in case of conflict. + +{% code title="user.model.ts" %} +```typescript +@Entity({ name: 'user' }) +@ObjectType() +export class User { + @PrimaryColumn() + @Field() + id: string; + + @Field() + username: string; + + @Field({ nullable: true }) + fullname?: string; + + @Field({ nullable: true }) + email?: string; + + @Column({ nullable: true, default: false }) + @Field({ nullable: true }) + agreeNDA?: boolean; + + extraFields?: Record<string, any>; +} +``` +{% endcode %} + +After merging data from both source and make some integrity check the gateway will be able to present a full user object in a flexible way. + +#### Update user profile + +So now we know that the data can be retrieve through two different sources, how will we handle updating our user profile ? The system is simple, the gateway will ask the connector if he can handle the user's update by looking if the function `updateUser` is defined in the connector. If it's defined it means that the engine can handle at least some part of the update, so we delay the work to the engine. Now if the engine cannot handle all the update data, the connector can decide to return some attributes back to the gateway.  + +{% code title="example return update data" %} +```typescript + async updateUser( + request: Request, + userId: string, + data: UpdateUserInput + ): Promise<UpdateUserInput | undefined> { + const path = this.options.baseurl + 'user'; + const response = await firstValueFrom( + this.post<string>(request, path, { + prop1: data.attrib1, + prop2: data.attrib2 + }), + ); + + const { attrib1, attrib2, ...subset } = UpdateUserInput // Subset of updateData + return subset; + } +``` +{% endcode %} + +The returned attributes will be provided back to the gateway and will be handle internally as far as it can do it.