From 1bc28929c6e880a5b05a885c6dfe35b909170118 Mon Sep 17 00:00:00 2001 From: Steve Reis <stevereis93@gmail.com> Date: Fri, 25 Mar 2022 18:44:37 +0000 Subject: [PATCH] fix: Logout issue when user can not be retrieve fix: Format graphql errors fix: User undefined cause exception in users module fix: Active user datashield and fix tos case senstive --- api/.env.defaults | 3 + api/assets/engines/datashield/logo.png | Bin 0 -> 6534 bytes api/package.json | 4 +- api/src/auth/auth-constants.ts | 1 + api/src/auth/auth.module.ts | 4 +- api/src/auth/auth.resolver.ts | 8 +- api/src/auth/auth.service.ts | 5 + api/src/auth/decorators/public.decorator.ts | 2 + api/src/auth/guards/jwt-auth.guard.ts | 15 ++- .../common/interfaces/utilities.interface.ts | 13 --- api/src/common/utilities.spec.ts | 95 ++++++++++++++++++ api/src/common/utilities.ts | 32 ++++-- .../connectors/datashield/main.connector.ts | 19 ++-- .../connectors/exareme/main.connector.ts | 2 +- api/src/engine/engine.constants.ts | 1 + api/src/engine/engine.controller.ts | 27 +---- api/src/engine/engine.interfaces.ts | 2 +- api/src/engine/engine.module.ts | 3 - api/src/engine/engine.resolver.ts | 24 ++++- .../engine/interceptors/errors.interceptor.ts | 4 +- api/src/engine/models/configuration.model.ts | 11 +- .../models/result/table-result.model.ts | 2 +- api/src/main/app.module.ts | 14 +++ api/src/schema.gql | 7 +- api/src/users/users.resolver.spec.ts | 8 ++ api/src/users/users.resolver.ts | 8 +- 26 files changed, 233 insertions(+), 81 deletions(-) create mode 100644 api/assets/engines/datashield/logo.png create mode 100644 api/src/auth/decorators/public.decorator.ts create mode 100644 api/src/common/utilities.spec.ts diff --git a/api/.env.defaults b/api/.env.defaults index 8fb46c2..ec63f00 100644 --- a/api/.env.defaults +++ b/api/.env.defaults @@ -2,6 +2,9 @@ ENGINE_TYPE=exareme ENGINE_BASE_URL=http://127.0.0.1:8080/services/ +# GLOBAL +TOS_SKIP=false + # SERVER GATEWAY_PORT=8081 diff --git a/api/assets/engines/datashield/logo.png b/api/assets/engines/datashield/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..19885d0405d59e2447045061a3d85fdf88dc7af1 GIT binary patch literal 6534 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 C6@pj* literal 0 HcmV?d00001 diff --git a/api/package.json b/api/package.json index b0a16f8..142d29f 100644 --- a/api/package.json +++ b/api/package.json @@ -100,8 +100,8 @@ "!**/*.decorator.ts", "!**/*.model.ts", "!**/*.input.ts", - "!src/jest.config.js", - "!src/main.js" + "!**/jest.config.ts", + "!**/main.ts" ], "coverageDirectory": "../coverage", "testEnvironment": "node" diff --git a/api/src/auth/auth-constants.ts b/api/src/auth/auth-constants.ts index c904d45..5e4748c 100644 --- a/api/src/auth/auth-constants.ts +++ b/api/src/auth/auth-constants.ts @@ -2,6 +2,7 @@ export const authConstants = { JWTSecret: 'AUTH_JWT_SECRET', skipAuth: 'AUTH_SKIP', expiresIn: 'AUTH_JWT_TOKEN_EXPIRES_IN', + enableSSO: 'AUTH_ENABLE_SSO', cookie: { name: 'jwt-gateway', sameSite: 'AUTH_COOKIE_SAME_SITE', diff --git a/api/src/auth/auth.module.ts b/api/src/auth/auth.module.ts index 3a5381e..805f0dc 100644 --- a/api/src/auth/auth.module.ts +++ b/api/src/auth/auth.module.ts @@ -17,9 +17,9 @@ import { LocalStrategy } from './strategies/local.strategy'; JwtModule.registerAsync({ imports: [ConfigModule], useFactory: async (configService: ConfigService) => ({ - secret: configService.get<string>(authConstants.JWTSecret), + secret: configService.get(authConstants.JWTSecret), signOptions: { - expiresIn: configService.get<string>(authConstants.expiresIn), + expiresIn: configService.get(authConstants.expiresIn, '2d'), }, }), inject: [ConfigService], diff --git a/api/src/auth/auth.resolver.ts b/api/src/auth/auth.resolver.ts index 5853fe0..228943e 100644 --- a/api/src/auth/auth.resolver.ts +++ b/api/src/auth/auth.resolver.ts @@ -7,18 +7,18 @@ import { import { ConfigService } from '@nestjs/config'; import { Args, Mutation, Resolver } from '@nestjs/graphql'; import { Response } from 'express'; -import { parseToBoolean } from '../common/interfaces/utilities.interface'; +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 { User } from '../users/models/user.model'; import { authConstants } from './auth-constants'; import { AuthService } from './auth.service'; import { CurrentUser } from './decorators/user.decorator'; import { JwtAuthGuard } from './guards/jwt-auth.guard'; import { LocalAuthGuard } from './guards/local-auth.guard'; import { AuthenticationInput } from './inputs/authentication.input'; -import { User } from '../users/models/user.model'; import { AuthenticationOutput } from './outputs/authentication.output'; -import { GQLResponse } from '../common/decorators/gql-response.decoractor'; //Custom defined type because Pick<CookieOptions, 'sameSite'> does not work type SameSiteType = boolean | 'lax' | 'strict' | 'none' | undefined; @@ -69,7 +69,7 @@ export class AuthResolver { @Mutation(() => Boolean) @UseGuards(JwtAuthGuard) logout(@GQLResponse() res: Response, @CurrentUser() user: User): boolean { - this.logger.verbose(`${user.username} logged out`); + if (user) this.logger.verbose(`${user.username} logged out`); res.clearCookie(authConstants.cookie.name); this.engineService.logout?.(); diff --git a/api/src/auth/auth.service.ts b/api/src/auth/auth.service.ts index 2c09c5b..18eaadf 100644 --- a/api/src/auth/auth.service.ts +++ b/api/src/auth/auth.service.ts @@ -17,6 +17,11 @@ export class AuthService { return await this.engineService.login?.(username, password); } + /** + * It takes a user and returns an access token + * @param {User} user - The user object that is being authenticated. + * @returns An object with an accessToken property. + */ async login(user: User): Promise<Pick<AuthenticationOutput, 'accessToken'>> { const payload = { username: user.username, sub: user }; return Promise.resolve({ diff --git a/api/src/auth/decorators/public.decorator.ts b/api/src/auth/decorators/public.decorator.ts new file mode 100644 index 0000000..466abc7 --- /dev/null +++ b/api/src/auth/decorators/public.decorator.ts @@ -0,0 +1,2 @@ +import { SetMetadata } from '@nestjs/common'; +export const Public = () => SetMetadata('isPublic', true); diff --git a/api/src/auth/guards/jwt-auth.guard.ts b/api/src/auth/guards/jwt-auth.guard.ts index 4afaca5..2ee1a63 100644 --- a/api/src/auth/guards/jwt-auth.guard.ts +++ b/api/src/auth/guards/jwt-auth.guard.ts @@ -1,14 +1,18 @@ import { ExecutionContext, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; +import { Reflector } from '@nestjs/core'; import { GqlExecutionContext } from '@nestjs/graphql'; import { AuthGuard } from '@nestjs/passport'; import { Observable } from 'rxjs'; -import { parseToBoolean } from '../../common/interfaces/utilities.interface'; +import { parseToBoolean } from '../../common/utilities'; import { authConstants } from '../auth-constants'; @Injectable() export class JwtAuthGuard extends AuthGuard(['jwt-cookies', 'jwt-bearer']) { - constructor(private readonly configService: ConfigService) { + constructor( + private readonly configService: ConfigService, + private readonly reflector: Reflector, + ) { super(); } @@ -22,11 +26,16 @@ export class JwtAuthGuard extends AuthGuard(['jwt-cookies', 'jwt-bearer']) { canActivate( context: ExecutionContext, ): boolean | Promise<boolean> | Observable<boolean> { + const isPublic = this.reflector.get<boolean>( + 'isPublic', + context.getHandler(), + ); + const skipAuth = parseToBoolean( this.configService.get(authConstants.skipAuth, 'false'), ); - if (skipAuth) { + if (skipAuth || isPublic) { return true; } diff --git a/api/src/common/interfaces/utilities.interface.ts b/api/src/common/interfaces/utilities.interface.ts index 762534a..db02ef9 100644 --- a/api/src/common/interfaces/utilities.interface.ts +++ b/api/src/common/interfaces/utilities.interface.ts @@ -16,16 +16,3 @@ export enum MIME_TYPES { HTML = 'text/html', TEXT = 'text/plain', } - -/** - * Utility method to convert string value to boolean - * @param value string value to be converted - * @returns true if string value equals to 'true', false otherwise - */ -export const parseToBoolean = (value: string): boolean => { - try { - return value.toLowerCase() == 'true'; - } catch { - return false; - } -}; diff --git a/api/src/common/utilities.spec.ts b/api/src/common/utilities.spec.ts new file mode 100644 index 0000000..88f5fb3 --- /dev/null +++ b/api/src/common/utilities.spec.ts @@ -0,0 +1,95 @@ +import { + HttpException, + InternalServerErrorException, + NotFoundException, + RequestTimeoutException, + UnauthorizedException, +} from '@nestjs/common'; +import axios from 'axios'; +import { response } from 'express'; +import { errorAxiosHandler, parseToBoolean } from './utilities'; + +describe('Utility parseToBoolean testing', () => { + it('Parse true string to boolean', () => { + expect(parseToBoolean('true')).toBe(true); + }); + + it('Parse false string to boolean', () => { + expect(parseToBoolean('false')).toBe(false); + }); + + it('Parse wrong string to boolean, should fallback to false', () => { + expect(parseToBoolean('truee')).toBe(false); + }); + + it('Parse wrong string to boolean, should fallback to false', () => { + expect(parseToBoolean('falseee')).toBe(false); + }); + + it('Parse wrong string to boolean, should fallback to default value', () => { + expect(parseToBoolean('trueee', true)).toBe(true); + }); + + it('Parse empty string to boolean, should fallback to false', () => { + expect(parseToBoolean('')).toBe(false); + }); + + it('Parse true uppercased string to boolean', () => { + expect(parseToBoolean('TRUE')).toBe(true); + }); + + it('Parse false uppercased string to boolean', () => { + expect(parseToBoolean('FALSE')).toBe(false); + }); +}); + +jest.mock('axios'); + +describe('Utility error handling testing', () => { + const error = { + response: { + status: 401, + data: 'Dummmy Data', + }, + }; + + beforeAll(() => { + // eslint-disable-next-line + // @ts-ignore + axios.isAxiosError.mockReturnValue(true).mockReturnValueOnce(false); + }); + + afterAll(() => { + jest.resetModules(); + }); + + it('Throw internal error', () => { + expect(() => errorAxiosHandler(error)).toThrow( + InternalServerErrorException, + ); + }); + + [ + { code: 401, type: UnauthorizedException }, + { code: 404, type: NotFoundException }, + { code: 408, type: RequestTimeoutException }, + { code: 500, type: InternalServerErrorException }, + ].forEach((errorIt) => { + it(`Throw ${errorIt.code} error`, () => { + error.response.status = errorIt.code; + expect(() => errorAxiosHandler(error)).toThrow(errorIt.type); + }); + }); + + it('Throw HttpException error', () => { + error.response.status = 505; + expect(() => errorAxiosHandler(error)).toThrow(HttpException); + }); + + it('Axios error with no response, should throw Internal server error with msg unknown error', () => { + error.response = undefined; + expect(() => errorAxiosHandler(error)).toThrow( + InternalServerErrorException, + ); + }); +}); diff --git a/api/src/common/utilities.ts b/api/src/common/utilities.ts index c109df5..92d0457 100644 --- a/api/src/common/utilities.ts +++ b/api/src/common/utilities.ts @@ -10,12 +10,32 @@ import axios from 'axios'; export const errorAxiosHandler = (e: any) => { if (!axios.isAxiosError(e)) throw new InternalServerErrorException(e); - if (e.response.status === 401) throw new UnauthorizedException(); - if (e.response.status === 404) throw new NotFoundException(); - if (e.response.status === 408) throw new RequestTimeoutException(); - if (e.response.status === 500) throw new InternalServerErrorException(); - - if (e.response) throw new HttpException(e.response.data, e.response.status); + if (e.response) { + if (e.response.status === 401) throw new UnauthorizedException(); + if (e.response.status === 404) throw new NotFoundException(); + if (e.response.status === 408) throw new RequestTimeoutException(); + if (e.response.status === 500) throw new InternalServerErrorException(); + if (e.response.status && e.response.status) + throw new HttpException(e.response.data, e.response.status); + } throw new InternalServerErrorException('Unknown error'); }; + +/** + * Parse a string to a boolean + * @param {string} value - The value to parse. + * @param [defaultValue=false] - The default value to return if the value is not a valid boolean. + * @returns A boolean value. + */ +export const parseToBoolean = ( + value: string, + defaultValue = false, +): boolean => { + try { + if (value.toLowerCase() == 'true') return true; + return value.toLowerCase() == 'false' ? false : defaultValue; + } catch { + return defaultValue; + } +}; diff --git a/api/src/engine/connectors/datashield/main.connector.ts b/api/src/engine/connectors/datashield/main.connector.ts index 3c34f0f..454654e 100644 --- a/api/src/engine/connectors/datashield/main.connector.ts +++ b/api/src/engine/connectors/datashield/main.connector.ts @@ -4,7 +4,6 @@ import { InternalServerErrorException, Logger, NotImplementedException, - UnauthorizedException, } from '@nestjs/common'; import { Request } from 'express'; import { catchError, firstValueFrom } from 'rxjs'; @@ -102,7 +101,9 @@ export default class DataShieldService implements IEngineService { DataShieldService.logger.verbose(path); return { rawdata: { - data: 'Engine result are inconsitent', + data: + 'Engine error when processing the request. Reason: ' + + response.data, type: MIME_TYPES.ERROR, }, }; @@ -234,15 +235,13 @@ export default class DataShieldService implements IEngineService { return [transformToDomains.evaluate(response.data)]; } - async getActiveUser(): Promise<User> { - const dummyUser = { - username: 'anonymous', - id: 'anonymousId', - fullname: 'anonymous', - email: 'anonymous@anonymous.com', - agreeNDA: true, + async getActiveUser(req: Request): Promise<User> { + const user = req.user as User; + return { + username: user.id, + id: user.id, + fullname: user.id, }; - return dummyUser; } getAlgorithmsREST(): string { diff --git a/api/src/engine/connectors/exareme/main.connector.ts b/api/src/engine/connectors/exareme/main.connector.ts index daa996d..8d25f9f 100644 --- a/api/src/engine/connectors/exareme/main.connector.ts +++ b/api/src/engine/connectors/exareme/main.connector.ts @@ -54,7 +54,7 @@ export default class ExaremeService implements IEngineService { getConfiguration(): IConfiguration { return { contactLink: 'https://ebrains.eu/support/', - galaxy: true, + hasGalaxy: true, }; } diff --git a/api/src/engine/engine.constants.ts b/api/src/engine/engine.constants.ts index e8fe577..3c9ae32 100644 --- a/api/src/engine/engine.constants.ts +++ b/api/src/engine/engine.constants.ts @@ -1,2 +1,3 @@ export const ENGINE_MODULE_OPTIONS = 'EngineModuleOption'; export const ENGINE_SERVICE = 'EngineService'; +export const ENGINE_SKIP_TOS = 'TOS_SKIP'; diff --git a/api/src/engine/engine.controller.ts b/api/src/engine/engine.controller.ts index 8a704c0..f00a8e7 100644 --- a/api/src/engine/engine.controller.ts +++ b/api/src/engine/engine.controller.ts @@ -1,20 +1,10 @@ -import { - Controller, - Get, - Inject, - Post, - Req, - UseGuards, - UseInterceptors, -} from '@nestjs/common'; +import { Controller, Get, Inject, Req, UseInterceptors } from '@nestjs/common'; import { Request } from 'express'; import { Observable } from 'rxjs'; -import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { ENGINE_SERVICE } from './engine.constants'; import { IEngineService } from './engine.interfaces'; import { ErrorsInterceptor } from './interceptors/errors.interceptor'; -@UseGuards(JwtAuthGuard) @UseInterceptors(ErrorsInterceptor) @Controller() export class EngineController { @@ -27,21 +17,6 @@ export class EngineController { return this.engineService.getAlgorithmsREST(request); } - @Get('activeUser') - async getActiveUser(@Req() request: Request) { - return await this.engineService.getActiveUser(request); - } - - @Post('activeUser/agreeNDA') - async agreeNDA(@Req() request: Request) { - return await this.engineService.updateUser(request); - } - - @Get('logout') - logout(@Req() request: Request): void { - this.engineService.logout(request); - } - @Get('galaxy') galaxy(@Req() request: Request): Observable<string> | string { return this.engineService.getPassthrough?.('galaxy', request); diff --git a/api/src/engine/engine.interfaces.ts b/api/src/engine/engine.interfaces.ts index 7d745e4..b8b21e9 100644 --- a/api/src/engine/engine.interfaces.ts +++ b/api/src/engine/engine.interfaces.ts @@ -18,7 +18,7 @@ export interface IEngineOptions { baseurl: string; } -export type IConfiguration = Pick<Configuration, 'contactLink' | 'galaxy'>; +export type IConfiguration = Pick<Configuration, 'contactLink' | 'hasGalaxy'>; export interface IEngineService { //GraphQL diff --git a/api/src/engine/engine.module.ts b/api/src/engine/engine.module.ts index 35c4138..efdf729 100644 --- a/api/src/engine/engine.module.ts +++ b/api/src/engine/engine.module.ts @@ -1,8 +1,5 @@ import { HttpModule, HttpService } from '@nestjs/axios'; import { DynamicModule, Global, Logger, Module } from '@nestjs/common'; -import { REQUEST } from '@nestjs/core'; -import { Request } from 'express'; -import { IncomingMessage } from 'http'; import { ENGINE_MODULE_OPTIONS, ENGINE_SERVICE } from './engine.constants'; import { EngineController } from './engine.controller'; import { IEngineOptions, IEngineService } from './engine.interfaces'; diff --git a/api/src/engine/engine.resolver.ts b/api/src/engine/engine.resolver.ts index a6a845d..2ea02a1 100644 --- a/api/src/engine/engine.resolver.ts +++ b/api/src/engine/engine.resolver.ts @@ -1,10 +1,14 @@ -import { Inject, UseGuards } from '@nestjs/common'; +import { Inject, UseGuards, UseInterceptors } from '@nestjs/common'; import { Args, Mutation, Query, Resolver } from '@nestjs/graphql'; import { Request } from 'express'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { GQLRequest } from '../common/decorators/gql-request.decoractor'; import { Md5 } from 'ts-md5'; -import { ENGINE_MODULE_OPTIONS, ENGINE_SERVICE } from './engine.constants'; +import { + ENGINE_MODULE_OPTIONS, + ENGINE_SERVICE, + ENGINE_SKIP_TOS, +} from './engine.constants'; import { IEngineOptions, IEngineService } from './engine.interfaces'; import { Configuration } from './models/configuration.model'; import { Domain } from './models/domain.model'; @@ -16,7 +20,13 @@ import { import { ExperimentCreateInput } from './models/experiment/input/experiment-create.input'; import { ExperimentEditInput } from './models/experiment/input/experiment-edit.input'; import { ListExperiments } from './models/experiment/list-experiments.model'; +import { ConfigService } from '@nestjs/config'; +import { parseToBoolean } from '../common/utilities'; +import { authConstants } from '../auth/auth-constants'; +import { Public } from 'src/auth/decorators/public.decorator'; +import { ErrorsInterceptor } from './interceptors/errors.interceptor'; +@UseInterceptors(ErrorsInterceptor) @UseGuards(JwtAuthGuard) @Resolver() export class EngineResolver { @@ -24,14 +34,24 @@ export class EngineResolver { @Inject(ENGINE_SERVICE) private readonly engineService: IEngineService, @Inject(ENGINE_MODULE_OPTIONS) private readonly engineOptions: IEngineOptions, + private readonly configSerivce: ConfigService, ) {} @Query(() => Configuration) + @Public() configuration(): Configuration { const config = this.engineService.getConfiguration?.(); const data = { ...(config ?? {}), + skipAuth: parseToBoolean( + this.configSerivce.get(authConstants.skipAuth), + true, + ), + skipTos: parseToBoolean(this.configSerivce.get(ENGINE_SKIP_TOS)), + enableSSO: parseToBoolean( + this.configSerivce.get(authConstants.enableSSO), + ), connectorId: this.engineOptions.type, }; diff --git a/api/src/engine/interceptors/errors.interceptor.ts b/api/src/engine/interceptors/errors.interceptor.ts index 47e61fc..17bd220 100644 --- a/api/src/engine/interceptors/errors.interceptor.ts +++ b/api/src/engine/interceptors/errors.interceptor.ts @@ -30,7 +30,9 @@ export class ErrorsInterceptor implements NestInterceptor { this.logger.log(e.message); this.logger.verbose( - `[Error ${e.response.status}] ${e.response.data.message}`, + `[Error ${e.response.status}] ${ + e.response.data.message ?? e.response.data + }`, ); throw new HttpException(e.response.data, e.response.status); // catch errors, maybe make it optional (module parameter) }), diff --git a/api/src/engine/models/configuration.model.ts b/api/src/engine/models/configuration.model.ts index 69edf36..d2d390d 100644 --- a/api/src/engine/models/configuration.model.ts +++ b/api/src/engine/models/configuration.model.ts @@ -5,11 +5,20 @@ export class Configuration { connectorId: string; @Field({ nullable: true, defaultValue: false }) - galaxy?: boolean; + hasGalaxy?: boolean; @Field({ nullable: true }) contactLink?: string; @Field() version: string; + + @Field({ nullable: true }) + skipAuth?: boolean; + + @Field({ nullable: true, defaultValue: false }) + skipTos?: boolean; + + @Field({ nullable: true, defaultValue: true }) + enableSSO?: boolean; } diff --git a/api/src/engine/models/result/table-result.model.ts b/api/src/engine/models/result/table-result.model.ts index 664d64c..a733683 100644 --- a/api/src/engine/models/result/table-result.model.ts +++ b/api/src/engine/models/result/table-result.model.ts @@ -22,6 +22,6 @@ export class TableResult extends Result { @Field(() => [Header]) headers: Header[]; - @Field(() => ThemeType, { defaultValue: ThemeType.DEFAULT }) + @Field(() => ThemeType, { defaultValue: ThemeType.DEFAULT, nullable: true }) theme?: ThemeType; } diff --git a/api/src/main/app.module.ts b/api/src/main/app.module.ts index 9ec49e6..1555caa 100644 --- a/api/src/main/app.module.ts +++ b/api/src/main/app.module.ts @@ -3,6 +3,7 @@ import { Module } from '@nestjs/common'; import { ConfigModule } from '@nestjs/config'; import { GraphQLModule } from '@nestjs/graphql'; import { TypeOrmModule } from '@nestjs/typeorm'; +import { GraphQLError } from 'graphql'; import { join } from 'path'; import { AuthModule } from 'src/auth/auth.module'; import { EngineModule } from 'src/engine/engine.module'; @@ -25,6 +26,19 @@ import { AppService } from './app.service'; credentials: true, origin: [/http:\/\/localhost($|:\d*)/, /http:\/\/127.0.0.1($|:\d*)/], }, + formatError: (error: GraphQLError) => { + const extensions = { + code: error.extensions.code, + status: + error.extensions?.response?.statusCode ?? + error.extensions.exception.status, + message: + error.extensions?.response?.message ?? + error.extensions?.exception?.message, + }; + + return { ...error, extensions: { ...error.extensions, ...extensions } }; + }, }), EngineModule.forRoot({ type: process.env.ENGINE_TYPE, diff --git a/api/src/schema.gql b/api/src/schema.gql index 5abb541..4d248dc 100644 --- a/api/src/schema.gql +++ b/api/src/schema.gql @@ -16,9 +16,12 @@ type AuthenticationOutput { type Configuration { connectorId: String! - galaxy: Boolean + hasGalaxy: Boolean contactLink: String version: String! + skipAuth: Boolean + skipTos: Boolean + enableSSO: Boolean } type Dataset { @@ -94,7 +97,7 @@ type TableResult { name: String! data: [[String!]!]! headers: [Header!]! - theme: ThemeType! + theme: ThemeType } enum ThemeType { diff --git a/api/src/users/users.resolver.spec.ts b/api/src/users/users.resolver.spec.ts index 6ea7e0f..e449f71 100644 --- a/api/src/users/users.resolver.spec.ts +++ b/api/src/users/users.resolver.spec.ts @@ -102,6 +102,10 @@ describe('UsersResolver', () => { }); }); + it('Undefined user should not throw exception', async () => { + expect(await resolver.getUser(req, undefined)).toBeTruthy(); + }); + it('Update user from engine ', async () => { expect(await resolver.updateUser(req, updateData, user)).toStrictEqual({ ...user, @@ -116,4 +120,8 @@ describe('UsersResolver', () => { ...internUser, }); }); + + it('Undefined user should not throw exception', async () => { + expect(await resolver.updateUser(req, updateData, user)).toBeTruthy(); + }); }); diff --git a/api/src/users/users.resolver.ts b/api/src/users/users.resolver.ts index 46c77cc..13ec63a 100644 --- a/api/src/users/users.resolver.ts +++ b/api/src/users/users.resolver.ts @@ -43,7 +43,9 @@ export class UsersResolver { // Checking if the user exists in the internal database. If it does, it will assign the user to the `user` object. try { - const internalUser = await this.usersService.findOne(reqUser.id); + const internalUser = reqUser + ? await this.usersService.findOne(reqUser.id) + : undefined; if (internalUser && (!user.id || internalUser.id === user.id)) { Object.assign(user, internalUser); @@ -71,10 +73,10 @@ export class UsersResolver { async updateUser( @GQLRequest() request: Request, @Args('updateUserInput') updateUserInput: UpdateUserInput, - @CurrentUser() user: User, + @CurrentUser() user?: User, ) { if (this.engineService.updateUser) - return this.engineService.updateUser(request, user.id, updateUserInput); + return this.engineService.updateUser(request, user?.id, updateUserInput); await this.usersService.update(user.id, updateUserInput); -- GitLab