From 0c7f7b4512a7b39e33150f575ad3fc12bbc42820 Mon Sep 17 00:00:00 2001 From: Elies Lou Date: Sun, 11 Mar 2018 16:54:41 +0100 Subject: [PATCH 1/6] Copy rest/packages/client => accounts/packages/rest-client --- packages/rest-client/.npmignore | 7 + packages/rest-client/README.md | 12 + packages/rest-client/__tests__/auth-fetch.ts | 51 ++++ packages/rest-client/__tests__/rest-client.ts | 251 ++++++++++++++++++ packages/rest-client/package.json | 61 +++++ packages/rest-client/src/auth-fetch.ts | 34 +++ packages/rest-client/src/index.ts | 2 + packages/rest-client/src/rest-client.ts | 195 ++++++++++++++ packages/rest-client/tsconfig.json | 16 ++ packages/rest-client/yarn.lock | 15 ++ 10 files changed, 644 insertions(+) create mode 100644 packages/rest-client/.npmignore create mode 100644 packages/rest-client/README.md create mode 100644 packages/rest-client/__tests__/auth-fetch.ts create mode 100644 packages/rest-client/__tests__/rest-client.ts create mode 100644 packages/rest-client/package.json create mode 100644 packages/rest-client/src/auth-fetch.ts create mode 100644 packages/rest-client/src/index.ts create mode 100644 packages/rest-client/src/rest-client.ts create mode 100644 packages/rest-client/tsconfig.json create mode 100644 packages/rest-client/yarn.lock diff --git a/packages/rest-client/.npmignore b/packages/rest-client/.npmignore new file mode 100644 index 000000000..987e688aa --- /dev/null +++ b/packages/rest-client/.npmignore @@ -0,0 +1,7 @@ +__tests__ +src/ +coverage/ +node_modules +.npmignore +tsconfig.json +yarn.lock \ No newline at end of file diff --git a/packages/rest-client/README.md b/packages/rest-client/README.md new file mode 100644 index 000000000..e4dbd96ed --- /dev/null +++ b/packages/rest-client/README.md @@ -0,0 +1,12 @@ +# @accounts/rest-client + +[![npm](https://img.shields.io/npm/v/@accounts/rest-client.svg?maxAge=2592000)](https://www.npmjs.com/package/@accounts/rest-client) +[![CircleCI](https://circleci.com/gh/accounts-js/rest.svg?style=shield)](https://circleci.com/gh/accounts-js/rest) +[![codecov](https://codecov.io/gh/accounts-js/rest/branch/master/graph/badge.svg)](https://codecov.io/gh/accounts-js/rest) +![MIT License](https://img.shields.io/badge/license-MIT-blue.svg) + +## Install + +``` +yarn add @accounts/rest-client +``` diff --git a/packages/rest-client/__tests__/auth-fetch.ts b/packages/rest-client/__tests__/auth-fetch.ts new file mode 100644 index 000000000..c890a7b1d --- /dev/null +++ b/packages/rest-client/__tests__/auth-fetch.ts @@ -0,0 +1,51 @@ +import fetch from 'node-fetch'; +import { authFetch } from '../src/auth-fetch'; + +window.fetch = jest.fn().mockImplementation(() => ({ + status: 200, + json: jest.fn().mockImplementation(() => ({ test: 'test' })), +})); + +describe('authFetch', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call fetch', async () => { + const accounts = { + refreshSession: jest.fn(() => Promise.resolve()), + tokens: jest.fn(() => Promise.resolve({})), + }; + await authFetch(accounts, 'path', {}); + expect(accounts.refreshSession).toBeCalled(); + expect(accounts.tokens).toBeCalled(); + }); + + it('should set access token header', async () => { + const accounts = { + refreshSession: jest.fn(() => Promise.resolve()), + tokens: jest.fn(() => Promise.resolve({ accessToken: 'accessToken' })), + }; + await authFetch(accounts, 'path', {}); + expect(accounts.refreshSession).toBeCalled(); + expect(accounts.tokens).toBeCalled(); + expect(window.fetch.mock.calls[0][1].headers['accounts-access-token']).toBe( + 'accessToken' + ); + }); + + it('should pass other headers', async () => { + const accounts = { + refreshSession: jest.fn(() => Promise.resolve()), + tokens: jest.fn(() => Promise.resolve({ accessToken: 'accessToken' })), + }; + await authFetch(accounts, 'path', { + headers: { + toto: 'toto', + }, + }); + expect(accounts.refreshSession).toBeCalled(); + expect(accounts.tokens).toBeCalled(); + expect(window.fetch.mock.calls[0][1].headers.toto).toBe('toto'); + }); +}); diff --git a/packages/rest-client/__tests__/rest-client.ts b/packages/rest-client/__tests__/rest-client.ts new file mode 100644 index 000000000..49a95e6fe --- /dev/null +++ b/packages/rest-client/__tests__/rest-client.ts @@ -0,0 +1,251 @@ +import fetch from 'node-fetch'; +import { RestClient } from '../src/rest-client'; + +window.fetch = jest.fn().mockImplementation(() => ({ + status: 200, + json: jest.fn().mockImplementation(() => ({ test: 'test' })), +})); + +window.Headers = fetch.Headers; + +describe('RestClient', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should have a way to configure api host address and root path', () => { + const client = new RestClient({ + apiHost: 'http://localhost:3000/', + rootPath: 'accounts', + }); + + expect(client.options.apiHost).toBe('http://localhost:3000/'); + expect(client.options.rootPath).toBe('accounts'); + + return client.fetch('try').then(() => { + expect(window.fetch.mock.calls[0][0]).toBe( + 'http://localhost:3000/accounts/try' + ); + }); + }); + + describe('fetch', () => { + const client = new RestClient({ + apiHost: 'http://localhost:3000/', + rootPath: 'accounts', + }); + + it('should enable custom headers', () => + client + .fetch('route', {}, { origin: 'localhost:3000' }) + .then(() => + expect(window.fetch.mock.calls[0][1].headers.origin).toBe( + 'localhost:3000' + ) + )); + + it('should throw error', async () => { + window.fetch = jest.fn().mockImplementation(() => ({ + status: 400, + json: jest.fn().mockImplementation(() => ({ test: 'test' })), + })); + + try { + await client.fetch('route', {}, { origin: 'localhost:3000' }); + throw new Error(); + } catch (err) { + expect(window.fetch.mock.calls[0][1].headers.origin).toBe( + 'localhost:3000' + ); + } + window.fetch = jest.fn().mockImplementation(() => ({ + status: 200, + json: jest.fn().mockImplementation(() => ({ test: 'test' })), + })); + }); + + it('should throw if server did not return a response', async () => { + window.fetch = jest.fn().mockImplementation(() => null); + + try { + await client.fetch('route', {}, { origin: 'localhost:3000' }); + throw new Error(); + } catch (err) { + expect(window.fetch.mock.calls[0][1].headers.origin).toBe( + 'localhost:3000' + ); + expect(err.message).toBe('Server did not return a response'); + } + window.fetch = jest.fn().mockImplementation(() => ({ + status: 200, + json: jest.fn().mockImplementation(() => ({ test: 'test' })), + })); + }); + }); + + describe('loginWithService', () => { + const client = new RestClient({ + apiHost: 'http://localhost:3000', + rootPath: '/accounts', + }); + + it('should call fetch with authenticate path', async () => { + await client.loginWithService('password', { + user: { + username: 'toto', + }, + password: 'password', + }); + expect(window.fetch.mock.calls[0][0]).toBe( + 'http://localhost:3000/accounts/password/authenticate' + ); + expect(window.fetch.mock.calls[0][1].body).toBe( + '{"user":{"username":"toto"},"password":"password"}' + ); + }); + }); + + describe('impersonate', () => { + const client = new RestClient({ + apiHost: 'http://localhost:3000', + rootPath: '/accounts', + }); + + it('should call fetch with impersonate path', () => + client + .impersonate('token', 'user') + .then(() => + expect(window.fetch.mock.calls[0][0]).toBe( + 'http://localhost:3000/accounts/impersonate' + ) + )); + }); + + describe('refreshTokens', () => { + const client = new RestClient({ + apiHost: 'http://localhost:3000', + rootPath: '/accounts', + }); + + it('should call fetch with refreshTokens path', () => + client + .refreshTokens('accessToken', 'refreshToken') + .then(() => + expect(window.fetch.mock.calls[0][0]).toBe( + 'http://localhost:3000/accounts/refreshTokens' + ) + )); + }); + + describe('logout', () => { + const client = new RestClient({ + apiHost: 'http://localhost:3000', + rootPath: '/accounts', + }); + + it('should call fetch with logout path', () => + client + .logout('accessToken') + .then(() => + expect(window.fetch.mock.calls[0][0]).toBe( + 'http://localhost:3000/accounts/logout' + ) + )); + }); + + describe('getUser', () => { + const client = new RestClient({ + apiHost: 'http://localhost:3000', + rootPath: '/accounts', + }); + + it('should call fetch with user path', () => + client + .getUser('accessToken') + .then(() => + expect(window.fetch.mock.calls[0][0]).toBe( + 'http://localhost:3000/accounts/user' + ) + )); + }); + + describe('createUser', () => { + const client = new RestClient({ + apiHost: 'http://localhost:3000', + rootPath: '/accounts', + }); + + it('should call fetch with register path', () => + client + .createUser('user') + .then(() => + expect(window.fetch.mock.calls[0][0]).toBe( + 'http://localhost:3000/accounts/password/register' + ) + )); + }); + + describe('resetPassword', () => { + const client = new RestClient({ + apiHost: 'http://localhost:3000', + rootPath: '/accounts', + }); + + it('should call fetch with resetPassword path', () => + client + .resetPassword('token', 'resetPassword') + .then(() => + expect(window.fetch.mock.calls[0][0]).toBe( + 'http://localhost:3000/accounts/password/resetPassword' + ) + )); + }); + + describe('verifyEmail', () => { + const client = new RestClient({ + apiHost: 'http://localhost:3000', + rootPath: '/accounts', + }); + + it('should call fetch with verifyEmail path', () => + client + .verifyEmail('token') + .then(() => + expect(window.fetch.mock.calls[0][0]).toBe( + 'http://localhost:3000/accounts/password/verifyEmail' + ) + )); + }); + + describe('sendVerificationEmail', () => { + const client = new RestClient({ + apiHost: 'http://localhost:3000', + rootPath: '/accounts', + }); + + it('should call fetch with verifyEmail path', () => + client + .sendVerificationEmail('email') + .then(() => + expect(window.fetch.mock.calls[0][0]).toBe( + 'http://localhost:3000/accounts/password/sendVerificationEmail' + ) + )); + }); + + describe('sendResetPasswordEmail', () => { + const client = new RestClient({ + apiHost: 'http://localhost:3000', + rootPath: '/accounts', + }); + + it('should call fetch with verifyEmail path', () => + client + .sendResetPasswordEmail('email') + .then(() => + expect(window.fetch.mock.calls[0][0]).toBe( + 'http://localhost:3000/accounts/password/sendResetPasswordEmail' + ) + )); + }); +}); diff --git a/packages/rest-client/package.json b/packages/rest-client/package.json new file mode 100644 index 000000000..d6ae191e2 --- /dev/null +++ b/packages/rest-client/package.json @@ -0,0 +1,61 @@ +{ + "name": "@accounts/rest-client", + "version": "0.1.0-beta.2", + "description": "REST client for accounts", + "main": "lib/index", + "typings": "lib/index", + "publishConfig": { + "access": "public" + }, + "scripts": { + "start": "tsc --watch", + "precompile": "rimraf ./lib", + "compile": "tsc", + "prepublish": "npm run compile", + "test": "npm run testonly", + "testonly": "jest", + "coverage": "npm run testonly -- --coverage", + "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" + }, + "jest": { + "testEnvironment": "jsdom", + "transform": { + ".(ts|tsx)": "/../../node_modules/ts-jest/preprocessor.js" + }, + "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx)$", + "moduleFileExtensions": [ + "ts", + "js" + ], + "mapCoverage": true + }, + "repository": { + "type": "git", + "url": "https://github.com/js-accounts/rest/tree/master/packages/rest-client" + }, + "keywords": [ + "rest", + "graphql", + "grant", + "auth", + "authentication", + "accounts", + "users", + "oauth" + ], + "author": "Tim Mikeladze", + "license": "MIT", + "devDependencies": { + "@accounts/client": "0.1.0-beta.3", + "@accounts/common": "0.1.0-beta.3", + "@types/lodash": "4.14.104", + "node-fetch": "2.1.1" + }, + "peerDependencies": { + "@accounts/client": "^0.1.0-beta.0", + "@accounts/common": "^0.1.0-beta.0" + }, + "dependencies": { + "lodash": "^4.17.4" + } +} diff --git a/packages/rest-client/src/auth-fetch.ts b/packages/rest-client/src/auth-fetch.ts new file mode 100644 index 000000000..37599432a --- /dev/null +++ b/packages/rest-client/src/auth-fetch.ts @@ -0,0 +1,34 @@ +import { forIn } from 'lodash'; +import { AccountsClient } from '@accounts/client'; + +const headers: { [key: string]: string } = { + 'Content-Type': 'application/json', +}; + +export const authFetch = async ( + accounts: AccountsClient, + path: string, + request: any +) => { + await accounts.refreshSession(); + const { accessToken } = await accounts.tokens(); + const headersCopy = { ...headers }; + + if (accessToken) { + headersCopy['accounts-access-token'] = accessToken; + } + + /* tslint:disable no-string-literal */ + if (request['headers']) { + forIn(request['headers'], (v: string, k: string) => { + headersCopy[v] = k; + }); + } + /* tslint:enable no-string-literal */ + + const fetchOptions = { + ...request, + headers: headersCopy, + }; + return fetch(path, fetchOptions); +}; diff --git a/packages/rest-client/src/index.ts b/packages/rest-client/src/index.ts new file mode 100644 index 000000000..5f16eacb7 --- /dev/null +++ b/packages/rest-client/src/index.ts @@ -0,0 +1,2 @@ +export { RestClient } from './rest-client'; +export { authFetch } from './auth-fetch'; diff --git a/packages/rest-client/src/rest-client.ts b/packages/rest-client/src/rest-client.ts new file mode 100644 index 000000000..75d8cb72b --- /dev/null +++ b/packages/rest-client/src/rest-client.ts @@ -0,0 +1,195 @@ +import { forIn, isPlainObject } from 'lodash'; +import { TransportInterface, AccountsClient } from '@accounts/client'; +import { + AccountsError, + CreateUserType, + LoginReturnType, + UserObjectType, + ImpersonateReturnType, +} from '@accounts/common'; + +export interface OptionsType { + apiHost: string; + rootPath: string; +} + +const headers: { [key: string]: string } = { + 'Content-Type': 'application/json', +}; + +export class RestClient implements TransportInterface { + private options: OptionsType; + + constructor(options: OptionsType) { + this.options = options; + } + + public async fetch( + route: string, + args: object, + customHeaders: object = {} + ): Promise { + const fetchOptions = { + headers: this._loadHeadersObject(customHeaders), + ...args, + }; + const res = await fetch( + `${this.options.apiHost}${this.options.rootPath}/${route}`, + fetchOptions + ); + + if (res) { + if (res.status >= 400 && res.status < 600) { + const { message, loginInfo, errorCode } = await res.json(); + throw new AccountsError(message, loginInfo, errorCode); + } + return res.json(); + } else { + throw new Error('Server did not return a response'); + } + } + + public loginWithService( + provider: string, + data: any, + customHeaders?: object + ): Promise { + const args = { + method: 'POST', + body: JSON.stringify({ + ...data, + }), + }; + return this.fetch(`${provider}/authenticate`, args, customHeaders); + } + + public impersonate( + accessToken: string, + username: string, + customHeaders?: object + ): Promise { + const args = { + method: 'POST', + body: JSON.stringify({ + accessToken, + username, + }), + }; + return this.fetch('impersonate', args, customHeaders); + } + + public refreshTokens( + accessToken: string, + refreshToken: string, + customHeaders?: object + ): Promise { + const args = { + method: 'POST', + body: JSON.stringify({ + accessToken, + refreshToken, + }), + }; + return this.fetch('refreshTokens', args, customHeaders); + } + + public logout(accessToken: string, customHeaders?: object): Promise { + const args = { + method: 'POST', + body: JSON.stringify({ + accessToken, + }), + }; + return this.fetch('logout', args, customHeaders); + } + + public async getUser( + accessToken: string, + customHeaders?: object + ): Promise { + const args = { + method: 'POST', + body: JSON.stringify({ + accessToken, + }), + }; + return this.fetch('user', args, customHeaders); + } + + public async createUser( + user: CreateUserType, + customHeaders?: object + ): Promise { + const args = { + method: 'POST', + body: JSON.stringify({ user }), + }; + return this.fetch('password/register', args, customHeaders); + } + + public resetPassword( + token: string, + newPassword: string, + customHeaders?: object + ): Promise { + const args = { + method: 'POST', + body: JSON.stringify({ + token, + newPassword, + }), + }; + return this.fetch('password/resetPassword', args, customHeaders); + } + + public verifyEmail(token: string, customHeaders?: object): Promise { + const args = { + method: 'POST', + body: JSON.stringify({ + token, + }), + }; + return this.fetch('password/verifyEmail', args, customHeaders); + } + + public sendVerificationEmail( + email: string, + customHeaders?: object + ): Promise { + const args = { + method: 'POST', + body: JSON.stringify({ + email, + }), + }; + return this.fetch('password/sendVerificationEmail', args, customHeaders); + } + + public sendResetPasswordEmail( + email: string, + customHeaders?: object + ): Promise { + const args = { + method: 'POST', + body: JSON.stringify({ + email, + }), + }; + return this.fetch('password/sendResetPasswordEmail', args, customHeaders); + } + + private _loadHeadersObject(plainHeaders: object): { [key: string]: string } { + if (isPlainObject(plainHeaders)) { + const customHeaders = headers; + forIn(plainHeaders, (v: string, k: string) => { + customHeaders[k] = v; + }); + + return customHeaders; + } + + return headers; + } +} + +export default RestClient; diff --git a/packages/rest-client/tsconfig.json b/packages/rest-client/tsconfig.json new file mode 100644 index 000000000..e0945a464 --- /dev/null +++ b/packages/rest-client/tsconfig.json @@ -0,0 +1,16 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "lib": ["dom", "es6", "es2015", "es2016", "es2017"], + "typeRoots": [ + "node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "__tests__", + "lib" + ] +} diff --git a/packages/rest-client/yarn.lock b/packages/rest-client/yarn.lock new file mode 100644 index 000000000..6a982f55b --- /dev/null +++ b/packages/rest-client/yarn.lock @@ -0,0 +1,15 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/lodash@4.14.104": + version "4.14.104" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.104.tgz#53ee2357fa2e6e68379341d92eb2ecea4b11bb80" + +lodash@^4.17.4: + version "4.17.5" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" + +node-fetch@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.1.1.tgz#369ca70b82f50c86496104a6c776d274f4e4a2d4" From 56480b38421a0266bee6ac16115a7457fa8c1bf1 Mon Sep 17 00:00:00 2001 From: Elies Lou Date: Sun, 11 Mar 2018 16:57:03 +0100 Subject: [PATCH 2/6] TSLint : allow tests to use dependencies listed only in devDependencies --- tslint.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tslint.json b/tslint.json index 785fd5f61..7befafb1d 100644 --- a/tslint.json +++ b/tslint.json @@ -3,6 +3,7 @@ "@accounts/tslint-config-accounts" ], "rules": { - "no-submodule-imports": [true, "lodash", "@accounts/server"] + "no-submodule-imports": [true, "lodash", "@accounts/server"], + "no-implicit-dependencies": [true, "dev"] } } From b88caabe901df3f847b4652fd4586e48bfa092c7 Mon Sep 17 00:00:00 2001 From: Elies Lou Date: Sun, 11 Mar 2018 16:58:00 +0100 Subject: [PATCH 3/6] Copy rest/packages/express => accounts/packages/rest-express --- packages/rest-express/.npmignore | 7 + packages/rest-express/README.md | 12 + .../__tests__/endpoints/get-user.ts | 61 +++ .../__tests__/endpoints/impersonate.ts | 73 ++++ .../__tests__/endpoints/logout.ts | 56 +++ .../endpoints/oauth/provider-callback.ts | 72 ++++ .../__tests__/endpoints/password/register.ts | 79 ++++ .../__tests__/endpoints/password/reset.ts | 143 +++++++ .../endpoints/password/verify-email.ts | 141 +++++++ .../endpoints/refresh-access-token.ts | 75 ++++ .../endpoints/service-authenticate.ts | 67 ++++ .../__tests__/express-middleware.ts | 76 ++++ .../rest-express/__tests__/user-loader.ts | 61 +++ .../__tests__/utils/get-user-agent.ts | 23 ++ packages/rest-express/package.json | 60 +++ .../rest-express/src/endpoints/get-user.ts | 16 + .../rest-express/src/endpoints/impersonate.ts | 25 ++ packages/rest-express/src/endpoints/logout.ts | 16 + .../src/endpoints/oauth/provider-callback.ts | 32 ++ .../src/endpoints/password/register.ts | 16 + .../src/endpoints/password/reset.ts | 30 ++ .../src/endpoints/password/verify-email.ts | 31 ++ .../src/endpoints/refresh-access-token.ts | 25 ++ .../src/endpoints/service-authenticate.ts | 24 ++ .../rest-express/src/express-middleware.ts | 88 +++++ packages/rest-express/src/index.ts | 3 + packages/rest-express/src/user-loader.ts | 23 ++ .../rest-express/src/utils/get-user-agent.ts | 10 + packages/rest-express/src/utils/send-error.ts | 6 + packages/rest-express/tsconfig.json | 15 + packages/rest-express/yarn.lock | 364 ++++++++++++++++++ 31 files changed, 1730 insertions(+) create mode 100644 packages/rest-express/.npmignore create mode 100644 packages/rest-express/README.md create mode 100644 packages/rest-express/__tests__/endpoints/get-user.ts create mode 100644 packages/rest-express/__tests__/endpoints/impersonate.ts create mode 100644 packages/rest-express/__tests__/endpoints/logout.ts create mode 100644 packages/rest-express/__tests__/endpoints/oauth/provider-callback.ts create mode 100644 packages/rest-express/__tests__/endpoints/password/register.ts create mode 100644 packages/rest-express/__tests__/endpoints/password/reset.ts create mode 100644 packages/rest-express/__tests__/endpoints/password/verify-email.ts create mode 100644 packages/rest-express/__tests__/endpoints/refresh-access-token.ts create mode 100644 packages/rest-express/__tests__/endpoints/service-authenticate.ts create mode 100644 packages/rest-express/__tests__/express-middleware.ts create mode 100644 packages/rest-express/__tests__/user-loader.ts create mode 100644 packages/rest-express/__tests__/utils/get-user-agent.ts create mode 100644 packages/rest-express/package.json create mode 100644 packages/rest-express/src/endpoints/get-user.ts create mode 100644 packages/rest-express/src/endpoints/impersonate.ts create mode 100644 packages/rest-express/src/endpoints/logout.ts create mode 100644 packages/rest-express/src/endpoints/oauth/provider-callback.ts create mode 100644 packages/rest-express/src/endpoints/password/register.ts create mode 100644 packages/rest-express/src/endpoints/password/reset.ts create mode 100644 packages/rest-express/src/endpoints/password/verify-email.ts create mode 100644 packages/rest-express/src/endpoints/refresh-access-token.ts create mode 100644 packages/rest-express/src/endpoints/service-authenticate.ts create mode 100644 packages/rest-express/src/express-middleware.ts create mode 100644 packages/rest-express/src/index.ts create mode 100644 packages/rest-express/src/user-loader.ts create mode 100644 packages/rest-express/src/utils/get-user-agent.ts create mode 100644 packages/rest-express/src/utils/send-error.ts create mode 100644 packages/rest-express/tsconfig.json create mode 100644 packages/rest-express/yarn.lock diff --git a/packages/rest-express/.npmignore b/packages/rest-express/.npmignore new file mode 100644 index 000000000..987e688aa --- /dev/null +++ b/packages/rest-express/.npmignore @@ -0,0 +1,7 @@ +__tests__ +src/ +coverage/ +node_modules +.npmignore +tsconfig.json +yarn.lock \ No newline at end of file diff --git a/packages/rest-express/README.md b/packages/rest-express/README.md new file mode 100644 index 000000000..eb5650079 --- /dev/null +++ b/packages/rest-express/README.md @@ -0,0 +1,12 @@ +# @accounts/rest-express + +[![npm](https://img.shields.io/npm/v/@accounts/rest-express.svg?maxAge=2592000)](https://www.npmjs.com/package/@accounts/rest-express) +[![CircleCI](https://circleci.com/gh/accounts-js/rest.svg?style=shield)](https://circleci.com/gh/accounts-js/rest) +[![codecov](https://codecov.io/gh/accounts-js/rest/branch/master/graph/badge.svg)](https://codecov.io/gh/accounts-js/rest) +![MIT License](https://img.shields.io/badge/license-MIT-blue.svg) + +## Install + +``` +yarn add @accounts/rest-express +``` diff --git a/packages/rest-express/__tests__/endpoints/get-user.ts b/packages/rest-express/__tests__/endpoints/get-user.ts new file mode 100644 index 000000000..f4ac97f69 --- /dev/null +++ b/packages/rest-express/__tests__/endpoints/get-user.ts @@ -0,0 +1,61 @@ +import { getUser } from '../../src/endpoints/get-user'; + +const res = { + json: jest.fn(), + status: jest.fn(() => res), +}; + +describe('getUser', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls getUser and returns the user json response', async () => { + const user = { + id: '1', + }; + const accountsServer = { + resumeSession: jest.fn(() => user), + }; + const middleware = getUser(accountsServer as any); + + const req = { + body: { + accessToken: 'token', + }, + headers: {}, + }; + const reqCopy = { ...req }; + + await middleware(req, res); + + expect(req).toEqual(reqCopy); + expect(accountsServer.resumeSession).toBeCalledWith('token'); + expect(res.json).toBeCalledWith(user); + expect(res.status).not.toBeCalled(); + }); + + it('Sends error if it was thrown on getUser', async () => { + const error = { message: 'Could not get user' }; + const accountsServer = { + resumeSession: jest.fn(() => { + throw error; + }), + }; + const middleware = getUser(accountsServer as any); + const req = { + body: { + accessToken: 'token', + }, + headers: {}, + }; + const reqCopy = { ...req }; + + await middleware(req, res); + + expect(req).toEqual(reqCopy); + expect(accountsServer.resumeSession).toBeCalledWith('token'); + expect(res.status).toBeCalledWith(400); + expect(res.json).toBeCalledWith(error); + }); +}); diff --git a/packages/rest-express/__tests__/endpoints/impersonate.ts b/packages/rest-express/__tests__/endpoints/impersonate.ts new file mode 100644 index 000000000..8c59e9781 --- /dev/null +++ b/packages/rest-express/__tests__/endpoints/impersonate.ts @@ -0,0 +1,73 @@ +import { impersonate } from '../../src/endpoints/impersonate'; + +const res = { + json: jest.fn(), + status: jest.fn(() => res), +}; + +describe('impersonate', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls impersonate and returns the impersonate json response', async () => { + const impersonateReturnType = { + id: '1', + }; + const accountsServer = { + impersonate: jest.fn(() => impersonateReturnType), + }; + const middleware = impersonate(accountsServer as any); + + const req = { + body: { + username: 'toto', + accessToken: 'token', + }, + headers: {}, + }; + const reqCopy = { ...req }; + + await middleware(req, res); + + expect(req).toEqual(reqCopy); + expect(accountsServer.impersonate).toBeCalledWith( + 'token', + 'toto', + null, + '' + ); + expect(res.json).toBeCalledWith(impersonateReturnType); + expect(res.status).not.toBeCalled(); + }); + + it('Sends error if it was thrown on impersonate', async () => { + const error = { message: 'Could not impersonate' }; + const accountsServer = { + impersonate: jest.fn(() => { + throw error; + }), + }; + const middleware = impersonate(accountsServer as any); + const req = { + body: { + username: 'toto', + accessToken: 'token', + }, + headers: {}, + }; + const reqCopy = { ...req }; + + await middleware(req, res); + + expect(req).toEqual(reqCopy); + expect(accountsServer.impersonate).toBeCalledWith( + 'token', + 'toto', + null, + '' + ); + expect(res.status).toBeCalledWith(400); + expect(res.json).toBeCalledWith(error); + }); +}); diff --git a/packages/rest-express/__tests__/endpoints/logout.ts b/packages/rest-express/__tests__/endpoints/logout.ts new file mode 100644 index 000000000..9a375c6df --- /dev/null +++ b/packages/rest-express/__tests__/endpoints/logout.ts @@ -0,0 +1,56 @@ +import { logout } from '../../src/endpoints/logout'; + +const res = { + json: jest.fn(), + status: jest.fn(() => res), +}; + +describe('logout', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls logout and returns a message if when logged out successfuly', async () => { + const accountsServer = { + logout: jest.fn(), + }; + const middleware = logout(accountsServer as any); + + const req = { + body: { + accessToken: 'token', + }, + }; + const reqCopy = { ...req }; + + await middleware(req, res); + + expect(req).toEqual(reqCopy); + expect(accountsServer.logout).toBeCalledWith('token'); + expect(res.json).toBeCalledWith({ message: 'Logged out' }); + expect(res.status).not.toBeCalled(); + }); + + it('Sends error if it was thrown on logout', async () => { + const error = { message: 'Could not logout' }; + const accountsServer = { + logout: jest.fn(() => { + throw error; + }), + }; + const middleware = logout(accountsServer as any); + const req = { + body: { + accessToken: 'token', + }, + }; + const reqCopy = { ...req }; + + await middleware(req, res); + + expect(req).toEqual(reqCopy); + expect(accountsServer.logout).toBeCalledWith('token'); + expect(res.status).toBeCalledWith(400); + expect(res.json).toBeCalledWith(error); + }); +}); diff --git a/packages/rest-express/__tests__/endpoints/oauth/provider-callback.ts b/packages/rest-express/__tests__/endpoints/oauth/provider-callback.ts new file mode 100644 index 000000000..f7a8002f2 --- /dev/null +++ b/packages/rest-express/__tests__/endpoints/oauth/provider-callback.ts @@ -0,0 +1,72 @@ +import { providerCallback } from '../../../src/endpoints/oauth/provider-callback'; + +const res = { + json: jest.fn(), + status: jest.fn(() => res), +}; + +describe('providerCallback', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls loginWithService and returns the user json response', async () => { + const user = { + id: '1', + }; + const accountsServer = { + loginWithService: jest.fn(() => user), + }; + const middleware = providerCallback(accountsServer as any); + + const req = { + params: { + accessToken: 'token', + }, + query: { + accessTokenSecret: 'secret', + }, + headers: {}, + }; + const reqCopy = { ...req }; + + await middleware(req, res); + + expect(req).toEqual(reqCopy); + expect(accountsServer.loginWithService).toBeCalledWith( + 'oauth', + { accessToken: 'token', accessTokenSecret: 'secret' }, + { ip: null, userAgent: '' } + ); + expect(res.json).toBeCalledWith(user); + expect(res.status).not.toBeCalled(); + }); + + it('Sends error if it was thrown on loginWithService', async () => { + const error = { message: 'Could not login' }; + const accountsServer = { + loginWithService: jest.fn(() => { + throw error; + }), + }; + const middleware = providerCallback(accountsServer as any); + const req = { + params: { + accessToken: 'token', + }, + headers: {}, + }; + const reqCopy = { ...req }; + + await middleware(req, res); + + expect(req).toEqual(reqCopy); + expect(accountsServer.loginWithService).toBeCalledWith( + 'oauth', + { accessToken: 'token' }, + { ip: null, userAgent: '' } + ); + expect(res.status).toBeCalledWith(400); + expect(res.json).toBeCalledWith(error); + }); +}); diff --git a/packages/rest-express/__tests__/endpoints/password/register.ts b/packages/rest-express/__tests__/endpoints/password/register.ts new file mode 100644 index 000000000..660ca2008 --- /dev/null +++ b/packages/rest-express/__tests__/endpoints/password/register.ts @@ -0,0 +1,79 @@ +import { registerPassword } from '../../../src/endpoints/password/register'; + +const res = { + json: jest.fn(), + status: jest.fn(() => res), +}; + +describe('registerPassword', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls password.createUser and returns the user json response', async () => { + const userId = '1'; + const passwordService = { + createUser: jest.fn(() => userId), + }; + const accountsServer = { + getServices: () => ({ + password: passwordService, + }), + }; + const middleware = registerPassword(accountsServer as any); + + const req = { + body: { + user: { + username: 'toto', + }, + extraFieldThatShouldNotBePassed: 'hey', + }, + headers: {}, + }; + const reqCopy = { ...req }; + + await middleware(req, res); + + expect(req).toEqual(reqCopy); + expect(accountsServer.getServices().password.createUser).toBeCalledWith({ + username: 'toto', + }); + expect(res.json).toBeCalledWith({ userId: '1' }); + expect(res.status).not.toBeCalled(); + }); + + it('Sends error if it was thrown on loginWithService', async () => { + const error = { message: 'Could not login' }; + const passwordService = { + createUser: jest.fn(() => { + throw error; + }), + }; + const accountsServer = { + getServices: () => ({ + password: passwordService, + }), + }; + const middleware = registerPassword(accountsServer as any); + const req = { + body: { + user: { + username: 'toto', + }, + extraFieldThatShouldNotBePassed: 'hey', + }, + headers: {}, + }; + const reqCopy = { ...req }; + + await middleware(req, res); + + expect(req).toEqual(reqCopy); + expect(accountsServer.getServices().password.createUser).toBeCalledWith({ + username: 'toto', + }); + expect(res.status).toBeCalledWith(400); + expect(res.json).toBeCalledWith(error); + }); +}); diff --git a/packages/rest-express/__tests__/endpoints/password/reset.ts b/packages/rest-express/__tests__/endpoints/password/reset.ts new file mode 100644 index 000000000..450c2db1e --- /dev/null +++ b/packages/rest-express/__tests__/endpoints/password/reset.ts @@ -0,0 +1,143 @@ +import { + resetPassword, + sendResetPasswordEmail, +} from '../../../src/endpoints/password/reset'; + +const res = { + json: jest.fn(), + status: jest.fn(() => res), +}; + +describe('resetPassword', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('resetPassword', () => { + it('calls password.resetPassword and returns a message', async () => { + const message = 'Password changed'; + const passwordService = { + resetPassword: jest.fn(() => null), + }; + const accountsServer = { + getServices: () => ({ + password: passwordService, + }), + }; + const middleware = resetPassword(accountsServer as any); + + const req = { + body: { + token: 'token', + newPassword: 'new-password', + }, + headers: {}, + }; + const reqCopy = { ...req }; + + await middleware(req, res); + + expect(req).toEqual(reqCopy); + expect( + accountsServer.getServices().password.resetPassword + ).toBeCalledWith('token', 'new-password'); + expect(res.json).toBeCalledWith({ message }); + expect(res.status).not.toBeCalled(); + }); + + it('Sends error if it was thrown on resetPassword', async () => { + const error = { message: 'Could not reset password' }; + const passwordService = { + resetPassword: jest.fn(() => { + throw error; + }), + }; + const accountsServer = { + getServices: () => ({ + password: passwordService, + }), + }; + const middleware = resetPassword(accountsServer as any); + const req = { + body: { + token: 'token', + newPassword: 'new-password', + }, + headers: {}, + }; + const reqCopy = { ...req }; + + await middleware(req, res); + + expect(req).toEqual(reqCopy); + expect( + accountsServer.getServices().password.resetPassword + ).toBeCalledWith('token', 'new-password'); + expect(res.status).toBeCalledWith(400); + expect(res.json).toBeCalledWith(error); + }); + }); + + describe('sendResetPasswordEmail', () => { + it('calls password.sendResetPasswordEmail and returns a message', async () => { + const message = 'Email sent'; + const passwordService = { + sendResetPasswordEmail: jest.fn(() => null), + }; + const accountsServer = { + getServices: () => ({ + password: passwordService, + }), + }; + const middleware = sendResetPasswordEmail(accountsServer as any); + + const req = { + body: { + email: 'email', + }, + headers: {}, + }; + const reqCopy = { ...req }; + + await middleware(req, res); + + expect(req).toEqual(reqCopy); + expect( + accountsServer.getServices().password.sendResetPasswordEmail + ).toBeCalledWith('email'); + expect(res.json).toBeCalledWith({ message }); + expect(res.status).not.toBeCalled(); + }); + + it('Sends error if it was thrown on sendResetPasswordEmail', async () => { + const error = { message: 'Could not send reset password' }; + const passwordService = { + sendResetPasswordEmail: jest.fn(() => { + throw error; + }), + }; + const accountsServer = { + getServices: () => ({ + password: passwordService, + }), + }; + const middleware = sendResetPasswordEmail(accountsServer as any); + const req = { + body: { + email: 'email', + }, + headers: {}, + }; + const reqCopy = { ...req }; + + await middleware(req, res); + + expect(req).toEqual(reqCopy); + expect( + accountsServer.getServices().password.sendResetPasswordEmail + ).toBeCalledWith('email'); + expect(res.status).toBeCalledWith(400); + expect(res.json).toBeCalledWith(error); + }); + }); +}); diff --git a/packages/rest-express/__tests__/endpoints/password/verify-email.ts b/packages/rest-express/__tests__/endpoints/password/verify-email.ts new file mode 100644 index 000000000..fb31379ae --- /dev/null +++ b/packages/rest-express/__tests__/endpoints/password/verify-email.ts @@ -0,0 +1,141 @@ +import { + verifyEmail, + sendVerificationEmail, +} from '../../../src/endpoints/password/verify-email'; + +const res = { + json: jest.fn(), + status: jest.fn(() => res), +}; + +describe('verifyEmail', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + describe('verifyEmail', () => { + it('calls password.verifyEmail and returns a message', async () => { + const message = 'Email verified'; + const passwordService = { + verifyEmail: jest.fn(() => null), + }; + const accountsServer = { + getServices: () => ({ + password: passwordService, + }), + }; + const middleware = verifyEmail(accountsServer as any); + + const req = { + body: { + token: 'token', + }, + headers: {}, + }; + const reqCopy = { ...req }; + + await middleware(req, res); + + expect(req).toEqual(reqCopy); + expect(accountsServer.getServices().password.verifyEmail).toBeCalledWith( + 'token' + ); + expect(res.json).toBeCalledWith({ message }); + expect(res.status).not.toBeCalled(); + }); + + it('Sends error if it was thrown on verifyEmail', async () => { + const error = { message: 'Could not verify email' }; + const passwordService = { + verifyEmail: jest.fn(() => { + throw error; + }), + }; + const accountsServer = { + getServices: () => ({ + password: passwordService, + }), + }; + const middleware = verifyEmail(accountsServer as any); + const req = { + body: { + token: 'token', + }, + headers: {}, + }; + const reqCopy = { ...req }; + + await middleware(req, res); + + expect(req).toEqual(reqCopy); + expect(accountsServer.getServices().password.verifyEmail).toBeCalledWith( + 'token' + ); + expect(res.status).toBeCalledWith(400); + expect(res.json).toBeCalledWith(error); + }); + }); + + describe('sendVerificationEmail', () => { + it('calls password.sendVerificationEmail and returns a message', async () => { + const message = 'Email sent'; + const passwordService = { + sendVerificationEmail: jest.fn(() => null), + }; + const accountsServer = { + getServices: () => ({ + password: passwordService, + }), + }; + const middleware = sendVerificationEmail(accountsServer as any); + + const req = { + body: { + email: 'email', + }, + headers: {}, + }; + const reqCopy = { ...req }; + + await middleware(req, res); + + expect(req).toEqual(reqCopy); + expect( + accountsServer.getServices().password.sendVerificationEmail + ).toBeCalledWith('email'); + expect(res.json).toBeCalledWith({ message }); + expect(res.status).not.toBeCalled(); + }); + + it('Sends error if it was thrown on sendVerificationEmail', async () => { + const error = { message: 'Could not send verification email' }; + const passwordService = { + sendVerificationEmail: jest.fn(() => { + throw error; + }), + }; + const accountsServer = { + getServices: () => ({ + password: passwordService, + }), + }; + const middleware = sendVerificationEmail(accountsServer as any); + const req = { + body: { + email: 'email', + }, + headers: {}, + }; + const reqCopy = { ...req }; + + await middleware(req, res); + + expect(req).toEqual(reqCopy); + expect( + accountsServer.getServices().password.sendVerificationEmail + ).toBeCalledWith('email'); + expect(res.status).toBeCalledWith(400); + expect(res.json).toBeCalledWith(error); + }); + }); +}); diff --git a/packages/rest-express/__tests__/endpoints/refresh-access-token.ts b/packages/rest-express/__tests__/endpoints/refresh-access-token.ts new file mode 100644 index 000000000..d97b3dcfe --- /dev/null +++ b/packages/rest-express/__tests__/endpoints/refresh-access-token.ts @@ -0,0 +1,75 @@ +import { refreshAccessToken } from '../../src/endpoints/refresh-access-token'; + +const res = { + json: jest.fn(), + status: jest.fn(() => res), +}; + +describe('refreshAccessToken', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls refreshTokens and returns the session in json format', async () => { + const session = { + user: { + id: '1', + }, + }; + const accountsServer = { + refreshTokens: jest.fn(() => session), + }; + const middleware = refreshAccessToken(accountsServer as any); + + const req = { + headers: {}, + body: { + accessToken: 'token', + refreshToken: 'refresh', + }, + }; + const reqCopy = { ...req }; + + await middleware(req, res); + + expect(accountsServer.refreshTokens).toBeCalledWith( + 'token', + 'refresh', + null, + '' + ); + expect(req).toEqual(reqCopy); + expect(res.json).toBeCalledWith(session); + expect(res.status).not.toBeCalled(); + }); + + it('Sends error if it was thrown on refreshTokens', async () => { + const error = { message: 'error' }; + const accountsServer = { + refreshTokens: jest.fn(() => { + throw error; + }), + }; + const middleware = refreshAccessToken(accountsServer as any); + const req = { + headers: {}, + body: { + accessToken: 'token', + refreshToken: 'refresh', + }, + }; + const reqCopy = { ...req }; + + await middleware(req, res); + + expect(req).toEqual(reqCopy); + expect(accountsServer.refreshTokens).toBeCalledWith( + 'token', + 'refresh', + null, + '' + ); + expect(res.status).toBeCalledWith(400); + expect(res.json).toBeCalledWith(error); + }); +}); diff --git a/packages/rest-express/__tests__/endpoints/service-authenticate.ts b/packages/rest-express/__tests__/endpoints/service-authenticate.ts new file mode 100644 index 000000000..3873ff667 --- /dev/null +++ b/packages/rest-express/__tests__/endpoints/service-authenticate.ts @@ -0,0 +1,67 @@ +import { serviceAuthenticate } from '../../src/endpoints/service-authenticate'; + +const res = { + json: jest.fn(), + status: jest.fn(() => res), +}; + +describe('serviceAuthenticate', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('calls loginWithService and returns the user in json format', async () => { + const user = { + id: '1', + }; + const accountsServer = { + loginWithService: jest.fn(() => user), + }; + const middleware = serviceAuthenticate(accountsServer as any); + + const req = { + params: { + service: 'sms', + }, + headers: {}, + }; + const reqCopy = { ...req }; + + await middleware(req, res); + + expect(req).toEqual(reqCopy); + expect(accountsServer.loginWithService).toBeCalledWith('sms', undefined, { + ip: null, + userAgent: '', + }); + expect(res.json).toBeCalledWith(user); + expect(res.status).not.toBeCalled(); + }); + + it('Sends error if it was thrown on loginWithService', async () => { + const error = { message: 'Could not login' }; + const accountsServer = { + loginWithService: jest.fn(() => { + throw error; + }), + }; + const middleware = serviceAuthenticate(accountsServer as any); + const req = { + params: { + service: 'sms', + }, + headers: {}, + }; + const reqCopy = { ...req }; + + await middleware(req, res); + + expect(req).toEqual(reqCopy); + expect(accountsServer.loginWithService).toBeCalledWith('sms', undefined, { + ip: null, + userAgent: '', + }); + 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 new file mode 100644 index 000000000..6bca1701c --- /dev/null +++ b/packages/rest-express/__tests__/express-middleware.ts @@ -0,0 +1,76 @@ +import accountsExpress from '../src'; +import * as express from 'express'; + +jest.mock('express', () => { + const mockRouter = { + post: jest.fn(), + get: jest.fn(), + }; + return { + Router: () => mockRouter, + }; +}); + +const router = express.Router(); + +describe('express middleware', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('Defines default endpoints on given path fragment', () => { + accountsExpress( + { + getServices: () => ({}), + } as any, + { path: 'test' } + ); + expect(router.post.mock.calls[0][0]).toBe('test/impersonate'); + expect(router.post.mock.calls[1][0]).toBe('test/user'); + expect(router.post.mock.calls[2][0]).toBe('test/refreshTokens'); + expect(router.post.mock.calls[3][0]).toBe('test/logout'); + expect(router.post.mock.calls[4][0]).toBe('test/:service/authenticate'); + }); + + it('Defines password endpoints when password service is present', () => { + accountsExpress( + { + getServices: () => ({ + password: {}, + }), + } as any, + { path: 'test' } + ); + expect(router.post.mock.calls[0][0]).toBe('test/impersonate'); + expect(router.post.mock.calls[1][0]).toBe('test/user'); + expect(router.post.mock.calls[2][0]).toBe('test/refreshTokens'); + expect(router.post.mock.calls[3][0]).toBe('test/logout'); + expect(router.post.mock.calls[4][0]).toBe('test/:service/authenticate'); + expect(router.post.mock.calls[5][0]).toBe('test/password/register'); + expect(router.post.mock.calls[6][0]).toBe('test/password/verifyEmail'); + expect(router.post.mock.calls[7][0]).toBe('test/password/resetPassword'); + expect(router.post.mock.calls[8][0]).toBe( + 'test/password/sendVerificationEmail' + ); + expect(router.post.mock.calls[9][0]).toBe( + 'test/password/sendResetPasswordEmail' + ); + }); + + it('Defines oauth endpoints when oauth service is present', () => { + accountsExpress( + { + getServices: () => ({ + oauth: {}, + }), + } as any, + { path: 'test' } + ); + expect(router.post.mock.calls[0][0]).toBe('test/impersonate'); + expect(router.post.mock.calls[1][0]).toBe('test/user'); + expect(router.post.mock.calls[2][0]).toBe('test/refreshTokens'); + expect(router.post.mock.calls[3][0]).toBe('test/logout'); + expect(router.post.mock.calls[4][0]).toBe('test/:service/authenticate'); + expect(router.get.mock.calls[0][0]).toBe('test/oauth/:provider/callback'); + }); +}); diff --git a/packages/rest-express/__tests__/user-loader.ts b/packages/rest-express/__tests__/user-loader.ts new file mode 100644 index 000000000..064c30a4b --- /dev/null +++ b/packages/rest-express/__tests__/user-loader.ts @@ -0,0 +1,61 @@ +import accountsExpress from '../src'; +import { userLoader } from '../src/user-loader'; + +const user = { id: '1' }; +const accountsServer = { + resumeSession: jest.fn(() => user), +}; +describe('userLoader', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('does noting when request has no accessToken', async () => { + const provider = userLoader(accountsServer as any); + const req = {}; + const res = {}; + const next = jest.fn(); + await provider(req, res, next); + + expect(accountsServer.resumeSession).not.toHaveBeenCalled(); + expect(req).toEqual({}); + expect(res).toEqual({}); + expect(next).toHaveBeenCalledTimes(1); + }); + + it('load user to req object when access token is present on the headers', async () => { + const provider = userLoader(accountsServer as any); + const req = { + headers: { + 'accounts-access-token': 'token', + }, + }; + const reqCopy = { ...req }; + const res = {}; + const next = jest.fn(); + await provider(req, res, next); + + expect(accountsServer.resumeSession).toHaveBeenCalledWith('token'); + expect(req).toEqual({ ...reqCopy, user, userId: user.id }); + expect(res).toEqual({}); + expect(next).toHaveBeenCalledTimes(1); + }); + + it('load user to req object when access token is present on the body', async () => { + const provider = userLoader(accountsServer as any); + const req = { + body: { + accessToken: 'token', + }, + }; + const reqCopy = { ...req }; + const res = {}; + const next = jest.fn(); + await provider(req, res, next); + + expect(accountsServer.resumeSession).toHaveBeenCalledWith('token'); + expect(req).toEqual({ ...reqCopy, user, userId: user.id }); + expect(res).toEqual({}); + expect(next).toHaveBeenCalledTimes(1); + }); +}); diff --git a/packages/rest-express/__tests__/utils/get-user-agent.ts b/packages/rest-express/__tests__/utils/get-user-agent.ts new file mode 100644 index 000000000..db1c9fd2f --- /dev/null +++ b/packages/rest-express/__tests__/utils/get-user-agent.ts @@ -0,0 +1,23 @@ +import { getUserAgent } from '../../src/utils/get-user-agent'; + +describe('getUserAgent', () => { + it('should return header user agent', () => { + const req = { + headers: { + 'user-agent': 'agent', + }, + }; + const userAgent = getUserAgent(req); + expect(userAgent).toBe('agent'); + }); + + it('should return header UC Browser user agent', () => { + const req = { + headers: { + 'x-ucbrowser-ua': 'agent', + }, + }; + const userAgent = getUserAgent(req); + expect(userAgent).toBe('agent'); + }); +}); diff --git a/packages/rest-express/package.json b/packages/rest-express/package.json new file mode 100644 index 000000000..587cb2c4d --- /dev/null +++ b/packages/rest-express/package.json @@ -0,0 +1,60 @@ +{ + "name": "@accounts/rest-express", + "version": "0.1.0-beta.2", + "description": "Server side REST express middleware for accounts", + "main": "lib/index", + "typings": "lib/index", + "publishConfig": { + "access": "public" + }, + "scripts": { + "start": "tsc --watch", + "compile": "tsc", + "prepublish": "npm run compile", + "test": "npm run testonly", + "test:watch": "npm run testonly -- --watch --coverage", + "testonly": "jest", + "coverage": "npm run testonly -- --coverage", + "coveralls": "cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js && rm -rf ./coverage" + }, + "jest": { + "testEnvironment": "node", + "transform": { + ".(ts|tsx)": "/../../node_modules/ts-jest/preprocessor.js" + }, + "testRegex": "(/__tests__/.*|\\.(test|spec))\\.(ts|tsx)$", + "moduleFileExtensions": [ + "ts", + "js" + ], + "mapCoverage": true + }, + "repository": { + "type": "git", + "url": "https://github.com/js-accounts/rest/tree/master/packages/rest-express" + }, + "keywords": [ + "users", + "accounts", + "rest", + "express" + ], + "author": "Tim Mikeladze", + "license": "MIT", + "devDependencies": { + "@accounts/common": "0.1.0-beta.3", + "@accounts/server": "0.1.0-beta.3", + "@types/express": "4.11.1", + "@types/lodash": "4.14.104", + "@types/request-ip": "0.0.33" + }, + "peerDependencies": { + "@accounts/common": "^0.1.0-beta.0", + "@accounts/server": "^0.1.0-beta.0" + }, + "dependencies": { + "express": "^4.16.2", + "lodash": "^4.17.4", + "request-ip": "^2.0.2" + } +} diff --git a/packages/rest-express/src/endpoints/get-user.ts b/packages/rest-express/src/endpoints/get-user.ts new file mode 100644 index 000000000..91a8c4b9f --- /dev/null +++ b/packages/rest-express/src/endpoints/get-user.ts @@ -0,0 +1,16 @@ +import * as express from 'express'; +import { AccountsServer } from '@accounts/server'; +import { sendError } from '../utils/send-error'; + +export const getUser = (accountsServer: AccountsServer) => async ( + req: express.Request, + res: express.Response +) => { + try { + const { accessToken } = req.body; + const user = await accountsServer.resumeSession(accessToken); + res.json(user); + } catch (err) { + sendError(res, err); + } +}; diff --git a/packages/rest-express/src/endpoints/impersonate.ts b/packages/rest-express/src/endpoints/impersonate.ts new file mode 100644 index 000000000..2c620f1fb --- /dev/null +++ b/packages/rest-express/src/endpoints/impersonate.ts @@ -0,0 +1,25 @@ +import * as express from 'express'; +import * as requestIp from 'request-ip'; +import { AccountsServer } from '@accounts/server'; +import { getUserAgent } from '../utils/get-user-agent'; +import { sendError } from '../utils/send-error'; + +export const impersonate = (accountsServer: AccountsServer) => async ( + req: express.Request, + res: express.Response +) => { + try { + const { username, accessToken } = req.body; + const userAgent = getUserAgent(req); + const ip = requestIp.getClientIp(req); + const impersonateRes = await accountsServer.impersonate( + accessToken, + username, + ip, + userAgent + ); + res.json(impersonateRes); + } catch (err) { + sendError(res, err); + } +}; diff --git a/packages/rest-express/src/endpoints/logout.ts b/packages/rest-express/src/endpoints/logout.ts new file mode 100644 index 000000000..83b7723d3 --- /dev/null +++ b/packages/rest-express/src/endpoints/logout.ts @@ -0,0 +1,16 @@ +import * as express from 'express'; +import { AccountsServer } from '@accounts/server'; +import { sendError } from '../utils/send-error'; + +export const logout = (accountsServer: AccountsServer) => async ( + req: express.Request, + res: express.Response +) => { + try { + const { accessToken } = req.body; + await accountsServer.logout(accessToken); + res.json({ message: 'Logged out' }); + } catch (err) { + sendError(res, err); + } +}; diff --git a/packages/rest-express/src/endpoints/oauth/provider-callback.ts b/packages/rest-express/src/endpoints/oauth/provider-callback.ts new file mode 100644 index 000000000..ec2fbafee --- /dev/null +++ b/packages/rest-express/src/endpoints/oauth/provider-callback.ts @@ -0,0 +1,32 @@ +import * as express from 'express'; +import * as requestIp from 'request-ip'; +import { AccountsServer } from '@accounts/server'; +import { getUserAgent } from '../../utils/get-user-agent'; +import { sendError } from '../../utils/send-error'; + +interface RequestWithSession extends express.Request { + session: { [key: string]: any }; +} + +export const providerCallback = (accountsServer: AccountsServer) => async ( + req: express.Request, + res: express.Response +) => { + try { + const userAgent = getUserAgent(req); + const ip = requestIp.getClientIp(req); + const loggedInUser = await accountsServer.loginWithService( + 'oauth', + { + ...(req.params || {}), + ...(req.query || {}), + ...(req.body || {}), + ...((req as RequestWithSession).session || {}), + }, + { ip, userAgent } + ); + res.json(loggedInUser); + } catch (err) { + sendError(res, err); + } +}; diff --git a/packages/rest-express/src/endpoints/password/register.ts b/packages/rest-express/src/endpoints/password/register.ts new file mode 100644 index 000000000..c44a9a4e9 --- /dev/null +++ b/packages/rest-express/src/endpoints/password/register.ts @@ -0,0 +1,16 @@ +import * as express from 'express'; +import { AccountsServer } from '@accounts/server'; +import { sendError } from '../../utils/send-error'; + +export const registerPassword = (accountsServer: AccountsServer) => async ( + req: express.Request, + res: express.Response +) => { + try { + const password: any = accountsServer.getServices().password; + const userId = await password.createUser(req.body.user); + res.json({ userId }); + } catch (err) { + sendError(res, err); + } +}; diff --git a/packages/rest-express/src/endpoints/password/reset.ts b/packages/rest-express/src/endpoints/password/reset.ts new file mode 100644 index 000000000..33e300849 --- /dev/null +++ b/packages/rest-express/src/endpoints/password/reset.ts @@ -0,0 +1,30 @@ +import * as express from 'express'; +import { AccountsServer } from '@accounts/server'; +import { sendError } from '../../utils/send-error'; + +export const resetPassword = (accountsServer: AccountsServer) => async ( + req: express.Request, + res: express.Response +) => { + try { + const { token, newPassword } = req.body; + const password: any = accountsServer.getServices().password; + await password.resetPassword(token, newPassword); + res.json({ message: 'Password changed' }); + } catch (err) { + sendError(res, err); + } +}; + +export const sendResetPasswordEmail = ( + accountsServer: AccountsServer +) => async (req: express.Request, res: express.Response) => { + try { + const { email } = req.body; + const password: any = accountsServer.getServices().password; + await password.sendResetPasswordEmail(email); + res.json({ message: 'Email sent' }); + } catch (err) { + sendError(res, err); + } +}; diff --git a/packages/rest-express/src/endpoints/password/verify-email.ts b/packages/rest-express/src/endpoints/password/verify-email.ts new file mode 100644 index 000000000..bbd162406 --- /dev/null +++ b/packages/rest-express/src/endpoints/password/verify-email.ts @@ -0,0 +1,31 @@ +import * as express from 'express'; +import { AccountsServer } from '@accounts/server'; +import { sendError } from '../../utils/send-error'; + +export const verifyEmail = (accountsServer: AccountsServer) => async ( + req: express.Request, + res: express.Response +) => { + try { + const { token } = req.body; + const password: any = accountsServer.getServices().password; + await password.verifyEmail(token); + res.json({ message: 'Email verified' }); + } catch (err) { + sendError(res, err); + } +}; + +export const sendVerificationEmail = (accountsServer: AccountsServer) => async ( + req: express.Request, + res: express.Response +) => { + try { + const { email } = req.body; + const password: any = accountsServer.getServices().password; + await password.sendVerificationEmail(email); + res.json({ message: 'Email sent' }); + } catch (err) { + sendError(res, err); + } +}; diff --git a/packages/rest-express/src/endpoints/refresh-access-token.ts b/packages/rest-express/src/endpoints/refresh-access-token.ts new file mode 100644 index 000000000..0e2f73803 --- /dev/null +++ b/packages/rest-express/src/endpoints/refresh-access-token.ts @@ -0,0 +1,25 @@ +import * as express from 'express'; +import * as requestIp from 'request-ip'; +import { AccountsServer } from '@accounts/server'; +import { getUserAgent } from '../utils/get-user-agent'; +import { sendError } from '../utils/send-error'; + +export const refreshAccessToken = (accountsServer: AccountsServer) => async ( + req: express.Request, + res: express.Response +) => { + try { + const { accessToken, refreshToken } = req.body; + const userAgent = getUserAgent(req); + const ip = requestIp.getClientIp(req); + const refreshedSession = await accountsServer.refreshTokens( + accessToken, + refreshToken, + ip, + userAgent + ); + res.json(refreshedSession); + } catch (err) { + sendError(res, err); + } +}; diff --git a/packages/rest-express/src/endpoints/service-authenticate.ts b/packages/rest-express/src/endpoints/service-authenticate.ts new file mode 100644 index 000000000..abc1bee57 --- /dev/null +++ b/packages/rest-express/src/endpoints/service-authenticate.ts @@ -0,0 +1,24 @@ +import * as express from 'express'; +import * as requestIp from 'request-ip'; +import { AccountsServer } from '@accounts/server'; +import { getUserAgent } from '../utils/get-user-agent'; +import { sendError } from '../utils/send-error'; + +export const serviceAuthenticate = (accountsServer: AccountsServer) => async ( + req: express.Request, + res: express.Response +) => { + try { + const serviceName = req.params.service; + const userAgent = getUserAgent(req); + const ip = requestIp.getClientIp(req); + const loggedInUser = await accountsServer.loginWithService( + serviceName, + req.body, + { ip, userAgent } + ); + res.json(loggedInUser); + } catch (err) { + sendError(res, err); + } +}; diff --git a/packages/rest-express/src/express-middleware.ts b/packages/rest-express/src/express-middleware.ts new file mode 100644 index 000000000..a11728179 --- /dev/null +++ b/packages/rest-express/src/express-middleware.ts @@ -0,0 +1,88 @@ +import { providerCallback } from './endpoints/oauth/provider-callback'; +import { + resetPassword, + sendResetPasswordEmail, +} from './endpoints/password/reset'; +import { + verifyEmail, + sendVerificationEmail, +} from './endpoints/password/verify-email'; +import * as express from 'express'; +import { get, isEmpty, pick } from 'lodash'; +import * as requestIp from 'request-ip'; +import { AccountsError } from '@accounts/common'; +import { AccountsServer } from '@accounts/server'; +import { refreshAccessToken } from './endpoints/refresh-access-token'; +import { getUser } from './endpoints/get-user'; +import { impersonate } from './endpoints/impersonate'; +import { logout } from './endpoints/logout'; +import { serviceAuthenticate } from './endpoints/service-authenticate'; +import { registerPassword } from './endpoints/password/register'; +import { userLoader } from './user-loader'; + +export interface AccountsExpressOptions { + path?: string; +} + +const defaultOptions: AccountsExpressOptions = { + path: '/accounts', +}; + +const accountsExpress = ( + accountsServer: AccountsServer, + options: AccountsExpressOptions = {} +): express.Router => { + options = { ...defaultOptions, ...options }; + const { path } = options; + + const router = express.Router(); + + router.post(`${path}/impersonate`, impersonate(accountsServer)); + + router.post(`${path}/user`, getUser(accountsServer)); + + router.post(`${path}/refreshTokens`, refreshAccessToken(accountsServer)); + + router.post(`${path}/logout`, logout(accountsServer)); + + router.post( + `${path}/:service/authenticate`, + serviceAuthenticate(accountsServer) + ); + + const services = accountsServer.getServices(); + + // @accounts/password + if (services.password) { + router.post(`${path}/password/register`, registerPassword(accountsServer)); + + router.post(`${path}/password/verifyEmail`, verifyEmail(accountsServer)); + + router.post( + `${path}/password/resetPassword`, + resetPassword(accountsServer) + ); + + router.post( + `${path}/password/sendVerificationEmail`, + sendVerificationEmail(accountsServer) + ); + + router.post( + `${path}/password/sendResetPasswordEmail`, + sendResetPasswordEmail(accountsServer) + ); + } + + // @accounts/oauth + if (services.oauth) { + router.get( + `${path}/oauth/:provider/callback`, + providerCallback(accountsServer) + ); + } + + return router; +}; + +export default accountsExpress; diff --git a/packages/rest-express/src/index.ts b/packages/rest-express/src/index.ts new file mode 100644 index 000000000..01980145d --- /dev/null +++ b/packages/rest-express/src/index.ts @@ -0,0 +1,3 @@ +import middleware from './express-middleware'; +export { userLoader } from './user-loader'; +export default middleware; diff --git a/packages/rest-express/src/user-loader.ts b/packages/rest-express/src/user-loader.ts new file mode 100644 index 000000000..09795a843 --- /dev/null +++ b/packages/rest-express/src/user-loader.ts @@ -0,0 +1,23 @@ +import * as express from 'express'; +import { get, isEmpty } from 'lodash'; +import { AccountsServer } from '@accounts/server'; + +export const userLoader = (accountsServer: AccountsServer) => async ( + req: express.Request, + res: express.Response, + next: any +) => { + const accessToken = + get(req.headers, 'accounts-access-token') || + get(req.body, 'accessToken', undefined); + if (!isEmpty(accessToken)) { + try { + const user = await accountsServer.resumeSession(accessToken); + (req as any).user = user; + (req as any).userId = user.id; + } catch (e) { + // Do nothing + } + } + next(); +}; diff --git a/packages/rest-express/src/utils/get-user-agent.ts b/packages/rest-express/src/utils/get-user-agent.ts new file mode 100644 index 000000000..bb59566e9 --- /dev/null +++ b/packages/rest-express/src/utils/get-user-agent.ts @@ -0,0 +1,10 @@ +import * as express from 'express'; + +export const getUserAgent = (req: express.Request) => { + let userAgent: string = (req.headers['user-agent'] as string) || ''; + if (req.headers['x-ucbrowser-ua']) { + // special case of UC Browser + userAgent = req.headers['x-ucbrowser-ua'] as string; + } + return userAgent; +}; diff --git a/packages/rest-express/src/utils/send-error.ts b/packages/rest-express/src/utils/send-error.ts new file mode 100644 index 000000000..36b38bbc9 --- /dev/null +++ b/packages/rest-express/src/utils/send-error.ts @@ -0,0 +1,6 @@ +export const sendError = (res: any, err: any) => + res.status(400).json({ + message: err.message, + loginInfo: err.loginInfo, + errorCode: err.errorCode, + }); diff --git a/packages/rest-express/tsconfig.json b/packages/rest-express/tsconfig.json new file mode 100644 index 000000000..7a3de3c2a --- /dev/null +++ b/packages/rest-express/tsconfig.json @@ -0,0 +1,15 @@ +{ + "extends": "../../tsconfig", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./lib", + "typeRoots": [ + "node_modules/@types" + ] + }, + "exclude": [ + "node_modules", + "__tests__", + "lib" + ] +} diff --git a/packages/rest-express/yarn.lock b/packages/rest-express/yarn.lock new file mode 100644 index 000000000..a768eba00 --- /dev/null +++ b/packages/rest-express/yarn.lock @@ -0,0 +1,364 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@types/body-parser@*": + version "1.16.8" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.16.8.tgz#687ec34140624a3bec2b1a8ea9268478ae8f3be3" + dependencies: + "@types/express" "*" + "@types/node" "*" + +"@types/events@*": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" + +"@types/express-serve-static-core@*": + version "4.11.1" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.11.1.tgz#f6f7212382d59b19d696677bcaa48a37280f5d45" + dependencies: + "@types/events" "*" + "@types/node" "*" + +"@types/express@*", "@types/express@4.11.1": + version "4.11.1" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.11.1.tgz#f99663b3ab32d04cb11db612ef5dd7933f75465b" + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/serve-static" "*" + +"@types/lodash@4.14.104": + version "4.14.104" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.104.tgz#53ee2357fa2e6e68379341d92eb2ecea4b11bb80" + +"@types/mime@*": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b" + +"@types/node@*": + version "9.4.6" + resolved "https://registry.yarnpkg.com/@types/node/-/node-9.4.6.tgz#d8176d864ee48753d053783e4e463aec86b8d82e" + +"@types/request-ip@0.0.33": + version "0.0.33" + resolved "https://registry.yarnpkg.com/@types/request-ip/-/request-ip-0.0.33.tgz#4c4a16f8b27a4ed906470fdac64168bc4de6c295" + dependencies: + "@types/node" "*" + +"@types/serve-static@*": + version "1.13.1" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.1.tgz#1d2801fa635d274cd97d4ec07e26b21b44127492" + dependencies: + "@types/express-serve-static-core" "*" + "@types/mime" "*" + +accepts@~1.3.4: + version "1.3.4" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" + dependencies: + mime-types "~2.1.16" + negotiator "0.6.1" + +array-flatten@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/array-flatten/-/array-flatten-1.1.1.tgz#9a5f699051b1e7073328f2a008968b64ea2955d2" + +body-parser@1.18.2: + version "1.18.2" + resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" + dependencies: + bytes "3.0.0" + content-type "~1.0.4" + debug "2.6.9" + depd "~1.1.1" + http-errors "~1.6.2" + iconv-lite "0.4.19" + on-finished "~2.3.0" + qs "6.5.1" + raw-body "2.3.2" + type-is "~1.6.15" + +bytes@3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/bytes/-/bytes-3.0.0.tgz#d32815404d689699f85a4ea4fa8755dd13a96048" + +content-disposition@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/content-disposition/-/content-disposition-0.5.2.tgz#0cf68bb9ddf5f2be7961c3a85178cb85dba78cb4" + +content-type@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" + +cookie-signature@1.0.6: + version "1.0.6" + resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" + +cookie@0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" + +debug@2.6.9: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + dependencies: + ms "2.0.0" + +depd@1.1.1, depd@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" + +destroy@~1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" + +ee-first@1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" + +encodeurl@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" + +escape-html@~1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" + +etag@~1.8.1: + version "1.8.1" + resolved "https://registry.yarnpkg.com/etag/-/etag-1.8.1.tgz#41ae2eeb65efa62268aebfea83ac7d79299b0887" + +express@^4.16.2: + version "4.16.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c" + dependencies: + accepts "~1.3.4" + array-flatten "1.1.1" + body-parser "1.18.2" + content-disposition "0.5.2" + content-type "~1.0.4" + cookie "0.3.1" + cookie-signature "1.0.6" + debug "2.6.9" + depd "~1.1.1" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.8.1" + finalhandler "1.1.0" + fresh "0.5.2" + merge-descriptors "1.0.1" + methods "~1.1.2" + on-finished "~2.3.0" + parseurl "~1.3.2" + path-to-regexp "0.1.7" + proxy-addr "~2.0.2" + qs "6.5.1" + range-parser "~1.2.0" + safe-buffer "5.1.1" + send "0.16.1" + serve-static "1.13.1" + setprototypeof "1.1.0" + statuses "~1.3.1" + type-is "~1.6.15" + utils-merge "1.0.1" + vary "~1.1.2" + +finalhandler@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/finalhandler/-/finalhandler-1.1.0.tgz#ce0b6855b45853e791b2fcc680046d88253dd7f5" + dependencies: + debug "2.6.9" + encodeurl "~1.0.1" + escape-html "~1.0.3" + on-finished "~2.3.0" + parseurl "~1.3.2" + statuses "~1.3.1" + unpipe "~1.0.0" + +forwarded@~0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" + +fresh@0.5.2: + version "0.5.2" + resolved "https://registry.yarnpkg.com/fresh/-/fresh-0.5.2.tgz#3d8cadd90d976569fa835ab1f8e4b23a105605a7" + +http-errors@1.6.2, http-errors@~1.6.2: + version "1.6.2" + resolved "https://registry.yarnpkg.com/http-errors/-/http-errors-1.6.2.tgz#0a002cc85707192a7e7946ceedc11155f60ec736" + dependencies: + depd "1.1.1" + inherits "2.0.3" + setprototypeof "1.0.3" + statuses ">= 1.3.1 < 2" + +iconv-lite@0.4.19: + version "0.4.19" + resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" + +inherits@2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" + +ipaddr.js@1.5.2: + version "1.5.2" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.5.2.tgz#d4b505bde9946987ccf0fc58d9010ff9607e3fa0" + +is_js@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/is_js/-/is_js-0.9.0.tgz#0ab94540502ba7afa24c856aa985561669e9c52d" + +lodash@^4.17.4: + version "4.17.4" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" + +media-typer@0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" + +merge-descriptors@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" + +methods@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" + +mime-db@~1.27.0: + version "1.27.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.27.0.tgz#820f572296bbd20ec25ed55e5b5de869e5436eb1" + +mime-db@~1.30.0: + version "1.30.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" + +mime-types@~2.1.15: + version "2.1.15" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.15.tgz#a4ebf5064094569237b8cf70046776d09fc92aed" + dependencies: + mime-db "~1.27.0" + +mime-types@~2.1.16: + version "2.1.17" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" + dependencies: + mime-db "~1.30.0" + +mime@1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + +negotiator@0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" + +on-finished@~2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/on-finished/-/on-finished-2.3.0.tgz#20f1336481b083cd75337992a16971aa2d906947" + dependencies: + ee-first "1.1.1" + +parseurl@~1.3.2: + version "1.3.2" + resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.2.tgz#fc289d4ed8993119460c156253262cdc8de65bf3" + +path-to-regexp@0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/path-to-regexp/-/path-to-regexp-0.1.7.tgz#df604178005f522f15eb4490e7247a1bfaa67f8c" + +proxy-addr@~2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec" + dependencies: + forwarded "~0.1.2" + ipaddr.js "1.5.2" + +qs@6.5.1: + version "6.5.1" + resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" + +range-parser@~1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/range-parser/-/range-parser-1.2.0.tgz#f49be6b487894ddc40dcc94a322f611092e00d5e" + +raw-body@2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" + dependencies: + bytes "3.0.0" + http-errors "1.6.2" + iconv-lite "0.4.19" + unpipe "1.0.0" + +request-ip@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/request-ip/-/request-ip-2.0.2.tgz#deeae6d4af21768497db8cd05fa37143f8f1257e" + dependencies: + is_js "^0.9.0" + +safe-buffer@5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" + +send@0.16.1: + version "0.16.1" + resolved "https://registry.yarnpkg.com/send/-/send-0.16.1.tgz#a70e1ca21d1382c11d0d9f6231deb281080d7ab3" + dependencies: + debug "2.6.9" + depd "~1.1.1" + destroy "~1.0.4" + encodeurl "~1.0.1" + escape-html "~1.0.3" + etag "~1.8.1" + fresh "0.5.2" + http-errors "~1.6.2" + mime "1.4.1" + ms "2.0.0" + on-finished "~2.3.0" + range-parser "~1.2.0" + statuses "~1.3.1" + +serve-static@1.13.1: + version "1.13.1" + resolved "https://registry.yarnpkg.com/serve-static/-/serve-static-1.13.1.tgz#4c57d53404a761d8f2e7c1e8a18a47dbf278a719" + dependencies: + encodeurl "~1.0.1" + escape-html "~1.0.3" + parseurl "~1.3.2" + send "0.16.1" + +setprototypeof@1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" + +setprototypeof@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.1.0.tgz#d0bd85536887b6fe7c0d818cb962d9d91c54e656" + +"statuses@>= 1.3.1 < 2", statuses@~1.3.1: + version "1.3.1" + resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" + +type-is@~1.6.15: + version "1.6.15" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" + dependencies: + media-typer "0.3.0" + mime-types "~2.1.15" + +unpipe@1.0.0, unpipe@~1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec" + +utils-merge@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" + +vary@~1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/vary/-/vary-1.1.2.tgz#2299f02c6ded30d4a5961b0b9f74524a18f634fc" From ae7b8099112bc663a20b7559219488f6ec718fb9 Mon Sep 17 00:00:00 2001 From: Elies Lou Date: Sun, 11 Mar 2018 16:59:14 +0100 Subject: [PATCH 4/6] rest-express : remove conflicting devDependencies --- packages/rest-express/package.json | 5 +-- packages/rest-express/yarn.lock | 51 ------------------------------ 2 files changed, 1 insertion(+), 55 deletions(-) diff --git a/packages/rest-express/package.json b/packages/rest-express/package.json index 587cb2c4d..58dbcc21c 100644 --- a/packages/rest-express/package.json +++ b/packages/rest-express/package.json @@ -43,10 +43,7 @@ "license": "MIT", "devDependencies": { "@accounts/common": "0.1.0-beta.3", - "@accounts/server": "0.1.0-beta.3", - "@types/express": "4.11.1", - "@types/lodash": "4.14.104", - "@types/request-ip": "0.0.33" + "@accounts/server": "0.1.0-beta.3" }, "peerDependencies": { "@accounts/common": "^0.1.0-beta.0", diff --git a/packages/rest-express/yarn.lock b/packages/rest-express/yarn.lock index a768eba00..f89f7fb55 100644 --- a/packages/rest-express/yarn.lock +++ b/packages/rest-express/yarn.lock @@ -2,57 +2,6 @@ # yarn lockfile v1 -"@types/body-parser@*": - version "1.16.8" - resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.16.8.tgz#687ec34140624a3bec2b1a8ea9268478ae8f3be3" - dependencies: - "@types/express" "*" - "@types/node" "*" - -"@types/events@*": - version "1.2.0" - resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" - -"@types/express-serve-static-core@*": - version "4.11.1" - resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.11.1.tgz#f6f7212382d59b19d696677bcaa48a37280f5d45" - dependencies: - "@types/events" "*" - "@types/node" "*" - -"@types/express@*", "@types/express@4.11.1": - version "4.11.1" - resolved "https://registry.yarnpkg.com/@types/express/-/express-4.11.1.tgz#f99663b3ab32d04cb11db612ef5dd7933f75465b" - dependencies: - "@types/body-parser" "*" - "@types/express-serve-static-core" "*" - "@types/serve-static" "*" - -"@types/lodash@4.14.104": - version "4.14.104" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.104.tgz#53ee2357fa2e6e68379341d92eb2ecea4b11bb80" - -"@types/mime@*": - version "2.0.0" - resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b" - -"@types/node@*": - version "9.4.6" - resolved "https://registry.yarnpkg.com/@types/node/-/node-9.4.6.tgz#d8176d864ee48753d053783e4e463aec86b8d82e" - -"@types/request-ip@0.0.33": - version "0.0.33" - resolved "https://registry.yarnpkg.com/@types/request-ip/-/request-ip-0.0.33.tgz#4c4a16f8b27a4ed906470fdac64168bc4de6c295" - dependencies: - "@types/node" "*" - -"@types/serve-static@*": - version "1.13.1" - resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.1.tgz#1d2801fa635d274cd97d4ec07e26b21b44127492" - dependencies: - "@types/express-serve-static-core" "*" - "@types/mime" "*" - accepts@~1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" From 26b57011616fa86ddab8afb5c289c58143f78693 Mon Sep 17 00:00:00 2001 From: Elies Lou Date: Sun, 11 Mar 2018 17:23:13 +0100 Subject: [PATCH 5/6] Fix bootstraping : prepublish => prepublishOnly --- packages/rest-client/package.json | 2 +- packages/rest-express/package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/rest-client/package.json b/packages/rest-client/package.json index d6ae191e2..78e40161e 100644 --- a/packages/rest-client/package.json +++ b/packages/rest-client/package.json @@ -11,7 +11,7 @@ "start": "tsc --watch", "precompile": "rimraf ./lib", "compile": "tsc", - "prepublish": "npm run compile", + "prepublishOnly": "npm run compile", "test": "npm run testonly", "testonly": "jest", "coverage": "npm run testonly -- --coverage", diff --git a/packages/rest-express/package.json b/packages/rest-express/package.json index 58dbcc21c..82ee08e2f 100644 --- a/packages/rest-express/package.json +++ b/packages/rest-express/package.json @@ -10,7 +10,7 @@ "scripts": { "start": "tsc --watch", "compile": "tsc", - "prepublish": "npm run compile", + "prepublishOnly": "npm run compile", "test": "npm run testonly", "test:watch": "npm run testonly -- --watch --coverage", "testonly": "jest", From 4e70296d8bc4cf42dc92d1654350ab7c15953d80 Mon Sep 17 00:00:00 2001 From: Elies Lou Date: Sun, 11 Mar 2018 20:22:35 +0100 Subject: [PATCH 6/6] rest-express : revert devDependencies --- packages/rest-express/package.json | 5 ++- packages/rest-express/yarn.lock | 51 ++++++++++++++++++++++++++++++ tsconfig.json | 2 +- 3 files changed, 56 insertions(+), 2 deletions(-) diff --git a/packages/rest-express/package.json b/packages/rest-express/package.json index 82ee08e2f..0d2757ea3 100644 --- a/packages/rest-express/package.json +++ b/packages/rest-express/package.json @@ -43,7 +43,10 @@ "license": "MIT", "devDependencies": { "@accounts/common": "0.1.0-beta.3", - "@accounts/server": "0.1.0-beta.3" + "@accounts/server": "0.1.0-beta.3", + "@types/express": "4.11.1", + "@types/lodash": "4.14.104", + "@types/request-ip": "0.0.33" }, "peerDependencies": { "@accounts/common": "^0.1.0-beta.0", diff --git a/packages/rest-express/yarn.lock b/packages/rest-express/yarn.lock index f89f7fb55..07322add7 100644 --- a/packages/rest-express/yarn.lock +++ b/packages/rest-express/yarn.lock @@ -2,6 +2,57 @@ # yarn lockfile v1 +"@types/body-parser@*": + version "1.16.8" + resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.16.8.tgz#687ec34140624a3bec2b1a8ea9268478ae8f3be3" + dependencies: + "@types/express" "*" + "@types/node" "*" + +"@types/events@*": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86" + +"@types/express-serve-static-core@*": + version "4.11.1" + resolved "https://registry.yarnpkg.com/@types/express-serve-static-core/-/express-serve-static-core-4.11.1.tgz#f6f7212382d59b19d696677bcaa48a37280f5d45" + dependencies: + "@types/events" "*" + "@types/node" "*" + +"@types/express@*", "@types/express@4.11.1": + version "4.11.1" + resolved "https://registry.yarnpkg.com/@types/express/-/express-4.11.1.tgz#f99663b3ab32d04cb11db612ef5dd7933f75465b" + dependencies: + "@types/body-parser" "*" + "@types/express-serve-static-core" "*" + "@types/serve-static" "*" + +"@types/lodash@4.14.104": + version "4.14.104" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.104.tgz#53ee2357fa2e6e68379341d92eb2ecea4b11bb80" + +"@types/mime@*": + version "2.0.0" + resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b" + +"@types/node@*": + version "9.4.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-9.4.7.tgz#57d81cd98719df2c9de118f2d5f3b1120dcd7275" + +"@types/request-ip@0.0.33": + version "0.0.33" + resolved "https://registry.yarnpkg.com/@types/request-ip/-/request-ip-0.0.33.tgz#4c4a16f8b27a4ed906470fdac64168bc4de6c295" + dependencies: + "@types/node" "*" + +"@types/serve-static@*": + version "1.13.1" + resolved "https://registry.yarnpkg.com/@types/serve-static/-/serve-static-1.13.1.tgz#1d2801fa635d274cd97d4ec07e26b21b44127492" + dependencies: + "@types/express-serve-static-core" "*" + "@types/mime" "*" + accepts@~1.3.4: version "1.3.4" resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" diff --git a/tsconfig.json b/tsconfig.json index fe38d4bc4..a0c280f20 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,7 +9,7 @@ "removeComments": true, "lib": ["es6", "es2015", "es2016", "es2017"], "types": [ - "@types/node" + "node" ] } }