From c9c4453be12e1fc392793a982a02a32a57d591b8 Mon Sep 17 00:00:00 2001 From: ozsay Date: Wed, 4 Sep 2019 15:59:45 +0300 Subject: [PATCH 01/15] work on mfa --- .../__tests__/database-manager.ts | 31 +++++++ .../database-manager/src/database-manager.ts | 20 +++- .../src/types/configuration.ts | 7 +- .../__tests__/database-tests.ts | 5 +- packages/database-mongo/__tests__/index.ts | 77 ++++++++++++++++ packages/database-mongo/package.json | 2 +- packages/database-mongo/src/mongo.ts | 42 ++++++++- packages/database-mongo/src/types/index.ts | 4 + packages/server/__tests__/account-server.ts | 3 +- packages/server/src/accounts-server.ts | 91 ++++++++++++++++++- packages/server/src/utils/tokens.ts | 9 +- packages/types/src/index.ts | 2 + packages/types/src/types/create-user.ts | 1 + .../types/src/types/database-interface.ts | 11 ++- packages/types/src/types/mfa-login-attempt.ts | 6 ++ packages/types/src/types/mfa-login-result.ts | 4 + packages/types/src/types/user.ts | 1 + 17 files changed, 304 insertions(+), 12 deletions(-) create mode 100644 packages/types/src/types/mfa-login-attempt.ts create mode 100644 packages/types/src/types/mfa-login-result.ts diff --git a/packages/database-manager/__tests__/database-manager.ts b/packages/database-manager/__tests__/database-manager.ts index 31c89ec5c..a8baf9099 100644 --- a/packages/database-manager/__tests__/database-manager.ts +++ b/packages/database-manager/__tests__/database-manager.ts @@ -106,11 +106,24 @@ export default class Database { public setUserDeactivated() { return this.name; } + + public createMfaLoginAttempt() { + return this.name; + } + + public getMfaLoginAttempt() { + return this.name; + } + + public removeMfaLoginAttempt() { + return this.name; + } } const databaseManager = new DatabaseManager({ userStorage: new Database('userStorage'), sessionStorage: new Database('sessionStorage'), + mfaLoginAttemptsStorage: new Database('mfaLoginAttemptsStorage'), }); describe('DatabaseManager configuration', () => { @@ -128,6 +141,10 @@ describe('DatabaseManager configuration', () => { expect(() => (databaseManager as any).validateConfiguration({ userStorage: true })).toThrow(); }); + it('should throw if no mfaLoginAttemptsStorage specified', () => { + expect(() => (databaseManager as any).validateConfiguration({ userStorage: true })).toThrow(); + }); + it('should throw if no sessionStorage specified', () => { expect(() => (databaseManager as any).validateConfiguration({ @@ -244,4 +261,18 @@ describe('DatabaseManager', () => { it('setUserDeactivated should be called on sessionStorage', () => { expect(databaseManager.setUserDeactivated('userId', true)).toBe('userStorage'); }); + + it('createMfaLoginAttempt should be called on mfaLoginAttemptsStorage', () => { + expect(databaseManager.createMfaLoginAttempt('mfaToken', 'loginToken', 'userId')).toBe( + 'mfaLoginAttemptsStorage' + ); + }); + + it('getMfaLoginAttempt should be called on mfaLoginAttemptsStorage', () => { + expect(databaseManager.getMfaLoginAttempt('mfaToken')).toBe('mfaLoginAttemptsStorage'); + }); + + it('removeMfaLoginAttempt should be called on mfaLoginAttemptsStorage', () => { + expect(databaseManager.removeMfaLoginAttempt('mfaToken')).toBe('mfaLoginAttemptsStorage'); + }); }); diff --git a/packages/database-manager/src/database-manager.ts b/packages/database-manager/src/database-manager.ts index 0c5878f4e..f144806e4 100644 --- a/packages/database-manager/src/database-manager.ts +++ b/packages/database-manager/src/database-manager.ts @@ -1,15 +1,21 @@ -import { DatabaseInterface, DatabaseInterfaceSessions } from '@accounts/types'; +import { + DatabaseInterface, + DatabaseInterfaceSessions, + DatabaseInterfaceMfaLoginAttempts, +} from '@accounts/types'; import { Configuration } from './types/configuration'; export class DatabaseManager implements DatabaseInterface { private userStorage: DatabaseInterface; private sessionStorage: DatabaseInterface | DatabaseInterfaceSessions; + private mfaLoginAttemptsStorage: DatabaseInterface | DatabaseInterfaceMfaLoginAttempts; constructor(configuration: Configuration) { this.validateConfiguration(configuration); this.userStorage = configuration.userStorage; this.sessionStorage = configuration.sessionStorage; + this.mfaLoginAttemptsStorage = configuration.mfaLoginAttemptsStorage; } private validateConfiguration(configuration: Configuration): void { @@ -154,4 +160,16 @@ export class DatabaseManager implements DatabaseInterface { public get setUserDeactivated(): DatabaseInterface['setUserDeactivated'] { return this.userStorage.setUserDeactivated.bind(this.userStorage); } + + public get createMfaLoginAttempt(): DatabaseInterface['createMfaLoginAttempt'] { + return this.mfaLoginAttemptsStorage.createMfaLoginAttempt.bind(this.mfaLoginAttemptsStorage); + } + + public get getMfaLoginAttempt(): DatabaseInterface['getMfaLoginAttempt'] { + return this.mfaLoginAttemptsStorage.getMfaLoginAttempt.bind(this.mfaLoginAttemptsStorage); + } + + public get removeMfaLoginAttempt(): DatabaseInterface['removeMfaLoginAttempt'] { + return this.mfaLoginAttemptsStorage.removeMfaLoginAttempt.bind(this.mfaLoginAttemptsStorage); + } } diff --git a/packages/database-manager/src/types/configuration.ts b/packages/database-manager/src/types/configuration.ts index ef339f2e3..fb394883b 100644 --- a/packages/database-manager/src/types/configuration.ts +++ b/packages/database-manager/src/types/configuration.ts @@ -1,6 +1,11 @@ -import { DatabaseInterface, DatabaseInterfaceSessions } from '@accounts/types'; +import { + DatabaseInterface, + DatabaseInterfaceSessions, + DatabaseInterfaceMfaLoginAttempts, +} from '@accounts/types'; export interface Configuration { userStorage: DatabaseInterface; sessionStorage: DatabaseInterface | DatabaseInterfaceSessions; + mfaLoginAttemptsStorage: DatabaseInterface | DatabaseInterfaceMfaLoginAttempts; } diff --git a/packages/database-mongo/__tests__/database-tests.ts b/packages/database-mongo/__tests__/database-tests.ts index 9af26cd0d..11e116558 100644 --- a/packages/database-mongo/__tests__/database-tests.ts +++ b/packages/database-mongo/__tests__/database-tests.ts @@ -27,7 +27,10 @@ export class DatabaseTests { public createConnection = async () => { const url = 'mongodb://localhost:27017'; - this.client = await mongodb.MongoClient.connect(url, { useNewUrlParser: true }); + this.client = await mongodb.MongoClient.connect(url, { + useNewUrlParser: true, + useUnifiedTopology: true, + }); this.db = this.client.db('accounts-mongo-tests'); this.database = new Mongo(this.db, this.options); }; diff --git a/packages/database-mongo/__tests__/index.ts b/packages/database-mongo/__tests__/index.ts index 090996c81..048269790 100644 --- a/packages/database-mongo/__tests__/index.ts +++ b/packages/database-mongo/__tests__/index.ts @@ -1,6 +1,7 @@ // tslint:disable-next-line import { randomBytes } from 'crypto'; import { ObjectID, ObjectId } from 'mongodb'; +import { MfaLoginAttempt } from '@accounts/types'; import { Mongo } from '../src'; import { DatabaseTests } from './database-tests'; @@ -836,4 +837,80 @@ describe('Mongo', () => { expect((retUser as any).createdAt).not.toEqual((retUser as any).updatedAt); }); }); + + describe('MfaLoginAttempts', () => { + it('should create a new mfa login attempt', async () => { + const attempt = { _id: '123', loginToken: '456', userId: '789' }; + await databaseTests.database.createMfaLoginAttempt( + attempt._id, + attempt.loginToken, + attempt.userId + ); + + const dbObject = await databaseTests.db + .collection('mfa-login-attempts') + .findOne({ _id: attempt._id }); + + expect(dbObject).toEqual(attempt); + }); + + it('should not create a new mfa login attempt if already exists', async () => { + const attempt = { _id: '123', loginToken: '456', userId: '789' }; + await databaseTests.db.collection('mfa-login-attempts').insertOne(attempt); + + try { + await databaseTests.database.createMfaLoginAttempt( + attempt._id, + attempt.loginToken, + attempt.userId + ); + } catch (e) { + const db = await databaseTests.db + .collection('mfa-login-attempts') + .find() + .toArray(); + + expect(db).toHaveLength(1); + } + + expect.assertions(1); + }); + + it('should get a mfa login attempt', async () => { + const attempt = { _id: '123', loginToken: '456', userId: '789' }; + await databaseTests.db.collection('mfa-login-attempts').insertOne(attempt); + + const attemptFromDb = (await databaseTests.database.getMfaLoginAttempt( + attempt._id + )) as MfaLoginAttempt; + + expect(attemptFromDb.id).toEqual(attempt._id); + expect(attemptFromDb.mfaToken).toEqual(attempt._id); + expect(attemptFromDb.loginToken).toEqual(attempt.loginToken); + expect(attemptFromDb.userId).toEqual(attempt.userId); + }); + + it('should return null while getting a mfa login attempt with wrong id', async () => { + const attempt = { _id: '123', loginToken: '456', userId: '789' }; + await databaseTests.db.collection('mfa-login-attempts').insertOne(attempt); + + const attemptFromDb = await databaseTests.database.getMfaLoginAttempt('111'); + + expect(attemptFromDb).toBeNull(); + }); + + it('should remove a mfa login attempt', async () => { + const attempt = { _id: '123', loginToken: '456', userId: '789' }; + await databaseTests.db.collection('mfa-login-attempts').insertOne(attempt); + + await databaseTests.database.removeMfaLoginAttempt(attempt._id); + + const db = await databaseTests.db + .collection('mfa-login-attempts') + .find() + .toArray(); + + expect(db).toHaveLength(0); + }); + }); }); diff --git a/packages/database-mongo/package.json b/packages/database-mongo/package.json index b42732e73..5d749b75a 100644 --- a/packages/database-mongo/package.json +++ b/packages/database-mongo/package.json @@ -10,7 +10,7 @@ "precompile": "yarn clean", "compile": "tsc", "prepublishOnly": "yarn compile", - "testonly": "jest --runInBand --forceExit", + "testonly": "jest --runInBand", "test:watch": "jest --watch", "coverage": "yarn testonly --coverage" }, diff --git a/packages/database-mongo/src/mongo.ts b/packages/database-mongo/src/mongo.ts index 45a01e44e..42217afb9 100644 --- a/packages/database-mongo/src/mongo.ts +++ b/packages/database-mongo/src/mongo.ts @@ -4,10 +4,11 @@ import { CreateUser, DatabaseInterface, Session, + MfaLoginAttempt, User, } from '@accounts/types'; import { get, merge } from 'lodash'; -import { Collection, Db, ObjectID } from 'mongodb'; +import { Collection, Db, ObjectID, MongoError } from 'mongodb'; import { AccountsMongoOptions, MongoUser } from './types'; @@ -21,6 +22,7 @@ const toMongoID = (objectId: string | ObjectID) => { const defaultOptions = { collectionName: 'users', sessionCollectionName: 'sessions', + mfaLoginCollectionName: 'mfa-login-attempts', timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt', @@ -40,6 +42,8 @@ export class Mongo implements DatabaseInterface { private collection: Collection; // Session collection private sessionCollection: Collection; + // Session collection + private mfaLoginCollection: Collection; constructor(db: any, options?: AccountsMongoOptions) { this.options = merge({ ...defaultOptions }, options); @@ -49,6 +53,7 @@ export class Mongo implements DatabaseInterface { this.db = db; this.collection = this.db.collection(this.options.collectionName); this.sessionCollection = this.db.collection(this.options.sessionCollectionName); + this.mfaLoginCollection = this.db.collection(this.options.mfaLoginCollectionName); } public async setupIndexes(): Promise { @@ -427,4 +432,39 @@ export class Mongo implements DatabaseInterface { public async setResetPassword(userId: string, email: string, newPassword: string): Promise { await this.setPassword(userId, newPassword); } + + public async createMfaLoginAttempt( + mfaToken: string, + loginToken: string, + userId: string + ): Promise { + try { + await this.mfaLoginCollection.insertOne({ _id: mfaToken, loginToken, userId }); + } catch (e) { + const me = e as MongoError; + if (me.code === 11000) { + // duplicate key + throw new Error('mfa login attempt already exists'); + } + } + } + + public async getMfaLoginAttempt(mfaToken: string): Promise { + const dbObject = await this.mfaLoginCollection.findOne({ _id: mfaToken }); + + if (!dbObject) { + return null; + } + + return { + id: dbObject._id, + mfaToken: dbObject._id, + loginToken: dbObject.loginToken, + userId: dbObject.userId, + }; + } + + public async removeMfaLoginAttempt(mfaToken: string): Promise { + await this.mfaLoginCollection.deleteOne({ _id: mfaToken }); + } } diff --git a/packages/database-mongo/src/types/index.ts b/packages/database-mongo/src/types/index.ts index 04c77c42e..33edb6fae 100644 --- a/packages/database-mongo/src/types/index.ts +++ b/packages/database-mongo/src/types/index.ts @@ -7,6 +7,10 @@ export interface AccountsMongoOptions { * The sessions collection name, default 'sessions'. */ sessionCollectionName?: string; + /** + * The MFA login attempts collection name, default 'mfa-login-attempts'. + */ + mfaLoginCollectionName?: string; /** * The timestamps for the users and sessions collection, default 'createdAt' and 'updatedAt'. */ diff --git a/packages/server/__tests__/account-server.ts b/packages/server/__tests__/account-server.ts index 36edf45bd..6f27e828b 100644 --- a/packages/server/__tests__/account-server.ts +++ b/packages/server/__tests__/account-server.ts @@ -2,6 +2,7 @@ import * as jwtDecode from 'jwt-decode'; import { AccountsServer } from '../src/accounts-server'; import { JwtData } from '../src/types/jwt-data'; import { ServerHooks } from '../src/utils/server-hooks'; +import { LoginResult } from '@accounts/types'; const delay = (timeout: number) => new Promise(resolve => setTimeout(resolve, timeout)); @@ -90,7 +91,7 @@ describe('AccountsServer', () => { } ); const res = await accountServer.loginWithService('facebook', {}, {}); - expect(res.tokens).toBeTruthy(); + expect((res as LoginResult).tokens).toBeTruthy(); }); }); diff --git a/packages/server/src/accounts-server.ts b/packages/server/src/accounts-server.ts index 447b64c8a..4d4b44f7e 100644 --- a/packages/server/src/accounts-server.ts +++ b/packages/server/src/accounts-server.ts @@ -4,6 +4,7 @@ import * as Emittery from 'emittery'; import { User, LoginResult, + MFALoginResult, Tokens, Session, ImpersonationResult, @@ -11,9 +12,15 @@ import { DatabaseInterface, AuthenticationService, ConnectionInformations, + MfaLoginAttempt, } from '@accounts/types'; -import { generateAccessToken, generateRefreshToken, generateRandomToken } from './utils/tokens'; +import { + generateAccessToken, + generateRefreshToken, + generateRandomToken, + hashToken, +} from './utils/tokens'; import { emailTemplates, sendMail } from './utils/email'; import { ServerHooks } from './utils/server-hooks'; @@ -94,7 +101,7 @@ Please change it with a strong random token.`); serviceName: string, params: any, infos: ConnectionInformations - ): Promise { + ): Promise { const hooksInfo: any = { // The service name, such as “password” or “twitter”. service: serviceName, @@ -104,11 +111,22 @@ Please change it with a strong random token.`); params, }; try { - if (!this.services[serviceName]) { + if (serviceName !== 'mfa' && !this.services[serviceName]) { throw new Error(`No service with the name ${serviceName} was registered.`); } - const user: User | null = await this.services[serviceName].authenticate(params); + let user: User | null; + + if (serviceName !== 'mfa') { + user = await this.services[serviceName].authenticate(params); + } else { + user = await this.getUserFromMfaToken({ skipValidation: false, ...params }); + + if (user) { + await this.db.removeMfaLoginAttempt(params.mfaToken); + } + } + hooksInfo.user = user; if (!user) { throw new Error(`Service ${serviceName} was not able to authenticate user`); @@ -119,6 +137,11 @@ Please change it with a strong random token.`); // Let the user validate the login attempt await this.hooks.emitSerial(ServerHooks.ValidateLogin, hooksInfo); + + if (serviceName !== 'mfa' && user.mfaChallenges && user.mfaChallenges.length > 0) { + return this.createMfaLoginProcess(user); + } + const loginResult = await this.loginWithUser(user, infos); this.hooks.emit(ServerHooks.LoginSuccess, hooksInfo); return loginResult; @@ -128,6 +151,32 @@ Please change it with a strong random token.`); } } + public async performMfaChallenge( + challenge: string, + mfaToken: string, + params: any + ): Promise { + const userFromMfa = await this.getUserFromMfaToken({ skipValidation: true, mfaToken }); + + if ( + !userFromMfa || + !userFromMfa.mfaChallenges || + userFromMfa.mfaChallenges.indexOf(challenge) === -1 + ) { + throw new Error('Performing the mfa challenge is not available'); + } + + const userFromChallenge: User | null = await this.services[challenge].authenticate(params); + + if (!userFromChallenge || userFromMfa.id !== userFromChallenge.id) { + throw new Error(`Service ${challenge} was not able to authenticate user`); + } + + const mfaLoginAttempt = (await this.db.getMfaLoginAttempt(mfaToken)) as MfaLoginAttempt; + + return mfaLoginAttempt.loginToken; + } + /** * @description Server use only. * This method creates a session without authenticating any user identity. @@ -528,6 +577,40 @@ Please change it with a strong random token.`); ? this.options.tokenCreator.createToken(user) : generateRandomToken(); } + + private async getUserFromMfaToken({ + mfaToken, + loginToken, + skipValidation = false, + }: { + mfaToken: string; + loginToken?: string; + skipValidation: boolean; + }): Promise { + const loginAttempt = await this.db.getMfaLoginAttempt(mfaToken); + + if (!loginAttempt) { + return null; + } + + if (!skipValidation && loginAttempt.loginToken !== loginToken) { + return null; + } + + return this.db.findUserById(loginAttempt.userId); + } + + private async createMfaLoginProcess(user: User): Promise { + const loginToken = generateRandomToken(); + const mfaToken = hashToken(loginToken); + + await this.db.createMfaLoginAttempt(mfaToken, loginToken, user.id); + + return { + mfaToken, + challenges: user.mfaChallenges as string[], + }; + } } export default AccountsServer; diff --git a/packages/server/src/utils/tokens.ts b/packages/server/src/utils/tokens.ts index 6d1e020e1..8d3345564 100644 --- a/packages/server/src/utils/tokens.ts +++ b/packages/server/src/utils/tokens.ts @@ -1,5 +1,5 @@ import * as jwt from 'jsonwebtoken'; -import { randomBytes } from 'crypto'; +import { randomBytes, createHash } from 'crypto'; /** * Generate a random token string @@ -7,6 +7,13 @@ import { randomBytes } from 'crypto'; export const generateRandomToken = (length: number = 43): string => randomBytes(length).toString('hex'); +export const hashToken = (token: string): string => { + const hash = createHash('sha256'); + hash.update(token); + + return hash.digest('hex'); +}; + export const generateAccessToken = ({ secret, data, diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 3b84ba8c4..ef859c098 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -7,6 +7,8 @@ export * from './types/user'; export * from './types/create-user'; export * from './types/email-record'; export * from './types/login-result'; +export * from './types/mfa-login-result'; +export * from './types/mfa-login-attempt'; export * from './types/impersonation-result'; export * from './types/login-user-identity'; export * from './types/hook-listener'; diff --git a/packages/types/src/types/create-user.ts b/packages/types/src/types/create-user.ts index acbc6c471..06d28ad95 100644 --- a/packages/types/src/types/create-user.ts +++ b/packages/types/src/types/create-user.ts @@ -1,5 +1,6 @@ export interface CreateUser { username?: string; email?: string; + mfaChallenges?: string[]; [additionalKey: string]: any; } diff --git a/packages/types/src/types/database-interface.ts b/packages/types/src/types/database-interface.ts index 175e5acdb..6e0d759ed 100644 --- a/packages/types/src/types/database-interface.ts +++ b/packages/types/src/types/database-interface.ts @@ -1,9 +1,12 @@ import { User } from './user'; import { Session } from './session'; +import { MfaLoginAttempt } from './mfa-login-attempt'; import { CreateUser } from './create-user'; import { ConnectionInformations } from './connection-informations'; -export interface DatabaseInterface extends DatabaseInterfaceSessions { +export interface DatabaseInterface + extends DatabaseInterfaceSessions, + DatabaseInterfaceMfaLoginAttempts { // Find user by identity fields findUserByEmail(email: string): Promise; @@ -58,6 +61,12 @@ export interface DatabaseInterface extends DatabaseInterfaceSessions { setUserDeactivated(userId: string, deactivated: boolean): Promise; } +export interface DatabaseInterfaceMfaLoginAttempts { + createMfaLoginAttempt(mfaToken: string, loginToken: string, userId: string): Promise; + getMfaLoginAttempt(mfaToken: string): Promise; + removeMfaLoginAttempt(mfaToken: string): Promise; +} + export interface DatabaseInterfaceSessions { findSessionById(sessionId: string): Promise; diff --git a/packages/types/src/types/mfa-login-attempt.ts b/packages/types/src/types/mfa-login-attempt.ts new file mode 100644 index 000000000..6ccb33d2a --- /dev/null +++ b/packages/types/src/types/mfa-login-attempt.ts @@ -0,0 +1,6 @@ +export interface MfaLoginAttempt { + id: string; + mfaToken: string; + loginToken: string; + userId: string; +} diff --git a/packages/types/src/types/mfa-login-result.ts b/packages/types/src/types/mfa-login-result.ts new file mode 100644 index 000000000..53042ed67 --- /dev/null +++ b/packages/types/src/types/mfa-login-result.ts @@ -0,0 +1,4 @@ +export interface MFALoginResult { + mfaToken: string; + challenges: string[]; +} diff --git a/packages/types/src/types/user.ts b/packages/types/src/types/user.ts index b62f787bf..abc470f0f 100644 --- a/packages/types/src/types/user.ts +++ b/packages/types/src/types/user.ts @@ -6,4 +6,5 @@ export interface User { id: string; services?: object; deactivated: boolean; + mfaChallenges?: string[]; } From b2c250b119429ed71682d2f2208d0393cd7b4cc8 Mon Sep 17 00:00:00 2001 From: ozsay Date: Wed, 11 Sep 2019 14:14:38 +0300 Subject: [PATCH 02/15] continue working on mfa --- .../__snapshots__/accounts-client.ts.snap | 3 + packages/client/__tests__/accounts-client.ts | 41 ++++++++++- packages/client/src/accounts-client.ts | 35 ++++++++- packages/client/src/transport-interface.ts | 11 ++- .../modules/accounts/resolvers/mutation.ts | 18 +++++ packages/graphql-api/src/models.ts | 70 +++++++++++++++++- .../modules/accounts/resolvers/mutation.ts | 9 +++ .../src/modules/accounts/schema/mutation.ts | 4 +- .../src/modules/accounts/schema/types.ts | 7 ++ packages/graphql-client/src/graphql-client.ts | 23 +++++- .../graphql/login-with-service.mutation.ts | 18 +++-- .../graphql/perform-mfa-challenge.mutation.ts | 7 ++ packages/rest-client/__tests__/rest-client.ts | 12 ++++ packages/rest-client/src/rest-client.ts | 13 ++++ .../endpoints/perform-mfa-challenge.ts | 71 +++++++++++++++++++ .../__tests__/express-middleware.ts | 14 ++-- .../src/endpoints/oauth/provider-callback.ts | 4 +- .../src/endpoints/perform-mfa-challenge.ts | 16 +++++ .../rest-express/src/express-middleware.ts | 3 + 19 files changed, 356 insertions(+), 23 deletions(-) create mode 100644 packages/client/__tests__/__snapshots__/accounts-client.ts.snap create mode 100644 packages/graphql-client/src/graphql/perform-mfa-challenge.mutation.ts create mode 100644 packages/rest-express/__tests__/endpoints/perform-mfa-challenge.ts create mode 100644 packages/rest-express/src/endpoints/perform-mfa-challenge.ts diff --git a/packages/client/__tests__/__snapshots__/accounts-client.ts.snap b/packages/client/__tests__/__snapshots__/accounts-client.ts.snap new file mode 100644 index 000000000..f99740cd8 --- /dev/null +++ b/packages/client/__tests__/__snapshots__/accounts-client.ts.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`Accounts performMfaChallenge throws error when no mfaToken exists in storage: mfaToken-not-available 1`] = `[Error: mfaToken is not available in storage]`; diff --git a/packages/client/__tests__/accounts-client.ts b/packages/client/__tests__/accounts-client.ts index eac57b8ef..d0b2be5c4 100644 --- a/packages/client/__tests__/accounts-client.ts +++ b/packages/client/__tests__/accounts-client.ts @@ -11,6 +11,10 @@ const loggedInResponse = { }, }; +const mfaLoginResult = { + mfaToken: 'mfaToken', +}; + const impersonateResult = { authorized: true, tokens: { accessToken: 'newAccessToken', refreshToken: 'newRefreshToken' }, @@ -22,7 +26,10 @@ const tokens = { }; const mockTransport = { - loginWithService: jest.fn(() => Promise.resolve(loggedInResponse)), + loginWithService: jest.fn((service: string) => + service === 'mfa-login' ? Promise.resolve(mfaLoginResult) : Promise.resolve(loggedInResponse) + ), + performMfaChallenge: jest.fn(() => Promise.resolve('login-token')), logout: jest.fn(() => Promise.resolve()), refreshTokens: jest.fn(() => Promise.resolve(loggedInResponse)), sendResetPasswordEmail: jest.fn(() => Promise.resolve()), @@ -150,6 +157,38 @@ describe('Accounts', () => { loggedInResponse.tokens.refreshToken ); }); + + it('set the mfa token when mfa is enabled', async () => { + await accountsClient.loginWithService('mfa-login', { + username: 'user', + password: 'password', + }); + expect(localStorage.setItem).toHaveBeenCalledTimes(1); + expect(localStorage.getItem('accounts:mfaToken')).toEqual(mfaLoginResult.mfaToken); + }); + }); + + describe('performMfaChallenge', () => { + it('throws error when no mfaToken exists in storage', async () => { + await expect(accountsClient.performMfaChallenge('sms', {})).rejects.toMatchSnapshot( + 'mfaToken-not-available' + ); + }); + + it('performs the challenge and return the login result', async () => { + localStorage.setItem('accounts:mfaToken', 'mfa-token'); + + await accountsClient.performMfaChallenge('sms', {}); + + expect(localStorage.setItem).toHaveBeenCalledTimes(3); + expect(localStorage.getItem('accounts:accessToken')).toEqual( + loggedInResponse.tokens.accessToken + ); + expect(localStorage.getItem('accounts:refreshToken')).toEqual( + loggedInResponse.tokens.refreshToken + ); + expect(localStorage.getItem('accounts:mfaToken')).toBeNull(); + }); }); describe('refreshSession', () => { diff --git a/packages/client/src/accounts-client.ts b/packages/client/src/accounts-client.ts index dca9ffdd6..a90cf58a7 100644 --- a/packages/client/src/accounts-client.ts +++ b/packages/client/src/accounts-client.ts @@ -1,4 +1,4 @@ -import { LoginResult, Tokens, ImpersonationResult, User } from '@accounts/types'; +import { LoginResult, MFALoginResult, Tokens, ImpersonationResult, User } from '@accounts/types'; import { TransportInterface } from './transport-interface'; import { TokenStorage, AccountsClientOptions } from './types'; import { tokenStorageLocal } from './token-storage-local'; @@ -7,6 +7,7 @@ import { isTokenExpired } from './utils'; enum TokenKey { AccessToken = 'accessToken', RefreshToken = 'refreshToken', + MfaToken = 'mfaToken', OriginalAccessToken = 'originalAccessToken', OriginalRefreshToken = 'originalRefreshToken', } @@ -92,12 +93,40 @@ export class AccountsClient { public async loginWithService( service: string, credentials: { [key: string]: any } - ): Promise { + ): Promise> { const response = await this.transport.loginWithService(service, credentials); - await this.setTokens(response.tokens); + + if ((response as LoginResult).tokens) { + await this.setTokens((response as LoginResult).tokens); + } else { + await this.storage.setItem( + this.getTokenKey(TokenKey.MfaToken), + (response as MFALoginResult).mfaToken + ); + } + return response; } + /** + * Performs the mfa needed challenge and logs in afterwards + */ + public async performMfaChallenge(challenge: string, params: any): Promise { + const mfaToken = await this.storage.getItem(this.getTokenKey(TokenKey.MfaToken)); + + if (!mfaToken) { + throw new Error('mfaToken is not available in storage'); + } + + const loginToken = await this.transport.performMfaChallenge(challenge, mfaToken, params); + + const result = await this.loginWithService('mfa', { mfaToken, loginToken }); + + await this.storage.removeItem(this.getTokenKey(TokenKey.MfaToken)); + + return result as any; + } + /** * Refresh the user session * If the tokens have expired try to refresh them diff --git a/packages/client/src/transport-interface.ts b/packages/client/src/transport-interface.ts index c8e16297a..b8838c894 100644 --- a/packages/client/src/transport-interface.ts +++ b/packages/client/src/transport-interface.ts @@ -1,4 +1,10 @@ -import { LoginResult, ImpersonationResult, CreateUser, User } from '@accounts/types'; +import { + LoginResult, + MFALoginResult, + ImpersonationResult, + CreateUser, + User, +} from '@accounts/types'; import { AccountsClient } from './accounts-client'; export interface TransportInterface { @@ -9,7 +15,8 @@ export interface TransportInterface { authenticateParams: { [key: string]: string | object; } - ): Promise; + ): Promise; + performMfaChallenge(challenge: string, mfaToken: string, params: any): Promise; logout(): Promise; getUser(): Promise; refreshTokens(accessToken: string, refreshToken: string): Promise; diff --git a/packages/graphql-api/__tests__/modules/accounts/resolvers/mutation.ts b/packages/graphql-api/__tests__/modules/accounts/resolvers/mutation.ts index 50f5a65d3..4efe50ef2 100644 --- a/packages/graphql-api/__tests__/modules/accounts/resolvers/mutation.ts +++ b/packages/graphql-api/__tests__/modules/accounts/resolvers/mutation.ts @@ -4,6 +4,7 @@ import { Mutation } from '../../../../src/modules/accounts/resolvers/mutation'; describe('accounts resolvers mutation', () => { const accountsServerMock = { options: {}, + performMfaChallenge: jest.fn(), loginWithService: jest.fn(), impersonate: jest.fn(), logout: jest.fn(), @@ -39,6 +40,23 @@ describe('accounts resolvers mutation', () => { }); }); + describe('performMfaChallenge', () => { + const challenge = 'sms'; + const mfaToken = 'mfa-token'; + const params = {}; + + it('should call performMfaChallenge', async () => { + await Mutation.performMfaChallenge!( + {}, + { challenge, mfaToken, params } as any, + { injector, ip, userAgent } as any, + {} as any + ); + expect(injector.get).toBeCalledWith(AccountsServer); + expect(accountsServerMock.performMfaChallenge).toBeCalledWith(challenge, mfaToken, params); + }); + }); + describe('impersonate', () => { const accessToken = 'accessTokenTest'; const username = 'usernameTest'; diff --git a/packages/graphql-api/src/models.ts b/packages/graphql-api/src/models.ts index 33934612c..ba1d9625a 100644 --- a/packages/graphql-api/src/models.ts +++ b/packages/graphql-api/src/models.ts @@ -114,7 +114,9 @@ export interface Mutation { logout?: Maybe; - authenticate?: Maybe; + authenticate?: Maybe; + + performMfaChallenge?: Maybe; } export interface LoginResult { @@ -137,6 +139,12 @@ export interface ImpersonateReturn { user?: Maybe; } +export interface MfaLoginResult { + mfaToken?: Maybe; + + challenges?: Maybe<(Maybe)[]>; +} + // ==================================================== // Arguments // ==================================================== @@ -186,6 +194,19 @@ export interface AuthenticateMutationArgs { params: AuthenticateParamsInput; } +export interface PerformMfaChallengeMutationArgs { + challenge: string; + + mfaToken: string; + + params: AuthenticateParamsInput; +} + +// ==================================================== +// Unions +// ==================================================== + +export type LoginWithServiceResult = LoginResult | MfaLoginResult; import { GraphQLResolveInfo } from 'graphql'; @@ -379,7 +400,9 @@ export interface MutationResolvers { logout?: MutationLogoutResolver, TypeParent, TContext>; - authenticate?: MutationAuthenticateResolver, TypeParent, TContext>; + authenticate?: MutationAuthenticateResolver, TypeParent, TContext>; + + performMfaChallenge?: MutationPerformMfaChallengeResolver, TypeParent, TContext>; } export type MutationCreateUserResolver, Parent = {}, TContext = {}> = Resolver< @@ -491,7 +514,7 @@ export type MutationLogoutResolver, Parent = {}, TContext = { TContext >; export type MutationAuthenticateResolver< - R = Maybe, + R = Maybe, Parent = {}, TContext = {} > = Resolver; @@ -501,6 +524,19 @@ export interface MutationAuthenticateArgs { params: AuthenticateParamsInput; } +export type MutationPerformMfaChallengeResolver< + R = Maybe, + Parent = {}, + TContext = {} +> = Resolver; +export interface MutationPerformMfaChallengeArgs { + challenge: string; + + mfaToken: string; + + params: AuthenticateParamsInput; +} + export interface LoginResultResolvers { sessionId?: LoginResultSessionIdResolver, TypeParent, TContext>; @@ -559,6 +595,32 @@ export type ImpersonateReturnUserResolver< TContext = {} > = Resolver; +export interface MfaLoginResultResolvers { + mfaToken?: MfaLoginResultMfaTokenResolver, TypeParent, TContext>; + + challenges?: MfaLoginResultChallengesResolver)[]>, TypeParent, TContext>; +} + +export type MfaLoginResultMfaTokenResolver< + R = Maybe, + Parent = MfaLoginResult, + TContext = {} +> = Resolver; +export type MfaLoginResultChallengesResolver< + R = Maybe<(Maybe)[]>, + Parent = MfaLoginResult, + TContext = {} +> = Resolver; + +export interface LoginWithServiceResultResolvers { + __resolveType: LoginWithServiceResultResolveType; +} +export type LoginWithServiceResultResolveType< + R = 'LoginResult' | 'MFALoginResult', + Parent = LoginResult | MfaLoginResult, + TContext = {} +> = TypeResolveFn; + export type AuthDirectiveResolver = DirectiveResolverFn< Result, {}, @@ -601,6 +663,8 @@ export type IResolvers = { LoginResult?: LoginResultResolvers; Tokens?: TokensResolvers; ImpersonateReturn?: ImpersonateReturnResolvers; + MfaLoginResult?: MfaLoginResultResolvers; + LoginWithServiceResult?: LoginWithServiceResultResolvers; } & { [typeName: string]: never }; export type IDirectiveResolvers = { diff --git a/packages/graphql-api/src/modules/accounts/resolvers/mutation.ts b/packages/graphql-api/src/modules/accounts/resolvers/mutation.ts index 31e398d92..e85274915 100644 --- a/packages/graphql-api/src/modules/accounts/resolvers/mutation.ts +++ b/packages/graphql-api/src/modules/accounts/resolvers/mutation.ts @@ -14,6 +14,15 @@ export const Mutation: MutationResolvers> = }); return authenticated; }, + performMfaChallenge: async (_, args, ctx) => { + const { challenge, mfaToken, params } = args; + const { injector } = ctx; + + const loginToken = await injector + .get(AccountsServer) + .performMfaChallenge(challenge, mfaToken, params); + return loginToken; + }, impersonate: async (_, args, ctx) => { const { accessToken, username } = args; const { ip, userAgent, injector } = ctx; diff --git a/packages/graphql-api/src/modules/accounts/schema/mutation.ts b/packages/graphql-api/src/modules/accounts/schema/mutation.ts index 647a3c85a..6ce2cdee2 100644 --- a/packages/graphql-api/src/modules/accounts/schema/mutation.ts +++ b/packages/graphql-api/src/modules/accounts/schema/mutation.ts @@ -9,6 +9,8 @@ export default (config: AccountsModuleConfig) => gql` # Example: Login with password # authenticate(serviceName: "password", params: {password: "", user: {email: ""}}) - authenticate(serviceName: String!, params: AuthenticateParamsInput!): LoginResult + authenticate(serviceName: String!, params: AuthenticateParamsInput!): LoginWithServiceResult + + performMfaChallenge(challenge: String!, mfaToken: String!, params: AuthenticateParamsInput!): String } `; diff --git a/packages/graphql-api/src/modules/accounts/schema/types.ts b/packages/graphql-api/src/modules/accounts/schema/types.ts index 74354c5fa..48c0aa7cf 100644 --- a/packages/graphql-api/src/modules/accounts/schema/types.ts +++ b/packages/graphql-api/src/modules/accounts/schema/types.ts @@ -14,6 +14,13 @@ export default ({ userAsInterface }: AccountsModuleConfig) => gql` tokens: Tokens } + type MFALoginResult { + mfaToken: String + challenges: [String] + } + + union LoginWithServiceResult = LoginResult | MFALoginResult + type ImpersonateReturn { authorized: Boolean tokens: Tokens diff --git a/packages/graphql-client/src/graphql-client.ts b/packages/graphql-client/src/graphql-client.ts index f11787965..aecbd458c 100644 --- a/packages/graphql-client/src/graphql-client.ts +++ b/packages/graphql-client/src/graphql-client.ts @@ -1,8 +1,15 @@ import { TransportInterface, AccountsClient } from '@accounts/client'; -import { CreateUser, LoginResult, ImpersonationResult, User } from '@accounts/types'; +import { + CreateUser, + LoginResult, + ImpersonationResult, + User, + MFALoginResult, +} from '@accounts/types'; import { createUserMutation } from './graphql/create-user.mutation'; import { loginWithServiceMutation } from './graphql/login-with-service.mutation'; import { logoutMutation } from './graphql/logout.mutation'; +import { performMfaChallengeMutation } from './graphql/perform-mfa-challenge.mutation'; import { refreshTokensMutation } from './graphql/refresh-tokens.mutation'; import { verifyEmailMutation } from './graphql/verify-email.mutation'; import { sendResetPasswordEmailMutation } from './graphql/send-reset-password-email.mutation'; @@ -62,13 +69,25 @@ export default class GraphQLClient implements TransportInterface { public async loginWithService( service: string, authenticateParams: IAuthenticateParams - ): Promise { + ): Promise { return this.mutate(loginWithServiceMutation, 'authenticate', { serviceName: service, params: authenticateParams, }); } + public async performMfaChallenge( + challenge: string, + mfaToken: string, + params: IAuthenticateParams + ): Promise { + return this.mutate(performMfaChallengeMutation, 'performMfaChallenge', { + challenge, + mfaToken, + params, + }); + } + public async getUser(): Promise { return this.query(getUserQuery(this.options.userFieldsFragment), 'getUser'); } diff --git a/packages/graphql-client/src/graphql/login-with-service.mutation.ts b/packages/graphql-client/src/graphql/login-with-service.mutation.ts index ee7227a50..15a7ba1b1 100644 --- a/packages/graphql-client/src/graphql/login-with-service.mutation.ts +++ b/packages/graphql-client/src/graphql/login-with-service.mutation.ts @@ -3,10 +3,20 @@ import gql from 'graphql-tag'; export const loginWithServiceMutation = gql` mutation($serviceName: String!, $params: AuthenticateParamsInput!) { authenticate(serviceName: $serviceName, params: $params) { - sessionId - tokens { - refreshToken - accessToken + __typename + ... on LoginResult { + sessionId + tokens { + refreshToken + accessToken + } + } + ... on LoginResult { + sessionId + tokens { + refreshToken + accessToken + } } } } diff --git a/packages/graphql-client/src/graphql/perform-mfa-challenge.mutation.ts b/packages/graphql-client/src/graphql/perform-mfa-challenge.mutation.ts new file mode 100644 index 000000000..ba43761eb --- /dev/null +++ b/packages/graphql-client/src/graphql/perform-mfa-challenge.mutation.ts @@ -0,0 +1,7 @@ +import gql from 'graphql-tag'; + +export const performMfaChallengeMutation = gql` + mutation($challenge: String!, $mfaToken: String!, $params: AuthenticateParamsInput!) { + performMfaChallenge(challenge: $challenge, mfaToken: $mfaToken, params: $params) + } +`; diff --git a/packages/rest-client/__tests__/rest-client.ts b/packages/rest-client/__tests__/rest-client.ts index 4721f063e..90facc196 100644 --- a/packages/rest-client/__tests__/rest-client.ts +++ b/packages/rest-client/__tests__/rest-client.ts @@ -75,6 +75,18 @@ describe('RestClient', () => { }); }); + describe('performMfaChallenge', () => { + it('should call fetch with performMfaChallenge path', async () => { + await restClient.performMfaChallenge('sms', 'mfa-token', {}); + expect((window.fetch as jest.Mock).mock.calls[0][0]).toBe( + 'http://localhost:3000/accounts/performMfaChallenge' + ); + expect((window.fetch as jest.Mock).mock.calls[0][1].body).toBe( + '{"challenge":"sms","mfaToken":"mfa-token","params":{}}' + ); + }); + }); + describe('loginWithService', () => { it('should call fetch with authenticate path', async () => { await restClient.loginWithService('password', { diff --git a/packages/rest-client/src/rest-client.ts b/packages/rest-client/src/rest-client.ts index a564e087c..b80e86c42 100644 --- a/packages/rest-client/src/rest-client.ts +++ b/packages/rest-client/src/rest-client.ts @@ -68,6 +68,19 @@ export class RestClient implements TransportInterface { return this.fetch(`${provider}/authenticate`, args, customHeaders); } + public async performMfaChallenge( + challenge: string, + mfaToken: string, + params: any, + customHeaders?: object + ): Promise { + const args = { + method: 'POST', + body: JSON.stringify({ challenge, mfaToken, params }), + }; + return this.fetch(`performMfaChallenge`, args, customHeaders); + } + public impersonate( accessToken: string, impersonated: LoginUserIdentity, diff --git a/packages/rest-express/__tests__/endpoints/perform-mfa-challenge.ts b/packages/rest-express/__tests__/endpoints/perform-mfa-challenge.ts new file mode 100644 index 000000000..b4e1ebdad --- /dev/null +++ b/packages/rest-express/__tests__/endpoints/perform-mfa-challenge.ts @@ -0,0 +1,71 @@ +import { performMfaChallenge } from '../../src/endpoints/perform-mfa-challenge'; + +const res: any = { + json: jest.fn(), + status: jest.fn(() => res), +}; + +describe('performMfaChallenge', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls performMfaChallenge and returns the login token in json format', async () => { + const loginToken = 'login-token'; + const accountsServer = { + performMfaChallenge: jest.fn(() => loginToken), + }; + const middleware = performMfaChallenge(accountsServer as any); + + const req = { + body: { + challenge: 'sms', + mfaToken: 'mfa-token', + params: { + phone: '1122', + }, + }, + headers: {}, + }; + const reqCopy = { ...req }; + + await middleware(req as any, res); + + expect(req).toEqual(reqCopy); + expect(accountsServer.performMfaChallenge).toBeCalledWith('sms', 'mfa-token', { + phone: '1122', + }); + expect(res.json).toBeCalledWith(loginToken); + expect(res.status).not.toBeCalled(); + }); + + it('Sends error if it was thrown on performMfaChallenge', async () => { + const error = { message: 'Could not performMfaChallenge' }; + const accountsServer = { + performMfaChallenge: jest.fn(() => { + throw error; + }), + }; + const middleware = performMfaChallenge(accountsServer as any); + const req = { + body: { + challenge: 'sms', + mfaToken: 'mfa-token', + params: { + phone: '1122', + }, + }, + headers: {}, + }; + const reqCopy = { ...req }; + + await middleware(req as any, res); + + expect(req).toEqual(reqCopy); + expect(accountsServer.performMfaChallenge).toBeCalledWith('sms', 'mfa-token', { + phone: '1122', + }); + expect(res.status).toBeCalledWith(400); + expect(res.json).toBeCalledWith(error); + }); +}); diff --git a/packages/rest-express/__tests__/express-middleware.ts b/packages/rest-express/__tests__/express-middleware.ts index 9e0327225..f20b317b5 100644 --- a/packages/rest-express/__tests__/express-middleware.ts +++ b/packages/rest-express/__tests__/express-middleware.ts @@ -30,6 +30,7 @@ describe('express middleware', () => { expect((router.post as jest.Mock).mock.calls[2][0]).toBe('test/refreshTokens'); expect((router.post as jest.Mock).mock.calls[3][0]).toBe('test/logout'); expect((router.post as jest.Mock).mock.calls[4][0]).toBe('test/:service/authenticate'); + expect((router.post as jest.Mock).mock.calls[5][0]).toBe('test/performMfaChallenge'); expect((router.get as jest.Mock).mock.calls[0][0]).toBe('test/user'); }); @@ -48,11 +49,12 @@ describe('express middleware', () => { expect((router.post as jest.Mock).mock.calls[2][0]).toBe('test/refreshTokens'); expect((router.post as jest.Mock).mock.calls[3][0]).toBe('test/logout'); expect((router.post as jest.Mock).mock.calls[4][0]).toBe('test/:service/authenticate'); - expect((router.post as jest.Mock).mock.calls[5][0]).toBe('test/password/register'); - expect((router.post as jest.Mock).mock.calls[6][0]).toBe('test/password/verifyEmail'); - expect((router.post as jest.Mock).mock.calls[7][0]).toBe('test/password/resetPassword'); - expect((router.post as jest.Mock).mock.calls[8][0]).toBe('test/password/sendVerificationEmail'); - expect((router.post as jest.Mock).mock.calls[9][0]).toBe( + expect((router.post as jest.Mock).mock.calls[5][0]).toBe('test/performMfaChallenge'); + expect((router.post as jest.Mock).mock.calls[6][0]).toBe('test/password/register'); + expect((router.post as jest.Mock).mock.calls[7][0]).toBe('test/password/verifyEmail'); + expect((router.post as jest.Mock).mock.calls[8][0]).toBe('test/password/resetPassword'); + expect((router.post as jest.Mock).mock.calls[9][0]).toBe('test/password/sendVerificationEmail'); + expect((router.post as jest.Mock).mock.calls[10][0]).toBe( 'test/password/sendResetPasswordEmail' ); @@ -73,6 +75,7 @@ describe('express middleware', () => { expect((router.post as jest.Mock).mock.calls[2][0]).toBe('test/refreshTokens'); expect((router.post as jest.Mock).mock.calls[3][0]).toBe('test/logout'); expect((router.post as jest.Mock).mock.calls[4][0]).toBe('test/:service/authenticate'); + expect((router.post as jest.Mock).mock.calls[5][0]).toBe('test/performMfaChallenge'); expect((router.get as jest.Mock).mock.calls[0][0]).toBe('test/user'); expect((router.get as jest.Mock).mock.calls[1][0]).toBe('test/oauth/:provider/callback'); @@ -90,6 +93,7 @@ describe('express middleware', () => { expect((router.post as jest.Mock).mock.calls[2][0]).toBe('/refreshTokens'); expect((router.post as jest.Mock).mock.calls[3][0]).toBe('/logout'); expect((router.post as jest.Mock).mock.calls[4][0]).toBe('/:service/authenticate'); + expect((router.post as jest.Mock).mock.calls[5][0]).toBe('/performMfaChallenge'); expect((router.get as jest.Mock).mock.calls[0][0]).toBe('/user'); }); diff --git a/packages/rest-express/src/endpoints/oauth/provider-callback.ts b/packages/rest-express/src/endpoints/oauth/provider-callback.ts index 2002a5789..de548a3b6 100644 --- a/packages/rest-express/src/endpoints/oauth/provider-callback.ts +++ b/packages/rest-express/src/endpoints/oauth/provider-callback.ts @@ -28,11 +28,11 @@ export const providerCallback = ( ); if (options && options.onOAuthSuccess) { - options.onOAuthSuccess(req, res, loggedInUser); + options.onOAuthSuccess(req, res, loggedInUser as any); } if (options && options.transformOAuthResponse) { - res.json(options.transformOAuthResponse(loggedInUser)); + res.json(options.transformOAuthResponse(loggedInUser as any)); } else { res.json(loggedInUser); } diff --git a/packages/rest-express/src/endpoints/perform-mfa-challenge.ts b/packages/rest-express/src/endpoints/perform-mfa-challenge.ts new file mode 100644 index 000000000..c291a3c1b --- /dev/null +++ b/packages/rest-express/src/endpoints/perform-mfa-challenge.ts @@ -0,0 +1,16 @@ +import * as express from 'express'; +import { AccountsServer } from '@accounts/server'; +import { sendError } from '../utils/send-error'; + +export const performMfaChallenge = (accountsServer: AccountsServer) => async ( + req: express.Request, + res: express.Response +) => { + try { + const { challenge, mfaToken, params } = req.body; + const loginToken = await accountsServer.performMfaChallenge(challenge, mfaToken, params); + res.json(loginToken); + } catch (err) { + sendError(res, err); + } +}; diff --git a/packages/rest-express/src/express-middleware.ts b/packages/rest-express/src/express-middleware.ts index a3394eb0a..c95e84cac 100644 --- a/packages/rest-express/src/express-middleware.ts +++ b/packages/rest-express/src/express-middleware.ts @@ -8,6 +8,7 @@ import { getUser } from './endpoints/get-user'; import { impersonate } from './endpoints/impersonate'; import { logout } from './endpoints/logout'; import { serviceAuthenticate } from './endpoints/service-authenticate'; +import { performMfaChallenge } from './endpoints/perform-mfa-challenge'; import { registerPassword } from './endpoints/password/register'; import { twoFactorSecret, twoFactorSet, twoFactorUnset } from './endpoints/password/two-factor'; import { changePassword } from './endpoints/password/change-password'; @@ -43,6 +44,8 @@ const accountsExpress = ( router.post(`${path}/:service/authenticate`, serviceAuthenticate(accountsServer)); + router.post(`${path}/performMfaChallenge`, performMfaChallenge(accountsServer)); + const services = accountsServer.getServices(); // @accounts/password From b38f1621e581c81fa6f03e0d426752a5e7105787 Mon Sep 17 00:00:00 2001 From: ozsay Date: Wed, 11 Sep 2019 14:58:34 +0300 Subject: [PATCH 03/15] add typeorm implementation --- .../__tests__/database-tests.ts | 3 +- packages/database-typeorm/__tests__/index.ts | 65 ++++++++++++++++++- packages/database-typeorm/package.json | 3 + .../src/entity/MfaLoginAttempt.ts | 22 +++++++ packages/database-typeorm/src/index.ts | 5 +- packages/database-typeorm/src/typeorm.ts | 30 ++++++++- packages/database-typeorm/src/types/index.ts | 2 + 7 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 packages/database-typeorm/src/entity/MfaLoginAttempt.ts diff --git a/packages/database-typeorm/__tests__/database-tests.ts b/packages/database-typeorm/__tests__/database-tests.ts index 2bfde8829..27e8e7380 100644 --- a/packages/database-typeorm/__tests__/database-tests.ts +++ b/packages/database-typeorm/__tests__/database-tests.ts @@ -5,6 +5,7 @@ import { User } from '../src/entity/User'; import { UserEmail } from '../src/entity/UserEmail'; import { UserService } from '../src/entity/UserService'; import { UserSession } from '../src/entity/UserSession'; +import { MfaLoginAttempt } from '../src/entity/MfaLoginAttempt'; const database = 'accounts-js-tests-e2e'; @@ -35,7 +36,7 @@ export class DatabaseTests { type: 'postgres', host: 'localhost', port: 5432, - entities: [User, UserEmail, UserService, UserSession], + entities: [User, UserEmail, UserService, UserSession, MfaLoginAttempt], username: 'postgres', password: '', database, diff --git a/packages/database-typeorm/__tests__/index.ts b/packages/database-typeorm/__tests__/index.ts index 39784a589..61be4a2ae 100644 --- a/packages/database-typeorm/__tests__/index.ts +++ b/packages/database-typeorm/__tests__/index.ts @@ -2,7 +2,7 @@ import 'reflect-metadata'; import { randomBytes } from 'crypto'; import { DatabaseTests } from './database-tests'; import { AccountsTypeorm } from '../src/typeorm'; - +import { MfaLoginAttempt } from '../src/entity/MfaLoginAttempt'; const databaseTests = new DatabaseTests(); const generateRandomToken = (length: number = 43): string => randomBytes(length).toString('hex'); @@ -571,4 +571,67 @@ describe('AccountsTypeorm', () => { expect((retUser as any).createdAt).not.toEqual((retUser as any).updatedAt); }); }); + + describe('createMfaLoginAttempt', () => { + it('should create mfa login attempt', async () => { + const mfaToken = generateRandomToken(); + const loginToken = generateRandomToken(); + const userId = '123'; + + await databaseTests.database.createMfaLoginAttempt(mfaToken, loginToken, userId); + + const resFromDb = await databaseTests + .connection!.getRepository(MfaLoginAttempt) + .findOne(mfaToken); + + expect(resFromDb).toEqual({ + id: mfaToken, + mfaToken, + loginToken, + userId, + createdAt: expect.any(Date), + updatedAt: expect.any(Date), + }); + }); + }); + + describe('removeMfaLoginAttempt', () => { + it('should remove mfa login attempt', async () => { + const entity: any = { + mfaToken: generateRandomToken(), + loginToken: generateRandomToken(), + userId: '123', + }; + + entity.id = entity.mfaToken; + + await databaseTests.connection!.getRepository(MfaLoginAttempt).save(entity); + + await databaseTests.database.removeMfaLoginAttempt(entity.mfaToken); + + const resFromDb = await databaseTests + .connection!.getRepository(MfaLoginAttempt) + .findOne(entity.mfaToken); + + expect(resFromDb).toBeUndefined(); + }); + }); + + describe('getMfaLoginAttempt', () => { + it('should return mfa login attempt', async () => { + const entity: any = { + mfaToken: generateRandomToken(), + loginToken: generateRandomToken(), + userId: '123', + }; + + entity.id = entity.mfaToken; + + await databaseTests.connection!.getRepository(MfaLoginAttempt).save(entity); + + const attempt = await databaseTests.database.getMfaLoginAttempt(entity.mfaToken); + + expect(attempt).toEqual(entity); + }); + }); }); diff --git a/packages/database-typeorm/package.json b/packages/database-typeorm/package.json index 5c2e1dbfa..34e9ada22 100644 --- a/packages/database-typeorm/package.json +++ b/packages/database-typeorm/package.json @@ -7,6 +7,9 @@ "scripts": { "clean": "rimraf lib", "start": "tsc --watch", + "start:db": "docker run --name accounts-postgres -e POSTGRES_USER=postgres -e POSTGRES_DB=accounts-js-tests-e2e -p 5432:5432 -d postgres", + "stop:db": "docker stop accounts-postgres", + "rm:db": "docker rm accounts-postgres", "precompile": "yarn clean", "compile": "tsc", "prepublishOnly": "yarn compile", diff --git a/packages/database-typeorm/src/entity/MfaLoginAttempt.ts b/packages/database-typeorm/src/entity/MfaLoginAttempt.ts new file mode 100644 index 000000000..0e6242019 --- /dev/null +++ b/packages/database-typeorm/src/entity/MfaLoginAttempt.ts @@ -0,0 +1,22 @@ +import { Entity, Column, PrimaryColumn, UpdateDateColumn, CreateDateColumn } from 'typeorm'; + +@Entity() +export class MfaLoginAttempt { + @PrimaryColumn() + public id!: string; + + @Column() + public mfaToken!: string; + + @Column() + public loginToken!: string; + + @Column() + public userId!: string; + + @CreateDateColumn() + public createdAt!: string; + + @UpdateDateColumn() + public updatedAt!: string; +} diff --git a/packages/database-typeorm/src/index.ts b/packages/database-typeorm/src/index.ts index 3f53f4a6e..04cb50e6e 100644 --- a/packages/database-typeorm/src/index.ts +++ b/packages/database-typeorm/src/index.ts @@ -3,8 +3,9 @@ import { User } from './entity/User'; import { UserEmail } from './entity/UserEmail'; import { UserService } from './entity/UserService'; import { UserSession } from './entity/UserSession'; +import { MfaLoginAttempt } from './entity/MfaLoginAttempt'; -const entities = [User, UserEmail, UserService, UserSession]; +const entities = [User, UserEmail, UserService, UserSession, MfaLoginAttempt]; -export { AccountsTypeorm, User, UserEmail, UserService, UserSession, entities }; +export { AccountsTypeorm, User, UserEmail, UserService, UserSession, MfaLoginAttempt, entities }; export default AccountsTypeorm; diff --git a/packages/database-typeorm/src/typeorm.ts b/packages/database-typeorm/src/typeorm.ts index a14303125..f533ae1ad 100644 --- a/packages/database-typeorm/src/typeorm.ts +++ b/packages/database-typeorm/src/typeorm.ts @@ -1,9 +1,15 @@ -import { ConnectionInformations, CreateUser, DatabaseInterface } from '@accounts/types'; +import { + ConnectionInformations, + CreateUser, + DatabaseInterface, + MfaLoginAttempt as MfaLoginAttemptType, +} from '@accounts/types'; import { Repository, getRepository } from 'typeorm'; import { User } from './entity/User'; import { UserEmail } from './entity/UserEmail'; import { UserService } from './entity/UserService'; import { UserSession } from './entity/UserSession'; +import { MfaLoginAttempt } from './entity/MfaLoginAttempt'; import { AccountsTypeormOptions } from './types'; const defaultOptions = { @@ -11,6 +17,7 @@ const defaultOptions = { userEmailEntity: UserEmail, userServiceEntity: UserService, userSessionEntity: UserSession, + mfaLoginAttemptEntity: MfaLoginAttempt, }; export class AccountsTypeorm implements DatabaseInterface { @@ -19,6 +26,7 @@ export class AccountsTypeorm implements DatabaseInterface { private emailRepository: Repository = null as any; private serviceRepository: Repository = null as any; private sessionRepository: Repository = null as any; + private mfaLoginAttemptRepository: Repository = null as any; constructor(options?: AccountsTypeormOptions) { this.options = { ...defaultOptions, ...options }; @@ -30,6 +38,7 @@ export class AccountsTypeorm implements DatabaseInterface { userEmailEntity, userServiceEntity, userSessionEntity, + mfaLoginAttemptEntity, } = this.options; const setRepositories = () => { @@ -38,11 +47,13 @@ export class AccountsTypeorm implements DatabaseInterface { this.emailRepository = connection.getRepository(userEmailEntity); this.serviceRepository = connection.getRepository(userServiceEntity); this.sessionRepository = connection.getRepository(userSessionEntity); + this.mfaLoginAttemptRepository = connection.getRepository(mfaLoginAttemptEntity); } else { this.userRepository = getRepository(userEntity, connectionName); this.emailRepository = getRepository(userEmailEntity, connectionName); this.serviceRepository = getRepository(userServiceEntity, connectionName); this.sessionRepository = getRepository(userSessionEntity, connectionName); + this.mfaLoginAttemptRepository = getRepository(mfaLoginAttemptEntity, connectionName); } }; @@ -427,4 +438,21 @@ export class AccountsTypeorm implements DatabaseInterface { } ); } + + public async createMfaLoginAttempt( + mfaToken: string, + loginToken: string, + userId: string + ): Promise { + await this.mfaLoginAttemptRepository.save({ id: mfaToken, mfaToken, loginToken, userId }); + } + public async getMfaLoginAttempt(mfaToken: string): Promise { + const res = await this.mfaLoginAttemptRepository.findOne(mfaToken); + + return res || null; + } + public async removeMfaLoginAttempt(mfaToken: string): Promise { + const mfaLoginAttempt = await this.getMfaLoginAttempt(mfaToken); + await this.mfaLoginAttemptRepository.remove(mfaLoginAttempt as MfaLoginAttempt); + } } diff --git a/packages/database-typeorm/src/types/index.ts b/packages/database-typeorm/src/types/index.ts index 493d96d04..de8c96a5d 100644 --- a/packages/database-typeorm/src/types/index.ts +++ b/packages/database-typeorm/src/types/index.ts @@ -2,6 +2,7 @@ import { User } from '../entity/User'; import { UserEmail } from '../entity/UserEmail'; import { UserService } from '../entity/UserService'; import { UserSession } from '../entity/UserSession'; +import { MfaLoginAttempt } from '../entity/MfaLoginAttempt'; import { Connection } from 'typeorm'; export interface AccountsTypeormOptions { @@ -12,4 +13,5 @@ export interface AccountsTypeormOptions { userServiceEntity?: typeof UserService; userEmailEntity?: typeof UserEmail; userSessionEntity?: typeof UserSession; + mfaLoginAttemptEntity?: typeof MfaLoginAttempt; } From 5eb219dce300a10a9202b8d43cc9519e690970f9 Mon Sep 17 00:00:00 2001 From: ozsay Date: Wed, 11 Sep 2019 15:02:57 +0300 Subject: [PATCH 04/15] fix client-password --- packages/client-password/src/client-password.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/client-password/src/client-password.ts b/packages/client-password/src/client-password.ts index 607630d41..af2777e27 100644 --- a/packages/client-password/src/client-password.ts +++ b/packages/client-password/src/client-password.ts @@ -1,5 +1,5 @@ import { AccountsClient } from '@accounts/client'; -import { LoginResult, CreateUser } from '@accounts/types'; +import { LoginResult, CreateUser, MFALoginResult } from '@accounts/types'; import { AccountsClientPasswordOptions } from './types'; export class AccountsClientPassword { @@ -25,7 +25,7 @@ export class AccountsClientPassword { /** * Log the user in with a password. */ - public async login(user: any): Promise { + public async login(user: any): Promise> { const hashedPassword = this.hashPassword(user.password); return this.client.loginWithService('password', { ...user, From c2baa53532432bc4af2c6a2b923b95f4d7d4302d Mon Sep 17 00:00:00 2001 From: ozsay Date: Wed, 11 Sep 2019 15:09:40 +0300 Subject: [PATCH 05/15] fix graphql-client --- .../src/graphql/login-with-service.mutation.ts | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/graphql-client/src/graphql/login-with-service.mutation.ts b/packages/graphql-client/src/graphql/login-with-service.mutation.ts index 15a7ba1b1..3e39782e0 100644 --- a/packages/graphql-client/src/graphql/login-with-service.mutation.ts +++ b/packages/graphql-client/src/graphql/login-with-service.mutation.ts @@ -11,12 +11,9 @@ export const loginWithServiceMutation = gql` accessToken } } - ... on LoginResult { - sessionId - tokens { - refreshToken - accessToken - } + ... on MFALoginResult { + mfaToken + challenges } } } From f9e08bc65b7829e6d4a52f25c1a4b89a985c2cb7 Mon Sep 17 00:00:00 2001 From: ozsay Date: Wed, 11 Sep 2019 15:22:18 +0300 Subject: [PATCH 06/15] fix accounts-boost --- packages/boost/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/boost/src/index.ts b/packages/boost/src/index.ts index 9a2acc990..613f44b55 100644 --- a/packages/boost/src/index.ts +++ b/packages/boost/src/index.ts @@ -67,6 +67,7 @@ export const accountsBoost = async (userOptions?: AccountsBoostOptions): Promise options.db = new DatabaseManager({ userStorage: storage, sessionStorage: storage, + mfaLoginAttemptsStorage: storage, }); const servicePackages = { From a23142f188572a6c01f38fe0357187b9b5f6b290 Mon Sep 17 00:00:00 2001 From: ozsay Date: Wed, 11 Sep 2019 16:44:14 +0300 Subject: [PATCH 07/15] fixed e2e tests and graphql client --- docker-compose.yml | 23 +- packages/database-manager/package.json | 2 +- packages/database-typeorm/package.json | 3 - packages/e2e/__tests__/password.ts | 13 +- .../e2e/__tests__/servers/server-graphql.ts | 11 +- packages/e2e/package.json | 4 +- .../graphql-api/src/modules/accounts/index.ts | 6 +- .../modules/accounts/resolvers/loginResult.ts | 10 + packages/graphql-client/codegen.yml | 8 + packages/graphql-client/package.json | 11 +- packages/graphql-client/src/index.ts | 1 + .../src/introspection-result.ts | 34 ++ packages/graphql-client/src/schema.ts | 4 + packages/password/package.json | 2 +- packages/two-factor/package.json | 2 +- yarn.lock | 331 +++++++++++++++++- 16 files changed, 420 insertions(+), 45 deletions(-) create mode 100644 packages/graphql-client/codegen.yml create mode 100644 packages/graphql-client/src/introspection-result.ts create mode 100644 packages/graphql-client/src/schema.ts diff --git a/docker-compose.yml b/docker-compose.yml index fc479dd98..7abe9801e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,16 +1,17 @@ -version: '3.6' +version: '3' services: - postgres: - image: circleci/postgres:10.10 - ports: - - '5432:5432' - environment: - POSTGRES_DB: accounts-js-tests-e2e mongo: - image: circleci/mongo:3 + image: mongo ports: - - '27017:27017' + - "27017:27017" redis: - image: circleci/redis:4 + image: redis ports: - - '6379:6379' + - "6379:6379" + postgres: + image: postgres + ports: + - "5432:5432" + environment: + - POSTGRES_USER=postgres + - POSTGRES_DB=accounts-js-tests-e2e \ No newline at end of file diff --git a/packages/database-manager/package.json b/packages/database-manager/package.json index 6f86d2494..d87993698 100644 --- a/packages/database-manager/package.json +++ b/packages/database-manager/package.json @@ -14,7 +14,7 @@ "compile": "tsc", "prepublishOnly": "yarn compile", "test": "npm run test", - "testonly": "jest --coverage", + "testonly": "jest", "coverage": "jest --coverage" }, "jest": { diff --git a/packages/database-typeorm/package.json b/packages/database-typeorm/package.json index 34e9ada22..5c2e1dbfa 100644 --- a/packages/database-typeorm/package.json +++ b/packages/database-typeorm/package.json @@ -7,9 +7,6 @@ "scripts": { "clean": "rimraf lib", "start": "tsc --watch", - "start:db": "docker run --name accounts-postgres -e POSTGRES_USER=postgres -e POSTGRES_DB=accounts-js-tests-e2e -p 5432:5432 -d postgres", - "stop:db": "docker stop accounts-postgres", - "rm:db": "docker rm accounts-postgres", "precompile": "yarn clean", "compile": "tsc", "prepublishOnly": "yarn compile", diff --git a/packages/e2e/__tests__/password.ts b/packages/e2e/__tests__/password.ts index 8c3903f69..dec386363 100644 --- a/packages/e2e/__tests__/password.ts +++ b/packages/e2e/__tests__/password.ts @@ -1,3 +1,4 @@ +import { LoginResult } from '@accounts/types'; import { servers } from './servers'; const user = { @@ -38,12 +39,12 @@ Object.keys(servers).forEach(key => { }); it('should login the user and get the session', async () => { - const loginResult = await server.accountsClientPassword.login({ + const loginResult = (await server.accountsClientPassword.login({ user: { email: user.email, }, password: user.password, - }); + })) as LoginResult; expect(loginResult.sessionId).toBeTruthy(); expect(loginResult.tokens.accessToken).toBeTruthy(); expect(loginResult.tokens.refreshToken).toBeTruthy(); @@ -99,12 +100,12 @@ Object.keys(servers).forEach(key => { user.password = newPassword; expect(data).toBeNull(); - const loginResult = await server.accountsClientPassword.login({ + const loginResult = (await server.accountsClientPassword.login({ user: { email: user.email, }, password: user.password, - }); + })) as LoginResult; expect(loginResult.sessionId).toBeTruthy(); expect(loginResult.tokens.accessToken).toBeTruthy(); expect(loginResult.tokens.refreshToken).toBeTruthy(); @@ -127,12 +128,12 @@ Object.keys(servers).forEach(key => { user.password = newPassword; expect(data).toBeNull(); - const loginResult = await server.accountsClientPassword.login({ + const loginResult = (await server.accountsClientPassword.login({ user: { email: user.email, }, password: user.password, - }); + })) as LoginResult; expect(loginResult.sessionId).toBeTruthy(); expect(loginResult.tokens.accessToken).toBeTruthy(); expect(loginResult.tokens.refreshToken).toBeTruthy(); diff --git a/packages/e2e/__tests__/servers/server-graphql.ts b/packages/e2e/__tests__/servers/server-graphql.ts index a97957d00..fcc89c527 100644 --- a/packages/e2e/__tests__/servers/server-graphql.ts +++ b/packages/e2e/__tests__/servers/server-graphql.ts @@ -1,13 +1,14 @@ import { AccountsClient } from '@accounts/client'; import { AccountsClientPassword } from '@accounts/client-password'; import { AccountsModule } from '@accounts/graphql-api'; -import { AccountsGraphQLClient } from '@accounts/graphql-client'; +import { AccountsGraphQLClient, IntrospectionResult } from '@accounts/graphql-client'; import { AccountsPassword } from '@accounts/password'; import { AccountsServer } from '@accounts/server'; import { DatabaseInterface, User } from '@accounts/types'; import ApolloClient from 'apollo-boost'; import { ApolloServer } from 'apollo-server'; import fetch from 'node-fetch'; +import { IntrospectionFragmentMatcher, InMemoryCache } from 'apollo-cache-inmemory'; import { ServerTestInterface } from '.'; import { DatabaseTestInterface } from '../databases'; @@ -82,7 +83,13 @@ export class ServerGraphqlTest implements ServerTestInterface { context, }); - const apolloClient = new ApolloClient({ uri: `http://localhost:${this.port}` }); + const fragmentMatcher = new IntrospectionFragmentMatcher({ + introspectionQueryResultData: IntrospectionResult, + }); + + const cache = new InMemoryCache({ fragmentMatcher }); + + const apolloClient = new ApolloClient({ uri: `http://localhost:${this.port}`, cache }); const accountsClientGraphQL = new AccountsGraphQLClient({ graphQLClient: apolloClient, diff --git a/packages/e2e/package.json b/packages/e2e/package.json index e2d1e59e2..5bd6fb413 100644 --- a/packages/e2e/package.json +++ b/packages/e2e/package.json @@ -5,7 +5,8 @@ "main": "lib/index.js", "typings": "lib/index.d.ts", "scripts": { - "coverage": "jest --forceExit --runInBand" + "testonly": "jest --forceExit --runInBand", + "coverage": "yarn testonly" }, "jest": { "testEnvironment": "node", @@ -48,6 +49,7 @@ "@types/mongoose": "5.5.17", "@types/node-fetch": "2.5.0", "apollo-boost": "0.4.4", + "apollo-cache-inmemory": "^1.6.3", "apollo-server": "2.9.3", "body-parser": "1.19.0", "core-js": "3.2.1", diff --git a/packages/graphql-api/src/modules/accounts/index.ts b/packages/graphql-api/src/modules/accounts/index.ts index 9c95f1306..486286530 100644 --- a/packages/graphql-api/src/modules/accounts/index.ts +++ b/packages/graphql-api/src/modules/accounts/index.ts @@ -8,7 +8,10 @@ import getSchemaDef from './schema/schema-def'; import { Query } from './resolvers/query'; import { Mutation } from './resolvers/mutation'; import { User as UserResolvers } from './resolvers/user'; -import { LoginResult as LoginResultResolvers } from './resolvers/loginResult'; +import { + LoginResult as LoginResultResolvers, + LoginWithServiceResult, +} from './resolvers/loginResult'; import { User } from '@accounts/types'; import { AccountsPasswordModule } from '../accounts-password'; import { AuthenticatedDirective } from '../../utils/authenticated-directive'; @@ -65,6 +68,7 @@ export const AccountsModule: GraphQLModule< [config.rootMutationName || 'Mutation']: Mutation, User: UserResolvers, LoginResult: LoginResultResolvers, + LoginWithServiceResult, } as any), // If necessary, import AccountsPasswordModule together with this module imports: ({ config }) => diff --git a/packages/graphql-api/src/modules/accounts/resolvers/loginResult.ts b/packages/graphql-api/src/modules/accounts/resolvers/loginResult.ts index 9d27a7062..6d88318a6 100644 --- a/packages/graphql-api/src/modules/accounts/resolvers/loginResult.ts +++ b/packages/graphql-api/src/modules/accounts/resolvers/loginResult.ts @@ -1 +1,11 @@ export const LoginResult = {}; + +export const LoginWithServiceResult = { + __resolveType(obj: any) { + if (obj.tokens) { + return 'LoginResult'; + } + + return 'MFALoginResult'; + }, +}; diff --git a/packages/graphql-client/codegen.yml b/packages/graphql-client/codegen.yml new file mode 100644 index 000000000..dbf2923e7 --- /dev/null +++ b/packages/graphql-client/codegen.yml @@ -0,0 +1,8 @@ +overwrite: true +schema: ./src/schema.ts +require: ts-node/register/transpile-only +generates: + ./src/introspection-result.ts: + plugins: + - add: /* tslint:disable */ + - fragment-matcher diff --git a/packages/graphql-client/package.json b/packages/graphql-client/package.json index cd45cac38..cc574bf4c 100644 --- a/packages/graphql-client/package.json +++ b/packages/graphql-client/package.json @@ -7,7 +7,8 @@ "scripts": { "clean": "rimraf lib", "start": "tsc --watch", - "precompile": "yarn clean", + "precompile": "yarn clean && yarn gen:types", + "gen:types": "graphql-codegen --config codegen.yml", "compile": "tsc", "prepublishOnly": "yarn compile" }, @@ -25,10 +26,16 @@ }, "homepage": "https://github.com/js-accounts/graphql#readme", "devDependencies": { + "@accounts/graphql-api": "^0.19.0", + "@graphql-codegen/add": "^1.7.0", + "@graphql-codegen/cli": "^1.7.0", + "@graphql-codegen/fragment-matcher": "^1.7.0", "@types/jest": "24.0.18", + "graphql": "^14.5.4", "jest": "24.9.0", "lodash": "4.17.15", - "nock": "10.0.6" + "nock": "10.0.6", + "ts-node": "8.3.0" }, "dependencies": { "@accounts/client": "^0.19.0", diff --git a/packages/graphql-client/src/index.ts b/packages/graphql-client/src/index.ts index 933ac76a9..0e298a67c 100644 --- a/packages/graphql-client/src/index.ts +++ b/packages/graphql-client/src/index.ts @@ -1,3 +1,4 @@ export * from './graphql-client'; export { default } from './graphql-client'; export { default as AccountsGraphQLClient } from './graphql-client'; +export { IntrospectionResultData, default as IntrospectionResult } from './introspection-result'; diff --git a/packages/graphql-client/src/introspection-result.ts b/packages/graphql-client/src/introspection-result.ts new file mode 100644 index 000000000..66771b833 --- /dev/null +++ b/packages/graphql-client/src/introspection-result.ts @@ -0,0 +1,34 @@ +/* tslint:disable */ + +export interface IntrospectionResultData { + __schema: { + types: { + kind: string; + name: string; + possibleTypes: { + name: string; + }[]; + }[]; + }; +} + +const result: IntrospectionResultData = { + __schema: { + types: [ + { + kind: 'UNION', + name: 'LoginWithServiceResult', + possibleTypes: [ + { + name: 'LoginResult', + }, + { + name: 'MFALoginResult', + }, + ], + }, + ], + }, +}; + +export default result; diff --git a/packages/graphql-client/src/schema.ts b/packages/graphql-client/src/schema.ts new file mode 100644 index 000000000..c83d65399 --- /dev/null +++ b/packages/graphql-client/src/schema.ts @@ -0,0 +1,4 @@ +// tslint:disable-next-line: no-submodule-imports +import typedefs from '@accounts/graphql-api/lib/schema'; + +export default typedefs; diff --git a/packages/password/package.json b/packages/password/package.json index 93eed209a..60d3ee9e0 100644 --- a/packages/password/package.json +++ b/packages/password/package.json @@ -10,7 +10,7 @@ "precompile": "yarn clean", "compile": "tsc", "prepublishOnly": "yarn compile", - "testonly": "jest --coverage", + "testonly": "jest", "coverage": "jest --coverage" }, "jest": { diff --git a/packages/two-factor/package.json b/packages/two-factor/package.json index 3e9748d9e..3dba9d1ad 100644 --- a/packages/two-factor/package.json +++ b/packages/two-factor/package.json @@ -14,7 +14,7 @@ "compile": "tsc", "prepublishOnly": "yarn compile", "test": "npm run test", - "testonly": "jest --coverage", + "testonly": "jest", "coverage": "jest --coverage" }, "jest": { diff --git a/yarn.lock b/yarn.lock index 140f5bd68..85f1fafd0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -253,7 +253,7 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.2.0", "@babel/parser@^7.4.3", "@babel/parser@^7.4.4", "@babel/parser@^7.5.5": +"@babel/parser@7.5.5", "@babel/parser@^7.0.0", "@babel/parser@^7.1.0", "@babel/parser@^7.2.0", "@babel/parser@^7.4.3", "@babel/parser@^7.4.4", "@babel/parser@^7.5.5": version "7.5.5" resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.5.5.tgz#02f077ac8817d3df4a832ef59de67565e71cca4b" integrity sha512-E5BN68cqR7dhKan1SfqgPGhQ178bkVKpXTPEXnFJBrEt8/DKRZlybmy+IgYLTeN7tp1R5Ccmbm2rBk17sHYU3g== @@ -868,6 +868,83 @@ resolved "https://registry.yarnpkg.com/@gql2ts/util/-/util-1.9.0.tgz#d07a54832757d2f2d1fc9891e5b0e3e3b4886c6a" integrity sha512-mkHar7AdyShUFJE6Mlke1tUbb+lPCK1EozZeAhCuRrhQ5aCCBAG6RxzNUYX1Q2jeGeyU0WRAtQu1oE/GoIsNXA== +"@graphql-codegen/add@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@graphql-codegen/add/-/add-1.7.0.tgz#8969e51913c4013e2ab098205184f38ca517cf7e" + integrity sha512-sk561sxOurcPTUS864pXDEh0sh/E5t0BRWPEdbSIcQ80Ia6WrwA6tuZfs59rkMlw99fpDTOGwjOHzqprZ9DzwQ== + dependencies: + "@graphql-codegen/plugin-helpers" "1.7.0" + tslib "1.10.0" + +"@graphql-codegen/cli@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@graphql-codegen/cli/-/cli-1.7.0.tgz#aade52f5c265450258e8fe0e45dc91268e8939a8" + integrity sha512-glrk7A7vzazF8mfR3fUL7baorjxL9w3hFqPmLMp9uEMXyIP3Z0MrRacbDkFzeYXcGsm7K4k+ZK5Og2g8shVCuA== + dependencies: + "@babel/parser" "7.5.5" + "@graphql-codegen/core" "1.7.0" + "@graphql-codegen/plugin-helpers" "1.7.0" + "@types/debounce" "1.2.0" + "@types/is-glob" "4.0.1" + "@types/mkdirp" "0.5.2" + "@types/valid-url" "1.0.2" + babel-types "7.0.0-beta.3" + chalk "2.4.2" + change-case "3.1.0" + chokidar "3.0.2" + commander "3.0.1" + common-tags "1.8.0" + debounce "1.2.0" + detect-indent "6.0.0" + glob "7.1.4" + graphql-config "2.2.1" + graphql-import "0.7.1" + graphql-tag-pluck "0.8.4" + graphql-toolkit "0.5.11" + graphql-tools "4.0.5" + indent-string "4.0.0" + inquirer "7.0.0" + is-glob "4.0.1" + is-valid-path "0.1.1" + js-yaml "3.13.1" + json-to-pretty-yaml "1.2.2" + listr "0.14.3" + listr-update-renderer "0.5.0" + log-symbols "3.0.0" + log-update "3.2.0" + mkdirp "0.5.1" + prettier "1.18.2" + request "2.88.0" + ts-log "2.1.4" + tslib "1.10.0" + valid-url "1.0.9" + +"@graphql-codegen/core@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@graphql-codegen/core/-/core-1.7.0.tgz#fdad4d3ea9de998f9effd0df492ac3c4c2633bbc" + integrity sha512-NghsdPhI4eqjOJvzC2f8sHPJL7vx4hMTXeg2U90YWtv07lQoxefsJwi4UND6dyALUoH5MdgMyxJl6LM9mYzOVA== + dependencies: + "@graphql-codegen/plugin-helpers" "1.7.0" + graphql-toolkit "0.5.11" + tslib "1.10.0" + +"@graphql-codegen/fragment-matcher@^1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@graphql-codegen/fragment-matcher/-/fragment-matcher-1.7.0.tgz#4ecc7ac2efd1ee52847a2334980dd9c6124652cf" + integrity sha512-uzOsoiKbLGSqiJ5hH+KsnTb1qhbytBz0ko+uhU4XzkHzOFJj8wgJO2j+B38fuZSjOVPuM98Zb/sBSkczEimdmA== + dependencies: + "@graphql-codegen/plugin-helpers" "1.7.0" + +"@graphql-codegen/plugin-helpers@1.7.0": + version "1.7.0" + resolved "https://registry.yarnpkg.com/@graphql-codegen/plugin-helpers/-/plugin-helpers-1.7.0.tgz#b1870a166cf34b2c67c053f2a9ec77823f78ac2d" + integrity sha512-lUWd5A9BQNbPqlMr38Gh5sLsBgMnn26n90/hyTw2J7CFCKFKSMnNBjxfCZU5AFHGxwi6rsNEpwBHRBx3OVWsTA== + dependencies: + change-case "3.1.0" + common-tags "1.8.0" + import-from "3.0.0" + tslib "1.10.0" + "@graphql-modules/core@0.7.11": version "0.7.11" resolved "https://registry.yarnpkg.com/@graphql-modules/core/-/core-0.7.11.tgz#5eab67d25045967c33bf40de5c0eb972370ce0df" @@ -1457,6 +1534,11 @@ dependencies: "@types/express" "*" +"@types/debounce@1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@types/debounce/-/debounce-1.2.0.tgz#9ee99259f41018c640b3929e1bb32c3dcecdb192" + integrity sha512-bWG5wapaWgbss9E238T0R6bfo5Fh3OkeoSt245CM7JJwVwpw6MEBCbIxLq5z8KzsE3uJhzcIuQkyiZmzV3M/Dw== + "@types/eslint-visitor-keys@^1.0.0": version "1.0.0" resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" @@ -1540,6 +1622,11 @@ resolved "https://registry.yarnpkg.com/@types/is-glob/-/is-glob-4.0.0.tgz#fb8a2bff539025d4dcd6d5efe7689e03341b876d" integrity sha512-zC/2EmD8scdsGIeE+Xg7kP7oi9VP90zgMQtm9Cr25av4V+a+k8slQyiT60qSw8KORYrOKlPXfHwoa1bQbRzskQ== +"@types/is-glob@4.0.1": + version "4.0.1" + resolved "https://registry.yarnpkg.com/@types/is-glob/-/is-glob-4.0.1.tgz#a93eec1714172c8eb3225a1cc5eb88c2477b7d00" + integrity sha512-k3RS5HyBPu4h+5hTmIEfPB2rl5P3LnGdQEZrV2b9OWTJVtsUQ2VBcedqYKGqxvZqle5UALUXdSfVA8nf3HfyWQ== + "@types/istanbul-lib-coverage@*", "@types/istanbul-lib-coverage@^2.0.0": version "2.0.1" resolved "https://registry.yarnpkg.com/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.1.tgz#42995b446db9a48a11a07ec083499a860e9138ff" @@ -1633,6 +1720,13 @@ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d" integrity sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA== +"@types/mkdirp@0.5.2": + version "0.5.2" + resolved "https://registry.yarnpkg.com/@types/mkdirp/-/mkdirp-0.5.2.tgz#503aacfe5cc2703d5484326b1b27efa67a339c1f" + integrity sha512-U5icWpv7YnZYGsN4/cmh3WD2onMY0aJIiTE6+51TwJCttdHvtCYmkBNOobHlXwrJRL0nkH9jH4kD+1FAdMN4Tg== + dependencies: + "@types/node" "*" + "@types/mongodb@*", "@types/mongodb@3.3.1": version "3.3.1" resolved "https://registry.yarnpkg.com/@types/mongodb/-/mongodb-3.3.1.tgz#9569ffcb356fbb5313ae2d3afa88c230bf8cf0d1" @@ -1656,11 +1750,16 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@12.7.4", "@types/node@>=6", "@types/node@^10.1.0": +"@types/node@*", "@types/node@12.7.4", "@types/node@>=6": version "12.7.4" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.4.tgz#64db61e0359eb5a8d99b55e05c729f130a678b04" integrity sha512-W0+n1Y+gK/8G2P/piTkBBN38Qc5Q1ZSO6B5H3QmPCUewaiXOo2GCAWZ4ElZCcNhjJuBSUSLGFUJnmlCn5+nxOQ== +"@types/node@^10.1.0": + version "10.14.18" + resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.18.tgz#b7d45fc950e6ffd7edc685e890d13aa7b8535dce" + integrity sha512-ryO3Q3++yZC/+b8j8BdKd/dn9JlzlHBPdm80656xwYUdmPkpTGTjkAdt6BByiNupGPE8w0FhBgvYy/fX9hRNGQ== + "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -2185,6 +2284,13 @@ ansi-escapes@^3.0.0, ansi-escapes@^3.2.0: resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.2.0.tgz#8780b98ff9dbf5638152d1f1fe5c1d7b4442976b" integrity sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ== +ansi-escapes@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-4.2.1.tgz#4dccdb846c3eee10f6d64dea66273eab90c37228" + integrity sha512-Cg3ymMAdN10wOk/VYfLV7KCQyv7EDirJ64500sU7n9UlmioEtDuU5Gd+hj73hXSU/ex7tHJSssmyftDdkMLO8Q== + dependencies: + type-fest "^0.5.2" + ansi-html@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/ansi-html/-/ansi-html-0.0.7.tgz#813584021962a9e9e6fd039f940d12f56ca7859e" @@ -2235,6 +2341,14 @@ anymatch@^2.0.0: micromatch "^3.1.4" normalize-path "^2.1.1" +anymatch@^3.0.1: + version "3.1.0" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.0.tgz#e609350e50a9313b472789b2f14ef35808ee14d6" + integrity sha512-Ozz7l4ixzI7Oxj2+cw+p0tVUt27BpaJ+1+q1TCeANWxHpvyn2+Un+YamBdfKu0uh8xLodGhoa1v7595NhKDAuA== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + apollo-boost@0.1.27: version "0.1.27" resolved "https://registry.yarnpkg.com/apollo-boost/-/apollo-boost-0.1.27.tgz#77cc796359503a330d5b31780043430afed47899" @@ -3031,6 +3145,11 @@ binary-extensions@^1.0.0: resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-1.13.1.tgz#598afe54755b2868a5330d2aff9d4ebb53209b65" integrity sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw== +binary-extensions@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.0.0.tgz#23c0df14f6a88077f5f986c0d167ec03c3d5537c" + integrity sha512-Phlt0plgpIIBOGTT/ehfFnbNlfsDEiqmzE2KRXoX1bLIlir4X/MR+zSyBEkL05ffWgnRSf/DXv+WrUAVr93/ow== + bluebird@3.5.1: version "3.5.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" @@ -3116,7 +3235,7 @@ braces@^2.3.1, braces@^2.3.2: split-string "^3.0.2" to-regex "^3.0.1" -braces@^3.0.1: +braces@^3.0.1, braces@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.2.tgz#3454e1a462ee8d599e236df336cd9ea4f8afe107" integrity sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A== @@ -3547,6 +3666,21 @@ chokidar@2.1.2: optionalDependencies: fsevents "^1.2.7" +chokidar@3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.0.2.tgz#0d1cd6d04eb2df0327446188cd13736a3367d681" + integrity sha512-c4PR2egjNjI1um6bamCQ6bUNPDiyofNQruHvKgHQ4gDUP/ITSVSzNsiI5OWtHOsX323i5ha/kk4YmOZ1Ktg7KA== + dependencies: + anymatch "^3.0.1" + braces "^3.0.2" + glob-parent "^5.0.0" + is-binary-path "^2.1.0" + is-glob "^4.0.1" + normalize-path "^3.0.0" + readdirp "^3.1.1" + optionalDependencies: + fsevents "^2.0.6" + chokidar@^2.0.0, chokidar@^2.0.2, chokidar@^2.0.4, chokidar@^2.1.5: version "2.1.8" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-2.1.8.tgz#804b3a7b6a99358c3c5c61e71d8728f041cff917" @@ -3630,6 +3764,13 @@ cli-cursor@^2.0.0, cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" +cli-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-3.1.0.tgz#264305a7ae490d1d03bf0c9ba7c925d1753af307" + integrity sha512-I/zHAwsKf9FqGoXM4WWRACob9+SNukZTd94DWF57E4toouRulbCxcUh6RKUEOQlYTHJnzkPMySvPNaaSLNfLZw== + dependencies: + restore-cursor "^3.1.0" + cli-highlight@^2.0.0: version "2.1.1" resolved "https://registry.yarnpkg.com/cli-highlight/-/cli-highlight-2.1.1.tgz#2180223d51618b112f4509cf96e4a6c750b07e97" @@ -3853,6 +3994,11 @@ commander@2.19.0, commander@~2.19.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== +commander@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.1.tgz#4595aec3530525e671fb6f85fb173df8ff8bf57a" + integrity sha512-UNgvDd+csKdc9GD4zjtkHKQbT8Aspt2jCBqNSPp53vAS0L1tS9sXB2TCEOPHJ7kt9bN/niWkYj8T3RQSoMXdSQ== + commander@^2.11.0, commander@^2.12.1, commander@^2.20.0, commander@~2.20.0: version "2.20.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" @@ -4651,6 +4797,11 @@ dateformat@^3.0.0: resolved "https://registry.yarnpkg.com/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" integrity sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q== +debounce@1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/debounce/-/debounce-1.2.0.tgz#44a540abc0ea9943018dc0eaa95cce87f65cd131" + integrity sha512-mYtLl1xfZLi1m4RtQYlZgJUNQjl4ZxVnHzIR8nLLgi4q1YT8o/WM+MK/f8yfcc9s5Ir5zRaPZyZU6xs1Syoocg== + debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.0, debug@^2.6.8, debug@^2.6.9: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -4866,6 +5017,11 @@ detect-indent@5.0.0, detect-indent@^5.0.0: resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-5.0.0.tgz#3871cc0a6a002e8c3e5b3cf7f336264675f06b9d" integrity sha1-OHHMCmoALow+Wzz38zYmRnXwa50= +detect-indent@6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/detect-indent/-/detect-indent-6.0.0.tgz#0abd0f549f69fc6659a254fe96786186b6f528fd" + integrity sha512-oSyFlqaTHCItVRGK5RmrmjB+CmaMOW7IaNA/kdxqhoa6d17j/5ce9O9eWXmV/KEdRwqpQA+Vqe8a8Bsybu4YnA== + detect-libc@^1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" @@ -5165,6 +5321,11 @@ emoji-regex@^7.0.1, emoji-regex@^7.0.2: resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-7.0.3.tgz#933a04052860c85e83c122479c4748a8e4c72156" integrity sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA== +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + emojis-list@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/emojis-list/-/emojis-list-2.1.0.tgz#4daa4d9db00f9819880c79fa457ae5b09a1fd389" @@ -5872,6 +6033,13 @@ figures@^2.0.0: dependencies: escape-string-regexp "^1.0.5" +figures@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-3.0.0.tgz#756275c964646163cc6f9197c7a0295dbfd04de9" + integrity sha512-HKri+WoWoUgr83pehn/SIgLOMZ9nAWC6dcGj26RY2R4F50u4+RTUz0RCrUlOV3nKRAICW1UGzyb+kcX2qK1S/g== + dependencies: + escape-string-regexp "^1.0.5" + file-entry-cache@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/file-entry-cache/-/file-entry-cache-5.0.1.tgz#ca0f6efa6dd3d561333fb14515065c2fafdf439c" @@ -6128,7 +6296,7 @@ fs.realpath@^1.0.0: resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" integrity sha1-FQStJSMVjKpA20onh8sBQRmU6k8= -fsevents@2.0.7: +fsevents@2.0.7, fsevents@^2.0.6: version "2.0.7" resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.0.7.tgz#382c9b443c6cbac4c57187cdda23aa3bf1ccfc2a" integrity sha512-a7YT0SV3RB+DjYcppwVDLtn13UQnmg0SWZS7ezZD0UjnLwXmy8Zm21GMVGLaFGimIqcvyMQaOJBrop8MyOp1kQ== @@ -6308,7 +6476,7 @@ glob@7.1.3: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: +glob@7.1.4, glob@^7.0.3, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: version "7.1.4" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.4.tgz#aa608a2f6c577ad357e1ae5a5c26d9a8d1969255" integrity sha512-hkLPepehmnKk41pUGm3sYxoFs/umurYfYJCerbXEyFIWcAzvpipAgVkBqqT9RBKMGjnq6kMuyYwha6csxbiM1A== @@ -6563,6 +6731,16 @@ graphql-tag-pluck@0.6.0: source-map-support "^0.5.9" typescript "^3.2.2" +graphql-tag-pluck@0.8.4: + version "0.8.4" + resolved "https://registry.yarnpkg.com/graphql-tag-pluck/-/graphql-tag-pluck-0.8.4.tgz#9425627a9358365be519d532acaa38edde049d28" + integrity sha512-weT9fZPILIOkdW26ZkkiGf2OGvSfHQZBudYxkxnNoiLU+9RH+I0THE95iAvzMWbtKVmoBovLF/qQyK4ay/D7Bw== + dependencies: + "@babel/parser" "^7.4.4" + "@babel/traverse" "^7.4.4" + "@babel/types" "^7.4.4" + source-map-support "^0.5.12" + graphql-tag@2.10.1, graphql-tag@^2.10.0, graphql-tag@^2.4.2, graphql-tag@^2.9.2: version "2.10.1" resolved "https://registry.yarnpkg.com/graphql-tag/-/graphql-tag-2.10.1.tgz#10aa41f1cd8fae5373eaf11f1f67260a3cad5e02" @@ -6585,6 +6763,25 @@ graphql-toolkit@0.2.0: tslib "^1.9.3" valid-url "1.0.9" +graphql-toolkit@0.5.11: + version "0.5.11" + resolved "https://registry.yarnpkg.com/graphql-toolkit/-/graphql-toolkit-0.5.11.tgz#f9adf1ecc4df455802d0cc223acbd35556f7d78e" + integrity sha512-CKYzzqcAUbG3mzeQ1+KDqggQMj1lcleanhU4h8EH9bKV2+IyY+vMXQcuxBuLF4BgxYeX04LQnPUfGi9F+lo0qw== + dependencies: + "@kamilkisiela/graphql-tools" "4.0.6" + "@types/glob" "7.1.1" + aggregate-error "3.0.0" + asyncro "^3.0.0" + cross-fetch "^3.0.4" + deepmerge "4.0.0" + globby "10.0.1" + graphql-import "0.7.1" + is-glob "4.0.1" + is-valid-path "0.1.1" + lodash "4.17.15" + tslib "^1.9.3" + valid-url "1.0.9" + graphql-toolkit@0.5.12, graphql-toolkit@^0.5.12: version "0.5.12" resolved "https://registry.yarnpkg.com/graphql-toolkit/-/graphql-toolkit-0.5.12.tgz#2ab4a81ff2e67bd591be5c660f09744024d98617" @@ -6604,7 +6801,18 @@ graphql-toolkit@0.5.12, graphql-toolkit@^0.5.12: tslib "^1.9.3" valid-url "1.0.9" -graphql-tools@4.0.4, graphql-tools@4.0.5, graphql-tools@^4.0.0, graphql-tools@^4.0.5: +graphql-tools@4.0.4: + version "4.0.4" + resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-4.0.4.tgz#ca08a63454221fdde825fe45fbd315eb2a6d566b" + integrity sha512-chF12etTIGVVGy3fCTJ1ivJX2KB7OSG4c6UOJQuqOHCmBQwTyNgCDuejZKvpYxNZiEx7bwIjrodDgDe9RIkjlw== + dependencies: + apollo-link "^1.2.3" + apollo-utilities "^1.0.1" + deprecated-decorator "^0.1.6" + iterall "^1.1.3" + uuid "^3.1.0" + +graphql-tools@4.0.5, graphql-tools@^4.0.0, graphql-tools@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-4.0.5.tgz#d2b41ee0a330bfef833e5cdae7e1f0b0d86b1754" integrity sha512-kQCh3IZsMqquDx7zfIGWBau42xe46gmqabwYkpPlCLIjcEY1XK+auP7iGRD9/205BPyoQdY8hT96MPpgERdC9Q== @@ -7103,6 +7311,13 @@ import-from@2.1.0, import-from@^2.1.0: dependencies: resolve-from "^3.0.0" +import-from@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/import-from/-/import-from-3.0.0.tgz#055cfec38cd5a27d8057ca51376d7d3bf0891966" + integrity sha512-CiuXOFFSzkU5x/CR0+z7T91Iht4CXgfCxVOFRhh2Zyhg5wOpWvvDLQUsWl+gcN+QscYBjez8hDCt85O7RLDttQ== + dependencies: + resolve-from "^5.0.0" + import-lazy@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/import-lazy/-/import-lazy-2.1.0.tgz#05698e3d45c88e8d7e9d92cb0584e77f096f3e43" @@ -7126,6 +7341,11 @@ indent-string@3.2.0, indent-string@^3.0.0, indent-string@^3.2.0: resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" integrity sha1-Sl/W0nzDMvN+VBmlBNu4NxBckok= +indent-string@4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-4.0.0.tgz#624f8f4497d619b2d9768531d58f4122854d7251" + integrity sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg== + indent-string@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" @@ -7228,6 +7448,25 @@ inquirer@6.5.0: strip-ansi "^5.1.0" through "^2.3.6" +inquirer@7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-7.0.0.tgz#9e2b032dde77da1db5db804758b8fea3a970519a" + integrity sha512-rSdC7zelHdRQFkWnhsMu2+2SO41mpv2oF2zy4tMhmiLWkcKbOAs87fWAJhVXttKVwhdZvymvnuM95EyEXg2/tQ== + dependencies: + ansi-escapes "^4.2.1" + chalk "^2.4.2" + cli-cursor "^3.1.0" + cli-width "^2.0.0" + external-editor "^3.0.3" + figures "^3.0.0" + lodash "^4.17.15" + mute-stream "0.0.8" + run-async "^2.2.0" + rxjs "^6.4.0" + string-width "^4.1.0" + strip-ansi "^5.1.0" + through "^2.3.6" + inquirer@^3.2.2: version "3.3.0" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" @@ -7368,6 +7607,13 @@ is-binary-path@^1.0.0: dependencies: binary-extensions "^1.0.0" +is-binary-path@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + is-buffer@^1.0.2, is-buffer@^1.1.5: version "1.1.6" resolved "https://registry.yarnpkg.com/is-buffer/-/is-buffer-1.1.6.tgz#efaa2ea9daa0d7ab2ea13a97b2b8ad51fefbe8be" @@ -7487,6 +7733,11 @@ is-fullwidth-code-point@^2.0.0: resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz#a3b30a5c4f199183167aaab93beefae3ddfb654f" integrity sha1-o7MKXE8ZkYMWeqq5O+764937ZU8= +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + is-generator-fn@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-generator-fn/-/is-generator-fn-2.1.0.tgz#7d140adc389aaf3011a8f2a2a4cfa6faadffb118" @@ -8937,6 +9188,13 @@ log-symbols@2.2.0: dependencies: chalk "^2.0.1" +log-symbols@3.0.0, log-symbols@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" + integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== + dependencies: + chalk "^2.4.2" + log-symbols@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" @@ -8944,13 +9202,6 @@ log-symbols@^1.0.2: dependencies: chalk "^1.0.0" -log-symbols@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-3.0.0.tgz#f3a08516a5dea893336a7dee14d18a1cfdab77c4" - integrity sha512-dSkNGuI7iG3mfvDzUuYZyvk5dD9ocYCYzNU6CYDE6+Xqd+gwme6Z00NS3dUh8mq/73HaEtT7m6W+yUPtU6BZnQ== - dependencies: - chalk "^2.4.2" - log-update@2.3.0, log-update@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/log-update/-/log-update-2.3.0.tgz#88328fd7d1ce7938b29283746f0b1bc126b24708" @@ -8960,6 +9211,15 @@ log-update@2.3.0, log-update@^2.3.0: cli-cursor "^2.0.0" wrap-ansi "^3.0.1" +log-update@3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-3.2.0.tgz#719f24293250d65d0165f4e2ec2ed805ff062eec" + integrity sha512-KJ6zAPIHWo7Xg1jYror6IUDFJBq1bQ4Bi4wAEp2y/0ScjBBVi/g0thr0sUVhuvuXauWzczt7T2QHghPDNnKBuw== + dependencies: + ansi-escapes "^3.2.0" + cli-cursor "^2.1.0" + wrap-ansi "^5.0.0" + logform@^2.1.1: version "2.1.2" resolved "https://registry.yarnpkg.com/logform/-/logform-2.1.2.tgz#957155ebeb67a13164069825ce67ddb5bb2dd360" @@ -9519,6 +9779,11 @@ mute-stream@0.0.7: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= +mute-stream@0.0.8: + version "0.0.8" + resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" + integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== + mz@^2.4.0: version "2.7.0" resolved "https://registry.yarnpkg.com/mz/-/mz-2.7.0.tgz#95008057a56cafadc2bc63dde7f9ff6955948e32" @@ -10488,7 +10753,7 @@ pgpass@1.x: dependencies: split "^1.0.0" -picomatch@^2.0.5: +picomatch@^2.0.4, picomatch@^2.0.5: version "2.0.7" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.0.7.tgz#514169d8c7cd0bdbeecc8a2609e34a7163de69f6" integrity sha512-oLHIdio3tZ0qH76NybpeneBhYVj0QFTfXEFTc/B3zKQspYfYYkWYgFsmzo+4kvId/bQRcNkVeguI3y+CD22BtA== @@ -11884,6 +12149,13 @@ readdirp@^2.2.1: micromatch "^3.1.10" readable-stream "^2.0.2" +readdirp@^3.1.1: + version "3.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.1.2.tgz#fa85d2d14d4289920e4671dead96431add2ee78a" + integrity sha512-8rhl0xs2cxfVsqzreYCvs8EwBfn/DhVdqtoLmw19uI3SC5avYX9teCurlErfpPXGmYtMHReGaP2RsLnFvz/lnw== + dependencies: + picomatch "^2.0.4" + realpath-native@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/realpath-native/-/realpath-native-1.1.0.tgz#2003294fea23fb0672f2476ebe22fcf498a2d65c" @@ -12201,6 +12473,11 @@ resolve-from@^4.0.0: resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-4.0.0.tgz#4abcd852ad32dd7baabfe9b40e00a36db5f392e6" integrity sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g== +resolve-from@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/resolve-from/-/resolve-from-5.0.0.tgz#c35225843df8f776df21c57557bc087e9dfdfc69" + integrity sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw== + resolve-pathname@^2.2.0: version "2.2.0" resolved "https://registry.yarnpkg.com/resolve-pathname/-/resolve-pathname-2.2.0.tgz#7e9ae21ed815fd63ab189adeee64dc831eefa879" @@ -12247,6 +12524,14 @@ restore-cursor@^2.0.0: onetime "^2.0.0" signal-exit "^3.0.2" +restore-cursor@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-3.1.0.tgz#39f67c54b3a7a58cea5236d95cf0034239631f7e" + integrity sha512-l+sSefzHpj5qimhFSE5a8nufZYAM3sBSVMAPtYkmC+4EH2anSGaEMXSD0izRQbu9nfyQ9y5JrVmp7E8oZrUjvA== + dependencies: + onetime "^5.1.0" + signal-exit "^3.0.2" + ret@~0.1.10: version "0.1.15" resolved "https://registry.yarnpkg.com/ret/-/ret-0.1.15.tgz#b8a4825d5bdb1fc3f6f53c2bc33f81388681c7bc" @@ -12783,7 +13068,7 @@ source-map-resolve@^0.5.0, source-map-resolve@^0.5.2: source-map-url "^0.4.0" urix "^0.1.0" -source-map-support@^0.5.6, source-map-support@^0.5.9, source-map-support@~0.5.12: +source-map-support@^0.5.12, source-map-support@^0.5.6, source-map-support@^0.5.9, source-map-support@~0.5.12: version "0.5.13" resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.13.tgz#31b24a9c2e73c2de85066c0feb7d44767ed52932" integrity sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w== @@ -13036,6 +13321,15 @@ string-width@^3.0.0, string-width@^3.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^5.1.0" +string-width@^4.1.0: + version "4.1.0" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.1.0.tgz#ba846d1daa97c3c596155308063e075ed1c99aff" + integrity sha512-NrX+1dVVh+6Y9dnQ19pR0pP4FiEIlUvdTGn8pw6CKTNq5sgib2nIhmUNT5TAmhWmvKr3WcxBcP3E8nWezuipuQ== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^5.2.0" + string.prototype.trimleft@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/string.prototype.trimleft/-/string.prototype.trimleft-2.0.0.tgz#68b6aa8e162c6a80e76e3a8a0c2e747186e271ff" @@ -13705,6 +13999,11 @@ type-detect@^4.0.0, type-detect@^4.0.5: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== +type-fest@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.5.2.tgz#d6ef42a0356c6cd45f49485c3b6281fc148e48a2" + integrity sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw== + type-fest@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b" @@ -14498,7 +14797,7 @@ wrap-ansi@^3.0.1: string-width "^2.1.1" strip-ansi "^4.0.0" -wrap-ansi@^5.1.0: +wrap-ansi@^5.0.0, wrap-ansi@^5.1.0: version "5.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-5.1.0.tgz#1fd1f67235d5b6d0fee781056001bfb694c03b09" integrity sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q== From c44bd59543d26549a3f1dd1c1220763fc2a3c5d5 Mon Sep 17 00:00:00 2001 From: ozsay Date: Wed, 11 Sep 2019 16:47:27 +0300 Subject: [PATCH 08/15] mistake --- docker-compose-dev.yml | 17 +++++++++++++++++ docker-compose.yml | 23 +++++++++++------------ 2 files changed, 28 insertions(+), 12 deletions(-) create mode 100644 docker-compose-dev.yml diff --git a/docker-compose-dev.yml b/docker-compose-dev.yml new file mode 100644 index 000000000..7abe9801e --- /dev/null +++ b/docker-compose-dev.yml @@ -0,0 +1,17 @@ +version: '3' +services: + mongo: + image: mongo + ports: + - "27017:27017" + redis: + image: redis + ports: + - "6379:6379" + postgres: + image: postgres + ports: + - "5432:5432" + environment: + - POSTGRES_USER=postgres + - POSTGRES_DB=accounts-js-tests-e2e \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 7abe9801e..fc479dd98 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,17 +1,16 @@ -version: '3' +version: '3.6' services: + postgres: + image: circleci/postgres:10.10 + ports: + - '5432:5432' + environment: + POSTGRES_DB: accounts-js-tests-e2e mongo: - image: mongo + image: circleci/mongo:3 ports: - - "27017:27017" + - '27017:27017' redis: - image: redis + image: circleci/redis:4 ports: - - "6379:6379" - postgres: - image: postgres - ports: - - "5432:5432" - environment: - - POSTGRES_USER=postgres - - POSTGRES_DB=accounts-js-tests-e2e \ No newline at end of file + - '6379:6379' From dfd2623e3b4c0428b6abe31b9cedb807b3981ad3 Mon Sep 17 00:00:00 2001 From: ozsay Date: Wed, 11 Sep 2019 16:57:51 +0300 Subject: [PATCH 09/15] fix examples --- examples/graphql-server-typescript/src/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/examples/graphql-server-typescript/src/index.ts b/examples/graphql-server-typescript/src/index.ts index 2f3ecf321..b3829d2fe 100644 --- a/examples/graphql-server-typescript/src/index.ts +++ b/examples/graphql-server-typescript/src/index.ts @@ -21,6 +21,7 @@ const start = async () => { const accountsDb = new DatabaseManager({ sessionStorage: userStorage, userStorage, + mfaLoginAttemptsStorage: userStorage, }); const accountsPassword = new AccountsPassword({ From 2b3ccc5ed570bd0563edfc2ebcf75dd53cca6062 Mon Sep 17 00:00:00 2001 From: ozsay Date: Mon, 16 Sep 2019 13:01:17 +0300 Subject: [PATCH 10/15] add accounts-server tests --- .../__snapshots__/account-server.ts.snap | 12 ++ packages/server/__tests__/account-server.ts | 204 +++++++++++++++++- yarn.lock | 20 +- 3 files changed, 217 insertions(+), 19 deletions(-) diff --git a/packages/server/__tests__/__snapshots__/account-server.ts.snap b/packages/server/__tests__/__snapshots__/account-server.ts.snap index 3dd6e8dcd..5da1360bb 100644 --- a/packages/server/__tests__/__snapshots__/account-server.ts.snap +++ b/packages/server/__tests__/__snapshots__/account-server.ts.snap @@ -2,8 +2,20 @@ exports[`AccountsServer config throws on invalid db 1`] = `"A database driver is required"`; +exports[`AccountsServer loginWithService throws error when MFA login token is invalid 1`] = `[Error: Service mfa was not able to authenticate user]`; + exports[`AccountsServer loginWithService throws on invalid service 1`] = `"No service with the name facebook was registered."`; exports[`AccountsServer loginWithService throws when user is deactivated 1`] = `"Your account has been deactivated"`; exports[`AccountsServer loginWithService throws when user not found 1`] = `"Service facebook was not able to authenticate user"`; + +exports[`AccountsServer performMfaChallenge throws error the challenge is failing #2 1`] = `[Error: Service sms was not able to authenticate user]`; + +exports[`AccountsServer performMfaChallenge throws error the challenge is failing 1`] = `[Error: Service sms was not able to authenticate user]`; + +exports[`AccountsServer performMfaChallenge throws error when mfa is not enabled for user #2 1`] = `[Error: Performing the mfa challenge is not available]`; + +exports[`AccountsServer performMfaChallenge throws error when mfa is not enabled for user 1`] = `[Error: Performing the mfa challenge is not available]`; + +exports[`AccountsServer performMfaChallenge throws error when mfaToken is wrong 1`] = `[Error: Performing the mfa challenge is not available]`; diff --git a/packages/server/__tests__/account-server.ts b/packages/server/__tests__/account-server.ts index 6f27e828b..b160ddc71 100644 --- a/packages/server/__tests__/account-server.ts +++ b/packages/server/__tests__/account-server.ts @@ -1,8 +1,10 @@ import * as jwtDecode from 'jwt-decode'; +import { LoginResult, MFALoginResult } from '@accounts/types'; + import { AccountsServer } from '../src/accounts-server'; import { JwtData } from '../src/types/jwt-data'; import { ServerHooks } from '../src/utils/server-hooks'; -import { LoginResult } from '@accounts/types'; +import * as tokens from '../src/utils/tokens'; const delay = (timeout: number) => new Promise(resolve => setTimeout(resolve, timeout)); @@ -93,6 +95,81 @@ describe('AccountsServer', () => { const res = await accountServer.loginWithService('facebook', {}, {}); expect((res as LoginResult).tokens).toBeTruthy(); }); + + it('should create an MFA login process when enabled', async () => { + const loginToken = 'login-token'; + const mfaToken = 'mfa-token'; + jest.spyOn(tokens, 'generateRandomToken').mockImplementation(() => loginToken); + jest.spyOn(tokens, 'hashToken').mockImplementation(() => mfaToken); + + const authenticate = jest.fn(() => Promise.resolve({ id: 'userId', mfaChallenges: ['sms'] })); + const createMfaLoginAttempt = jest.fn(() => Promise.resolve()); + const service: any = { authenticate, setStore: jest.fn() }; + const accountServer = new AccountsServer( + { + db: { createMfaLoginAttempt } as any, + tokenSecret: 'secret1', + }, + { + facebook: service, + } + ); + const res = (await accountServer.loginWithService('facebook', {}, {})) as MFALoginResult; + + expect(res.challenges).toEqual(['sms']); + expect(res.mfaToken).toEqual(mfaToken); + expect(createMfaLoginAttempt).toHaveBeenCalledWith(mfaToken, loginToken, 'userId'); + expect(tokens.hashToken).toHaveBeenCalledWith(loginToken); + }); + + it('should finish MFA login process', async () => { + const loginToken = 'login-token'; + const userId = 'user-id'; + const mfaToken = 'mfa-token'; + + const getMfaLoginAttempt = jest.fn(() => Promise.resolve({ loginToken, userId })); + const removeMfaLoginAttempt = jest.fn(() => Promise.resolve()); + const findUserById = jest.fn(() => Promise.resolve({ id: userId })); + const createSession = jest.fn(() => Promise.resolve('sessionId')); + + const accountServer = new AccountsServer( + { + db: { getMfaLoginAttempt, findUserById, removeMfaLoginAttempt, createSession } as any, + tokenSecret: 'secret1', + }, + {} + ); + const res = (await accountServer.loginWithService( + 'mfa', + { loginToken, mfaToken }, + {} + )) as LoginResult; + + expect((res as LoginResult).tokens).toBeTruthy(); + expect(getMfaLoginAttempt).toHaveBeenCalledWith(mfaToken); + expect(removeMfaLoginAttempt).toHaveBeenCalledWith(mfaToken); + expect(findUserById).toHaveBeenCalledWith(userId); + }); + + it('throws error when MFA login token is invalid', async () => { + const loginToken = 'login-token'; + const wrongLoginToken = 'wrong-login-token'; + const userId = 'user-id'; + const mfaToken = 'mfa-token'; + + const getMfaLoginAttempt = jest.fn(() => Promise.resolve({ loginToken, userId })); + + const accountServer = new AccountsServer( + { + db: { getMfaLoginAttempt } as any, + tokenSecret: 'secret1', + }, + {} + ); + await expect( + accountServer.loginWithService('mfa', { loginToken: wrongLoginToken, mfaToken }, {}) + ).rejects.toMatchSnapshot(); + }); }); describe('loginWithUser', () => { @@ -122,6 +199,131 @@ describe('AccountsServer', () => { }); }); + describe('performMfaChallenge', () => { + it('throws error when mfaToken is wrong', async () => { + const mfaToken = 'mfa-token'; + + const getMfaLoginAttempt = jest.fn(() => Promise.resolve(null)); + + const accountServer = new AccountsServer( + { + db: { getMfaLoginAttempt } as any, + tokenSecret: 'secret1', + }, + {} + ); + await expect( + accountServer.performMfaChallenge('sms', mfaToken, {}) + ).rejects.toMatchSnapshot(); + }); + + it('throws error when mfa is not enabled for user', async () => { + const userId = 'userId'; + const mfaToken = 'mfa-token'; + + const getMfaLoginAttempt = jest.fn(() => Promise.resolve({ id: userId })); + const findUserById = jest.fn(() => Promise.resolve({ id: userId })); + + const accountServer = new AccountsServer( + { + db: { getMfaLoginAttempt, findUserById } as any, + tokenSecret: 'secret1', + }, + {} + ); + await expect( + accountServer.performMfaChallenge('sms', mfaToken, {}) + ).rejects.toMatchSnapshot(); + }); + + it('throws error when mfa is not enabled for user #2', async () => { + const userId = 'userId'; + const mfaToken = 'mfa-token'; + + const getMfaLoginAttempt = jest.fn(() => Promise.resolve({ id: userId })); + const findUserById = jest.fn(() => Promise.resolve({ id: userId, mfaChallenges: [] })); + + const accountServer = new AccountsServer( + { + db: { getMfaLoginAttempt, findUserById } as any, + tokenSecret: 'secret1', + }, + {} + ); + await expect( + accountServer.performMfaChallenge('sms', mfaToken, {}) + ).rejects.toMatchSnapshot(); + }); + + it('throws error the challenge is failing', async () => { + const userId = 'userId'; + const mfaToken = 'mfa-token'; + const authenticate = jest.fn(() => Promise.resolve()); + const getMfaLoginAttempt = jest.fn(() => Promise.resolve({ id: userId })); + const findUserById = jest.fn(() => Promise.resolve({ id: userId, mfaChallenges: ['sms'] })); + const service: any = { authenticate, setStore: jest.fn() }; + + const accountServer = new AccountsServer( + { + db: { getMfaLoginAttempt, findUserById } as any, + tokenSecret: 'secret1', + }, + { + sms: service, + } + ); + await expect( + accountServer.performMfaChallenge('sms', mfaToken, {}) + ).rejects.toMatchSnapshot(); + }); + + it('throws error the challenge is failing #2', async () => { + const userId = 'userId'; + const mfaToken = 'mfa-token'; + const authenticate = jest.fn(() => Promise.resolve({ id: 'userId2' })); + const getMfaLoginAttempt = jest.fn(() => Promise.resolve({ id: userId })); + const findUserById = jest.fn(() => Promise.resolve({ id: userId, mfaChallenges: ['sms'] })); + const service: any = { authenticate, setStore: jest.fn() }; + + const accountServer = new AccountsServer( + { + db: { getMfaLoginAttempt, findUserById } as any, + tokenSecret: 'secret1', + }, + { + sms: service, + } + ); + await expect( + accountServer.performMfaChallenge('sms', mfaToken, {}) + ).rejects.toMatchSnapshot(); + }); + + it('should return the loginToken upon success', async () => { + const userId = 'userId'; + const loginToken = 'login-token'; + const mfaToken = 'mfa-token'; + const authenticate = jest.fn(() => Promise.resolve({ id: userId })); + const findUserById = jest.fn(() => Promise.resolve({ id: userId, mfaChallenges: ['sms'] })); + + const getMfaLoginAttempt = jest.fn(() => Promise.resolve({ id: userId, loginToken })); + const service: any = { authenticate, setStore: jest.fn() }; + + const accountServer = new AccountsServer( + { + db: { getMfaLoginAttempt, findUserById } as any, + tokenSecret: 'secret1', + }, + { + sms: service, + } + ); + const res = await accountServer.performMfaChallenge('sms', mfaToken, {}); + + expect(res).toEqual(loginToken); + }); + }); + describe('logout', () => { it('invalidates session', async () => { const invalidateSession = jest.fn(() => Promise.resolve()); diff --git a/yarn.lock b/yarn.lock index 85f1fafd0..247f5e7aa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1750,16 +1750,11 @@ dependencies: "@types/node" "*" -"@types/node@*", "@types/node@12.7.4", "@types/node@>=6": +"@types/node@*", "@types/node@12.7.4", "@types/node@>=6", "@types/node@^10.1.0": version "12.7.4" resolved "https://registry.yarnpkg.com/@types/node/-/node-12.7.4.tgz#64db61e0359eb5a8d99b55e05c729f130a678b04" integrity sha512-W0+n1Y+gK/8G2P/piTkBBN38Qc5Q1ZSO6B5H3QmPCUewaiXOo2GCAWZ4ElZCcNhjJuBSUSLGFUJnmlCn5+nxOQ== -"@types/node@^10.1.0": - version "10.14.18" - resolved "https://registry.yarnpkg.com/@types/node/-/node-10.14.18.tgz#b7d45fc950e6ffd7edc685e890d13aa7b8535dce" - integrity sha512-ryO3Q3++yZC/+b8j8BdKd/dn9JlzlHBPdm80656xwYUdmPkpTGTjkAdt6BByiNupGPE8w0FhBgvYy/fX9hRNGQ== - "@types/normalize-package-data@^2.4.0": version "2.4.0" resolved "https://registry.yarnpkg.com/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz#e486d0d97396d79beedd0a6e33f4534ff6b4973e" @@ -6801,18 +6796,7 @@ graphql-toolkit@0.5.12, graphql-toolkit@^0.5.12: tslib "^1.9.3" valid-url "1.0.9" -graphql-tools@4.0.4: - version "4.0.4" - resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-4.0.4.tgz#ca08a63454221fdde825fe45fbd315eb2a6d566b" - integrity sha512-chF12etTIGVVGy3fCTJ1ivJX2KB7OSG4c6UOJQuqOHCmBQwTyNgCDuejZKvpYxNZiEx7bwIjrodDgDe9RIkjlw== - dependencies: - apollo-link "^1.2.3" - apollo-utilities "^1.0.1" - deprecated-decorator "^0.1.6" - iterall "^1.1.3" - uuid "^3.1.0" - -graphql-tools@4.0.5, graphql-tools@^4.0.0, graphql-tools@^4.0.5: +graphql-tools@4.0.4, graphql-tools@4.0.5, graphql-tools@^4.0.0, graphql-tools@^4.0.5: version "4.0.5" resolved "https://registry.yarnpkg.com/graphql-tools/-/graphql-tools-4.0.5.tgz#d2b41ee0a330bfef833e5cdae7e1f0b0d86b1754" integrity sha512-kQCh3IZsMqquDx7zfIGWBau42xe46gmqabwYkpPlCLIjcEY1XK+auP7iGRD9/205BPyoQdY8hT96MPpgERdC9Q== From 04807f451d92b72e9677fb722f5e169f8965e45a Mon Sep 17 00:00:00 2001 From: ozsay Date: Mon, 16 Sep 2019 14:46:10 +0300 Subject: [PATCH 11/15] increase coverage --- .../modules/accounts/resolvers/loginResult.ts | 15 +++++++++++++++ packages/server/__tests__/account-server.ts | 4 ++-- 2 files changed, 17 insertions(+), 2 deletions(-) create mode 100644 packages/graphql-api/__tests__/modules/accounts/resolvers/loginResult.ts diff --git a/packages/graphql-api/__tests__/modules/accounts/resolvers/loginResult.ts b/packages/graphql-api/__tests__/modules/accounts/resolvers/loginResult.ts new file mode 100644 index 000000000..225f334e0 --- /dev/null +++ b/packages/graphql-api/__tests__/modules/accounts/resolvers/loginResult.ts @@ -0,0 +1,15 @@ +import { LoginWithServiceResult } from '../../../../src/modules/accounts/resolvers/loginResult'; + +describe('LoginWithServiceResult', () => { + it('returns LoginResult when tokens are available', () => { + const res = LoginWithServiceResult.__resolveType({ tokens: {} }); + + expect(res).toEqual('LoginResult'); + }); + + it('returns MFALoginResult when tokens are not available', () => { + const res = LoginWithServiceResult.__resolveType({ mfaToken: {} }); + + expect(res).toEqual('MFALoginResult'); + }); +}); diff --git a/packages/server/__tests__/account-server.ts b/packages/server/__tests__/account-server.ts index b160ddc71..ce114fadf 100644 --- a/packages/server/__tests__/account-server.ts +++ b/packages/server/__tests__/account-server.ts @@ -98,9 +98,9 @@ describe('AccountsServer', () => { it('should create an MFA login process when enabled', async () => { const loginToken = 'login-token'; - const mfaToken = 'mfa-token'; + const mfaToken = '936cef147c65abc808defb3598daa752851176e3505b5879620b4d4200f00462'; // hash of loginToken jest.spyOn(tokens, 'generateRandomToken').mockImplementation(() => loginToken); - jest.spyOn(tokens, 'hashToken').mockImplementation(() => mfaToken); + jest.spyOn(tokens, 'hashToken'); const authenticate = jest.fn(() => Promise.resolve({ id: 'userId', mfaChallenges: ['sms'] })); const createMfaLoginAttempt = jest.fn(() => Promise.resolve()); From 09ee96788146b79d88595009c3958d04ad702d9d Mon Sep 17 00:00:00 2001 From: ozsay Date: Mon, 16 Sep 2019 14:56:50 +0300 Subject: [PATCH 12/15] fixes of CR --- .../graphql-server-typescript/src/index.ts | 1 - .../__snapshots__/database-manager.ts.snap | 7 ++++++ .../__tests__/database-manager.ts | 25 +++++++++++++++++++ .../database-manager/src/database-manager.ts | 11 +++++++- .../src/types/configuration.ts | 2 +- 5 files changed, 43 insertions(+), 3 deletions(-) create mode 100644 packages/database-manager/__tests__/__snapshots__/database-manager.ts.snap diff --git a/examples/graphql-server-typescript/src/index.ts b/examples/graphql-server-typescript/src/index.ts index b3829d2fe..2f3ecf321 100644 --- a/examples/graphql-server-typescript/src/index.ts +++ b/examples/graphql-server-typescript/src/index.ts @@ -21,7 +21,6 @@ const start = async () => { const accountsDb = new DatabaseManager({ sessionStorage: userStorage, userStorage, - mfaLoginAttemptsStorage: userStorage, }); const accountsPassword = new AccountsPassword({ diff --git a/packages/database-manager/__tests__/__snapshots__/database-manager.ts.snap b/packages/database-manager/__tests__/__snapshots__/database-manager.ts.snap new file mode 100644 index 000000000..f059635b0 --- /dev/null +++ b/packages/database-manager/__tests__/__snapshots__/database-manager.ts.snap @@ -0,0 +1,7 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`DatabaseManager configuration without MFA createMfaLoginAttempt should throw error 1`] = `"No mfaLoginAttemptsStorage defined for manager"`; + +exports[`DatabaseManager configuration without MFA getMfaLoginAttempt should throw error 1`] = `"No mfaLoginAttemptsStorage defined for manager"`; + +exports[`DatabaseManager configuration without MFA removeMfaLoginAttempt should throw error 1`] = `"No mfaLoginAttemptsStorage defined for manager"`; diff --git a/packages/database-manager/__tests__/database-manager.ts b/packages/database-manager/__tests__/database-manager.ts index a8baf9099..d45b917bc 100644 --- a/packages/database-manager/__tests__/database-manager.ts +++ b/packages/database-manager/__tests__/database-manager.ts @@ -276,3 +276,28 @@ describe('DatabaseManager', () => { expect(databaseManager.removeMfaLoginAttempt('mfaToken')).toBe('mfaLoginAttemptsStorage'); }); }); + +describe('DatabaseManager configuration without MFA', () => { + const databaseManagerNoMfa = new DatabaseManager({ + userStorage: new Database('userStorage'), + sessionStorage: new Database('sessionStorage'), + }); + + it('createMfaLoginAttempt should throw error', () => { + expect(() => + databaseManagerNoMfa.createMfaLoginAttempt('mfaToken', 'loginToken', 'userId') + ).toThrowErrorMatchingSnapshot(); + }); + + it('getMfaLoginAttempt should throw error', () => { + expect(() => + databaseManagerNoMfa.getMfaLoginAttempt('mfaToken') + ).toThrowErrorMatchingSnapshot(); + }); + + it('removeMfaLoginAttempt should throw error', () => { + expect(() => + databaseManagerNoMfa.removeMfaLoginAttempt('mfaToken') + ).toThrowErrorMatchingSnapshot(); + }); +}); diff --git a/packages/database-manager/src/database-manager.ts b/packages/database-manager/src/database-manager.ts index f144806e4..3bc5cb0d7 100644 --- a/packages/database-manager/src/database-manager.ts +++ b/packages/database-manager/src/database-manager.ts @@ -9,7 +9,7 @@ import { Configuration } from './types/configuration'; export class DatabaseManager implements DatabaseInterface { private userStorage: DatabaseInterface; private sessionStorage: DatabaseInterface | DatabaseInterfaceSessions; - private mfaLoginAttemptsStorage: DatabaseInterface | DatabaseInterfaceMfaLoginAttempts; + private mfaLoginAttemptsStorage?: DatabaseInterface | DatabaseInterfaceMfaLoginAttempts; constructor(configuration: Configuration) { this.validateConfiguration(configuration); @@ -162,14 +162,23 @@ export class DatabaseManager implements DatabaseInterface { } public get createMfaLoginAttempt(): DatabaseInterface['createMfaLoginAttempt'] { + if (!this.mfaLoginAttemptsStorage) { + throw new Error('No mfaLoginAttemptsStorage defined for manager'); + } return this.mfaLoginAttemptsStorage.createMfaLoginAttempt.bind(this.mfaLoginAttemptsStorage); } public get getMfaLoginAttempt(): DatabaseInterface['getMfaLoginAttempt'] { + if (!this.mfaLoginAttemptsStorage) { + throw new Error('No mfaLoginAttemptsStorage defined for manager'); + } return this.mfaLoginAttemptsStorage.getMfaLoginAttempt.bind(this.mfaLoginAttemptsStorage); } public get removeMfaLoginAttempt(): DatabaseInterface['removeMfaLoginAttempt'] { + if (!this.mfaLoginAttemptsStorage) { + throw new Error('No mfaLoginAttemptsStorage defined for manager'); + } return this.mfaLoginAttemptsStorage.removeMfaLoginAttempt.bind(this.mfaLoginAttemptsStorage); } } diff --git a/packages/database-manager/src/types/configuration.ts b/packages/database-manager/src/types/configuration.ts index fb394883b..3f5f77632 100644 --- a/packages/database-manager/src/types/configuration.ts +++ b/packages/database-manager/src/types/configuration.ts @@ -7,5 +7,5 @@ import { export interface Configuration { userStorage: DatabaseInterface; sessionStorage: DatabaseInterface | DatabaseInterfaceSessions; - mfaLoginAttemptsStorage: DatabaseInterface | DatabaseInterfaceMfaLoginAttempts; + mfaLoginAttemptsStorage?: DatabaseInterface | DatabaseInterfaceMfaLoginAttempts; } From 23d5dcba9bb8c4d1251c1779915d528b3f9d72cb Mon Sep 17 00:00:00 2001 From: ozsay Date: Mon, 16 Sep 2019 15:03:19 +0300 Subject: [PATCH 13/15] remove graphql schema from client code --- packages/graphql-client/codegen.yml | 2 +- packages/graphql-client/{src => dev}/schema.ts | 0 packages/graphql-client/tsconfig.json | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename packages/graphql-client/{src => dev}/schema.ts (100%) diff --git a/packages/graphql-client/codegen.yml b/packages/graphql-client/codegen.yml index dbf2923e7..bc59cb46a 100644 --- a/packages/graphql-client/codegen.yml +++ b/packages/graphql-client/codegen.yml @@ -1,5 +1,5 @@ overwrite: true -schema: ./src/schema.ts +schema: ./dev/schema.ts require: ts-node/register/transpile-only generates: ./src/introspection-result.ts: diff --git a/packages/graphql-client/src/schema.ts b/packages/graphql-client/dev/schema.ts similarity index 100% rename from packages/graphql-client/src/schema.ts rename to packages/graphql-client/dev/schema.ts diff --git a/packages/graphql-client/tsconfig.json b/packages/graphql-client/tsconfig.json index 4ec56d0f8..cbb5e9862 100644 --- a/packages/graphql-client/tsconfig.json +++ b/packages/graphql-client/tsconfig.json @@ -5,5 +5,5 @@ "outDir": "./lib", "importHelpers": true }, - "exclude": ["node_modules", "__tests__", "lib"] + "exclude": ["node_modules", "__tests__", "lib", "dev"] } From 52ac28726f60fed6f81db73134ddc18f346284cf Mon Sep 17 00:00:00 2001 From: ozsay Date: Mon, 23 Sep 2019 16:05:54 +0300 Subject: [PATCH 14/15] add typing to resolver --- .../src/modules/accounts/resolvers/loginResult.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/graphql-api/src/modules/accounts/resolvers/loginResult.ts b/packages/graphql-api/src/modules/accounts/resolvers/loginResult.ts index 6d88318a6..d94f017e6 100644 --- a/packages/graphql-api/src/modules/accounts/resolvers/loginResult.ts +++ b/packages/graphql-api/src/modules/accounts/resolvers/loginResult.ts @@ -1,8 +1,14 @@ +import { + LoginWithServiceResultResolvers, + LoginResult as GeneratedLoginResult, + MfaLoginResult as GeneratedMfaLoginResult, +} from '../../../models'; + export const LoginResult = {}; -export const LoginWithServiceResult = { - __resolveType(obj: any) { - if (obj.tokens) { +export const LoginWithServiceResult: LoginWithServiceResultResolvers = { + __resolveType(obj: GeneratedLoginResult | GeneratedMfaLoginResult) { + if ((obj as GeneratedLoginResult).tokens) { return 'LoginResult'; } From 06c8d67c9cd387f3280aa124599f8deee97036b3 Mon Sep 17 00:00:00 2001 From: ozsay Date: Tue, 24 Sep 2019 15:44:02 +0300 Subject: [PATCH 15/15] fix lock --- yarn.lock | 50 ++++++++++++++++++++++++-------------------------- 1 file changed, 24 insertions(+), 26 deletions(-) diff --git a/yarn.lock b/yarn.lock index 090832828..853724a1f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2491,6 +2491,11 @@ "@types/koa-compose" "*" "@types/node" "*" +"@types/lodash@4.14.136": + version "4.14.136" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.136.tgz#413e85089046b865d960c9ff1d400e04c31ab60f" + integrity sha512-0GJhzBdvsW2RUccNHOBkabI8HZVdOXmXbXhuKlDEd5Vv12P7oAVGfomGp3Ne21o5D/qu1WmthlNKFaoZJJeErA== + "@types/lodash@4.14.138", "@types/lodash@^4.14.138": version "4.14.138" resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.138.tgz#34f52640d7358230308344e579c15b378d91989e" @@ -2526,6 +2531,14 @@ "@types/bson" "*" "@types/node" "*" +"@types/mongoose@5.5.11": + version "5.5.11" + resolved "https://registry.yarnpkg.com/@types/mongoose/-/mongoose-5.5.11.tgz#8562bb84b4f3f41aebec27f263607bfe2183729e" + integrity sha512-Z1W2V3zrB+SeDGI6G1G5XR3JJkkMl4ni7a2Kmq10abdY0wapbaTtUT2/31N+UTPEzhB0KPXUgtQExeKxrc+hxQ== + dependencies: + "@types/mongodb" "*" + "@types/node" "*" + "@types/mongoose@5.5.17": version "5.5.17" resolved "https://registry.yarnpkg.com/@types/mongoose/-/mongoose-5.5.17.tgz#1f8eb3799368ae266758d2df1bd1a7cfca0f6875" @@ -4844,6 +4857,11 @@ commander@2.19.0, commander@~2.19.0: resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== +commander@3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-3.0.1.tgz#4595aec3530525e671fb6f85fb173df8ff8bf57a" + integrity sha512-UNgvDd+csKdc9GD4zjtkHKQbT8Aspt2jCBqNSPp53vAS0L1tS9sXB2TCEOPHJ7kt9bN/niWkYj8T3RQSoMXdSQ== + commander@^2.11.0, commander@^2.20.0, commander@~2.20.0: version "2.20.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.0.tgz#d58bb2b5c1ee8f87b0d340027e9e94e222c5a422" @@ -8390,26 +8408,6 @@ inquirer@7.0.0: strip-ansi "^5.1.0" through "^2.3.6" -inquirer@^3.2.2: - version "3.3.0" - resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-3.3.0.tgz#9dd2f2ad765dcab1ff0443b491442a20ba227dc9" - integrity sha512-h+xtnyk4EwKvFWHrUYsWErEVR+igKtLdchu+o0Z1RL7VU/jVMFbYir2bp6bAj8efFNxWqHX0dIss6fJQ+/+qeQ== - dependencies: - ansi-escapes "^3.0.0" - chalk "^2.0.0" - cli-cursor "^2.1.0" - cli-width "^2.0.0" - external-editor "^2.0.4" - figures "^2.0.0" - lodash "^4.3.0" - mute-stream "0.0.7" - run-async "^2.2.0" - rx-lite "^4.0.8" - rx-lite-aggregates "^4.0.8" - string-width "^2.1.0" - strip-ansi "^4.0.0" - through "^2.3.6" - inquirer@^6.2.0, inquirer@^6.4.1: version "6.5.2" resolved "https://registry.yarnpkg.com/inquirer/-/inquirer-6.5.2.tgz#ad50942375d036d327ff528c08bd5fab089928ca" @@ -10723,7 +10721,7 @@ mute-stream@0.0.7: resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.7.tgz#3075ce93bc21b8fab43e1bc4da7e8115ed1e7bab" integrity sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s= -mute-stream@0.0.8: +mute-stream@0.0.8, mute-stream@~0.0.4: version "0.0.8" resolved "https://registry.yarnpkg.com/mute-stream/-/mute-stream-0.0.8.tgz#1630c42b2251ff81e2a283de96a5497ea92e5e0d" integrity sha512-nnbWWOkoWyUsTjKrhgD0dcz22mdkSnpYqbEjIm2nhwhuxlSkpywJmBo8h0ZqJdkp73mb90SssHkN4rsRaBAfAA== @@ -15110,16 +15108,16 @@ type-detect@^4.0.0, type-detect@^4.0.5: resolved "https://registry.yarnpkg.com/type-detect/-/type-detect-4.0.8.tgz#7646fb5f18871cfbb7749e69bd39a6388eb7450c" integrity sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g== -type-fest@^0.5.2: - version "0.5.2" - resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.5.2.tgz#d6ef42a0356c6cd45f49485c3b6281fc148e48a2" - integrity sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw== - type-fest@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" integrity sha512-cUGJnCdr4STbePCgqNFbpVNCepa+kAVohJs1sLhxzdH+gnEoOd8VhbYa7pD3zZYGiURWM2xzEII3fQcRizDkYQ== +type-fest@^0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.5.2.tgz#d6ef42a0356c6cd45f49485c3b6281fc148e48a2" + integrity sha512-DWkS49EQKVX//Tbupb9TFa19c7+MK1XmzkrZUR8TAktmE/DizXoaoJV6TZ/tSIPXipqNiRI6CyAe7x69Jb6RSw== + type-fest@^0.6.0: version "0.6.0" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.6.0.tgz#8d2a2370d3df886eb5c90ada1c5bf6188acf838b"