Skip to content
Snippets Groups Projects
Commit f4952b0f authored by Steve Reis's avatar Steve Reis
Browse files

Merge branch 'feat/auth-refresh-token' into 'develop'

Feature refresh Json Web Token

See merge request sibmip/gateway!75
parents 5cde8c3c 5422d765
No related branches found
No related tags found
No related merge requests found
Showing
with 276 additions and 111 deletions
......@@ -6,16 +6,4 @@ ENGINE_BASE_URL=http://127.0.0.1:8080/services/
TOS_SKIP=false
# SERVER
GATEWAY_PORT=8081
# AUTHENTICATION
AUTH_SKIP=false
AUTH_JWT_SECRET=SecretForDevPurposeOnly
AUTH_JWT_TOKEN_EXPIRES_IN=2d
AUTH_COOKIE_SAME_SITE=strict
AUTH_COOKIE_SECURE=true
# Cache
CACHE_ENABLED=true
CACHE_TTL=1800 # 1800 == 30 minutes
CACHE_MAX_ITEMS=100
\ No newline at end of file
GATEWAY_PORT=8081
\ No newline at end of file
......@@ -19,6 +19,7 @@
"@nestjs/passport": "^8.2.1",
"@nestjs/platform-express": "^8.0.0",
"@nestjs/typeorm": "^8.0.3",
"@types/object-hash": "^2.2.1",
"apollo-server-express": "^3.6.3",
"axios": "^0.21.1",
"cache-manager": "^4.0.1",
......@@ -26,6 +27,7 @@
"graphql": "^15.5.3",
"graphql-type-json": "^0.3.2",
"jsonata": "^1.8.5",
"object-hash": "^3.0.0",
"passport": "^0.5.2",
"passport-custom": "^1.1.1",
"passport-jwt": "^4.0.0",
......@@ -2773,6 +2775,11 @@
"integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==",
"dev": true
},
"node_modules/@types/object-hash": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@types/object-hash/-/object-hash-2.2.1.tgz",
"integrity": "sha512-i/rtaJFCsPljrZvP/akBqEwUP2y5cZLOmvO+JaYnz01aPknrQ+hB5MRcO7iqCUsFaYfTG8kGfKUyboA07xeDHQ=="
},
"node_modules/@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
......@@ -17552,6 +17559,11 @@
"integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==",
"dev": true
},
"@types/object-hash": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/@types/object-hash/-/object-hash-2.2.1.tgz",
"integrity": "sha512-i/rtaJFCsPljrZvP/akBqEwUP2y5cZLOmvO+JaYnz01aPknrQ+hB5MRcO7iqCUsFaYfTG8kGfKUyboA07xeDHQ=="
},
"@types/parse-json": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz",
......
......@@ -36,6 +36,7 @@
"@nestjs/passport": "^8.2.1",
"@nestjs/platform-express": "^8.0.0",
"@nestjs/typeorm": "^8.0.3",
"@types/object-hash": "^2.2.1",
"apollo-server-express": "^3.6.3",
"axios": "^0.21.1",
"cache-manager": "^4.0.1",
......@@ -43,6 +44,7 @@
"graphql": "^15.5.3",
"graphql-type-json": "^0.3.2",
"jsonata": "^1.8.5",
"object-hash": "^3.0.0",
"passport": "^0.5.2",
"passport-custom": "^1.1.1",
"passport-jwt": "^4.0.0",
......
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',
secure: 'AUTH_COOKIE_SECURE',
httpOnly: 'AUTH_COOKIE_HTTPONLY',
},
};
import { Module } from '@nestjs/common';
import { ConfigModule, ConfigService } from '@nestjs/config';
import { ConfigModule, ConfigService, ConfigType } from '@nestjs/config';
import { JwtModule } from '@nestjs/jwt';
import { PassportModule } from '@nestjs/passport';
import { authConstants } from './auth-constants';
import authConfig from 'src/config/auth.config';
import { UsersModule } from 'src/users/users.module';
import { AuthResolver } from './auth.resolver';
import { AuthService } from './auth.service';
import { EngineStrategy } from './strategies/engine.strategy';
......@@ -12,17 +13,22 @@ import { LocalStrategy } from './strategies/local.strategy';
@Module({
imports: [
UsersModule,
PassportModule.register({
session: false,
}),
JwtModule.registerAsync({
imports: [ConfigModule],
useFactory: async (configService: ConfigService) => ({
secret: configService.get(authConstants.JWTSecret),
signOptions: {
expiresIn: configService.get(authConstants.expiresIn, '2d'),
},
}),
useFactory: async (configService: ConfigService) => {
const authConf =
configService.get<ConfigType<typeof authConfig>>('auth');
return {
secret: authConf.JWTSecret,
signOptions: {
expiresIn: authConf.expiresIn,
},
};
},
inject: [ConfigService],
}),
],
......
import { getMockRes } from '@jest-mock/express';
import { getMockReq, getMockRes } from '@jest-mock/express';
import { ConfigType } from '@nestjs/config';
import { Test, TestingModule } from '@nestjs/testing';
import { Request } from 'express';
import { MockFunctionMetadata, ModuleMocker } from 'jest-mock';
import EngineService from '../engine/engine.service';
import LocalService from '../engine/connectors/local/local.connector';
import authConfig from '../config/auth.config';
import { ENGINE_MODULE_OPTIONS } from '../engine/engine.constants';
import EngineService from '../engine/engine.service';
import { User } from '../users/models/user.model';
import { authConstants } from './auth-constants';
import { AuthResolver } from './auth.resolver';
import { AuthService } from './auth.service';
......@@ -14,8 +15,10 @@ const moduleMocker = new ModuleMocker(global);
describe('AuthResolver', () => {
let resolver: AuthResolver;
const { res } = getMockRes();
const req: Request = getMockReq();
const mockCookie = jest.fn();
const mockClearCookie = jest.fn();
let config: ConfigType<typeof authConfig>;
res.cookie = mockCookie;
res.clearCookie = mockClearCookie;
......@@ -37,9 +40,15 @@ describe('AuthResolver', () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: authConfig.KEY,
useValue: authConfig(),
},
{
provide: EngineService,
useClass: LocalService,
useValue: {
clearCache: jest.fn(),
},
},
{
provide: ENGINE_MODULE_OPTIONS,
......@@ -67,12 +76,13 @@ describe('AuthResolver', () => {
.compile();
resolver = module.get<AuthResolver>(AuthResolver);
config = module.get<ConfigType<typeof authConfig>>(authConfig.KEY);
});
it('login', async () => {
const data = await resolver.login(res, user, credentials);
const data = await resolver.login(res, req, user, credentials);
expect(mockCookie.mock.calls[0][0]).toBe(authConstants.cookie.name);
expect(mockCookie.mock.calls[0][0]).toBe(config.cookie.name);
expect(mockCookie.mock.calls[0][1]).toBe(authData.accessToken);
expect(data.accessToken).toBe(authData.accessToken);
});
......@@ -81,6 +91,6 @@ describe('AuthResolver', () => {
const request: any = jest.fn();
await resolver.logout(request, res, user);
expect(mockClearCookie.mock.calls[0][0]).toBe(authConstants.cookie.name);
expect(mockClearCookie.mock.calls[0][0]).toBe(config.cookie.name);
});
});
......@@ -4,23 +4,23 @@ import {
Logger,
UseGuards,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ConfigType } from '@nestjs/config';
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { Response, Request } from 'express';
import { CurrentUser } from '../common/decorators/user.decorator';
import { Request, Response } from 'express';
import { GQLRequest } from '../common/decorators/gql-request.decoractor';
import { GQLResponse } from '../common/decorators/gql-response.decoractor';
import { CurrentUser } from '../common/decorators/user.decorator';
import { parseToBoolean } from '../common/utils/shared.utils';
import authConfig from '../config/auth.config';
import { ENGINE_MODULE_OPTIONS } from '../engine/engine.constants';
import EngineService from '../engine/engine.service';
import EngineOptions from '../engine/interfaces/engine-options.interface';
import { User } from '../users/models/user.model';
import { authConstants } from './auth-constants';
import { AuthService } from './auth.service';
import { GlobalAuthGuard } from './guards/global-auth.guard';
import { LocalAuthGuard } from './guards/local-auth.guard';
import { AuthenticationInput } from './inputs/authentication.input';
import { AuthenticationOutput } from './outputs/authentication.output';
import { parseToBoolean } from '../common/utils/shared.utils';
import EngineOptions from '../engine/interfaces/engine-options.interface';
import EngineService from '../engine/engine.service';
//Custom defined type because Pick<CookieOptions, 'sameSite'> does not work
type SameSiteType = boolean | 'lax' | 'strict' | 'none' | undefined;
......@@ -34,40 +34,52 @@ export class AuthResolver {
@Inject(ENGINE_MODULE_OPTIONS)
private readonly engineOptions: EngineOptions,
private readonly authService: AuthService,
private readonly configService: ConfigService,
@Inject(authConfig.KEY) private authConf: ConfigType<typeof authConfig>,
) {}
@Mutation(() => AuthenticationOutput)
@UseGuards(LocalAuthGuard)
async login(
@GQLResponse() res: Response,
@GQLRequest() req: Request,
@CurrentUser() user: User,
@Args('variables') inputs: AuthenticationInput,
): Promise<AuthenticationOutput> {
this.logger.verbose(`${inputs.username} logged in`);
this.engineService.clearCache(req);
const data = await this.authService.login(user);
if (!data)
throw new InternalServerErrorException(
`Error during the authentication process`,
);
res.cookie(authConstants.cookie.name, data.accessToken, {
httpOnly: parseToBoolean(
this.configService.get(authConstants.cookie.httpOnly, 'true'),
),
sameSite: this.configService.get<SameSiteType>(
authConstants.cookie.sameSite,
'strict',
),
secure: parseToBoolean(
this.configService.get(authConstants.cookie.secure, 'true'),
),
res.cookie(this.authConf.cookie.name, data.accessToken, {
httpOnly: parseToBoolean(this.authConf.cookie.httpOnly),
sameSite: this.authConf.cookie.sameSite as SameSiteType,
secure: parseToBoolean(this.authConf.cookie.secure),
});
return data;
}
@Mutation(() => AuthenticationOutput)
async refresh(
@GQLResponse() res: Response,
@Args('refreshToken', { type: () => String }) refreshToken: string,
): Promise<AuthenticationOutput> {
const data = await this.authService.createTokensWithRefreshToken(
refreshToken,
);
res.cookie(this.authConf.cookie.name, data.accessToken, {
httpOnly: parseToBoolean(this.authConf.cookie.httpOnly),
sameSite: this.authConf.cookie.sameSite as SameSiteType,
secure: parseToBoolean(this.authConf.cookie.secure),
});
return {
accessToken: data.accessToken,
};
return data;
}
@Mutation(() => Boolean)
......@@ -83,14 +95,16 @@ export class AuthResolver {
if (this.engineService.has('logout')) {
await this.engineService.logout(req);
}
this.authService.logout(user);
} catch (e) {
this.logger.debug(
this.logger.warn(
`Service ${this.engineOptions.type} produce an error when logging out ${user.username}`,
);
this.logger.debug(e);
}
}
res.clearCookie(authConstants.cookie.name);
res.clearCookie(this.authConf.cookie.name);
return true;
}
......
......@@ -5,6 +5,7 @@ import { ENGINE_MODULE_OPTIONS } from '../engine/engine.constants';
import { AuthService } from './auth.service';
import { User } from '../users/models/user.model';
import EngineService from '../engine/engine.service';
import authConfig from '../config/auth.config';
const moduleMocker = new ModuleMocker(global);
......@@ -27,6 +28,10 @@ describe('AuthService', () => {
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
{
provide: authConfig.KEY,
useValue: authConfig(),
},
{
provide: EngineService,
useValue: createEngineService(),
......@@ -45,6 +50,7 @@ describe('AuthService', () => {
if (token === JwtService) {
return {
sign: jest.fn().mockReturnValue(jwtToken),
signAsync: jest.fn().mockResolvedValue(jwtToken),
};
}
if (typeof token === 'function') {
......
import { Injectable, NotImplementedException } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import {
Inject,
Injectable,
Logger,
NotImplementedException,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigType } from '@nestjs/config';
import { JwtService, JwtSignOptions } from '@nestjs/jwt';
import { UsersService } from '../users/users.service';
import authConfig from '../config/auth.config';
import EngineService from '../engine/engine.service';
import { User } from '../users/models/user.model';
import { AuthenticationOutput } from './outputs/authentication.output';
import * as hashing from 'object-hash';
type TokenPayload = {
context: User;
};
@Injectable()
export class AuthService {
private static readonly logger = new Logger(AuthService.name);
constructor(
@Inject(authConfig.KEY) private authConf: ConfigType<typeof authConfig>,
private readonly engineService: EngineService,
private jwtService: JwtService,
private readonly usersService: UsersService,
private readonly jwtService: JwtService,
) {}
async validateUser(username: string, password: string): Promise<User> {
......@@ -17,14 +35,86 @@ export class AuthService {
}
/**
* It takes a user and returns an access token
* It takes a user and returns an access and refresh tokens
* @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({
accessToken: this.jwtService.sign(payload),
async login(user: User): Promise<AuthenticationOutput> {
const payload: TokenPayload = {
context: {
...user,
refreshToken: undefined,
agreeNDA: undefined,
},
};
const [accessToken, refreshToken] = await Promise.all([
this.jwtService.signAsync(payload),
this.jwtService.signAsync(payload, this.getRefreshTokenOptions()),
]);
const hashRefresh = await this.getHash(refreshToken);
this.usersService.update(user.id, {
refreshToken: hashRefresh,
});
return {
accessToken,
refreshToken,
};
}
/**
* Logout the user by deleting the refresh token from the database.
* @param {User} user - User object that is being logged out.
*/
async logout(user: User): Promise<void> {
try {
if (user.id)
await this.usersService.update(user.id, { refreshToken: null });
} catch (err) {
//user not found or others errors
AuthService.logger.debug('Error while logging out user', err);
}
}
async createTokensWithRefreshToken(
refreshToken: string,
): Promise<AuthenticationOutput> {
try {
const payload = this.jwtService.verify<TokenPayload>(
refreshToken,
this.getRefreshTokenOptions(),
);
const user = await this.usersService.findOne(payload.context.id);
const isMatchingTokens =
user.refreshToken === (await this.getHash(refreshToken));
if (!isMatchingTokens) {
this.logout(payload.context);
throw new UnauthorizedException();
}
return this.login(payload.context);
} catch (error) {
throw new UnauthorizedException('Invalid refresh token');
}
}
/**
* Make a hash out of an object. This function is not cryptographically secure
* and should only be used for hashing purposes.
* @param {any} obj - The object to be hashed.
* @returns A promise that resolves to a string.
*/
private async getHash(obj: any): Promise<string> {
return hashing(obj);
}
private getRefreshTokenOptions(): JwtSignOptions {
return {
expiresIn: this.authConf.refreshExperiesIn,
secret: this.authConf.JWTResfreshSecret,
};
}
}
import { ExecutionContext, Injectable } from '@nestjs/common';
import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Reflector } from '@nestjs/core';
import { GqlExecutionContext } from '@nestjs/graphql';
......@@ -18,6 +22,14 @@ export class GlobalAuthGuard extends AuthGuard([
super();
}
handleRequest<TUser = any>(err: any, user: any): TUser {
if (err || !user) {
throw new UnauthorizedException();
}
return user;
}
getRequest(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context);
const gqlReq = ctx.getContext().req;
......
......@@ -4,4 +4,7 @@ import { Field, ObjectType } from '@nestjs/graphql';
export class AuthenticationOutput {
@Field()
accessToken: string;
@Field()
refreshToken: string;
}
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Inject, Injectable } from '@nestjs/common';
import { ConfigType } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { authConstants } from '../auth-constants';
import authConfig from 'src/config/auth.config';
@Injectable()
export class JwtBearerStrategy extends PassportStrategy(
Strategy,
'jwt-bearer',
) {
constructor(private readonly configService: ConfigService) {
constructor(
@Inject(authConfig.KEY) private authConf: ConfigType<typeof authConfig>,
) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
ignoreExpiration: false,
secretOrKey: configService.get<string>(authConstants.JWTSecret),
secretOrKey: authConf.JWTSecret,
});
}
async validate(payload: any) {
return payload.sub;
return payload.context;
}
}
import { Injectable } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { Inject, Injectable } from '@nestjs/common';
import { ConfigType } from '@nestjs/config';
import { PassportStrategy } from '@nestjs/passport';
import { Request } from 'express';
import { Strategy } from 'passport-jwt';
import { authConstants } from '../auth-constants';
import authConfig from 'src/config/auth.config';
@Injectable()
export class JwtCookiesStrategy extends PassportStrategy(
Strategy,
'jwt-cookies',
) {
constructor(private readonly configService: ConfigService) {
constructor(
@Inject(authConfig.KEY) private authConf: ConfigType<typeof authConfig>,
) {
super({
jwtFromRequest: JwtCookiesStrategy.extractFromCookie,
ignoreExpiration: false,
secretOrKey: configService.get<string>(authConstants.JWTSecret),
secretOrKey: authConf.JWTSecret,
});
}
static extractFromCookie = function (req: Request) {
let token = null;
if (req && req.cookies) {
token = req.cookies[authConstants.cookie.name];
token = req.cookies[this.authConf.cookie.name];
}
return token;
};
async validate(payload: any) {
return payload.sub;
return payload.context;
}
}
import { registerAs } from '@nestjs/config';
export default registerAs('auth', () => ({
JWTSecret: process.env.AUTH_JWT_SECRET || 'access-secret',
JWTResfreshSecret: process.env.AUTH_JWT_REFRESH_SECRET || 'refresh-secret',
skipAuth: process.env.AUTH_SKIP || 'false',
expiresIn: process.env.AUTH_JWT_TOKEN_EXPIRES_IN || '1h',
refreshExperiesIn: process.env.AUTH_JWT_REFRESH_TOKEN_EXPIRES_IN || '2d',
enableSSO: process.env.AUTH_ENABLE_SSO || 'false',
cookie: {
name: process.env.AUTH_COOKIE_NAME || 'jwt-gateway',
sameSite: process.env.AUTH_COOKIE_SAME_SITE || 'strict',
secure: process.env.AUTH_COOKIE_SECURE || 'false',
httpOnly: process.env.AUTH_COOKIE_HTTPONLY || 'true',
},
}));
......@@ -5,8 +5,8 @@ export default registerAs('cache', () => {
const max = process.env.CACHE_MAX_ITEMS;
const ttl = process.env.CACHE_TTL;
return {
enabled: parseToBoolean(process.env.CACHE_ENABLED, false),
ttl: ttl ? parseInt(ttl) : undefined,
max: max ? parseInt(max) : undefined,
enabled: parseToBoolean(process.env.CACHE_ENABLED, true),
ttl: ttl ? parseInt(ttl) : 1800,
max: max ? parseInt(max) : 100,
};
});
import { Inject, UseGuards, UseInterceptors } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { ConfigService, ConfigType } from '@nestjs/config';
import { Query, Resolver } from '@nestjs/graphql';
import { Request } from 'express';
import { Public } from 'src/auth/decorators/public.decorator';
import { GlobalAuthGuard } from 'src/auth/guards/global-auth.guard';
import { parseToBoolean } from 'src/common/utils/shared.utils';
import authConfig from 'src/config/auth.config';
import { Md5 } from 'ts-md5';
import { authConstants } from '../auth/auth-constants';
import { GQLRequest } from '../common/decorators/gql-request.decoractor';
import {
ENGINE_CONTACT_LINK,
......@@ -37,20 +37,17 @@ export class EngineResolver {
@Query(() => Configuration)
@Public()
configuration(): Configuration {
const config = this.engineService.getConfiguration();
const engineConf = this.engineService.getConfiguration();
const matomo = this.configSerivce.get('matomo');
const authConf: ConfigType<typeof authConfig> =
this.configSerivce.get('auth');
const data = {
...(config ?? {}),
...(engineConf ?? {}),
connectorId: this.engineOptions.type,
skipTos: parseToBoolean(this.configSerivce.get(ENGINE_SKIP_TOS)),
enableSSO: parseToBoolean(
this.configSerivce.get(authConstants.enableSSO),
),
skipAuth: parseToBoolean(
this.configSerivce.get(authConstants.skipAuth),
true,
),
enableSSO: parseToBoolean(authConf.enableSSO),
skipAuth: parseToBoolean(authConf.skipAuth, true),
matomo,
ontologyUrl: this.configSerivce.get(ENGINE_ONTOLOGY_URL),
contactLink: this.configSerivce.get(ENGINE_CONTACT_LINK),
......
......@@ -221,11 +221,10 @@ export default class EngineService implements Connector {
return this.connector.getFilterConfiguration(req);
}
async logout?(req: Request): Promise<void> {
async logout(req: Request): Promise<void> {
await this.clearCache(req);
if (!this.connector.logout) throw new NotImplementedException();
return this.connector.logout(req);
if (this.connector.logout) this.connector.logout(req);
}
/**
......
......@@ -6,6 +6,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { GraphQLError } from 'graphql';
import { join } from 'path';
import { AuthModule } from 'src/auth/auth.module';
import authConfig from 'src/config/auth.config';
import cacheConfig from 'src/config/cache.config';
import dbConfig from 'src/config/db.config';
import matomoConfig from 'src/config/matomo.config';
......@@ -21,7 +22,7 @@ import { AppService } from './app.service';
ConfigModule.forRoot({
isGlobal: true,
envFilePath: ['.env', '.env.defaults'],
load: [dbConfig, matomoConfig, cacheConfig],
load: [dbConfig, matomoConfig, cacheConfig, authConfig],
}),
GraphQLModule.forRoot<ApolloDriverConfig>({
driver: ApolloDriver,
......@@ -59,8 +60,8 @@ import { AppService } from './app.service';
autoLoadEntities: true,
}),
}),
AuthModule,
UsersModule,
AuthModule,
ExperimentsModule,
FilesModule,
],
......
import { MigrationInterface, QueryRunner } from 'typeorm';
export class UsersRefreshToken1659096310828 implements MigrationInterface {
name = 'UsersRefreshToken1659096310828';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "user" ADD "refreshToken" character varying`,
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`ALTER TABLE "user" DROP COLUMN "refreshToken"`);
}
}
......@@ -23,6 +23,7 @@ type User {
type AuthenticationOutput {
accessToken: String!
refreshToken: String!
}
type Matomo {
......@@ -385,23 +386,24 @@ type Query {
}
type Mutation {
updateUser(updateUserInput: UpdateUserInput!): User!
login(variables: AuthenticationInput!): AuthenticationOutput!
refresh(refreshToken: String!): AuthenticationOutput!
logout: Boolean!
updateUser(updateUserInput: UpdateUserInput!): User!
createExperiment(data: ExperimentCreateInput!, isTransient: Boolean = false): Experiment!
editExperiment(id: String!, data: ExperimentEditInput!): Experiment!
removeExperiment(id: String!): PartialExperiment!
}
input UpdateUserInput {
agreeNDA: Boolean!
}
input AuthenticationInput {
username: String!
password: String!
}
input UpdateUserInput {
agreeNDA: Boolean!
}
input ExperimentCreateInput {
datasets: [String!]!
filter: String
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment