From aa2bacdb0b1e7a5c59d8bcc6a5cce0544a9e1ff8 Mon Sep 17 00:00:00 2001 From: Kevin Hughes Date: Fri, 24 Nov 2017 16:20:41 -0500 Subject: [PATCH 01/62] don't expect body parser to be used --- routes/shopifyApiProxy.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/routes/shopifyApiProxy.js b/routes/shopifyApiProxy.js index c6d6c36..1a3d253 100644 --- a/routes/shopifyApiProxy.js +++ b/routes/shopifyApiProxy.js @@ -3,7 +3,7 @@ const { URL } = require('url'); const ALLOWED_URLS = ['/products', '/orders']; module.exports = function shopifyApiProxy(request, response, next) { - const { query, method, path, body, session } = request; + const { query, method, path, session } = request; const { shop, accessToken } = session; const strippedPath = path.split('?')[0].split('.json')[0]; @@ -18,7 +18,7 @@ module.exports = function shopifyApiProxy(request, response, next) { const fetchOptions = { method, - body, + body: request.read(), headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': accessToken, From 9e4d333bd09a491e5bc723cbf1210fa5f541f2bd Mon Sep 17 00:00:00 2001 From: Kevin Hughes Date: Mon, 27 Nov 2017 13:55:44 -0500 Subject: [PATCH 02/62] configure bodyParser.raw for the api proxy --- package.json | 1 + routes/index.js | 4 +++- routes/shopifyApiProxy.js | 20 +++++++++++--------- yarn.lock | 32 ++++++++++++++++++-------------- 4 files changed, 33 insertions(+), 24 deletions(-) diff --git a/package.json b/package.json index bfca4e5..3cef2a8 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,7 @@ "test": "jest" }, "dependencies": { + "body-parser": "^1.18.2", "connect-redis": "^3.3.0", "express": "^4.16.2", "express-session": "^1.15.3", diff --git a/routes/index.js b/routes/index.js index ff1e559..dba2b7f 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,4 +1,5 @@ const express = require('express'); +const bodyParser = require('body-parser'); const createWithShop = require('../middleware/withShop'); const createShopifyAuthRouter = require('./shopifyAuth'); @@ -6,9 +7,10 @@ const shopifyApiProxy = require('./shopifyApiProxy'); module.exports = function createRouter(shopifyConfig) { const router = express.Router(); + const rawParser = bodyParser.raw({type: '*/*'}); router.use('/auth/shopify', createShopifyAuthRouter(shopifyConfig)); - router.use('/api', createWithShop({ redirect: false }), shopifyApiProxy); + router.use('/api', rawParser, createWithShop({ redirect: false }), shopifyApiProxy); return router; }; diff --git a/routes/shopifyApiProxy.js b/routes/shopifyApiProxy.js index 1a3d253..ee5a33c 100644 --- a/routes/shopifyApiProxy.js +++ b/routes/shopifyApiProxy.js @@ -3,22 +3,16 @@ const { URL } = require('url'); const ALLOWED_URLS = ['/products', '/orders']; module.exports = function shopifyApiProxy(request, response, next) { - const { query, method, path, session } = request; + const { query, method, path, body, session } = request; const { shop, accessToken } = session; - const strippedPath = path.split('?')[0].split('.json')[0]; - - const inAllowed = ALLOWED_URLS.some(resource => { - return strippedPath === resource; - }); - - if (!inAllowed) { + if (!validRequest(path)) { return response.status(403).send('Endpoint not in whitelist'); } const fetchOptions = { method, - body: request.read(), + body, headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': accessToken, @@ -36,6 +30,14 @@ module.exports = function shopifyApiProxy(request, response, next) { .catch(err => response.err(err)); }; +function validRequest(path) { + const strippedPath = path.split('?')[0].split('.json')[0]; + + return ALLOWED_URLS.some(resource => { + return strippedPath === resource; + }); +} + function fetchWithParams(url, fetchOpts, query) { const parsedUrl = new URL(url); diff --git a/yarn.lock b/yarn.lock index eb90bfd..baacda4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35,8 +35,8 @@ ajv@^4.9.1: json-stable-stringify "^1.0.1" ajv@^5.1.0: - version "5.4.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.4.0.tgz#32d1cf08dbc80c432f426f12e10b2511f6b46474" + version "5.5.0" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.0.tgz#eb2840746e9dc48bd5e063a36e3fd400c5eab5a9" dependencies: co "^4.6.0" fast-deep-equal "^1.0.0" @@ -339,7 +339,7 @@ bluebird@^3.4.6: version "3.5.1" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" -body-parser@1.18.2: +body-parser@1.18.2, body-parser@^1.18.2: version "1.18.2" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.18.2.tgz#87678a19d84b47d859b83199bd59bce222b10454" dependencies: @@ -493,8 +493,8 @@ combined-stream@^1.0.5, combined-stream@~1.0.5: delayed-stream "~1.0.0" commander@^2.2.0: - version "2.11.0" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.11.0.tgz#157152fd1e7a6c8d98a5b715cf376df928004563" + version "2.12.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.1.tgz#468635c4168d06145b9323356d1da84d14ac4a7a" component-emitter@^1.2.0: version "1.2.1" @@ -528,8 +528,8 @@ content-type@~1.0.4: resolved "https://registry.yarnpkg.com/content-type/-/content-type-1.0.4.tgz#e138cc75e040c727b1966fe5e5f8c9aee256fe3b" convert-source-map@^1.4.0, convert-source-map@^1.5.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.0.tgz#9acd70851c6d5dfdd93d9282e5edf94a03ff46b5" + version "1.5.1" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" cookie-signature@1.0.6: version "1.0.6" @@ -650,8 +650,8 @@ detect-indent@^4.0.0: repeating "^2.0.0" detect-libc@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.2.tgz#71ad5d204bf17a6a6ca8f450c61454066ef461e1" + version "1.0.3" + resolved "https://registry.yarnpkg.com/detect-libc/-/detect-libc-1.0.3.tgz#fa137c4bd698edf55cd5cd02ac559f91a4c4ba9b" diff@^3.2.0: version "3.4.0" @@ -1214,8 +1214,8 @@ inherits@2, inherits@2.0.3, inherits@~2.0.0, inherits@~2.0.1, inherits@~2.0.3: resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.3.tgz#633c2c83e3da42a502f52466022480f4208261de" ini@^1.3.4, ini@~1.3.0: - version "1.3.4" - resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.4.tgz#0537cb79daf59b59a1a517dff706c86ec039162e" + version "1.3.5" + resolved "https://registry.yarnpkg.com/ini/-/ini-1.3.5.tgz#eee25f56db1c9ec6085e0c22778083f596abf927" interpret@^0.6.5: version "0.6.6" @@ -1888,10 +1888,14 @@ mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17, dependencies: mime-db "~1.30.0" -mime@1.4.1, mime@^1.4.1: +mime@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" +mime@^1.4.1: + version "1.6.0" + resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" + mimic-fn@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" @@ -2214,8 +2218,8 @@ preserve@^0.2.0: resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" prettier@^1.5.2: - version "1.7.4" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.7.4.tgz#5e8624ae9363c80f95ec644584ecdf55d74f93fa" + version "1.8.2" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.8.2.tgz#bff83e7fd573933c607875e5ba3abbdffb96aeb8" pretty-format@^21.2.1: version "21.2.1" From 4b3f2825082399c0f46ac0433689835fc2cb6afe Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Mon, 27 Nov 2017 14:59:32 -0500 Subject: [PATCH 03/62] v1.0.0-alpha.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 3cef2a8..9f49aeb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@shopify/shopify-express", - "version": "1.0.0-alpha.3", + "version": "1.0.0-alpha.4", "author": "Shopify Inc.", "description": "Get up and running quickly with Express.js and the Shopify API.", "keywords": [ From 65c7e585d435a40ec79cfc2fd221cd3dac82c3c8 Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Mon, 27 Nov 2017 20:32:35 -0500 Subject: [PATCH 04/62] =?UTF-8?q?=E2=9E=95=20add=20husky?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + yarn.lock | 16 ++++++++++++++++ 2 files changed, 17 insertions(+) diff --git a/package.json b/package.json index 3cef2a8..ac0d083 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "connect-redis": "^3.3.0", "express": "^4.16.2", "express-session": "^1.15.3", + "husky": "^0.14.3", "knex": "^0.13.0", "redis": "^2.7.1", "sqlite3": "^3.1.9", diff --git a/yarn.lock b/yarn.lock index baacda4..8eeed9d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1194,6 +1194,14 @@ http-signature@~1.2.0: jsprim "^1.2.2" sshpk "^1.7.0" +husky@^0.14.3: + version "0.14.3" + resolved "https://registry.yarnpkg.com/husky/-/husky-0.14.3.tgz#c69ed74e2d2779769a17ba8399b54ce0b63c12c3" + dependencies: + is-ci "^1.0.10" + normalize-path "^1.0.0" + strip-indent "^2.0.0" + iconv-lite@0.4.19: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" @@ -1993,6 +2001,10 @@ normalize-package-data@^2.3.2: semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" +normalize-path@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-1.0.0.tgz#32d0e472f91ff345701c15a8311018d3b0a90379" + normalize-path@^2.0.0, normalize-path@^2.0.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" @@ -2720,6 +2732,10 @@ strip-eof@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/strip-eof/-/strip-eof-1.0.0.tgz#bb43ff5598a6eb05d89b59fcd129c983313606bf" +strip-indent@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/strip-indent/-/strip-indent-2.0.0.tgz#5ef8db295d01e6ed6cbf7aab96998d7822527b68" + strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" From b6acab53dd260fc282b7f3155807fda86b089b7c Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Mon, 27 Nov 2017 20:40:01 -0500 Subject: [PATCH 05/62] =?UTF-8?q?=F0=9F=91=8C=20pretty=20command=20doesn't?= =?UTF-8?q?=20autofix?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ac0d083..485aed5 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "node": ">=8.1.0" }, "scripts": { - "pretty": "prettier --single-quote --trailing-comma es5 --write {middleware,routes,shopStore}{/*,/**/*}.js", + "pretty": "prettier --single-quote --trailing-comma es5 {middleware,routes,shopStore}{/*,/**/*}.js", "precommit": "yarn run pretty", "test": "jest" }, From f49396e6092ad0a079237c3587437954f7558f0b Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Mon, 27 Nov 2017 20:48:57 -0500 Subject: [PATCH 06/62] =?UTF-8?q?=E2=9C=A8=20lint=20on=20commit?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 25 ++-- yarn.lock | 338 +++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 344 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 485aed5..5e50b89 100644 --- a/package.json +++ b/package.json @@ -2,12 +2,9 @@ "name": "@shopify/shopify-express", "version": "1.0.0-alpha.3", "author": "Shopify Inc.", - "description": "Get up and running quickly with Express.js and the Shopify API.", - "keywords": [ - "shopify", - "express.js", - "express middleware" - ], + "description": + "Get up and running quickly with Express.js and the Shopify API.", + "keywords": ["shopify", "express.js", "express middleware"], "bugs": "https://github.com/Shopify/shopify-express/issues", "license": "MIT", "repository": { @@ -18,24 +15,32 @@ "node": ">=8.1.0" }, "scripts": { - "pretty": "prettier --single-quote --trailing-comma es5 {middleware,routes,shopStore}{/*,/**/*}.js", - "precommit": "yarn run pretty", + "prettier:check": + "prettier-check --single-quote --trailing-comma=all {middleware,routes,shopStore}{/*,/**/*}.{js,json}", + "prettier:fix": + "prettier --single-quote --trailing-comma --write {middleware,routes,shopStore}{/*,/**/*}.{js,json}", + "precommit": "lint-staged", "test": "jest" }, + "lint-staged": { + "*.{js,json}": ["prettier-check --single-quote --trailing-comma=all"] + }, "dependencies": { "body-parser": "^1.18.2", "connect-redis": "^3.3.0", "express": "^4.16.2", "express-session": "^1.15.3", - "husky": "^0.14.3", "knex": "^0.13.0", "redis": "^2.7.1", "sqlite3": "^3.1.9", "url": "^0.11.0" }, "devDependencies": { + "husky": "^0.14.3", "jest": "^21.2.1", - "prettier": "^1.5.2", + "lint-staged": "^5.0.0", + "prettier": "^1.8.2", + "prettier-check": "^2.0.0", "supertest": "^3.0.0" } } diff --git a/yarn.lock b/yarn.lock index 8eeed9d..92c9349 100644 --- a/yarn.lock +++ b/yarn.lock @@ -55,6 +55,10 @@ amdefine@>=0.0.4: version "1.0.1" resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" +ansi-escapes@^1.0.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" + ansi-escapes@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-3.0.0.tgz#ec3e8b4e9f8064fc02c3ac9b65f1c275bda8ef92" @@ -77,6 +81,10 @@ ansi-styles@^3.1.0, ansi-styles@^3.2.0: dependencies: color-convert "^1.9.0" +any-observable@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.2.0.tgz#c67870058003579009083f54ac0abafb5c33d242" + anymatch@^1.3.0: version "1.3.2" resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-1.3.2.tgz#553dcb8f91e3c889845dfdba34c77721b90b9d7a" @@ -84,6 +92,10 @@ anymatch@^1.3.0: micromatch "^2.1.5" normalize-path "^2.0.0" +app-root-path@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.0.1.tgz#cd62dcf8e4fd5a417efc664d2e5b10653c651b46" + append-transform@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991" @@ -430,7 +442,7 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" -chalk@^1.0.0, chalk@^1.1.3: +chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98" dependencies: @@ -440,7 +452,7 @@ chalk@^1.0.0, chalk@^1.1.3: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.1: +chalk@^2.0.1, chalk@^2.1.0: version "2.3.0" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" dependencies: @@ -452,6 +464,23 @@ ci-info@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/ci-info/-/ci-info-1.1.2.tgz#03561259db48d0474c8bdc90f5b47b068b6bbfb4" +cli-cursor@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" + dependencies: + restore-cursor "^1.0.1" + +cli-spinners@^0.1.2: + version "0.1.2" + resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c" + +cli-truncate@^0.2.1: + version "0.2.1" + resolved "https://registry.yarnpkg.com/cli-truncate/-/cli-truncate-0.2.1.tgz#9f15cfbb0705005369216c626ac7d05ab90dd574" + dependencies: + slice-ansi "0.0.4" + string-width "^1.0.1" + cliui@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/cliui/-/cliui-2.1.0.tgz#4b475760ff80264c762c3a1719032e91c7fea0d1" @@ -492,7 +521,7 @@ combined-stream@^1.0.5, combined-stream@~1.0.5: dependencies: delayed-stream "~1.0.0" -commander@^2.2.0: +commander@^2.11.0, commander@^2.2.0, commander@^2.9.0: version "2.12.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.1.tgz#468635c4168d06145b9323356d1da84d14ac4a7a" @@ -551,6 +580,15 @@ core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.2.tgz#b5fd54220aa2bc5ab57aab7140c940754503c1a7" +cosmiconfig@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-3.1.0.tgz#640a94bf9847f321800403cd273af60665c73397" + dependencies: + is-directory "^0.3.1" + js-yaml "^3.9.0" + parse-json "^3.0.0" + require-from-string "^2.0.1" + crc@3.4.4: version "3.4.4" resolved "https://registry.yarnpkg.com/crc/-/crc-3.4.4.tgz#9da1e980e3bd44fc5c93bf5ab3da3378d85e466b" @@ -591,6 +629,10 @@ dashdash@^1.12.0: dependencies: assert-plus "^1.0.0" +date-fns@^1.27.2: + version "1.29.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.29.0.tgz#12e609cdcb935127311d04d33334e2960a2a54e6" + debug@2.6.9, debug@^2.1.3, debug@^2.2.0, debug@^2.6.8: version "2.6.9" resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" @@ -607,6 +649,10 @@ decamelize@^1.0.0, decamelize@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" +dedent@^0.7.0: + version "0.7.0" + resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" + deep-extend@~0.4.0: version "0.4.2" resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f" @@ -671,6 +717,10 @@ ee-first@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ee-first/-/ee-first-1.1.1.tgz#590c61156b0ae2f4f0255732a158b266bc56b21d" +elegant-spinner@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" + encodeurl@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" @@ -681,7 +731,7 @@ errno@^0.1.4: dependencies: prr "~0.0.0" -error-ex@^1.2.0: +error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/error-ex/-/error-ex-1.3.1.tgz#f855a86ce61adc4e8621c3cda21e7a7612c3a8dc" dependencies: @@ -732,6 +782,18 @@ exec-sh@^0.2.0: dependencies: merge "^1.1.3" +execa@^0.6.0: + version "0.6.3" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.6.3.tgz#57b69a594f081759c69e5370f0d17b9cb11658fe" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + execa@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/execa/-/execa-0.7.0.tgz#944becd34cc41ee32a63a9faf27ad5a65fc59777" @@ -744,6 +806,22 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" +execa@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/execa/-/execa-0.8.0.tgz#d8d76bbc1b55217ed190fd6dd49d3c774ecfc8da" + dependencies: + cross-spawn "^5.0.1" + get-stream "^3.0.0" + is-stream "^1.1.0" + npm-run-path "^2.0.0" + p-finally "^1.0.0" + signal-exit "^3.0.0" + strip-eof "^1.0.0" + +exit-hook@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/exit-hook/-/exit-hook-1.1.1.tgz#f05ca233b48c05d54fff07765df8507e95c02ff8" + expand-brackets@^0.1.4: version "0.1.5" resolved "https://registry.yarnpkg.com/expand-brackets/-/expand-brackets-0.1.5.tgz#df07284e342a807cd733ac5af72411e581d1177b" @@ -854,6 +932,13 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" +figures@^1.7.0: + version "1.7.0" + resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" + dependencies: + escape-string-regexp "^1.0.5" + object-assign "^4.1.0" + filename-regex@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/filename-regex/-/filename-regex-2.0.1.tgz#c1c4b9bee3e09725ddb106b75c1e301fe2f18b26" @@ -887,6 +972,10 @@ finalhandler@1.1.0: statuses "~1.3.1" unpipe "~1.0.0" +find-parent-dir@^0.3.0: + version "0.3.0" + resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54" + find-up@^1.0.0: version "1.1.2" resolved "https://registry.yarnpkg.com/find-up/-/find-up-1.1.2.tgz#6b2e9822b1a2ce0a60ab64d610eccad53cb24d0f" @@ -1008,6 +1097,10 @@ get-caller-file@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-1.0.2.tgz#f702e63127e7e231c160a80c1554acb70d5047e5" +get-own-enumerable-property-symbols@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-2.0.1.tgz#5c4ad87f2834c4b9b4e84549dc1e0650fb38c24b" + get-stream@^3.0.0: version "3.0.0" resolved "https://registry.yarnpkg.com/get-stream/-/get-stream-3.0.0.tgz#8e943d1358dc37555054ecbe2edb05aa174ede14" @@ -1210,6 +1303,16 @@ imurmurhash@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/imurmurhash/-/imurmurhash-0.1.4.tgz#9218b9b2b928a238b13dc4fb6b6d576f231453ea" +indent-string@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-2.1.0.tgz#8e2d48348742121b4a8218b7a137e9a52049dc80" + dependencies: + repeating "^2.0.0" + +indent-string@^3.0.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/indent-string/-/indent-string-3.2.0.tgz#4a5fd6d27cc332f37e5419a504dbb837105c9289" + inflight@^1.0.4: version "1.0.6" resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" @@ -1263,6 +1366,10 @@ is-ci@^1.0.10: dependencies: ci-info "^1.0.0" +is-directory@^0.3.1: + version "0.3.1" + resolved "https://registry.yarnpkg.com/is-directory/-/is-directory-0.3.1.tgz#61339b6f2475fc772fd9c9d83f5c8575dc154ae1" + is-dotfile@^1.0.0: version "1.0.3" resolved "https://registry.yarnpkg.com/is-dotfile/-/is-dotfile-1.0.3.tgz#a6a2f32ffd2dfb04f5ca25ecd0f6b83cf798a1e1" @@ -1281,6 +1388,10 @@ is-extglob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + is-finite@^1.0.0: version "1.0.2" resolved "https://registry.yarnpkg.com/is-finite/-/is-finite-1.0.2.tgz#cc6677695602be550ef11e8b4aa6305342b6d0aa" @@ -1303,6 +1414,12 @@ is-glob@^2.0.0, is-glob@^2.0.1: dependencies: is-extglob "^1.0.0" +is-glob@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" + dependencies: + is-extglob "^2.1.1" + is-number@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/is-number/-/is-number-2.1.0.tgz#01fcbbb393463a548f2f466cce16dece49db908f" @@ -1315,6 +1432,16 @@ is-number@^3.0.0: dependencies: kind-of "^3.0.2" +is-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + +is-observable@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-0.2.0.tgz#b361311d83c6e5d726cabf5e250b0237106f5ae2" + dependencies: + symbol-observable "^0.2.2" + is-posix-bracket@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/is-posix-bracket/-/is-posix-bracket-0.1.1.tgz#3334dc79774368e92f016e6fbc0a88f5cd6e6bc4" @@ -1323,6 +1450,14 @@ is-primitive@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/is-primitive/-/is-primitive-2.0.0.tgz#207bab91638499c07b2adf240a41a87210034575" +is-promise@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-promise/-/is-promise-2.1.0.tgz#79a2a9ece7f096e80f36d2b2f3bc16c1ff4bf3fa" + +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -1634,7 +1769,7 @@ jest-util@^21.2.1: jest-validate "^21.2.1" mkdirp "^0.5.1" -jest-validate@^21.2.1: +jest-validate@^21.1.0, jest-validate@^21.2.1: version "21.2.1" resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-21.2.1.tgz#cc0cbca653cd54937ba4f2a111796774530dd3c7" dependencies: @@ -1653,7 +1788,7 @@ js-tokens@^3.0.0, js-tokens@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-3.0.2.tgz#9866df395102130e38f7f996bceb65443209c25b" -js-yaml@^3.7.0: +js-yaml@^3.7.0, js-yaml@^3.9.0: version "3.10.0" resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-3.10.0.tgz#2e78441646bd4682e963f22b6e92823c309c62dc" dependencies: @@ -1793,6 +1928,78 @@ liftoff@~2.2.0: rechoir "^0.6.2" resolve "^1.1.7" +lint-staged@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-5.0.0.tgz#f1c670e03e2fdf3f3d0eb81f72d3bcf658770e54" + dependencies: + app-root-path "^2.0.0" + chalk "^2.1.0" + commander "^2.11.0" + cosmiconfig "^3.1.0" + dedent "^0.7.0" + execa "^0.8.0" + find-parent-dir "^0.3.0" + is-glob "^4.0.0" + jest-validate "^21.1.0" + listr "^0.13.0" + lodash "^4.17.4" + log-symbols "^2.0.0" + minimatch "^3.0.0" + npm-which "^3.0.1" + p-map "^1.1.1" + path-is-inside "^1.0.2" + pify "^3.0.0" + staged-git-files "0.0.4" + stringify-object "^3.2.0" + +listr-silent-renderer@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" + +listr-update-renderer@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.4.0.tgz#344d980da2ca2e8b145ba305908f32ae3f4cc8a7" + dependencies: + chalk "^1.1.3" + cli-truncate "^0.2.1" + elegant-spinner "^1.0.1" + figures "^1.7.0" + indent-string "^3.0.0" + log-symbols "^1.0.2" + log-update "^1.0.2" + strip-ansi "^3.0.1" + +listr-verbose-renderer@^0.4.0: + version "0.4.1" + resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#8206f4cf6d52ddc5827e5fd14989e0e965933a35" + dependencies: + chalk "^1.1.3" + cli-cursor "^1.0.2" + date-fns "^1.27.2" + figures "^1.7.0" + +listr@^0.13.0: + version "0.13.0" + resolved "https://registry.yarnpkg.com/listr/-/listr-0.13.0.tgz#20bb0ba30bae660ee84cc0503df4be3d5623887d" + dependencies: + chalk "^1.1.3" + cli-truncate "^0.2.1" + figures "^1.7.0" + indent-string "^2.1.0" + is-observable "^0.2.0" + is-promise "^2.1.0" + is-stream "^1.1.0" + listr-silent-renderer "^1.1.1" + listr-update-renderer "^0.4.0" + listr-verbose-renderer "^0.4.0" + log-symbols "^1.0.2" + log-update "^1.0.2" + ora "^0.2.3" + p-map "^1.1.1" + rxjs "^5.4.2" + stream-to-observable "^0.2.0" + strip-ansi "^3.0.1" + load-json-file@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/load-json-file/-/load-json-file-1.1.0.tgz#956905708d58b4bab4c2261b04f59f31c99374c0" @@ -1823,6 +2030,25 @@ lodash@^4.14.0, lodash@^4.17.4, lodash@^4.6.0: version "4.17.4" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" +log-symbols@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-1.0.2.tgz#376ff7b58ea3086a0f09facc74617eca501e1a18" + dependencies: + chalk "^1.0.0" + +log-symbols@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.1.0.tgz#f35fa60e278832b538dc4dddcbb478a45d3e3be6" + dependencies: + chalk "^2.0.1" + +log-update@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/log-update/-/log-update-1.0.2.tgz#19929f64c4093d2d2e7075a1dad8af59c296b8d1" + dependencies: + ansi-escapes "^1.0.0" + cli-cursor "^1.0.2" + longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" @@ -2011,12 +2237,26 @@ normalize-path@^2.0.0, normalize-path@^2.0.1: dependencies: remove-trailing-separator "^1.0.1" +npm-path@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/npm-path/-/npm-path-2.0.3.tgz#15cff4e1c89a38da77f56f6055b24f975dfb2bbe" + dependencies: + which "^1.2.10" + npm-run-path@^2.0.0: version "2.0.2" resolved "https://registry.yarnpkg.com/npm-run-path/-/npm-run-path-2.0.2.tgz#35a9232dfa35d7067b4cb2ddf2357b1871536c5f" dependencies: path-key "^2.0.0" +npm-which@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/npm-which/-/npm-which-3.0.1.tgz#9225f26ec3a285c209cae67c3b11a6b4ab7140aa" + dependencies: + commander "^2.9.0" + npm-path "^2.0.2" + which "^1.2.10" + npmlog@^4.0.2: version "4.1.2" resolved "https://registry.yarnpkg.com/npmlog/-/npmlog-4.1.2.tgz#08a7f2a8bf734604779a9efa4ad5cc717abb954b" @@ -2038,7 +2278,7 @@ oauth-sign@~0.8.1, oauth-sign@~0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" -object-assign@^4.1.0: +object-assign@^4.0.1, object-assign@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -2065,6 +2305,10 @@ once@^1.3.0, once@^1.3.3, once@^1.4.0: dependencies: wrappy "1" +onetime@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/onetime/-/onetime-1.1.0.tgz#a1f7838f8314c516f05ecefcbc4ccfe04b4ed789" + optimist@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/optimist/-/optimist-0.6.1.tgz#da3ea74686fa21a19a111c326e90eb15a0196686" @@ -2083,6 +2327,15 @@ optionator@^0.8.1: type-check "~0.3.2" wordwrap "~1.0.0" +ora@^0.2.3: + version "0.2.3" + resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4" + dependencies: + chalk "^1.1.1" + cli-cursor "^1.0.2" + cli-spinners "^0.1.2" + object-assign "^4.0.1" + os-homedir@^1.0.0, os-homedir@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/os-homedir/-/os-homedir-1.0.2.tgz#ffbc4988336e0e833de0c168c7ef152121aa7fb3" @@ -2124,6 +2377,10 @@ p-locate@^2.0.0: dependencies: p-limit "^1.1.0" +p-map@^1.1.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" + parse-glob@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" @@ -2139,6 +2396,12 @@ parse-json@^2.2.0: dependencies: error-ex "^1.2.0" +parse-json@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/parse-json/-/parse-json-3.0.0.tgz#fa6f47b18e23826ead32f263e744d0e1e847fb13" + dependencies: + error-ex "^1.3.1" + parse-passwd@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/parse-passwd/-/parse-passwd-1.0.0.tgz#6d5b934a456993b23d37f40a382d6f1666a8e5c6" @@ -2165,6 +2428,10 @@ path-is-absolute@^1.0.0, path-is-absolute@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" +path-is-inside@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/path-is-inside/-/path-is-inside-1.0.2.tgz#365417dede44430d1c11af61027facf074bdfc53" + path-key@^2.0.0: version "2.0.1" resolved "https://registry.yarnpkg.com/path-key/-/path-key-2.0.1.tgz#411cadb574c5a140d3a4b1910d40d80cc9f40b40" @@ -2229,7 +2496,13 @@ preserve@^0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b" -prettier@^1.5.2: +prettier-check@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/prettier-check/-/prettier-check-2.0.0.tgz#edd086ee12d270579233ccb136a16e6afcfba1ae" + dependencies: + execa "^0.6.0" + +prettier@^1.8.2: version "1.8.2" resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.8.2.tgz#bff83e7fd573933c607875e5ba3abbdffb96aeb8" @@ -2475,6 +2748,10 @@ require-directory@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42" +require-from-string@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.1.tgz#c545233e9d7da6616e9d59adfb39fc9f588676ff" + require-main-filename@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-1.0.1.tgz#97f717b69d48784f5f526a6c5aa8ffdda055a4d1" @@ -2496,6 +2773,13 @@ resolve@^1.1.6, resolve@^1.1.7: dependencies: path-parse "^1.0.5" +restore-cursor@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/restore-cursor/-/restore-cursor-1.0.1.tgz#34661f46886327fed2991479152252df92daa541" + dependencies: + exit-hook "^1.0.0" + onetime "^1.0.0" + right-align@^0.1.1: version "0.1.3" resolved "https://registry.yarnpkg.com/right-align/-/right-align-0.1.3.tgz#61339b722fe6a3515689210d24e14c96148613ef" @@ -2508,6 +2792,12 @@ rimraf@2, rimraf@^2.5.1, rimraf@^2.6.1: dependencies: glob "^7.0.5" +rxjs@^5.4.2: + version "5.5.2" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.2.tgz#28d403f0071121967f18ad665563255d54236ac3" + dependencies: + symbol-observable "^1.0.1" + safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" @@ -2595,6 +2885,10 @@ slash@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/slash/-/slash-1.0.0.tgz#c41f2f6c39fc16d1cd17ad4b5d896114ae470d55" +slice-ansi@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/slice-ansi/-/slice-ansi-0.0.4.tgz#edbf8903f66f7ce2f8eafd6ceed65e264c831b35" + sntp@1.x.x: version "1.0.9" resolved "https://registry.yarnpkg.com/sntp/-/sntp-1.0.9.tgz#6541184cc90aeea6c6e7b35e2659082443c66198" @@ -2662,6 +2956,10 @@ sshpk@^1.7.0: jsbn "~0.1.0" tweetnacl "~0.14.0" +staged-git-files@0.0.4: + version "0.0.4" + resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-0.0.4.tgz#d797e1b551ca7a639dec0237dc6eb4bb9be17d35" + "statuses@>= 1.3.1 < 2": version "1.4.0" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.4.0.tgz#bb73d446da2796106efcc1b601a253d6c46bd087" @@ -2670,6 +2968,12 @@ statuses@~1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/statuses/-/statuses-1.3.1.tgz#faf51b9eb74aaef3b3acf4ad5f61abf24cb7b93e" +stream-to-observable@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.2.0.tgz#59d6ea393d87c2c0ddac10aa0d561bc6ba6f0e10" + dependencies: + any-observable "^0.2.0" + string-length@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" @@ -2702,6 +3006,14 @@ string_decoder@~1.0.3: dependencies: safe-buffer "~5.1.0" +stringify-object@^3.2.0: + version "3.2.1" + resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.1.tgz#2720c2eff940854c819f6ee252aaeb581f30624d" + dependencies: + get-own-enumerable-property-symbols "^2.0.1" + is-obj "^1.0.1" + is-regexp "^1.0.0" + stringstream@~0.0.4, stringstream@~0.0.5: version "0.0.5" resolved "https://registry.yarnpkg.com/stringstream/-/stringstream-0.0.5.tgz#4e484cd4de5a0bbbee18e46307710a8a81621878" @@ -2778,6 +3090,14 @@ supports-color@^4.0.0: dependencies: has-flag "^2.0.0" +symbol-observable@^0.2.2: + version "0.2.4" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40" + +symbol-observable@^1.0.1: + version "1.0.4" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" + symbol-tree@^3.2.1: version "3.2.2" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" @@ -2981,7 +3301,7 @@ which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" -which@^1.2.12, which@^1.2.9: +which@^1.2.10, which@^1.2.12, which@^1.2.9: version "1.3.0" resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" dependencies: From 9067292362232100538a40ae26bf8841d05c5a1f Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Mon, 27 Nov 2017 20:57:06 -0500 Subject: [PATCH 07/62] =?UTF-8?q?=F0=9F=92=85=20pretty?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- routes/index.js | 9 ++++++-- routes/shopifyAuth.js | 6 ++++-- routes/tests/shopifyAuth.test.js | 35 +++++++++++++++++++------------- 4 files changed, 33 insertions(+), 19 deletions(-) diff --git a/package.json b/package.json index 5e50b89..5d20baa 100644 --- a/package.json +++ b/package.json @@ -18,7 +18,7 @@ "prettier:check": "prettier-check --single-quote --trailing-comma=all {middleware,routes,shopStore}{/*,/**/*}.{js,json}", "prettier:fix": - "prettier --single-quote --trailing-comma --write {middleware,routes,shopStore}{/*,/**/*}.{js,json}", + "prettier --single-quote --trailing-comma=all --write {middleware,routes,shopStore}{/*,/**/*}.{js,json}", "precommit": "lint-staged", "test": "jest" }, diff --git a/routes/index.js b/routes/index.js index dba2b7f..b61d48d 100644 --- a/routes/index.js +++ b/routes/index.js @@ -7,10 +7,15 @@ const shopifyApiProxy = require('./shopifyApiProxy'); module.exports = function createRouter(shopifyConfig) { const router = express.Router(); - const rawParser = bodyParser.raw({type: '*/*'}); + const rawParser = bodyParser.raw({ type: '*/*' }); router.use('/auth/shopify', createShopifyAuthRouter(shopifyConfig)); - router.use('/api', rawParser, createWithShop({ redirect: false }), shopifyApiProxy); + router.use( + '/api', + rawParser, + createWithShop({ redirect: false }), + shopifyApiProxy, + ); return router; }; diff --git a/routes/shopifyAuth.js b/routes/shopifyAuth.js index 0700374..fada197 100644 --- a/routes/shopifyAuth.js +++ b/routes/shopifyAuth.js @@ -22,7 +22,9 @@ module.exports = function createShopifyAuthRouter({ } const redirectTo = `https://${shop}/admin/oauth/authorize`; - const redirectParams = `?client_id=${apiKey}&scope=${scope}&redirect_uri=${host}/auth/shopify/callback`; + const redirectParams = + `?client_id=${apiKey}&scope=${scope}&redirect_uri=${host}` + + '/auth/shopify/callback'; response.send( ` @@ -32,7 +34,7 @@ module.exports = function createShopifyAuthRouter({ window.top.location.href = "${redirectTo}${redirectParams}" - ` + `, ); }); diff --git a/routes/tests/shopifyAuth.test.js b/routes/tests/shopifyAuth.test.js index ed42d9e..9a2608a 100644 --- a/routes/tests/shopifyAuth.test.js +++ b/routes/tests/shopifyAuth.test.js @@ -2,7 +2,7 @@ const request = require('supertest'); const http = require('http'); const express = require('express'); -const {MemoryStrategy} = require('../../strategies'); +const { MemoryStrategy } = require('../../strategies'); const createShopifyAuthRouter = require('../shopifyAuth'); let server; @@ -13,8 +13,10 @@ describe('shopifyAuth', async () => { describe('/', () => { it('responds to get requests by returning a redirect page', async () => { - const {app, server} = await createServer(); - const {status, text} = await request(app).get('/?shop=shop1').then((resp) => resp); + const { app, server } = await createServer(); + const { status, text } = await request(app) + .get('/?shop=shop1') + .then(resp => resp); expect(status).toBe(200); expect(text).toMatchSnapshot(); @@ -23,8 +25,10 @@ describe('shopifyAuth', async () => { }); it('responds with a 400 when no shop query parameter is given', async () => { - const {app, server} = await createServer(); - const {status, text} = await request(app).get('/').then((resp) => resp); + const { app, server } = await createServer(); + const { status, text } = await request(app) + .get('/') + .then(resp => resp); expect(status).toBe(400); expect(text).toMatchSnapshot(); @@ -32,18 +36,21 @@ describe('shopifyAuth', async () => { server.close(); }); }); -}) +}); -function createServer({afterAuth = jest.fn()} = {}) { +function createServer({ afterAuth = jest.fn() } = {}) { const app = express(); - app.use('/', createShopifyAuthRouter({ - apiKey: 'key', - secret: 'secret', - scope: ['scope'], - shopStore: new MemoryStrategy(), - afterAuth, - })); + app.use( + '/', + createShopifyAuthRouter({ + apiKey: 'key', + secret: 'secret', + scope: ['scope'], + shopStore: new MemoryStrategy(), + afterAuth, + }), + ); server = http.createServer(app); From 20ff91d56e8d1d09493c4161aca22c7100ae4bbf Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Mon, 27 Nov 2017 20:59:05 -0500 Subject: [PATCH 08/62] =?UTF-8?q?=F0=9F=91=8C=20consolidate=20prettier=20c?= =?UTF-8?q?ommands?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5d20baa..70d563e 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,7 @@ "test": "jest" }, "lint-staged": { - "*.{js,json}": ["prettier-check --single-quote --trailing-comma=all"] + "*.{js,json}": ["prettier:check"] }, "dependencies": { "body-parser": "^1.18.2", From 528d0add8587ab70a7215f11b8284c496b67f2d5 Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Mon, 27 Nov 2017 21:19:02 -0500 Subject: [PATCH 09/62] =?UTF-8?q?=E2=9C=A8=20configurable=20SQL=20strategy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- strategies/MemoryStrategy.js | 4 +--- strategies/RedisStrategy.js | 4 ++-- .../{SqliteStrategy.js => SQLStrategy.js} | 24 +++++++++++-------- strategies/index.js | 4 ++-- 4 files changed, 19 insertions(+), 17 deletions(-) rename strategies/{SqliteStrategy.js => SQLStrategy.js} (72%) diff --git a/strategies/MemoryStrategy.js b/strategies/MemoryStrategy.js index 486d088..b5769e3 100644 --- a/strategies/MemoryStrategy.js +++ b/strategies/MemoryStrategy.js @@ -6,9 +6,7 @@ module.exports = class MemoryStrategy { storeShop({ shop, accessToken, data = {} }, done) { this.store[shop] = Object.assign( {}, - { - accessToken, - }, + { accessToken }, data ); diff --git a/strategies/RedisStrategy.js b/strategies/RedisStrategy.js index 43e29b4..41b658a 100644 --- a/strategies/RedisStrategy.js +++ b/strategies/RedisStrategy.js @@ -1,8 +1,8 @@ const Redis = require('redis'); module.exports = class RedisStrategy { - constructor() { - this.client = Redis.createClient(); + constructor(redisConfig) { + this.client = Redis.createClient(redisConfig); } storeShop({ shop, accessToken, data = {} }, done) { diff --git a/strategies/SqliteStrategy.js b/strategies/SQLStrategy.js similarity index 72% rename from strategies/SqliteStrategy.js rename to strategies/SQLStrategy.js index 1c89130..cb06588 100644 --- a/strategies/SqliteStrategy.js +++ b/strategies/SQLStrategy.js @@ -1,16 +1,20 @@ const Knex = require('knex'); -module.exports = class SqliteStrategy { - constructor() { - this.knex = Knex({ - dialect: 'sqlite3', - useNullAsDefault: true, - connection: { - filename: './db.sqlite3', - }, - }); +const defaultConfig = { + dialect: 'sqlite3', + useNullAsDefault: true, + connection: { + filename: './db.sqlite3', + }, +}; + +module.exports = class SQLStrategy { + constructor(config = defaultConfig) { + this.knex = Knex(config); + } - this.knex.schema + initialize() { + return this.knex.schema .createTableIfNotExists('shops', table => { table.increments('id'); table.string('shopify_domain'); diff --git a/strategies/index.js b/strategies/index.js index 8ad6d22..43e4117 100644 --- a/strategies/index.js +++ b/strategies/index.js @@ -1,9 +1,9 @@ const RedisStrategy = require('./RedisStrategy'); const MemoryStrategy = require('./MemoryStrategy'); -const SqliteStrategy = require('./SqliteStrategy'); +const SQLStrategy = require('./SQLStrategy'); module.exports = { RedisStrategy, MemoryStrategy, - SqliteStrategy, + SQLStrategy, } From 1b7baafdaad593a914a086c7f4005289a2cb76ef Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Mon, 27 Nov 2017 21:39:51 -0500 Subject: [PATCH 10/62] =?UTF-8?q?=F0=9F=97=92=EF=B8=8F=20update=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 58 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 50 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index 37aa780..aa8f062 100644 --- a/README.md +++ b/README.md @@ -62,30 +62,72 @@ endpoints from a client application without having to worry about CORS. By default the package comes with `MemoryStrategy`, `RedisStrategy`, and `SqliteStrategy`. If none are specified, the default is `MemoryStrategy`. -You can use them in a config like so: +#### MemoryStrategy + +Simple javascript object based memory store for development purposes. Do not use this in production! + +```javascript +const shopifyExpress = require('@shopify/shopify-express'); +const {MemoryStrategy} = require('@shopify/shopify-express/strategies'); + +const shopify = shopifyExpress({ + shopStore: new RedisStrategy(redisConfig), + ...restOfConfig, +}); +``` + +#### RedisStrategy + +Uses [redis](https://www.npmjs.com/package/redis) under the hood, so you can pass it any configuration that's valid for the library. ```javascript const shopifyExpress = require('@shopify/shopify-express'); const {RedisStrategy} = require('@shopify/shopify-express/strategies'); +const redisConfig = { + // your config here +}; + const shopify = shopifyExpress({ - shopStore: new RedisStrategy(), + shopStore: new RedisStrategy(redisConfig), ...restOfConfig, }); ``` -### Custom Strategy +#### SQLStrategy + +Uses [knex](https://www.npmjs.com/package/knex) under the hood, so you can pass it any configuration that's valid for the library. By default it uses `sqlite3` so you'll need to run `yarn add sqlite3` to use it. Knex also supports `postgreSQL` and `mySQL`. + +```javascript +const shopifyExpress = require('@shopify/shopify-express'); +const {SQLStrategy} = require('@shopify/shopify-express/strategies'); + +// uses sqlite3 if no settings are specified +const knexConfig = { + // your config here +}; + +const shopify = shopifyExpress({ + shopStore: new SQLStrategy(knexConfig), + ...restOfConfig, +}); +``` + +SQLStrategy expects a table named `shops` with a primary key `id`, and `string` fields for `shop_domain` and `access_token`. It's recommended you index `shop_domain` since it is used to look up tokens. + +If you do not have a table already created for your store, you can generate one with `new SQLStrategy(myConfig).initialize()`. This returns a promise so you can finish setting up your app after it if you like, but we suggest you make a separate db initialization script, or keep track of your schema yourself. + +#### Custom Strategy -`shopifyExpress` takes a `shopStore` parameter. This can be any javascript class matching the following interface: +`shopifyExpress` accepts any javascript class matching the following interface: ```javascript - class Strategy { - constructor(){} + interface Strategy { // shop refers to the shop's domain name - getShop({ shop }, done)){} + getShop({ shop }, done)): void // shop refers to the shop's domain name // data can by any serializable object - storeShop({ shop, accessToken, data }, done){} + storeShop({ shop, accessToken, data }, done): void } ``` From e01a874afc5b826e196ac3759332f827f39ee2ab Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Mon, 27 Nov 2017 21:55:29 -0500 Subject: [PATCH 11/62] =?UTF-8?q?=F0=9F=91=8C=20change=20whitelist=20to=20?= =?UTF-8?q?blacklist?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/shopifyApiProxy.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/routes/shopifyApiProxy.js b/routes/shopifyApiProxy.js index ee5a33c..6a9100e 100644 --- a/routes/shopifyApiProxy.js +++ b/routes/shopifyApiProxy.js @@ -1,6 +1,15 @@ const { URL } = require('url'); -const ALLOWED_URLS = ['/products', '/orders']; +const DISALLOWED_URLS = [ + '/admin/application_charges', + '/admin/application_credits', + '/admin/carrier_services', + '/admin/recurring_application_charges', + '/admin/script_tags', + '/admin/storefront_access_token', + '/admin/webhooks', + '/admin/oauth', +]; module.exports = function shopifyApiProxy(request, response, next) { const { query, method, path, body, session } = request; @@ -33,8 +42,8 @@ module.exports = function shopifyApiProxy(request, response, next) { function validRequest(path) { const strippedPath = path.split('?')[0].split('.json')[0]; - return ALLOWED_URLS.some(resource => { - return strippedPath === resource; + return DISALLOWED_URLS.every(resource => { + return strippedPath.indexOf(resource) === -1 }); } From 9ea77fe6006cffcae5a8d896792797d357f99f1c Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Mon, 27 Nov 2017 21:56:36 -0500 Subject: [PATCH 12/62] =?UTF-8?q?=E2=9C=85=20add=20tests=20to=20be=20fille?= =?UTF-8?q?d=20out=20in=20subsequent=20pr?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/tests/shopifyApiProxy.js | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 routes/tests/shopifyApiProxy.js diff --git a/routes/tests/shopifyApiProxy.js b/routes/tests/shopifyApiProxy.js new file mode 100644 index 0000000..f4a8f40 --- /dev/null +++ b/routes/tests/shopifyApiProxy.js @@ -0,0 +1,7 @@ +const shopifyApiProxy = require('../shopifyApiProxy'); + +describe('shopifyApiProxy', async () => { + it.skip('proxies requests with credentials from session'); + it.skip('does not proxy requests to dissallowed urls'); + it.skip('does not proxy requests urls which contain dissallowed urls'); +}); From bc7735fc45650886eec2772d201e4b98eac22b74 Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Tue, 28 Nov 2017 10:56:30 -0500 Subject: [PATCH 13/62] =?UTF-8?q?=F0=9F=91=8C=20remove=20unnecessary=20pre?= =?UTF-8?q?fix=20to=20routes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/shopifyApiProxy.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/routes/shopifyApiProxy.js b/routes/shopifyApiProxy.js index 6a9100e..52d2f46 100644 --- a/routes/shopifyApiProxy.js +++ b/routes/shopifyApiProxy.js @@ -1,14 +1,14 @@ const { URL } = require('url'); const DISALLOWED_URLS = [ - '/admin/application_charges', - '/admin/application_credits', - '/admin/carrier_services', - '/admin/recurring_application_charges', - '/admin/script_tags', - '/admin/storefront_access_token', - '/admin/webhooks', - '/admin/oauth', + '/application_charges', + '/application_credits', + '/carrier_services', + '/recurring_application_charges', + '/script_tags', + '/storefront_access_token', + '/webhooks', + '/oauth', ]; module.exports = function shopifyApiProxy(request, response, next) { @@ -43,7 +43,7 @@ function validRequest(path) { const strippedPath = path.split('?')[0].split('.json')[0]; return DISALLOWED_URLS.every(resource => { - return strippedPath.indexOf(resource) === -1 + return strippedPath.indexOf(resource) === -1; }); } From 91e9316a29e7f29d91645e0f32110e8ced5dd995 Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Tue, 28 Nov 2017 11:30:35 -0500 Subject: [PATCH 14/62] =?UTF-8?q?=F0=9F=91=8C=20=20add=20fulfillment=20ser?= =?UTF-8?q?vices?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/shopifyApiProxy.js | 1 + 1 file changed, 1 insertion(+) diff --git a/routes/shopifyApiProxy.js b/routes/shopifyApiProxy.js index 52d2f46..708de8e 100644 --- a/routes/shopifyApiProxy.js +++ b/routes/shopifyApiProxy.js @@ -4,6 +4,7 @@ const DISALLOWED_URLS = [ '/application_charges', '/application_credits', '/carrier_services', + '/fulfillment_services', '/recurring_application_charges', '/script_tags', '/storefront_access_token', From 4673002aad2f42dfa2106f92cc338c5550a7985f Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Tue, 28 Nov 2017 11:32:59 -0500 Subject: [PATCH 15/62] =?UTF-8?q?=F0=9F=91=8C=20=20polish=20readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index aa8f062..8cca38b 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ const shopifyExpress = require('@shopify/shopify-express'); const {MemoryStrategy} = require('@shopify/shopify-express/strategies'); const shopify = shopifyExpress({ - shopStore: new RedisStrategy(redisConfig), + shopStore: new MemoryStrategy(redisConfig), ...restOfConfig, }); ``` @@ -122,12 +122,12 @@ If you do not have a table already created for your store, you can generate one `shopifyExpress` accepts any javascript class matching the following interface: ```javascript - interface Strategy { + class Strategy { // shop refers to the shop's domain name - getShop({ shop }, done)): void + getShop({ shop }, done)) // shop refers to the shop's domain name // data can by any serializable object - storeShop({ shop, accessToken, data }, done): void + storeShop({ shop, accessToken, data }, done) } ``` From f788926fa9d506f914a43423d60eb032a6f62b5c Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Tue, 28 Nov 2017 11:38:04 -0500 Subject: [PATCH 16/62] 1.0.0-alpha.5 --- package.json | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 910c01b..b70e834 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,13 @@ { "name": "@shopify/shopify-express", - "version": "1.0.0-alpha.4", + "version": "1.0.0-alpha.5", "author": "Shopify Inc.", - "description": - "Get up and running quickly with Express.js and the Shopify API.", - "keywords": ["shopify", "express.js", "express middleware"], + "description": "Get up and running quickly with Express.js and the Shopify API.", + "keywords": [ + "shopify", + "express.js", + "express middleware" + ], "bugs": "https://github.com/Shopify/shopify-express/issues", "license": "MIT", "repository": { @@ -15,15 +18,15 @@ "node": ">=8.1.0" }, "scripts": { - "prettier:check": - "prettier-check --single-quote --trailing-comma=all {middleware,routes,shopStore}{/*,/**/*}.{js,json}", - "prettier:fix": - "prettier --single-quote --trailing-comma=all --write {middleware,routes,shopStore}{/*,/**/*}.{js,json}", + "prettier:check": "prettier-check --single-quote --trailing-comma=all {middleware,routes,shopStore}{/*,/**/*}.{js}", + "prettier:fix": "prettier --single-quote --trailing-comma=all --write {middleware,routes,shopStore}{/*,/**/*}.{js}", "precommit": "lint-staged", "test": "jest" }, "lint-staged": { - "*.{js,json}": ["prettier:check"] + "*.{js}": [ + "prettier:check" + ] }, "dependencies": { "body-parser": "^1.18.2", From e12b57114cbee08b41ee71f6382d7315a4cbddad Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Mon, 4 Dec 2017 09:58:48 -0800 Subject: [PATCH 17/62] =?UTF-8?q?=F0=9F=90=9B=20=20fix=20usage=20of=20fetc?= =?UTF-8?q?h=20with=20no=20polyfill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 + routes/shopifyApiProxy.js | 49 +++++++++++++++------------------------ yarn.lock | 13 +++++++++++ 3 files changed, 33 insertions(+), 30 deletions(-) diff --git a/package.json b/package.json index b70e834..d6dbb32 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,7 @@ ] }, "dependencies": { + "axios": "^0.17.1", "body-parser": "^1.18.2", "connect-redis": "^3.3.0", "express": "^4.16.2", diff --git a/routes/shopifyApiProxy.js b/routes/shopifyApiProxy.js index 708de8e..ee3b3b4 100644 --- a/routes/shopifyApiProxy.js +++ b/routes/shopifyApiProxy.js @@ -1,4 +1,5 @@ const { URL } = require('url'); +const request = require('axios'); const DISALLOWED_URLS = [ '/application_charges', @@ -12,32 +13,30 @@ const DISALLOWED_URLS = [ '/oauth', ]; -module.exports = function shopifyApiProxy(request, response, next) { - const { query, method, path, body, session } = request; +module.exports = async function shopifyApiProxy(incomingRequest, response, next) { + const { query, method, path, body, session } = incomingRequest; const { shop, accessToken } = session; if (!validRequest(path)) { return response.status(403).send('Endpoint not in whitelist'); } - const fetchOptions = { - method, - body, - headers: { - 'Content-Type': 'application/json', - 'X-Shopify-Access-Token': accessToken, - }, - }; - - fetchWithParams(`https://${shop}/admin${path}`, fetchOptions, query) - .then(remoteResponse => { - const { status } = remoteResponse; - return Promise.all([remoteResponse.json(), status]); - }) - .then(([responseBody, status]) => { - response.status(status).send(responseBody); - }) - .catch(err => response.err(err)); + try { + const { status, data } = await sendRequest({ + method, + url: `https://${shop}/admin${path}`, + data: body, + params: query, + headers: { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': accessToken, + }, + }); + + response.status(status).send(data); + } catch (error) { + response.status(500).send(error); + } }; function validRequest(path) { @@ -47,13 +46,3 @@ function validRequest(path) { return strippedPath.indexOf(resource) === -1; }); } - -function fetchWithParams(url, fetchOpts, query) { - const parsedUrl = new URL(url); - - Object.entries(query).forEach(([key, value]) => { - parsedUrl.searchParams.append(key, value); - }); - - return fetch(parsedUrl.href, fetchOpts); -} diff --git a/yarn.lock b/yarn.lock index 92c9349..28eaeaa 100644 --- a/yarn.lock +++ b/yarn.lock @@ -187,6 +187,13 @@ aws4@^1.2.1, aws4@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" +axios@^0.17.1: + version "0.17.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.17.1.tgz#2d8e3e5d0bdbd7327f91bc814f5c57660f81824d" + dependencies: + follow-redirects "^1.2.5" + is-buffer "^1.1.5" + babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" @@ -1002,6 +1009,12 @@ flagged-respawn@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-0.3.2.tgz#ff191eddcd7088a675b2610fffc976be9b8074b5" +follow-redirects@^1.2.5: + version "1.2.6" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.2.6.tgz#4dcdc7e4ab3dd6765a97ff89c3b4c258117c79bf" + dependencies: + debug "^3.1.0" + for-in@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" From fcd44f13ad050cd4f9460cdc5b94666101948066 Mon Sep 17 00:00:00 2001 From: extrablind Date: Tue, 5 Dec 2017 16:49:56 -0500 Subject: [PATCH 18/62] Correct typo in README Double presence of "the" --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8cca38b..15b9778 100644 --- a/README.md +++ b/README.md @@ -145,7 +145,7 @@ Express middleware that validates the presence of your shop session. `app.use('/someProtectedPath', withWebhook, someHandler);` -Express middleware that validates the the presence of a valid HMAC signature to allow webhook requests from shopify to your app. +Express middleware that validates the presence of a valid HMAC signature to allow webhook requests from shopify to your app. ## Example app From c2ca747e36def0d82dc8e0bde7926ed369dcf7fc Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Wed, 6 Dec 2017 17:39:30 -0800 Subject: [PATCH 19/62] =?UTF-8?q?=E2=9E=95=20add=20node-fetch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 5 +-- routes/shopifyApiProxy.js | 26 ++++++++---- yarn.lock | 86 +++++++++++---------------------------- 3 files changed, 43 insertions(+), 74 deletions(-) diff --git a/package.json b/package.json index d6dbb32..5b3c0e7 100644 --- a/package.json +++ b/package.json @@ -29,12 +29,12 @@ ] }, "dependencies": { - "axios": "^0.17.1", "body-parser": "^1.18.2", "connect-redis": "^3.3.0", "express": "^4.16.2", "express-session": "^1.15.3", "knex": "^0.13.0", + "node-fetch": "^1.7.3", "redis": "^2.7.1", "sqlite3": "^3.1.9", "url": "^0.11.0" @@ -44,7 +44,6 @@ "jest": "^21.2.1", "lint-staged": "^5.0.0", "prettier": "^1.8.2", - "prettier-check": "^2.0.0", - "supertest": "^3.0.0" + "prettier-check": "^2.0.0" } } diff --git a/routes/shopifyApiProxy.js b/routes/shopifyApiProxy.js index ee3b3b4..0a021e4 100644 --- a/routes/shopifyApiProxy.js +++ b/routes/shopifyApiProxy.js @@ -1,5 +1,5 @@ -const { URL } = require('url'); -const request = require('axios'); +const querystring = require('querystring'); +const fetch = require('node-fetch'); const DISALLOWED_URLS = [ '/application_charges', @@ -14,31 +14,39 @@ const DISALLOWED_URLS = [ ]; module.exports = async function shopifyApiProxy(incomingRequest, response, next) { - const { query, method, path, body, session } = incomingRequest; + const { query, method, path: pathname, body, session } = incomingRequest; const { shop, accessToken } = session; - if (!validRequest(path)) { + if (!validRequest(pathname)) { return response.status(403).send('Endpoint not in whitelist'); } try { - const { status, data } = await sendRequest({ + const searchParams = querystring.stringify(query); + const searchString = searchParams.length > 0 + ? `?${searchParams}` + : ''; + + const url = `https://${shop}/admin${pathname}${searchString}`; + const result = await fetch(url, { method, - url: `https://${shop}/admin${path}`, - data: body, - params: query, + body, headers: { 'Content-Type': 'application/json', 'X-Shopify-Access-Token': accessToken, }, }); - response.status(status).send(data); + const data = await result.text(); + response.status(result.status).send(data); } catch (error) { + console.log(error); response.status(500).send(error); } }; +module.exports.DISALLOWED_URLS = DISALLOWED_URLS; + function validRequest(path) { const strippedPath = path.split('?')[0].split('.json')[0]; diff --git a/yarn.lock b/yarn.lock index 28eaeaa..9a6618c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -187,13 +187,6 @@ aws4@^1.2.1, aws4@^1.6.0: version "1.6.0" resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.6.0.tgz#83ef5ca860b2b32e4a0deedee8c771b9db57471e" -axios@^0.17.1: - version "0.17.1" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.17.1.tgz#2d8e3e5d0bdbd7327f91bc814f5c57660f81824d" - dependencies: - follow-redirects "^1.2.5" - is-buffer "^1.1.5" - babel-code-frame@^6.26.0: version "6.26.0" resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b" @@ -532,10 +525,6 @@ commander@^2.11.0, commander@^2.2.0, commander@^2.9.0: version "2.12.1" resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.1.tgz#468635c4168d06145b9323356d1da84d14ac4a7a" -component-emitter@^1.2.0: - version "1.2.1" - resolved "https://registry.yarnpkg.com/component-emitter/-/component-emitter-1.2.1.tgz#137918d6d78283f7df7a6b7c5a63e140e69425e6" - concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" @@ -575,10 +564,6 @@ cookie@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" -cookiejar@^2.1.0: - version "2.1.1" - resolved "https://registry.yarnpkg.com/cookiejar/-/cookiejar-2.1.1.tgz#41ad57b1b555951ec171412a81942b1e8200d34a" - core-js@^2.4.0, core-js@^2.5.0: version "2.5.1" resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b" @@ -732,6 +717,12 @@ encodeurl@~1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" +encoding@^0.1.11: + version "0.1.12" + resolved "https://registry.yarnpkg.com/encoding/-/encoding-0.1.12.tgz#538b66f3ee62cd1ab51ec323829d1f9480c74beb" + dependencies: + iconv-lite "~0.4.13" + errno@^0.1.4: version "0.1.4" resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d" @@ -1009,12 +1000,6 @@ flagged-respawn@^0.3.2: version "0.3.2" resolved "https://registry.yarnpkg.com/flagged-respawn/-/flagged-respawn-0.3.2.tgz#ff191eddcd7088a675b2610fffc976be9b8074b5" -follow-redirects@^1.2.5: - version "1.2.6" - resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.2.6.tgz#4dcdc7e4ab3dd6765a97ff89c3b4c258117c79bf" - dependencies: - debug "^3.1.0" - for-in@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -1029,26 +1014,22 @@ forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" -form-data@^2.3.1, form-data@~2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" +form-data@~2.1.1: + version "2.1.4" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" dependencies: asynckit "^0.4.0" combined-stream "^1.0.5" mime-types "^2.1.12" -form-data@~2.1.1: - version "2.1.4" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.1.4.tgz#33c183acf193276ecaa98143a69e94bfee1750d1" +form-data@~2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" dependencies: asynckit "^0.4.0" combined-stream "^1.0.5" mime-types "^2.1.12" -formidable@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/formidable/-/formidable-1.1.1.tgz#96b8886f7c3c3508b932d6bd70c4d3a88f35f1a9" - forwarded@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/forwarded/-/forwarded-0.1.2.tgz#98c23dab1175657b8c0573e8ceccd91b0ff18c84" @@ -1308,7 +1289,7 @@ husky@^0.14.3: normalize-path "^1.0.0" strip-indent "^2.0.0" -iconv-lite@0.4.19: +iconv-lite@0.4.19, iconv-lite@~0.4.13: version "0.4.19" resolved "https://registry.yarnpkg.com/iconv-lite/-/iconv-lite-0.4.19.tgz#f7468f60135f5e5dad3399c0a81be9a1603a082b" @@ -1471,7 +1452,7 @@ is-regexp@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" -is-stream@^1.1.0: +is-stream@^1.0.1, is-stream@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-1.1.0.tgz#12d4a3dd4e68e0b79ceb8dbc84173ae80d91ca44" @@ -2103,7 +2084,7 @@ merge@^1.1.3: version "1.2.0" resolved "https://registry.yarnpkg.com/merge/-/merge-1.2.0.tgz#7531e39d4949c281a66b8c5a6e0265e8b05894da" -methods@^1.1.1, methods@~1.1.2: +methods@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" @@ -2139,10 +2120,6 @@ mime@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" -mime@^1.4.1: - version "1.6.0" - resolved "https://registry.yarnpkg.com/mime/-/mime-1.6.0.tgz#32cd9e5c64553bd58d19a568af452acff04981b1" - mimic-fn@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" @@ -2195,6 +2172,13 @@ negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" +node-fetch@^1.7.3: + version "1.7.3" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" + dependencies: + encoding "^0.1.11" + is-stream "^1.0.1" + node-int64@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" @@ -2557,7 +2541,7 @@ punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" -qs@6.5.1, qs@^6.5.1, qs@~6.5.1: +qs@6.5.1, qs@~6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8" @@ -2641,7 +2625,7 @@ readable-stream@^1.1.12: isarray "0.0.1" string_decoder "~0.10.x" -readable-stream@^2.0.5, readable-stream@^2.0.6, readable-stream@^2.1.4: +readable-stream@^2.0.6, readable-stream@^2.1.4: version "2.3.3" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" dependencies: @@ -3065,28 +3049,6 @@ strip-json-comments@~2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/strip-json-comments/-/strip-json-comments-2.0.1.tgz#3c531942e908c2697c0ec344858c286c7ca0a60a" -superagent@^3.0.0: - version "3.8.1" - resolved "https://registry.yarnpkg.com/superagent/-/superagent-3.8.1.tgz#2571fd921f3fcdba43ac68c3b35c91951532701f" - dependencies: - component-emitter "^1.2.0" - cookiejar "^2.1.0" - debug "^3.1.0" - extend "^3.0.0" - form-data "^2.3.1" - formidable "^1.1.1" - methods "^1.1.1" - mime "^1.4.1" - qs "^6.5.1" - readable-stream "^2.0.5" - -supertest@^3.0.0: - version "3.0.0" - resolved "https://registry.yarnpkg.com/supertest/-/supertest-3.0.0.tgz#8d4bb68fd1830ee07033b1c5a5a9a4021c965296" - dependencies: - methods "~1.1.2" - superagent "^3.0.0" - supports-color@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-2.0.0.tgz#535d045ce6b6363fa40117084629995e9df324c7" From 1bce52c2a77b561a6d6fd299878c53b0fa96e42e Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Wed, 6 Dec 2017 17:40:29 -0800 Subject: [PATCH 20/62] =?UTF-8?q?=E2=9C=85=20test=20api=20proxy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/tests/shopifyApiProxy.js | 7 -- routes/tests/shopifyApiProxy.test.js | 102 +++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 7 deletions(-) delete mode 100644 routes/tests/shopifyApiProxy.js create mode 100644 routes/tests/shopifyApiProxy.test.js diff --git a/routes/tests/shopifyApiProxy.js b/routes/tests/shopifyApiProxy.js deleted file mode 100644 index f4a8f40..0000000 --- a/routes/tests/shopifyApiProxy.js +++ /dev/null @@ -1,7 +0,0 @@ -const shopifyApiProxy = require('../shopifyApiProxy'); - -describe('shopifyApiProxy', async () => { - it.skip('proxies requests with credentials from session'); - it.skip('does not proxy requests to dissallowed urls'); - it.skip('does not proxy requests urls which contain dissallowed urls'); -}); diff --git a/routes/tests/shopifyApiProxy.test.js b/routes/tests/shopifyApiProxy.test.js new file mode 100644 index 0000000..8ab619a --- /dev/null +++ b/routes/tests/shopifyApiProxy.test.js @@ -0,0 +1,102 @@ +const express = require('express'); +const http = require('http'); +const fetch = require.requireActual('node-fetch'); +const fetchMock = require.requireMock('node-fetch'); + +const shopifyApiProxy = require('../shopifyApiProxy'); + +const { DISALLOWED_URLS } = shopifyApiProxy; +const PORT = 3000; +const BASE_URL = `http://localhost:${PORT}`; +const API_ROUTE = '/api'; + +jest.mock('node-fetch'); + +let session; +let server; +describe('shopifyApiProxy', async () => { + beforeEach(async () => { + fetchMock.mockImplementation(() => ({ status: 200, text: () => Promise.resolve(':)') })); + + session = { + shop: 'shop.com', + accessToken: 'token', + }; + + server = await createServer(); + }); + + afterEach(() => { + fetchMock.mockClear(); + server.close(); + }); + + it('proxies requests to the shop given in session', async () => { + const shop = 'some-shop.com'; + const endpoint = '/products'; + session.shop = shop; + + const expectedPath = `https://${shop}/admin${endpoint}`; + const response = await fetch(`${BASE_URL}${API_ROUTE}${endpoint}`); + + expect(fetchMock).toBeCalled(); + expect(fetchMock.mock.calls[0][0]).toBe(expectedPath); + expect(response.status).toBe(200); + }); + + it('includes the access token given in session and json content type', async () => { + const accessToken = 'foo-token'; + session.accessToken = accessToken; + + const expectedHeaders = { + 'Content-Type': 'application/json', + 'X-Shopify-Access-Token': accessToken, + }; + const expectedPath = `https://${session.shop}/admin/`; + + const response = await fetch(`${BASE_URL}${API_ROUTE}`); + + expect(fetchMock).toBeCalled(); + expect(fetchMock.mock.calls[0][1].headers).toMatchObject(expectedHeaders); + expect(response.status).toBe(200); + }); + + it('does not proxy requests to dissallowed urls', async () => { + for(const url of DISALLOWED_URLS) { + response = await fetch(`${BASE_URL}${API_ROUTE}${url}`); + expect(response.status).toBe(403); + } + }); + + it('returns body from proxied request', async () => { + const expectedBody = 'body text'; + fetchMock.mockImplementation(() => { + return {status: 200, text: () => Promise.resolve(expectedBody)}; + }); + + const response = await fetch(`${BASE_URL}${API_ROUTE}`); + const body = await response.text(); + + expect(response.status).toBe(200); + expect(body).toBe(expectedBody); + }); +}); + +function createServer() { + const app = express(); + + app.use( + API_ROUTE, + (req, _, next) => { + req.session = session; + next(); + }, + shopifyApiProxy + ); + + server = http.createServer(app); + + return new Promise((resolve, reject) => { + server.listen(3000, resolve(server)); + }); +} From 305fdf5349dfbce7b178fc212429d9aac278f1d1 Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Wed, 6 Dec 2017 17:40:46 -0800 Subject: [PATCH 21/62] =?UTF-8?q?=F0=9F=92=85=20refactor=20auth=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/tests/shopifyAuth.test.js | 45 +++++++++++++++----------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/routes/tests/shopifyAuth.test.js b/routes/tests/shopifyAuth.test.js index 9a2608a..06b368f 100644 --- a/routes/tests/shopifyAuth.test.js +++ b/routes/tests/shopifyAuth.test.js @@ -1,44 +1,46 @@ -const request = require('supertest'); +const fetch = require('node-fetch'); const http = require('http'); const express = require('express'); const { MemoryStrategy } = require('../../strategies'); const createShopifyAuthRouter = require('../shopifyAuth'); +const PORT = 3000; +const BASE_URL = `http://localhost:${PORT}` + let server; +let afterAuthSpy; describe('shopifyAuth', async () => { + beforeEach(async () => { + afterAuthSpy = jest.fn(); + + server = await createServer(afterAuthSpy); + }); + afterEach(() => { server.close(); }); describe('/', () => { it('responds to get requests by returning a redirect page', async () => { - const { app, server } = await createServer(); - const { status, text } = await request(app) - .get('/?shop=shop1') - .then(resp => resp); + const response = await fetch(`${BASE_URL}/?shop=shop1`); + const data = await response.text(); - expect(status).toBe(200); - expect(text).toMatchSnapshot(); - - server.close(); + expect(response.status).toBe(200); + expect(data).toMatchSnapshot(); }); it('responds with a 400 when no shop query parameter is given', async () => { - const { app, server } = await createServer(); - const { status, text } = await request(app) - .get('/') - .then(resp => resp); - - expect(status).toBe(400); - expect(text).toMatchSnapshot(); + const response = await fetch(BASE_URL); + const data = await response.text(); - server.close(); + expect(response.status).toBe(400); + expect(data).toMatchSnapshot(); }); }); }); -function createServer({ afterAuth = jest.fn() } = {}) { +function createServer(afterAuth) { const app = express(); app.use( @@ -55,11 +57,6 @@ function createServer({ afterAuth = jest.fn() } = {}) { server = http.createServer(app); return new Promise((resolve, reject) => { - const results = { - server, - app, - afterAuth, - }; - server.listen(3000, resolve(results)); + server.listen(PORT, resolve(server)); }); } From fadd515c435c44aaf6942db25e807b4388bb2d5c Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Fri, 8 Dec 2017 10:52:44 -0800 Subject: [PATCH 22/62] =?UTF-8?q?=F0=9F=91=8C=20remove=20smiley=20face=20f?= =?UTF-8?q?rom=20inline=20fixture?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/tests/shopifyApiProxy.test.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/tests/shopifyApiProxy.test.js b/routes/tests/shopifyApiProxy.test.js index 8ab619a..f209b37 100644 --- a/routes/tests/shopifyApiProxy.test.js +++ b/routes/tests/shopifyApiProxy.test.js @@ -16,7 +16,7 @@ let session; let server; describe('shopifyApiProxy', async () => { beforeEach(async () => { - fetchMock.mockImplementation(() => ({ status: 200, text: () => Promise.resolve(':)') })); + fetchMock.mockImplementation(() => ({ status: 200, text: () => Promise.resolve() })); session = { shop: 'shop.com', From 6258029a0a800fef1e390a1a4bb888843ad17cf1 Mon Sep 17 00:00:00 2001 From: Jamie Dwyer Date: Mon, 11 Dec 2017 22:24:33 -0500 Subject: [PATCH 23/62] Use correct secret and don't assume bodyParser.json --- middleware/webhooks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/middleware/webhooks.js b/middleware/webhooks.js index 5544ab6..c9e0029 100644 --- a/middleware/webhooks.js +++ b/middleware/webhooks.js @@ -8,8 +8,8 @@ module.exports = function createWithWebhook({ secret, shopStore }) { const shopDomain = request.get('X-Shopify-Shop-Domain'); const generated_hash = crypto - .createHmac('sha256', SHOPIFY_APP_SECRET) - .update(JSON.stringify(data)) + .createHmac('sha256', secret) + .update(data) .digest('base64'); if (generated_hash !== hmac) { From 905267895655e755de983f97fb4b0b05f05e12cb Mon Sep 17 00:00:00 2001 From: Jamie Dwyer Date: Fri, 15 Dec 2017 21:55:36 -0500 Subject: [PATCH 24/62] Clean up webhook interface and further improvements --- middleware/webhooks.js | 57 ++++++++++++++++++++++++++---------------- package.json | 3 ++- yarn.lock | 2 +- 3 files changed, 39 insertions(+), 23 deletions(-) diff --git a/middleware/webhooks.js b/middleware/webhooks.js index c9e0029..3c8b5be 100644 --- a/middleware/webhooks.js +++ b/middleware/webhooks.js @@ -1,29 +1,44 @@ const crypto = require('crypto'); +const getRawBody = require('raw-body'); -module.exports = function createWithWebhook({ secret, shopStore }) { - return function withWebhook(request, response, next) { - const { body: data } = request; - const hmac = request.get('X-Shopify-Hmac-Sha256'); - const topic = request.get('X-Shopify-Topic'); - const shopDomain = request.get('X-Shopify-Shop-Domain'); +module.exports = function configureWithWebhook({ secret, shopStore }) { + return function createWebhookHandler(onVerified) { + return async function withWebhook(request, response) { + const { body: data } = request; + const hmac = request.get('X-Shopify-Hmac-Sha256'); + const topic = request.get('X-Shopify-Topic'); + const shopDomain = request.get('X-Shopify-Shop-Domain'); - const generated_hash = crypto - .createHmac('sha256', secret) - .update(data) - .digest('base64'); + try { + const rawBody = await getRawBody(request); + const generated_hash = crypto + .createHmac('sha256', secret) + .update(rawBody) + .digest('base64'); - if (generated_hash !== hmac) { - return response.status(401).send("Request doesn't pass HMAC validation"); - } + if (generated_hash !== hmac) { + response.status(401).send(); + onVerified(new Error("Unable to verify request HMAC")); + return; + } - shopStore.getShop({ shop: shopDomain }, (error, { accessToken }) => { - if (error) { - next(error); - } + shopStore.getShop({ shop: shopDomain }, (error, { accessToken }) => { + if (error) { + response.status(401).send(); + onVerified(new Error("Couldn't fetch credentials for shop")); + return; + } + + request.body = rawBody.toString('utf8'); + request.webhook = { topic, shopDomain, accessToken }; - request.webhook = { topic, shopDomain, accessToken }; + response.status(200).send(); - next(); - }); - }; + onVerified(null, request); + }); + } catch(error) { + response.send(error); + } + }; + } }; diff --git a/package.json b/package.json index 5b3c0e7..f891528 100644 --- a/package.json +++ b/package.json @@ -37,7 +37,8 @@ "node-fetch": "^1.7.3", "redis": "^2.7.1", "sqlite3": "^3.1.9", - "url": "^0.11.0" + "url": "^0.11.0", + "raw-body": "^2.3.2" }, "devDependencies": { "husky": "^0.14.3", diff --git a/yarn.lock b/yarn.lock index 9a6618c..82fc232 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2568,7 +2568,7 @@ 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: +raw-body@2.3.2, raw-body@^2.3.2: version "2.3.2" resolved "https://registry.yarnpkg.com/raw-body/-/raw-body-2.3.2.tgz#bcd60c77d3eb93cde0050295c3f379389bc88f89" dependencies: From 0960060fb1e954a21dc82544d8de32b7be4518ca Mon Sep 17 00:00:00 2001 From: Jamie Dwyer Date: Tue, 9 Jan 2018 17:34:46 -0500 Subject: [PATCH 25/62] v1.0.0-alpha.6 --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index f891528..744cb13 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@shopify/shopify-express", - "version": "1.0.0-alpha.5", + "version": "1.0.0-alpha.6", "author": "Shopify Inc.", "description": "Get up and running quickly with Express.js and the Shopify API.", "keywords": [ @@ -35,10 +35,10 @@ "express-session": "^1.15.3", "knex": "^0.13.0", "node-fetch": "^1.7.3", + "raw-body": "^2.3.2", "redis": "^2.7.1", "sqlite3": "^3.1.9", - "url": "^0.11.0", - "raw-body": "^2.3.2" + "url": "^0.11.0" }, "devDependencies": { "husky": "^0.14.3", From a2a83223648e072d02663ed17921292c46943e5c Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Fri, 12 Jan 2018 19:28:20 -0500 Subject: [PATCH 26/62] =?UTF-8?q?=E2=9E=95=20add=20findFreePort=20and=20?= =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20fix=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- circle.yml | 9 +++++++++ package.json | 4 +++- routes/tests/shopifyApiProxy.test.js | 8 +++++++- routes/tests/shopifyAuth.test.js | 8 +++++++- yarn.lock | 4 ++++ 5 files changed, 30 insertions(+), 3 deletions(-) create mode 100644 circle.yml diff --git a/circle.yml b/circle.yml new file mode 100644 index 0000000..ca8e08e --- /dev/null +++ b/circle.yml @@ -0,0 +1,9 @@ +machine: + node: + version: 8.1 +dependencies: + override: + - yarn install --dev +test: + override: + - yarn run test:ci diff --git a/package.json b/package.json index 744cb13..4176528 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,8 @@ "prettier:check": "prettier-check --single-quote --trailing-comma=all {middleware,routes,shopStore}{/*,/**/*}.{js}", "prettier:fix": "prettier --single-quote --trailing-comma=all --write {middleware,routes,shopStore}{/*,/**/*}.{js}", "precommit": "lint-staged", - "test": "jest" + "test": "jest", + "test:ci": "jest --ci" }, "lint-staged": { "*.{js}": [ @@ -41,6 +42,7 @@ "url": "^0.11.0" }, "devDependencies": { + "find-free-port": "^1.1.0", "husky": "^0.14.3", "jest": "^21.2.1", "lint-staged": "^5.0.0", diff --git a/routes/tests/shopifyApiProxy.test.js b/routes/tests/shopifyApiProxy.test.js index f209b37..6f2bce7 100644 --- a/routes/tests/shopifyApiProxy.test.js +++ b/routes/tests/shopifyApiProxy.test.js @@ -1,3 +1,4 @@ +const findFreePort = require('find-free-port') const express = require('express'); const http = require('http'); const fetch = require.requireActual('node-fetch'); @@ -97,6 +98,11 @@ function createServer() { server = http.createServer(app); return new Promise((resolve, reject) => { - server.listen(3000, resolve(server)); + findFreePort(PORT, (err, freePort) => { + if (err) { + throw err; + } + server.listen(PORT, resolve(server)); + }) }); } diff --git a/routes/tests/shopifyAuth.test.js b/routes/tests/shopifyAuth.test.js index 06b368f..c6c7b8b 100644 --- a/routes/tests/shopifyAuth.test.js +++ b/routes/tests/shopifyAuth.test.js @@ -1,3 +1,4 @@ +const findFreePort = require('find-free-port') const fetch = require('node-fetch'); const http = require('http'); const express = require('express'); @@ -57,6 +58,11 @@ function createServer(afterAuth) { server = http.createServer(app); return new Promise((resolve, reject) => { - server.listen(PORT, resolve(server)); + findFreePort(PORT, (err, freePort) => { + if (err) { + throw err; + } + server.listen(PORT, resolve(server)); + }) }); } diff --git a/yarn.lock b/yarn.lock index 82fc232..11d9a04 100644 --- a/yarn.lock +++ b/yarn.lock @@ -970,6 +970,10 @@ finalhandler@1.1.0: statuses "~1.3.1" unpipe "~1.0.0" +find-free-port@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/find-free-port/-/find-free-port-1.1.0.tgz#abdfe8f64ffa1d868b183af6ea0728caaf4d935c" + find-parent-dir@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/find-parent-dir/-/find-parent-dir-0.3.0.tgz#33c44b429ab2b2f0646299c5f9f718f376ff8d54" From 320d9d016202fd9ede7a5363561593f9be0b92ad Mon Sep 17 00:00:00 2001 From: Marek Date: Mon, 29 Jan 2018 23:52:32 -0500 Subject: [PATCH 27/62] Fix missing fetch require --- routes/shopifyAuth.js | 1 + 1 file changed, 1 insertion(+) diff --git a/routes/shopifyAuth.js b/routes/shopifyAuth.js index fada197..0755c48 100644 --- a/routes/shopifyAuth.js +++ b/routes/shopifyAuth.js @@ -1,6 +1,7 @@ const express = require('express'); const querystring = require('querystring'); const crypto = require('crypto'); +const fetch = require('node-fetch'); module.exports = function createShopifyAuthRouter({ host, From 4c60a5bc6ea8d18e893e767b744eaab2473005d0 Mon Sep 17 00:00:00 2001 From: Tom Southall Date: Sun, 25 Feb 2018 20:16:36 -0500 Subject: [PATCH 28/62] Make correction to database field name in README The README describes the structure of the database table required to store shops and access tokens. One column shop_domain does not match the name in the code. Corrected to be shopify_domain to match code. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 15b9778..d24a486 100644 --- a/README.md +++ b/README.md @@ -113,7 +113,7 @@ const shopify = shopifyExpress({ }); ``` -SQLStrategy expects a table named `shops` with a primary key `id`, and `string` fields for `shop_domain` and `access_token`. It's recommended you index `shop_domain` since it is used to look up tokens. +SQLStrategy expects a table named `shops` with a primary key `id`, and `string` fields for `shopify_domain` and `access_token`. It's recommended you index `shopify_domain` since it is used to look up tokens. If you do not have a table already created for your store, you can generate one with `new SQLStrategy(myConfig).initialize()`. This returns a promise so you can finish setting up your app after it if you like, but we suggest you make a separate db initialization script, or keep track of your schema yourself. From 93963abdc71ea6c604decea88afa2d5589d1999a Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Thu, 1 Mar 2018 11:34:51 -0500 Subject: [PATCH 29/62] =?UTF-8?q?=E2=9E=95=E2=9C=85=20add=20prop-types=20f?= =?UTF-8?q?or=20validating=20config=20objects?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.js | 12 + package.json | 1 + tests/ShopifyConfig.test.js | 41 ++ .../__snapshots__/ShopifyConfig.test.js.snap | 44 ++ yarn.lock | 437 +++++++++++------- 5 files changed, 356 insertions(+), 179 deletions(-) create mode 100644 tests/ShopifyConfig.test.js create mode 100644 tests/__snapshots__/ShopifyConfig.test.js.snap diff --git a/index.js b/index.js index 42655d1..009e68c 100644 --- a/index.js +++ b/index.js @@ -1,8 +1,20 @@ +const PropTypes = require('prop-types'); const createRouter = require('./routes'); const createMiddleware = require('./middleware'); const {MemoryStrategy} = require('./strategies'); +const ShopifyConfigTypes = { + apiKey: PropTypes.string.isRequired, + host: PropTypes.string.isRequired, + secret: PropTypes.string.isRequired, + scope: PropTypes.arrayOf(PropTypes.string).isRequired, + afterAuth: PropTypes.func.isRequired, + shopStore: PropTypes.Object, +}; + module.exports = function shopify(shopifyConfig) { + PropTypes.checkPropTypes(ShopifyConfigTypes, shopifyConfig, 'option', 'ShopifyExpress'); + const config = Object.assign( {shopStore: new MemoryStrategy()}, shopifyConfig, diff --git a/package.json b/package.json index 4176528..cbd8238 100644 --- a/package.json +++ b/package.json @@ -36,6 +36,7 @@ "express-session": "^1.15.3", "knex": "^0.13.0", "node-fetch": "^1.7.3", + "prop-types": "^15.6.1", "raw-body": "^2.3.2", "redis": "^2.7.1", "sqlite3": "^3.1.9", diff --git a/tests/ShopifyConfig.test.js b/tests/ShopifyConfig.test.js new file mode 100644 index 0000000..c7113ec --- /dev/null +++ b/tests/ShopifyConfig.test.js @@ -0,0 +1,41 @@ +const shopifyExpress = require('../index'); + +describe('ShopifyConfig', async () => { + const originalConsoleError = console.error; + beforeEach(() => { + console.error = jest.fn(); + }); + + afterEach(() => { + console.error = originalConsoleError; + }); + + it('logs errors when given empty object', () => { + shopifyExpress({}); + expect(console.error).toBeCalled(); + expect(console.error.mock.calls).toMatchSnapshot(); + }); + + it('logs errors when given bad props', () => { + shopifyExpress({apiKey: 32, + host: { notGood: true }, + secret: true, + scope: 'orders', + afterAuth: true, + }); + expect(console.error).toBeCalled(); + expect(console.error.mock.calls).toMatchSnapshot(); + }); + + + it('does not log errors when given valid proptypes', () => { + shopifyExpress({ + apiKey: 'fake', + host: 'fake', + secret: 'cats', + scope: [], + afterAuth: () => null, + }); + expect(console.error).not.toBeCalled(); + }); +}); diff --git a/tests/__snapshots__/ShopifyConfig.test.js.snap b/tests/__snapshots__/ShopifyConfig.test.js.snap new file mode 100644 index 0000000..7c52da6 --- /dev/null +++ b/tests/__snapshots__/ShopifyConfig.test.js.snap @@ -0,0 +1,44 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ShopifyConfig logs errors when given bad props 1`] = ` +Array [ + Array [ + "Warning: Failed option type: Invalid option \`apiKey\` of type \`number\` supplied to \`ShopifyExpress\`, expected \`string\`.", + ], + Array [ + "Warning: Failed option type: Invalid option \`host\` of type \`object\` supplied to \`ShopifyExpress\`, expected \`string\`.", + ], + Array [ + "Warning: Failed option type: Invalid option \`secret\` of type \`boolean\` supplied to \`ShopifyExpress\`, expected \`string\`.", + ], + Array [ + "Warning: Failed option type: Invalid option \`scope\` of type \`string\` supplied to \`ShopifyExpress\`, expected an array.", + ], + Array [ + "Warning: Failed option type: Invalid option \`afterAuth\` of type \`boolean\` supplied to \`ShopifyExpress\`, expected \`function\`.", + ], +] +`; + +exports[`ShopifyConfig logs errors when given empty object 1`] = ` +Array [ + Array [ + "Warning: Failed option type: The option \`apiKey\` is marked as required in \`ShopifyExpress\`, but its value is \`undefined\`.", + ], + Array [ + "Warning: Failed option type: The option \`host\` is marked as required in \`ShopifyExpress\`, but its value is \`undefined\`.", + ], + Array [ + "Warning: Failed option type: The option \`secret\` is marked as required in \`ShopifyExpress\`, but its value is \`undefined\`.", + ], + Array [ + "Warning: Failed option type: The option \`scope\` is marked as required in \`ShopifyExpress\`, but its value is \`undefined\`.", + ], + Array [ + "Warning: Failed option type: The option \`afterAuth\` is marked as required in \`ShopifyExpress\`, but its value is \`undefined\`.", + ], + Array [ + "Warning: Failed option type: ShopifyExpress: option type \`shopStore\` is invalid; it must be a function, usually from the \`prop-types\` package, but received \`undefined\`.", + ], +] +`; diff --git a/yarn.lock b/yarn.lock index 11d9a04..67f9b80 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11,10 +11,10 @@ abbrev@1: resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.1.1.tgz#f8f2c887ad10bf67f634f005b6987fed3179aac8" accepts@~1.3.4: - version "1.3.4" - resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.4.tgz#86246758c7dd6d21a6474ff084a4740ec05eb21f" + version "1.3.5" + resolved "https://registry.yarnpkg.com/accepts/-/accepts-1.3.5.tgz#eb777df6011723a3b14e8a72c0805c8e86746bd2" dependencies: - mime-types "~2.1.16" + mime-types "~2.1.18" negotiator "0.6.1" acorn-globals@^3.1.0: @@ -35,8 +35,8 @@ ajv@^4.9.1: json-stable-stringify "^1.0.1" ajv@^5.1.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.0.tgz#eb2840746e9dc48bd5e063a36e3fd400c5eab5a9" + version "5.5.2" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-5.5.2.tgz#73b5eeca3fab653e3d3f9422b341ad42205dc965" dependencies: co "^4.6.0" fast-deep-equal "^1.0.0" @@ -75,7 +75,7 @@ ansi-styles@^2.2.1: version "2.2.1" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-2.2.1.tgz#b432dd3358b634cf75e1e4664368240533c1ddbe" -ansi-styles@^3.1.0, ansi-styles@^3.2.0: +ansi-styles@^3.2.0: version "3.2.0" resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-3.2.0.tgz#c159b8d5be0f9e5a6f346dab94f16ce022161b88" dependencies: @@ -114,8 +114,8 @@ are-we-there-yet@~1.1.2: readable-stream "^2.0.6" argparse@^1.0.7: - version "1.0.9" - resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.9.tgz#73d83bc263f86e97f8cc4f6bae1b0e90a7d22c86" + version "1.0.10" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-1.0.10.tgz#bcd6791ea5ae09725e17e5ad988134cd40b3d911" dependencies: sprintf-js "~1.0.2" @@ -145,6 +145,10 @@ arrify@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/arrify/-/arrify-1.0.1.tgz#898508da2226f380df904728456849c1501a4b0d" +asap@~2.0.3: + version "2.0.6" + resolved "https://registry.yarnpkg.com/asap/-/asap-2.0.6.tgz#e50347611d7e690943208bbdafebcbc2fb866d46" + asn1@~0.2.3: version "0.2.3" resolved "https://registry.yarnpkg.com/asn1/-/asn1-0.2.3.tgz#dac8787713c9966849fc8180777ebe9c1ddf3b86" @@ -220,8 +224,8 @@ babel-core@^6.0.0, babel-core@^6.26.0: source-map "^0.5.6" babel-generator@^6.18.0, babel-generator@^6.26.0: - version "6.26.0" - resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.0.tgz#ac1ae20070b79f6e3ca1d3269613053774f20dc5" + version "6.26.1" + resolved "https://registry.yarnpkg.com/babel-generator/-/babel-generator-6.26.1.tgz#1844408d3b8f0d35a404ea7ac180f087a601bd90" dependencies: babel-messages "^6.23.0" babel-runtime "^6.26.0" @@ -229,7 +233,7 @@ babel-generator@^6.18.0, babel-generator@^6.26.0: detect-indent "^4.0.0" jsesc "^1.3.0" lodash "^4.17.4" - source-map "^0.5.6" + source-map "^0.5.7" trim-right "^1.0.1" babel-helpers@^6.24.1: @@ -385,8 +389,8 @@ boom@5.x.x: hoek "4.x.x" brace-expansion@^1.1.7: - version "1.1.8" - resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.8.tgz#c07b211c7c952ec1f8efd51a77ef0d1d3990a292" + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" dependencies: balanced-match "^1.0.0" concat-map "0.0.1" @@ -453,12 +457,12 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: supports-color "^2.0.0" chalk@^2.0.1, chalk@^2.1.0: - version "2.3.0" - resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.0.tgz#b5ea48efc9c1793dccc9b4767c93914d3f2d52ba" + version "2.3.1" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.3.1.tgz#523fe2678aec7b04e8041909292fe8b17059b796" dependencies: - ansi-styles "^3.1.0" + ansi-styles "^3.2.0" escape-string-regexp "^1.0.5" - supports-color "^4.0.0" + supports-color "^5.2.0" ci-info@^1.0.0: version "1.1.2" @@ -515,23 +519,23 @@ color-name@^1.1.1: version "1.1.3" resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.3.tgz#a7d0558bd89c42f795dd42328f740831ca53bc25" -combined-stream@^1.0.5, combined-stream@~1.0.5: - version "1.0.5" - resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.5.tgz#938370a57b4a51dea2c77c15d5c5fdf895164009" +combined-stream@1.0.6, combined-stream@^1.0.5, combined-stream@~1.0.5: + version "1.0.6" + resolved "https://registry.yarnpkg.com/combined-stream/-/combined-stream-1.0.6.tgz#723e7df6e801ac5613113a7e445a9b69cb632818" dependencies: delayed-stream "~1.0.0" commander@^2.11.0, commander@^2.2.0, commander@^2.9.0: - version "2.12.1" - resolved "https://registry.yarnpkg.com/commander/-/commander-2.12.1.tgz#468635c4168d06145b9323356d1da84d14ac4a7a" + version "2.14.1" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.14.1.tgz#2235123e37af8ca3c65df45b026dbd357b01b9aa" concat-map@0.0.1: version "0.0.1" resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" connect-redis@^3.3.0: - version "3.3.2" - resolved "https://registry.yarnpkg.com/connect-redis/-/connect-redis-3.3.2.tgz#3706f9bfef1ec9b5d11c4b35b265de42c218b408" + version "3.3.3" + resolved "https://registry.yarnpkg.com/connect-redis/-/connect-redis-3.3.3.tgz#0fb8f370192f62da75ec7a9507807599fbe15b37" dependencies: debug "^3.1.0" redis "^2.1.0" @@ -564,9 +568,13 @@ cookie@0.3.1: version "0.3.1" resolved "https://registry.yarnpkg.com/cookie/-/cookie-0.3.1.tgz#e7e0a1f9ef43b4c8ba925c5c5a96e806d16873bb" +core-js@^1.0.0: + version "1.2.7" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-1.2.7.tgz#652294c14651db28fa93bd2d5ff2983a4f08c636" + core-js@^2.4.0, core-js@^2.5.0: - version "2.5.1" - resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.1.tgz#ae6874dc66937789b80754ff5428df66819ca50b" + version "2.5.3" + resolved "https://registry.yarnpkg.com/core-js/-/core-js-2.5.3.tgz#8acc38345824f16d8365b7c9b4259168e8ed603e" core-util-is@1.0.2, core-util-is@~1.0.0: version "1.0.2" @@ -667,10 +675,14 @@ delegates@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/delegates/-/delegates-1.0.0.tgz#84c6e159b81904fdca59a0ef44cd870d31250f9a" -depd@1.1.1, depd@~1.1.1: +depd@1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.1.tgz#5783b4e1c459f06fa5ca27f991f3d06e7a310359" +depd@~1.1.1: + version "1.1.2" + resolved "https://registry.yarnpkg.com/depd/-/depd-1.1.2.tgz#9bcd52e14c097763e749b274c4346ed2e560b5a9" + destroy@~1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.0.4.tgz#978857442c44749e4206613e37946205826abd80" @@ -714,8 +726,8 @@ elegant-spinner@^1.0.1: resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" encodeurl@~1.0.1: - version "1.0.1" - resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.1.tgz#79e3d58655346909fe6f0f45a5de68103b294d20" + version "1.0.2" + resolved "https://registry.yarnpkg.com/encodeurl/-/encodeurl-1.0.2.tgz#ad3ff4c86ec2d029322f5a02c3a9a606c95b3f59" encoding@^0.1.11: version "0.1.12" @@ -723,11 +735,11 @@ encoding@^0.1.11: dependencies: iconv-lite "~0.4.13" -errno@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.4.tgz#b896e23a9e5e8ba33871fc996abd3635fc9a1c7d" +errno@~0.1.7: + version "0.1.7" + resolved "https://registry.yarnpkg.com/errno/-/errno-0.1.7.tgz#4684d71779ad39af177e3f007996f7c67c852618" dependencies: - prr "~0.0.0" + prr "~1.0.1" error-ex@^1.2.0, error-ex@^1.3.1: version "1.3.1" @@ -744,15 +756,15 @@ escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" escodegen@^1.6.1: - version "1.9.0" - resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.0.tgz#9811a2f265dc1cd3894420ee3717064b632b8852" + version "1.9.1" + resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-1.9.1.tgz#dbae17ef96c8e4bedb1356f4504fa4cc2f7cb7e2" dependencies: esprima "^3.1.3" estraverse "^4.2.0" esutils "^2.0.2" optionator "^0.8.1" optionalDependencies: - source-map "~0.5.6" + source-map "~0.6.1" esprima@^3.1.3: version "3.1.3" @@ -908,13 +920,17 @@ extglob@^0.3.1: dependencies: is-extglob "^1.0.0" -extsprintf@1.3.0, extsprintf@^1.2.0: +extsprintf@1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.3.0.tgz#96918440e3041a7a414f8c52e3c574eb3c3e1e05" +extsprintf@^1.2.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" + fast-deep-equal@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.0.0.tgz#96256a3bc975595eb36d82e9929d060d893439ff" + version "1.1.0" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-1.1.0.tgz#c053477817c86b51daa853c81e059b733d023614" fast-json-stable-stringify@^2.0.0: version "2.0.0" @@ -930,6 +946,18 @@ fb-watchman@^2.0.0: dependencies: bser "^2.0.0" +fbjs@^0.8.16: + version "0.8.16" + resolved "https://registry.yarnpkg.com/fbjs/-/fbjs-0.8.16.tgz#5e67432f550dc41b572bf55847b8aca64e5337db" + dependencies: + core-js "^1.0.0" + isomorphic-fetch "^2.1.1" + loose-envify "^1.0.0" + object-assign "^4.1.0" + promise "^7.1.1" + setimmediate "^1.0.5" + ua-parser-js "^0.7.9" + figures@^1.7.0: version "1.7.0" resolved "https://registry.yarnpkg.com/figures/-/figures-1.7.0.tgz#cbe1e3affcf1cd44b80cadfed28dc793a9701d2e" @@ -971,8 +999,8 @@ finalhandler@1.1.0: unpipe "~1.0.0" find-free-port@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/find-free-port/-/find-free-port-1.1.0.tgz#abdfe8f64ffa1d868b183af6ea0728caaf4d935c" + version "1.2.0" + resolved "https://registry.yarnpkg.com/find-free-port/-/find-free-port-1.2.0.tgz#fba7fb3b5b933f1a276d10695584e3e445468636" find-parent-dir@^0.3.0: version "0.3.0" @@ -1027,11 +1055,11 @@ form-data@~2.1.1: mime-types "^2.1.12" form-data@~2.3.1: - version "2.3.1" - resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.1.tgz#6fb94fbd71885306d73d15cc497fe4cc4ecd44bf" + version "2.3.2" + resolved "https://registry.yarnpkg.com/form-data/-/form-data-2.3.2.tgz#4970498be604c20c005d4f5c23aecd21d6b49099" dependencies: asynckit "^0.4.0" - combined-stream "^1.0.5" + combined-stream "1.0.6" mime-types "^2.1.12" forwarded@~0.1.2: @@ -1203,9 +1231,9 @@ has-flag@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa" -has-flag@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-2.0.0.tgz#e8207af1cc7b30d446cc70b734b5e8be18f88d51" +has-flag@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-3.0.0.tgz#b5d454dc2199ae225699f3467e5a07f3b955bafd" has-unicode@^2.0.0: version "2.0.1" @@ -1234,8 +1262,8 @@ hoek@2.x.x: resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed" hoek@4.x.x: - version "4.2.0" - resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.0.tgz#72d9d0754f7fe25ca2d01ad8f8f9a9449a89526d" + version "4.2.1" + resolved "https://registry.yarnpkg.com/hoek/-/hoek-4.2.1.tgz#9634502aa12c445dd5a7c5734b572bb8738aacbb" home-or-tmp@^2.0.0: version "2.0.0" @@ -1331,8 +1359,8 @@ interpret@^0.6.5: resolved "https://registry.yarnpkg.com/interpret/-/interpret-0.6.6.tgz#fecd7a18e7ce5ca6abfb953e1f86213a49f1625b" invariant@^2.2.2: - version "2.2.2" - resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.2.tgz#9e1f56ac0acdb6bf303306f338be3b204ae60360" + version "2.2.3" + resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.3.tgz#1a827dfde7dcbd7c323f0ca826be8fa7c5e9d688" dependencies: loose-envify "^1.0.0" @@ -1340,9 +1368,9 @@ invert-kv@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/invert-kv/-/invert-kv-1.0.0.tgz#104a8e4aaca6d3d8cd157a8ef8bfab2d7a3ffdb6" -ipaddr.js@1.5.2: - version "1.5.2" - resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.5.2.tgz#d4b505bde9946987ccf0fc58d9010ff9607e3fa0" +ipaddr.js@1.6.0: + version "1.6.0" + resolved "https://registry.yarnpkg.com/ipaddr.js/-/ipaddr.js-1.6.0.tgz#e3fa357b773da619f26e95f049d055c72796f86b" is-arrayish@^0.2.1: version "0.2.1" @@ -1359,8 +1387,8 @@ is-builtin-module@^1.0.0: builtin-modules "^1.0.0" is-ci@^1.0.10: - version "1.0.10" - resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.0.10.tgz#f739336b2632365061a9d48270cd56ae3369318e" + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-ci/-/is-ci-1.1.0.tgz#247e4162e7860cebbdaf30b774d6b0ac7dcfe7a5" dependencies: ci-info "^1.0.0" @@ -1490,29 +1518,36 @@ isobject@^2.0.0: dependencies: isarray "1.0.0" +isomorphic-fetch@^2.1.1: + version "2.2.1" + resolved "https://registry.yarnpkg.com/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz#611ae1acf14f5e81f729507472819fe9733558a9" + dependencies: + node-fetch "^1.0.1" + whatwg-fetch ">=0.10.0" + isstream@~0.1.2: version "0.1.2" resolved "https://registry.yarnpkg.com/isstream/-/isstream-0.1.2.tgz#47e63f7af55afa6f92e1500e690eb8b8529c099a" istanbul-api@^1.1.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.2.1.tgz#0c60a0515eb11c7d65c6b50bba2c6e999acd8620" + version "1.2.2" + resolved "https://registry.yarnpkg.com/istanbul-api/-/istanbul-api-1.2.2.tgz#e17cd519dd5ec4141197f246fdf380b75487f3b1" dependencies: async "^2.1.4" fileset "^2.0.2" - istanbul-lib-coverage "^1.1.1" + istanbul-lib-coverage "^1.1.2" istanbul-lib-hook "^1.1.0" - istanbul-lib-instrument "^1.9.1" - istanbul-lib-report "^1.1.2" - istanbul-lib-source-maps "^1.2.2" - istanbul-reports "^1.1.3" + istanbul-lib-instrument "^1.9.2" + istanbul-lib-report "^1.1.3" + istanbul-lib-source-maps "^1.2.3" + istanbul-reports "^1.1.4" js-yaml "^3.7.0" mkdirp "^0.5.1" once "^1.4.0" -istanbul-lib-coverage@^1.0.1, istanbul-lib-coverage@^1.1.1: - version "1.1.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.1.tgz#73bfb998885299415c93d38a3e9adf784a77a9da" +istanbul-lib-coverage@^1.0.1, istanbul-lib-coverage@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-coverage/-/istanbul-lib-coverage-1.1.2.tgz#4113c8ff6b7a40a1ef7350b01016331f63afde14" istanbul-lib-hook@^1.1.0: version "1.1.0" @@ -1520,40 +1555,40 @@ istanbul-lib-hook@^1.1.0: dependencies: append-transform "^0.4.0" -istanbul-lib-instrument@^1.4.2, istanbul-lib-instrument@^1.7.5, istanbul-lib-instrument@^1.9.1: - version "1.9.1" - resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.9.1.tgz#250b30b3531e5d3251299fdd64b0b2c9db6b558e" +istanbul-lib-instrument@^1.4.2, istanbul-lib-instrument@^1.7.5, istanbul-lib-instrument@^1.9.2: + version "1.9.2" + resolved "https://registry.yarnpkg.com/istanbul-lib-instrument/-/istanbul-lib-instrument-1.9.2.tgz#84905bf47f7e0b401d6b840da7bad67086b4aab6" dependencies: babel-generator "^6.18.0" babel-template "^6.16.0" babel-traverse "^6.18.0" babel-types "^6.18.0" babylon "^6.18.0" - istanbul-lib-coverage "^1.1.1" + istanbul-lib-coverage "^1.1.2" semver "^5.3.0" -istanbul-lib-report@^1.1.2: - version "1.1.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.2.tgz#922be27c13b9511b979bd1587359f69798c1d425" +istanbul-lib-report@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-report/-/istanbul-lib-report-1.1.3.tgz#2df12188c0fa77990c0d2176d2d0ba3394188259" dependencies: - istanbul-lib-coverage "^1.1.1" + istanbul-lib-coverage "^1.1.2" mkdirp "^0.5.1" path-parse "^1.0.5" supports-color "^3.1.2" -istanbul-lib-source-maps@^1.1.0, istanbul-lib-source-maps@^1.2.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.2.tgz#750578602435f28a0c04ee6d7d9e0f2960e62c1c" +istanbul-lib-source-maps@^1.1.0, istanbul-lib-source-maps@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/istanbul-lib-source-maps/-/istanbul-lib-source-maps-1.2.3.tgz#20fb54b14e14b3fb6edb6aca3571fd2143db44e6" dependencies: debug "^3.1.0" - istanbul-lib-coverage "^1.1.1" + istanbul-lib-coverage "^1.1.2" mkdirp "^0.5.1" rimraf "^2.6.1" source-map "^0.5.3" -istanbul-reports@^1.1.3: - version "1.1.3" - resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.1.3.tgz#3b9e1e8defb6d18b1d425da8e8b32c5a163f2d10" +istanbul-reports@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/istanbul-reports/-/istanbul-reports-1.1.4.tgz#5ccba5e22b7b5a5d91d5e0a830f89be334bf97bd" dependencies: handlebars "^4.0.3" @@ -2025,8 +2060,8 @@ locate-path@^2.0.0: path-exists "^3.0.0" lodash@^4.14.0, lodash@^4.17.4, lodash@^4.6.0: - version "4.17.4" - resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.4.tgz#78203a4d1c328ae1d86dca6460e369b57f4055ae" + version "4.17.5" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" log-symbols@^1.0.2: version "1.0.2" @@ -2035,8 +2070,8 @@ log-symbols@^1.0.2: chalk "^1.0.0" log-symbols@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.1.0.tgz#f35fa60e278832b538dc4dddcbb478a45d3e3be6" + version "2.2.0" + resolved "https://registry.yarnpkg.com/log-symbols/-/log-symbols-2.2.0.tgz#5740e1c5d6f0dfda4ad9323b5332107ef6b4c40a" dependencies: chalk "^2.0.1" @@ -2051,7 +2086,7 @@ longest@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/longest/-/longest-1.0.1.tgz#30a0b2da38f73770e8294a0d22e6625ed77d0097" -loose-envify@^1.0.0: +loose-envify@^1.0.0, loose-envify@^1.3.1: version "1.3.1" resolved "https://registry.yarnpkg.com/loose-envify/-/loose-envify-1.3.1.tgz#d1a8ad33fa9ce0e713d65fdd0ac8b748d478c848" dependencies: @@ -2110,23 +2145,23 @@ micromatch@^2.1.5, micromatch@^2.3.11, micromatch@^2.3.7: parse-glob "^3.0.4" regex-cache "^0.4.2" -mime-db@~1.30.0: - version "1.30.0" - resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.30.0.tgz#74c643da2dd9d6a45399963465b26d5ca7d71f01" +mime-db@~1.33.0: + version "1.33.0" + resolved "https://registry.yarnpkg.com/mime-db/-/mime-db-1.33.0.tgz#a3492050a5cb9b63450541e39d9788d2272783db" -mime-types@^2.1.12, mime-types@~2.1.15, mime-types@~2.1.16, mime-types@~2.1.17, mime-types@~2.1.7: - version "2.1.17" - resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.17.tgz#09d7a393f03e995a79f8af857b70a9e0ab16557a" +mime-types@^2.1.12, mime-types@~2.1.17, mime-types@~2.1.18, mime-types@~2.1.7: + version "2.1.18" + resolved "https://registry.yarnpkg.com/mime-types/-/mime-types-2.1.18.tgz#6f323f60a83d11146f831ff11fd66e2fe5503bb8" dependencies: - mime-db "~1.30.0" + mime-db "~1.33.0" mime@1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/mime/-/mime-1.4.1.tgz#121f9ebc49e3766f311a76e1fa1c8003c4b03aa6" mimic-fn@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.1.0.tgz#e667783d92e89dbd342818b5230b9d62a672ad18" + version "1.2.0" + resolved "https://registry.yarnpkg.com/mimic-fn/-/mimic-fn-1.2.0.tgz#820c86a39334640e99516928bd03fca88057d022" minimatch@^3.0.0, minimatch@^3.0.2, minimatch@^3.0.3, minimatch@^3.0.4: version "3.0.4" @@ -2161,8 +2196,8 @@ ms@2.0.0: resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" nan@^2.3.0: - version "2.8.0" - resolved "https://registry.yarnpkg.com/nan/-/nan-2.8.0.tgz#ed715f3fe9de02b57a5e6252d90a96675e1f085a" + version "2.9.2" + resolved "https://registry.yarnpkg.com/nan/-/nan-2.9.2.tgz#f564d75f5f8f36a6d9456cca7a6c4fe488ab7866" nan@~2.7.0: version "2.7.0" @@ -2176,7 +2211,7 @@ negotiator@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/negotiator/-/negotiator-0.6.1.tgz#2b327184e8992101177b28563fb5e7102acd0ca9" -node-fetch@^1.7.3: +node-fetch@^1.0.1, node-fetch@^1.7.3: version "1.7.3" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-1.7.3.tgz#980f6f72d85211a5347c6b2bc18c5b84c3eb47ef" dependencies: @@ -2188,13 +2223,13 @@ node-int64@^0.4.0: resolved "https://registry.yarnpkg.com/node-int64/-/node-int64-0.4.0.tgz#87a9065cdb355d3182d8f94ce11188b825c68a3b" node-notifier@^5.0.2: - version "5.1.2" - resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.1.2.tgz#2fa9e12605fa10009d44549d6fcd8a63dde0e4ff" + version "5.2.1" + resolved "https://registry.yarnpkg.com/node-notifier/-/node-notifier-5.2.1.tgz#fa313dd08f5517db0e2502e5758d664ac69f9dea" dependencies: growly "^1.3.0" - semver "^5.3.0" - shellwords "^0.1.0" - which "^1.2.12" + semver "^5.4.1" + shellwords "^0.1.1" + which "^1.3.0" node-pre-gyp@^0.6.39, node-pre-gyp@~0.6.38: version "0.6.39" @@ -2239,8 +2274,8 @@ normalize-path@^2.0.0, normalize-path@^2.0.1: remove-trailing-separator "^1.0.1" npm-path@^2.0.2: - version "2.0.3" - resolved "https://registry.yarnpkg.com/npm-path/-/npm-path-2.0.3.tgz#15cff4e1c89a38da77f56f6055b24f975dfb2bbe" + version "2.0.4" + resolved "https://registry.yarnpkg.com/npm-path/-/npm-path-2.0.4.tgz#c641347a5ff9d6a09e4d9bce5580c4f505278e64" dependencies: which "^1.2.10" @@ -2279,7 +2314,7 @@ oauth-sign@~0.8.1, oauth-sign@~0.8.2: version "0.8.2" resolved "https://registry.yarnpkg.com/oauth-sign/-/oauth-sign-0.8.2.tgz#46a6ab7f0aead8deae9ec0565780b7d4efeb9d43" -object-assign@^4.0.1, object-assign@^4.1.0: +object-assign@^4.0.1, object-assign@^4.1.0, object-assign@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863" @@ -2354,8 +2389,8 @@ os-tmpdir@^1.0.0, os-tmpdir@^1.0.1: resolved "https://registry.yarnpkg.com/os-tmpdir/-/os-tmpdir-1.0.2.tgz#bbe67406c79aa85c5cfec766fe5734555dfa1274" osenv@^0.1.4: - version "0.1.4" - resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.4.tgz#42fe6d5953df06c8064be6f176c3d05aaaa34644" + version "0.1.5" + resolved "https://registry.yarnpkg.com/osenv/-/osenv-0.1.5.tgz#85cdfafaeb28e8677f416e287592b5f3f49ea410" dependencies: os-homedir "^1.0.0" os-tmpdir "^1.0.0" @@ -2369,8 +2404,10 @@ p-finally@^1.0.0: resolved "https://registry.yarnpkg.com/p-finally/-/p-finally-1.0.0.tgz#3fbcfb15b899a44123b34b6dcc18b724336a2cae" p-limit@^1.1.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.1.0.tgz#b07ff2d9a5d88bec806035895a2bab66a27988bc" + version "1.2.0" + resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-1.2.0.tgz#0e92b6bedcb59f022c13d0f1949dc82d15909f1c" + dependencies: + p-try "^1.0.0" p-locate@^2.0.0: version "2.0.0" @@ -2382,6 +2419,10 @@ p-map@^1.1.1: version "1.2.0" resolved "https://registry.yarnpkg.com/p-map/-/p-map-1.2.0.tgz#e4e94f311eabbc8633a1e79908165fca26241b6b" +p-try@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/p-try/-/p-try-1.0.0.tgz#cbc79cdbaf8fd4228e13f621f2b1a237c1b207b3" + parse-glob@^3.0.4: version "3.0.4" resolved "https://registry.yarnpkg.com/parse-glob/-/parse-glob-3.0.4.tgz#b2c376cfb11f35513badd173ef0bb6e3a388391c" @@ -2504,8 +2545,8 @@ prettier-check@^2.0.0: execa "^0.6.0" prettier@^1.8.2: - version "1.8.2" - resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.8.2.tgz#bff83e7fd573933c607875e5ba3abbdffb96aeb8" + version "1.11.1" + resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.11.1.tgz#61e43fc4cd44e68f2b0dfc2c38cd4bb0fccdcc75" pretty-format@^21.2.1: version "21.2.1" @@ -2518,20 +2559,34 @@ private@^0.1.7: version "0.1.8" resolved "https://registry.yarnpkg.com/private/-/private-0.1.8.tgz#2381edb3689f7a53d653190060fcf822d2f368ff" -process-nextick-args@~1.0.6: - version "1.0.7" - resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-1.0.7.tgz#150e20b756590ad3f91093f25a4f2ad8bff30ba3" +process-nextick-args@~2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.0.tgz#a37d732f4271b4ab1ad070d35508e8290788ffaa" + +promise@^7.1.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/promise/-/promise-7.3.1.tgz#064b72602b18f90f29192b8b1bc418ffd1ebd3bf" + dependencies: + asap "~2.0.3" + +prop-types@^15.6.1: + version "15.6.1" + resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.6.1.tgz#36644453564255ddda391191fb3a125cbdf654ca" + dependencies: + fbjs "^0.8.16" + loose-envify "^1.3.1" + object-assign "^4.1.1" proxy-addr@~2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.2.tgz#6571504f47bb988ec8180253f85dd7e14952bdec" + version "2.0.3" + resolved "https://registry.yarnpkg.com/proxy-addr/-/proxy-addr-2.0.3.tgz#355f262505a621646b3130a728eb647e22055341" dependencies: forwarded "~0.1.2" - ipaddr.js "1.5.2" + ipaddr.js "1.6.0" -prr@~0.0.0: - version "0.0.0" - resolved "https://registry.yarnpkg.com/prr/-/prr-0.0.0.tgz#1a84b85908325501411853d0081ee3fa86e2926a" +prr@~1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/prr/-/prr-1.0.1.tgz#d3fc114ba06995a45ec6893f484ceb1d78f5f476" pseudomap@^1.0.2: version "1.0.2" @@ -2582,8 +2637,8 @@ raw-body@2.3.2, raw-body@^2.3.2: unpipe "1.0.0" rc@^1.1.7: - version "1.2.2" - resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.2.tgz#d8ce9cb57e8d64d9c7badd9876c7c34cbe3c7077" + version "1.2.5" + resolved "https://registry.yarnpkg.com/rc/-/rc-1.2.5.tgz#275cd687f6e3b36cc756baa26dfee80a790301fd" dependencies: deep-extend "~0.4.0" ini "~1.3.0" @@ -2630,13 +2685,13 @@ readable-stream@^1.1.12: string_decoder "~0.10.x" readable-stream@^2.0.6, readable-stream@^2.1.4: - version "2.3.3" - resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.3.tgz#368f2512d79f9d46fdfc71349ae7878bbc1eb95c" + version "2.3.4" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.4.tgz#c946c3f47fa7d8eabc0b6150f4a12f69a4574071" dependencies: core-util-is "~1.0.0" inherits "~2.0.3" isarray "~1.0.0" - process-nextick-args "~1.0.6" + process-nextick-args "~2.0.0" safe-buffer "~5.1.1" string_decoder "~1.0.3" util-deprecate "~1.0.1" @@ -2648,8 +2703,8 @@ rechoir@^0.6.2: resolve "^1.1.6" redis-commands@^1.2.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.3.1.tgz#81d826f45fa9c8b2011f4cd7a0fe597d241d442b" + version "1.3.5" + resolved "https://registry.yarnpkg.com/redis-commands/-/redis-commands-1.3.5.tgz#4495889414f1e886261180b1442e7295602d83a2" redis-parser@^2.6.0: version "2.6.0" @@ -2664,8 +2719,8 @@ redis@^2.1.0, redis@^2.7.1: redis-parser "^2.6.0" regenerator-runtime@^0.11.0: - version "0.11.0" - resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.0.tgz#7e54fe5b5ccd5d6624ea6255c3473be090b802e1" + version "0.11.1" + resolved "https://registry.yarnpkg.com/regenerator-runtime/-/regenerator-runtime-0.11.1.tgz#be05ad7f9bf7d22e056f9726cee5017fbf19e2e9" regex-cache@^0.4.2: version "0.4.4" @@ -2794,18 +2849,18 @@ rimraf@2, rimraf@^2.5.1, rimraf@^2.6.1: glob "^7.0.5" rxjs@^5.4.2: - version "5.5.2" - resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.2.tgz#28d403f0071121967f18ad665563255d54236ac3" + version "5.5.6" + resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.6.tgz#e31fb96d6fd2ff1fd84bcea8ae9c02d007179c02" dependencies: - symbol-observable "^1.0.1" + symbol-observable "1.0.1" safe-buffer@5.1.1, safe-buffer@^5.0.1, safe-buffer@^5.1.1, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.1.tgz#893312af69b2123def71f57889001671eeb2c853" sane@^2.0.0: - version "2.2.0" - resolved "https://registry.yarnpkg.com/sane/-/sane-2.2.0.tgz#d6d2e2fcab00e3d283c93b912b7c3a20846f1d56" + version "2.4.1" + resolved "https://registry.yarnpkg.com/sane/-/sane-2.4.1.tgz#29f991208cf28636720efdc584293e7fd66663a5" dependencies: anymatch "^1.3.0" exec-sh "^0.2.0" @@ -2821,9 +2876,9 @@ sax@^1.2.1: version "1.2.4" resolved "https://registry.yarnpkg.com/sax/-/sax-1.2.4.tgz#2816234e2378bddc4e5354fab5caa895df7100d9" -"semver@2 || 3 || 4 || 5", semver@^5.3.0: - version "5.4.1" - resolved "https://registry.yarnpkg.com/semver/-/semver-5.4.1.tgz#e059c09d8571f0540823733433505d3a2f00b18e" +"semver@2 || 3 || 4 || 5", semver@^5.3.0, semver@^5.4.1: + version "5.5.0" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.0.tgz#dc4bbc7a6ca9d916dee5d43516f0092b58f7b8ab" send@0.16.1: version "0.16.1" @@ -2856,6 +2911,10 @@ set-blocking@^2.0.0, set-blocking@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7" +setimmediate@^1.0.5: + version "1.0.5" + resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285" + setprototypeof@1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/setprototypeof/-/setprototypeof-1.0.3.tgz#66567e37043eeb4f04d91bd658c0cbefb55b8e04" @@ -2874,7 +2933,7 @@ shebang-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-1.0.0.tgz#da42f49740c0b42db2ca9728571cb190c98efea3" -shellwords@^0.1.0: +shellwords@^0.1.1: version "0.1.1" resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b" @@ -2914,23 +2973,35 @@ source-map@^0.4.4: dependencies: amdefine ">=0.0.4" -source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.6: +source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" -spdx-correct@~1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-1.0.2.tgz#4b3073d933ff51f3912f03ac5519498a4150db40" +source-map@~0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + +spdx-correct@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-correct/-/spdx-correct-3.0.0.tgz#05a5b4d7153a195bc92c3c425b69f3b2a9524c82" dependencies: - spdx-license-ids "^1.0.2" + spdx-expression-parse "^3.0.0" + spdx-license-ids "^3.0.0" -spdx-expression-parse@~1.0.0: - version "1.0.4" - resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-1.0.4.tgz#9bdf2f20e1f40ed447fbe273266191fced51626c" +spdx-exceptions@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/spdx-exceptions/-/spdx-exceptions-2.1.0.tgz#2c7ae61056c714a5b9b9b2b2af7d311ef5c78fe9" -spdx-license-ids@^1.0.2: - version "1.2.2" - resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-1.2.2.tgz#c9df7a3424594ade6bd11900d596696dc06bac57" +spdx-expression-parse@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-expression-parse/-/spdx-expression-parse-3.0.0.tgz#99e119b7a5da00e05491c9fa338b7904823b41d0" + dependencies: + spdx-exceptions "^2.1.0" + spdx-license-ids "^3.0.0" + +spdx-license-ids@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/spdx-license-ids/-/spdx-license-ids-3.0.0.tgz#7a7cd28470cc6d3a1cfe6d66886f6bc430d3ac87" sprintf-js@~1.0.2: version "1.0.3" @@ -3008,8 +3079,8 @@ string_decoder@~1.0.3: safe-buffer "~5.1.0" stringify-object@^3.2.0: - version "3.2.1" - resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.1.tgz#2720c2eff940854c819f6ee252aaeb581f30624d" + version "3.2.2" + resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.2.2.tgz#9853052e5a88fb605a44cd27445aa257ad7ffbcd" dependencies: get-own-enumerable-property-symbols "^2.0.1" is-obj "^1.0.1" @@ -3063,20 +3134,20 @@ supports-color@^3.1.2: dependencies: has-flag "^1.0.0" -supports-color@^4.0.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-4.5.0.tgz#be7a0de484dec5c5cddf8b3d59125044912f635b" +supports-color@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-5.2.0.tgz#b0d5333b1184dd3666cbe5aa0b45c5ac7ac17a4a" dependencies: - has-flag "^2.0.0" + has-flag "^3.0.0" + +symbol-observable@1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4" symbol-observable@^0.2.2: version "0.2.4" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40" -symbol-observable@^1.0.1: - version "1.0.4" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.4.tgz#29bf615d4aa7121bdd898b22d4b3f9bc4e2aa03d" - symbol-tree@^3.2.1: version "3.2.2" resolved "https://registry.yarnpkg.com/symbol-tree/-/symbol-tree-3.2.2.tgz#ae27db38f660a7ae2e1c3b7d1bc290819b8519e6" @@ -3103,8 +3174,8 @@ tar@^2.2.1: inherits "2" test-exclude@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.1.1.tgz#4d84964b0966b0087ecc334a2ce002d3d9341e26" + version "4.2.0" + resolved "https://registry.yarnpkg.com/test-exclude/-/test-exclude-4.2.0.tgz#07e3613609a362c74516a717515e13322ab45b3c" dependencies: arrify "^1.0.1" micromatch "^2.3.11" @@ -3131,8 +3202,8 @@ to-fast-properties@^1.0.3: resolved "https://registry.yarnpkg.com/to-fast-properties/-/to-fast-properties-1.0.3.tgz#b83571fa4d8c25b82e231b06e3a3055de4ca1a47" tough-cookie@^2.3.2, tough-cookie@~2.3.0, tough-cookie@~2.3.3: - version "2.3.3" - resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.3.tgz#0b618a5565b6dea90bf3425d04d55edc475a7561" + version "2.3.4" + resolved "https://registry.yarnpkg.com/tough-cookie/-/tough-cookie-2.3.4.tgz#ec60cee38ac675063ffc97a5c18970578ee83655" dependencies: punycode "^1.4.1" @@ -3161,11 +3232,15 @@ type-check@~0.3.2: prelude-ls "~1.1.2" type-is@~1.6.15: - version "1.6.15" - resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.15.tgz#cab10fb4909e441c82842eafe1ad646c81804410" + version "1.6.16" + resolved "https://registry.yarnpkg.com/type-is/-/type-is-1.6.16.tgz#f89ce341541c672b25ee7ae3c73dee3b2be50194" dependencies: media-typer "0.3.0" - mime-types "~2.1.15" + mime-types "~2.1.18" + +ua-parser-js@^0.7.9: + version "0.7.17" + resolved "https://registry.yarnpkg.com/ua-parser-js/-/ua-parser-js-0.7.17.tgz#e9ec5f9498b9ec910e7ae3ac626a805c4d09ecac" uglify-js@^2.6: version "2.8.29" @@ -3214,8 +3289,8 @@ utils-merge@1.0.1: resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713" uuid@^3.0.0, uuid@^3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.1.0.tgz#3dd3d3e790abc24d7b0d3a034ffababe28ebbc04" + version "3.2.1" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.2.1.tgz#12c528bb9d58d0b9265d9a2f6f0fe8be17ff1f14" v8flags@^2.0.2: version "2.1.1" @@ -3224,11 +3299,11 @@ v8flags@^2.0.2: user-home "^1.1.1" validate-npm-package-license@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.1.tgz#2804babe712ad3379459acfbe24746ab2c303fbc" + version "3.0.3" + resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338" dependencies: - spdx-correct "~1.0.0" - spdx-expression-parse "~1.0.0" + spdx-correct "^3.0.0" + spdx-expression-parse "^3.0.0" vary@~1.1.2: version "1.1.2" @@ -3269,6 +3344,10 @@ whatwg-encoding@^1.0.1: dependencies: iconv-lite "0.4.19" +whatwg-fetch@>=0.10.0: + version "2.0.3" + resolved "https://registry.yarnpkg.com/whatwg-fetch/-/whatwg-fetch-2.0.3.tgz#9c84ec2dcf68187ff00bc64e1274b442176e1c84" + whatwg-url@^4.3.0: version "4.8.0" resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-4.8.0.tgz#d2981aa9148c1e00a41c5a6131166ab4683bbcc0" @@ -3280,7 +3359,7 @@ which-module@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.0.tgz#d9ef07dce77b9902b8a3a8fa4b31c3e3f7e6e87a" -which@^1.2.10, which@^1.2.12, which@^1.2.9: +which@^1.2.10, which@^1.2.12, which@^1.2.9, which@^1.3.0: version "1.3.0" resolved "https://registry.yarnpkg.com/which/-/which-1.3.0.tgz#ff04bdfc010ee547d780bec38e1ac1c2777d253a" dependencies: @@ -3309,11 +3388,11 @@ wordwrap@~1.0.0: resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-1.0.0.tgz#27584810891456a4171c8d0226441ade90cbcaeb" worker-farm@^1.3.1: - version "1.5.2" - resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.2.tgz#32b312e5dc3d5d45d79ef44acc2587491cd729ae" + version "1.5.4" + resolved "https://registry.yarnpkg.com/worker-farm/-/worker-farm-1.5.4.tgz#4debbe46b40edefcc717ebde74a90b1ae1e909a1" dependencies: - errno "^0.1.4" - xtend "^4.0.1" + errno "~0.1.7" + xtend "~4.0.1" wrap-ansi@^2.0.0: version "2.1.0" @@ -3338,7 +3417,7 @@ xml-name-validator@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-2.0.1.tgz#4d8b8f1eccd3419aa362061becef515e1e559635" -xtend@^4.0.1: +xtend@~4.0.1: version "4.0.1" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.1.tgz#a5c6d532be656e23db820efb943a1f04998d63af" From 17026661cf9b43234c21b9a6e473de92b652d53e Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Thu, 1 Mar 2018 12:10:33 -0500 Subject: [PATCH 30/62] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20=20add=20sane=20e?= =?UTF-8?q?rror=20when=20shop=20and=20/=20or=20accesstoken=20are=20not=20o?= =?UTF-8?q?n=20session?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/shopifyApiProxy.js | 8 +++++++- routes/tests/shopifyApiProxy.test.js | 14 ++++++++++++++ 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/routes/shopifyApiProxy.js b/routes/shopifyApiProxy.js index 0a021e4..82d7b50 100644 --- a/routes/shopifyApiProxy.js +++ b/routes/shopifyApiProxy.js @@ -17,8 +17,14 @@ module.exports = async function shopifyApiProxy(incomingRequest, response, next) const { query, method, path: pathname, body, session } = incomingRequest; const { shop, accessToken } = session; + if (shop == null || accessToken == null) { + response.status(401).send(new Error('Unauthorized')); + return; + } + if (!validRequest(pathname)) { - return response.status(403).send('Endpoint not in whitelist'); + response.status(403).send('Endpoint not in whitelist'); + return; } try { diff --git a/routes/tests/shopifyApiProxy.test.js b/routes/tests/shopifyApiProxy.test.js index 6f2bce7..91e5350 100644 --- a/routes/tests/shopifyApiProxy.test.js +++ b/routes/tests/shopifyApiProxy.test.js @@ -32,6 +32,20 @@ describe('shopifyApiProxy', async () => { server.close(); }); + it('errors when shop information is not in session', async () => { + const shop = 'some-shop.com'; + const endpoint = '/products'; + session.shop = null; + session.accessToken = null; + + const expectedPath = `https://${shop}/admin${endpoint}`; + const response = await fetch(`${BASE_URL}${API_ROUTE}${endpoint}`); + + expect(fetchMock).not.toBeCalled(); + expect(response.status).toBe(401); + }); + + it('proxies requests to the shop given in session', async () => { const shop = 'some-shop.com'; const endpoint = '/products'; From d8777b93f16586f635854933254d5a57253aaa8a Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Thu, 1 Mar 2018 12:12:03 -0500 Subject: [PATCH 31/62] =?UTF-8?q?=F0=9F=91=8C=20warn=20when=20no=20session?= =?UTF-8?q?=20is=20present=20on=20request=20context=20but=20dont=20break?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/shopifyAuth.js | 39 ++++++++++++++++++-------------- routes/tests/shopifyAuth.test.js | 18 +++++++++++++++ 2 files changed, 40 insertions(+), 17 deletions(-) diff --git a/routes/shopifyAuth.js b/routes/shopifyAuth.js index 0755c48..f18561a 100644 --- a/routes/shopifyAuth.js +++ b/routes/shopifyAuth.js @@ -42,7 +42,7 @@ module.exports = function createShopifyAuthRouter({ // Users are redirected here after clicking `Install`. // The redirect from Shopify contains the authorization_code query parameter, // which the app exchanges for an access token - router.get('/callback', (request, response) => { + router.get('/callback', async (request, response) => { const { query } = request; const { code, hmac, shop } = query; @@ -70,28 +70,33 @@ module.exports = function createShopifyAuthRouter({ client_secret: secret, }); - fetch(`https://${shop}/admin/oauth/access_token`, { + const remoteResponse = await fetch(`https://${shop}/admin/oauth/access_token`, { method: 'POST', headers: { 'Content-Type': 'application/x-www-form-urlencoded', 'Content-Length': Buffer.byteLength(requestBody), }, body: requestBody, - }) - .then(remoteResponse => remoteResponse.json()) - .then(responseBody => { - const accessToken = responseBody.access_token; - - shopStore.storeShop({ accessToken, shop }, (err, token) => { - if (err) { - console.error('🔴 Error storing shop access token', err); - } - - request.session.accessToken = accessToken; - request.session.shop = shop; - afterAuth(request, response); - }); - }); + }); + + const responseBody = await remoteResponse.json(); + + const accessToken = responseBody.access_token; + + shopStore.storeShop({ accessToken, shop }, (err, token) => { + if (err) { + console.error('🔴 Error storing shop access token', err); + } + + if (request.session) { + request.session.accessToken = accessToken; + request.session.shop = shop; + } else { + console.warn('Session not present on request, please install a session middleware.'); + } + + afterAuth(request, response); + }); }); return router; diff --git a/routes/tests/shopifyAuth.test.js b/routes/tests/shopifyAuth.test.js index c6c7b8b..27ed7e1 100644 --- a/routes/tests/shopifyAuth.test.js +++ b/routes/tests/shopifyAuth.test.js @@ -39,6 +39,24 @@ describe('shopifyAuth', async () => { expect(data).toMatchSnapshot(); }); }); + + describe('/callback', () => { + it('errors when hmac validation fails', () => { + pending(); + }); + + it('does not error when hmac validation succeds', () => { + pending(); + }); + + it('requests access token', () => { + pending(); + }); + + it('console warns when no session is present on request context', () => { + pending(); + }); + }); }); function createServer(afterAuth) { From 353eef1ef8d1cbf9bda592f4d55705baaf3e977d Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Thu, 1 Mar 2018 12:24:50 -0500 Subject: [PATCH 32/62] =?UTF-8?q?=F0=9F=93=93=20update=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/README.md b/README.md index d24a486..38ff09c 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ A small set of abstractions that will help you quickly build an Express.js app t ```javascript const express = require('express'); const shopifyExpress = require('@shopify/shopify-express'); +const session = require('express-session'); const app = express(); @@ -20,6 +21,9 @@ const { NODE_ENV, } = process.env; +// session is necessary for api proxy and auth verification +app.use(session({secret: SHOPIFY_APP_SECRET})); + const shopify = shopifyExpress({ host: SHOPIFY_APP_HOST, apiKey: SHOPIFY_APP_KEY, @@ -151,6 +155,35 @@ Express middleware that validates the presence of a valid HMAC signature to allo You can look at [shopify-node-app](https://github.com/shopify/shopify-node-app) for a complete working example. + +## Gotchas + +### Express Session +This library expects [express-session](https://www.npmjs.com/package/express-session) or a compatible library to be installed and set up for much of it's functionality. Api Proxy and auth verification functions won't work without something putting a `session` key on `request`. + +It is possible to use auth without a session key on your request, but not recommended. + +### Body Parser +This library handles body parsing on it's own for webhooks. If you're using webhooks you should make sure to follow express best-practices by only adding your body parsing middleware to specific routes that need it. + +**Good** +``` + app.use('/some-route', bodyParser.json(), myHandler); + + app.use('/webhook', withWebhook(myWebhookHandler)); + app.use('/', shopifyExpress.routes); +``` + +**Bad** +``` + app.use(bodyParser.json()); + app.use('/some-route', myHandler); + + app.use('/webhook', withWebhook(myWebhookHandler)); + app.use('/', shopifyExpress.routes); +``` + + ## Contributing Contributions are welcome. Please refer to the [contributing guide](https://github.com/Shopify/shopify-express/blob/master/CONTRIBUTING.md) for more details. From d2ae50372c8e8440315dbc8b43e4a6009a86fcf4 Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Thu, 1 Mar 2018 12:30:46 -0500 Subject: [PATCH 33/62] =?UTF-8?q?=F0=9F=91=8C=20error=20when=20session=20i?= =?UTF-8?q?s=20null=20in=20shopifyApiProxy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/shopifyApiProxy.js | 7 +++++++ routes/tests/shopifyApiProxy.test.js | 16 ++++++++++++++-- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/routes/shopifyApiProxy.js b/routes/shopifyApiProxy.js index 82d7b50..6abfc2b 100644 --- a/routes/shopifyApiProxy.js +++ b/routes/shopifyApiProxy.js @@ -15,6 +15,13 @@ const DISALLOWED_URLS = [ module.exports = async function shopifyApiProxy(incomingRequest, response, next) { const { query, method, path: pathname, body, session } = incomingRequest; + + if (session == null) { + console.error('A session middleware must be installed to use ApiProxy.') + response.status(401).send(new Error('Unauthorized')); + return; + } + const { shop, accessToken } = session; if (shop == null || accessToken == null) { diff --git a/routes/tests/shopifyApiProxy.test.js b/routes/tests/shopifyApiProxy.test.js index 91e5350..71065c2 100644 --- a/routes/tests/shopifyApiProxy.test.js +++ b/routes/tests/shopifyApiProxy.test.js @@ -15,6 +15,7 @@ jest.mock('node-fetch'); let session; let server; +const originalConsoleError = console.error; describe('shopifyApiProxy', async () => { beforeEach(async () => { fetchMock.mockImplementation(() => ({ status: 200, text: () => Promise.resolve() })); @@ -25,20 +26,31 @@ describe('shopifyApiProxy', async () => { }; server = await createServer(); + console.error = jest.fn(); }); afterEach(() => { fetchMock.mockClear(); server.close(); + console.error = originalConsoleError; + }); + + it('errors when no session is present', async () => { + const endpoint = '/products'; + session = null; + + const response = await fetch(`${BASE_URL}${API_ROUTE}${endpoint}`); + + expect(fetchMock).not.toBeCalled(); + expect(console.error).toBeCalledWith('A session middleware must be installed to use ApiProxy.'); + expect(response.status).toBe(401); }); it('errors when shop information is not in session', async () => { - const shop = 'some-shop.com'; const endpoint = '/products'; session.shop = null; session.accessToken = null; - const expectedPath = `https://${shop}/admin${endpoint}`; const response = await fetch(`${BASE_URL}${API_ROUTE}${endpoint}`); expect(fetchMock).not.toBeCalled(); From 2df84d81db385e41c29f219aa296ab626584e665 Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Thu, 1 Mar 2018 16:48:12 -0500 Subject: [PATCH 34/62] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20fix=20prop-types?= =?UTF-8?q?=20definition=20for=20shop=20store?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/index.js b/index.js index 009e68c..c894023 100644 --- a/index.js +++ b/index.js @@ -9,7 +9,7 @@ const ShopifyConfigTypes = { secret: PropTypes.string.isRequired, scope: PropTypes.arrayOf(PropTypes.string).isRequired, afterAuth: PropTypes.func.isRequired, - shopStore: PropTypes.Object, + shopStore: PropTypes.object, }; module.exports = function shopify(shopifyConfig) { From 52a5f7704e0c9cd56804ed061e892667171bdd66 Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Thu, 1 Mar 2018 18:16:24 -0500 Subject: [PATCH 35/62] =?UTF-8?q?=E2=9C=85=20update=20snapshots?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tests/__snapshots__/ShopifyConfig.test.js.snap | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/__snapshots__/ShopifyConfig.test.js.snap b/tests/__snapshots__/ShopifyConfig.test.js.snap index 7c52da6..08715f6 100644 --- a/tests/__snapshots__/ShopifyConfig.test.js.snap +++ b/tests/__snapshots__/ShopifyConfig.test.js.snap @@ -37,8 +37,5 @@ Array [ Array [ "Warning: Failed option type: The option \`afterAuth\` is marked as required in \`ShopifyExpress\`, but its value is \`undefined\`.", ], - Array [ - "Warning: Failed option type: ShopifyExpress: option type \`shopStore\` is invalid; it must be a function, usually from the \`prop-types\` package, but received \`undefined\`.", - ], ] `; From 0b3ab6ded4c29dd4bdf4ec17d246b45d3f873fe1 Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Thu, 1 Mar 2018 20:02:19 -0500 Subject: [PATCH 36/62] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20=20use=20baseUrl?= =?UTF-8?q?=20to=20generate=20callback=20url?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- middleware/index.js | 3 +- middleware/withShop.js | 19 +- routes/index.js | 21 ++- routes/shopifyAuth.js | 169 +++++++++--------- .../__snapshots__/shopifyAuth.test.js.snap | 14 +- routes/tests/shopifyAuth.test.js | 21 ++- strategies/SQLStrategy.js | 4 +- 7 files changed, 129 insertions(+), 122 deletions(-) diff --git a/middleware/index.js b/middleware/index.js index 79064f9..8c83f5f 100644 --- a/middleware/index.js +++ b/middleware/index.js @@ -1,9 +1,8 @@ const createWithWebhook = require('./webhooks'); -const createWithShop = require('./withShop'); +const withShop = require('./withShop'); module.exports = function createMiddleware(shopifyConfig) { const withWebhook = createWithWebhook(shopifyConfig); - const withShop = createWithShop(); return { withShop, diff --git a/middleware/withShop.js b/middleware/withShop.js index 487ea2c..4f69cbe 100644 --- a/middleware/withShop.js +++ b/middleware/withShop.js @@ -1,19 +1,18 @@ -module.exports = function withShop({ redirect } = { redirect: true }) { +module.exports = function withShop({ authBaseUrl } = {}) { return function verifyRequest(request, response, next) { - const { query: { shop }, session } = request; + const { query: { shop }, session, baseUrl } = request; if (session && session.accessToken) { - return next(); + next(); + return; } - if (shop && redirect) { - return response.redirect(`/auth/shopify?shop=${shop}`); + if (shop) { + response.redirect(`${authBaseUrl || baseUrl}/auth?shop=${shop}`); + return; } - if (redirect) { - return response.redirect('/install'); - } - - return response.status(401).json('Unauthorized'); + response.redirect('/install'); + return; }; }; diff --git a/routes/index.js b/routes/index.js index b61d48d..3bde606 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,21 +1,34 @@ const express = require('express'); const bodyParser = require('body-parser'); -const createWithShop = require('../middleware/withShop'); -const createShopifyAuthRouter = require('./shopifyAuth'); +const createShopifyAuthRoutes = require('./shopifyAuth'); const shopifyApiProxy = require('./shopifyApiProxy'); module.exports = function createRouter(shopifyConfig) { const router = express.Router(); const rawParser = bodyParser.raw({ type: '*/*' }); + const {auth, callback} = createShopifyAuthRoutes(shopifyConfig) - router.use('/auth/shopify', createShopifyAuthRouter(shopifyConfig)); + router.use('/auth/callback', callback); + router.use('/auth', auth); router.use( '/api', rawParser, - createWithShop({ redirect: false }), + verifyApiCall, shopifyApiProxy, ); return router; }; + +function verifyApiCall(request, response, next) { + const {session} = request; + + if (session && session.accessToken) { + next(); + return; + } + + response.status(401).send(); + return; +} diff --git a/routes/shopifyAuth.js b/routes/shopifyAuth.js index f18561a..aa5f1c3 100644 --- a/routes/shopifyAuth.js +++ b/routes/shopifyAuth.js @@ -1,9 +1,8 @@ -const express = require('express'); const querystring = require('querystring'); const crypto = require('crypto'); const fetch = require('node-fetch'); -module.exports = function createShopifyAuthRouter({ +module.exports = function createShopifyAuthRoutes({ host, apiKey, secret, @@ -11,93 +10,91 @@ module.exports = function createShopifyAuthRouter({ afterAuth, shopStore, }) { - const router = express.Router(); - - // This function initializes the Shopify OAuth Process - router.get('/', function(request, response) { - const { query } = request; - const { shop } = query; - - if (shop == null) { - return response.status(400).send('Expected a shop query parameter'); - } - - const redirectTo = `https://${shop}/admin/oauth/authorize`; - const redirectParams = - `?client_id=${apiKey}&scope=${scope}&redirect_uri=${host}` + - '/auth/shopify/callback'; - - response.send( - ` - - - - - `, - ); - }); - - // Users are redirected here after clicking `Install`. - // The redirect from Shopify contains the authorization_code query parameter, - // which the app exchanges for an access token - router.get('/callback', async (request, response) => { - const { query } = request; - const { code, hmac, shop } = query; - - const map = JSON.parse(JSON.stringify(query)); - delete map['signature']; - delete map['hmac']; - - const message = querystring.stringify(map); - const generated_hash = crypto - .createHmac('sha256', secret) - .update(message) - .digest('hex'); - - if (generated_hash !== hmac) { - return response.status(400).send('HMAC validation failed'); - } - - if (shop == null) { - return response.status(400).send('Expected a shop query parameter'); - } - - const requestBody = querystring.stringify({ - code, - client_id: apiKey, - client_secret: secret, - }); - - const remoteResponse = await fetch(`https://${shop}/admin/oauth/access_token`, { - method: 'POST', - headers: { - 'Content-Type': 'application/x-www-form-urlencoded', - 'Content-Length': Buffer.byteLength(requestBody), - }, - body: requestBody, - }); - - const responseBody = await remoteResponse.json(); - - const accessToken = responseBody.access_token; - - shopStore.storeShop({ accessToken, shop }, (err, token) => { - if (err) { - console.error('🔴 Error storing shop access token', err); + return { + // This function initializes the Shopify OAuth Process + auth(request, response) { + const { query, baseUrl } = request; + const { shop } = query; + + if (shop == null) { + return response.status(400).send('Expected a shop query parameter'); } - if (request.session) { - request.session.accessToken = accessToken; - request.session.shop = shop; - } else { - console.warn('Session not present on request, please install a session middleware.'); + const redirectTo = `https://${shop}/admin/oauth/authorize`; + const redirectParams = + `?client_id=${apiKey}&scope=${scope}&redirect_uri=${host}` + + `${baseUrl}/callback`; + + response.send( + ` + + + + + `, + ); + }, + + // Users are redirected here after clicking `Install`. + // The redirect from Shopify contains the authorization_code query parameter, + // which the app exchanges for an access token + async callback(request, response) { + const { query } = request; + const { code, hmac, shop } = query; + + const map = JSON.parse(JSON.stringify(query)); + delete map['signature']; + delete map['hmac']; + + const message = querystring.stringify(map); + const generated_hash = crypto + .createHmac('sha256', secret) + .update(message) + .digest('hex'); + + if (generated_hash !== hmac) { + return response.status(400).send('HMAC validation failed'); } - afterAuth(request, response); - }); - }); + if (shop == null) { + return response.status(400).send('Expected a shop query parameter'); + } - return router; + const requestBody = querystring.stringify({ + code, + client_id: apiKey, + client_secret: secret, + }); + + const remoteResponse = await fetch(`https://${shop}/admin/oauth/access_token`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'Content-Length': Buffer.byteLength(requestBody), + }, + body: requestBody, + }); + + const responseBody = await remoteResponse.json(); + + const accessToken = responseBody.access_token; + + shopStore.storeShop({ accessToken, shop }, (err, token) => { + if (err) { + console.error('🔴 Error storing shop access token', err); + } + + if (request.session) { + request.session.accessToken = accessToken; + request.session.shop = shop; + } else { + console.warn('Session not present on request, please install a session middleware.'); + } + + afterAuth(request, response); + }); + } + }; }; diff --git a/routes/tests/__snapshots__/shopifyAuth.test.js.snap b/routes/tests/__snapshots__/shopifyAuth.test.js.snap index 5ce5cb1..76614f8 100644 --- a/routes/tests/__snapshots__/shopifyAuth.test.js.snap +++ b/routes/tests/__snapshots__/shopifyAuth.test.js.snap @@ -2,13 +2,13 @@ exports[`shopifyAuth / responds to get requests by returning a redirect page 1`] = ` " - - - - - " + + + + + " `; exports[`shopifyAuth / responds with a 400 when no shop query parameter is given 1`] = `"Expected a shop query parameter"`; diff --git a/routes/tests/shopifyAuth.test.js b/routes/tests/shopifyAuth.test.js index 27ed7e1..3ff232b 100644 --- a/routes/tests/shopifyAuth.test.js +++ b/routes/tests/shopifyAuth.test.js @@ -4,7 +4,7 @@ const http = require('http'); const express = require('express'); const { MemoryStrategy } = require('../../strategies'); -const createShopifyAuthRouter = require('../shopifyAuth'); +const createShopifyAuthRoutes = require('../shopifyAuth'); const PORT = 3000; const BASE_URL = `http://localhost:${PORT}` @@ -62,17 +62,16 @@ describe('shopifyAuth', async () => { function createServer(afterAuth) { const app = express(); - app.use( - '/', - createShopifyAuthRouter({ - apiKey: 'key', - secret: 'secret', - scope: ['scope'], - shopStore: new MemoryStrategy(), - afterAuth, - }), - ); + const {auth, callback} = createShopifyAuthRoutes({ + apiKey: 'key', + secret: 'secret', + scope: ['scope'], + shopStore: new MemoryStrategy(), + afterAuth, + }); + app.use('/', auth); + app.use('/callback', callback); server = http.createServer(app); return new Promise((resolve, reject) => { diff --git a/strategies/SQLStrategy.js b/strategies/SQLStrategy.js index cb06588..8da19e8 100644 --- a/strategies/SQLStrategy.js +++ b/strategies/SQLStrategy.js @@ -21,8 +21,8 @@ module.exports = class SQLStrategy { table.string('access_token'); table.unique('shopify_domain'); }) - .catch(err => { - console.log(err); + .catch(error => { + console.log(error); }); } From 23c3f737ab5f9fc4de8e2c1caae82be40c204384 Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Thu, 1 Mar 2018 20:06:53 -0500 Subject: [PATCH 37/62] =?UTF-8?q?=F0=9F=93=93=20update=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 38ff09c..914ae44 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ const { // session is necessary for api proxy and auth verification app.use(session({secret: SHOPIFY_APP_SECRET})); -const shopify = shopifyExpress({ +const {routes, withShop} = shopifyExpress({ host: SHOPIFY_APP_HOST, apiKey: SHOPIFY_APP_KEY, secret: SHOPIFY_APP_SECRET, @@ -36,8 +36,11 @@ const shopify = shopifyExpress({ }, }); -// mounts '/auth/shopify' and '/api' off of '/' -app.use('/', shopify.routes); +// mounts '/auth' and '/api' off of '/' +app.use('/shopify', routes); + +// shields myAppMiddleware from being accessed without session +app.use('/myApp', withShop('/shopify'), myAppMiddleware) ``` ## Shopify routes @@ -141,9 +144,9 @@ If you do not have a table already created for your store, you can generate one ### `withShop` -`app.use('/someProtectedPath', withShop, someHandler);` +`app.use('/someProtectedPath', withShop('/shopify'), someHandler);` -Express middleware that validates the presence of your shop session. +Express middleware that validates the presence of your shop session. The parameter you pass to it should match the base URL for where you've mounted the shopify routes. ### `withWebhook` @@ -155,9 +158,11 @@ Express middleware that validates the presence of a valid HMAC signature to allo You can look at [shopify-node-app](https://github.com/shopify/shopify-node-app) for a complete working example. - ## Gotchas +### Install route +For the moment the app expects you to mount your install route at `/install`. See [shopify-node-app](https://github.com/shopify/shopify-node-app) for details. + ### Express Session This library expects [express-session](https://www.npmjs.com/package/express-session) or a compatible library to be installed and set up for much of it's functionality. Api Proxy and auth verification functions won't work without something putting a `session` key on `request`. From 24e57e5075a1a1583d8754d1c9eb2c7669c52436 Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Thu, 1 Mar 2018 20:11:59 -0500 Subject: [PATCH 38/62] =?UTF-8?q?=E2=9C=85=20update=20tests?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/tests/__snapshots__/shopifyAuth.test.js.snap | 2 +- routes/tests/shopifyAuth.test.js | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/routes/tests/__snapshots__/shopifyAuth.test.js.snap b/routes/tests/__snapshots__/shopifyAuth.test.js.snap index 76614f8..82bb90a 100644 --- a/routes/tests/__snapshots__/shopifyAuth.test.js.snap +++ b/routes/tests/__snapshots__/shopifyAuth.test.js.snap @@ -5,7 +5,7 @@ exports[`shopifyAuth / responds to get requests by returning a redirect page 1`] " diff --git a/routes/tests/shopifyAuth.test.js b/routes/tests/shopifyAuth.test.js index 3ff232b..49cdb06 100644 --- a/routes/tests/shopifyAuth.test.js +++ b/routes/tests/shopifyAuth.test.js @@ -24,7 +24,7 @@ describe('shopifyAuth', async () => { describe('/', () => { it('responds to get requests by returning a redirect page', async () => { - const response = await fetch(`${BASE_URL}/?shop=shop1`); + const response = await fetch(`${BASE_URL}/auth?shop=shop1`); const data = await response.text(); expect(response.status).toBe(200); @@ -32,7 +32,7 @@ describe('shopifyAuth', async () => { }); it('responds with a 400 when no shop query parameter is given', async () => { - const response = await fetch(BASE_URL); + const response = await fetch(`${BASE_URL}/auth`); const data = await response.text(); expect(response.status).toBe(400); @@ -63,6 +63,7 @@ function createServer(afterAuth) { const app = express(); const {auth, callback} = createShopifyAuthRoutes({ + host: 'http://myshop.myshopify.com', apiKey: 'key', secret: 'secret', scope: ['scope'], @@ -70,8 +71,8 @@ function createServer(afterAuth) { afterAuth, }); - app.use('/', auth); - app.use('/callback', callback); + app.use('/auth', auth); + app.use('/auth/callback', callback); server = http.createServer(app); return new Promise((resolve, reject) => { From 86c54dde504bb37a6fa809271f2fb0c4699d5967 Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Thu, 1 Mar 2018 20:21:19 -0500 Subject: [PATCH 39/62] =?UTF-8?q?=F0=9F=97=92=EF=B8=8F=20tweak=20readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 914ae44..fd87f72 100644 --- a/README.md +++ b/README.md @@ -40,7 +40,7 @@ const {routes, withShop} = shopifyExpress({ app.use('/shopify', routes); // shields myAppMiddleware from being accessed without session -app.use('/myApp', withShop('/shopify'), myAppMiddleware) +app.use('/myApp', withShop({authBaseUrl: '/shopify'}), myAppMiddleware) ``` ## Shopify routes @@ -144,7 +144,7 @@ If you do not have a table already created for your store, you can generate one ### `withShop` -`app.use('/someProtectedPath', withShop('/shopify'), someHandler);` +`app.use('/someProtectedPath', withShop({authBaseUrl: '/shopify'}), someHandler);` Express middleware that validates the presence of your shop session. The parameter you pass to it should match the base URL for where you've mounted the shopify routes. From 0371f87a7d682486c45df2dca198ed64fdac3bab Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Fri, 2 Mar 2018 11:20:40 -0500 Subject: [PATCH 40/62] =?UTF-8?q?=E2=9C=A8=20add=20accessMode=20option=20t?= =?UTF-8?q?o=20config=20and=20generate=20redirect=20urls=20based=20on=20it?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- index.js | 11 ++++--- routes/shopifyAuth.js | 17 ++++++++--- .../__snapshots__/shopifyAuth.test.js.snap | 2 +- routes/tests/shopifyAuth.test.js | 29 ++++++++++++++----- tests/ShopifyConfig.test.js | 5 ++-- .../__snapshots__/ShopifyConfig.test.js.snap | 3 ++ 6 files changed, 48 insertions(+), 19 deletions(-) diff --git a/index.js b/index.js index c894023..9fe156f 100644 --- a/index.js +++ b/index.js @@ -10,15 +10,18 @@ const ShopifyConfigTypes = { scope: PropTypes.arrayOf(PropTypes.string).isRequired, afterAuth: PropTypes.func.isRequired, shopStore: PropTypes.object, + accessMode: PropTypes.oneOf(['offline', 'online']), +}; + +const defaults = { + shopStore: new MemoryStrategy(), + accessMode: 'offline' }; module.exports = function shopify(shopifyConfig) { PropTypes.checkPropTypes(ShopifyConfigTypes, shopifyConfig, 'option', 'ShopifyExpress'); - const config = Object.assign( - {shopStore: new MemoryStrategy()}, - shopifyConfig, - ); + const config = Object.assign({}, defaults, shopifyConfig); return { middleware: createMiddleware(config), diff --git a/routes/shopifyAuth.js b/routes/shopifyAuth.js index aa5f1c3..d282a7e 100644 --- a/routes/shopifyAuth.js +++ b/routes/shopifyAuth.js @@ -9,6 +9,7 @@ module.exports = function createShopifyAuthRoutes({ scope, afterAuth, shopStore, + accessMode, }) { return { // This function initializes the Shopify OAuth Process @@ -21,16 +22,24 @@ module.exports = function createShopifyAuthRoutes({ } const redirectTo = `https://${shop}/admin/oauth/authorize`; - const redirectParams = - `?client_id=${apiKey}&scope=${scope}&redirect_uri=${host}` + - `${baseUrl}/callback`; + + const redirectParams = { + baseUrl, + scope, + client_id: apiKey, + redirect_uri: `${host}${baseUrl}/callback`, + }; + + if (accessMode === 'online') { + redirectParams['grant_options[]'] = 'per-user'; + } response.send( ` `, diff --git a/routes/tests/__snapshots__/shopifyAuth.test.js.snap b/routes/tests/__snapshots__/shopifyAuth.test.js.snap index 82bb90a..90f0a12 100644 --- a/routes/tests/__snapshots__/shopifyAuth.test.js.snap +++ b/routes/tests/__snapshots__/shopifyAuth.test.js.snap @@ -5,7 +5,7 @@ exports[`shopifyAuth / responds to get requests by returning a redirect page 1`] " diff --git a/routes/tests/shopifyAuth.test.js b/routes/tests/shopifyAuth.test.js index 49cdb06..adc0b6c 100644 --- a/routes/tests/shopifyAuth.test.js +++ b/routes/tests/shopifyAuth.test.js @@ -10,12 +10,11 @@ const PORT = 3000; const BASE_URL = `http://localhost:${PORT}` let server; -let afterAuthSpy; +let afterAuth; describe('shopifyAuth', async () => { beforeEach(async () => { - afterAuthSpy = jest.fn(); - - server = await createServer(afterAuthSpy); + afterAuth = jest.fn(); + server = await createServer({afterAuth}); }); afterEach(() => { @@ -31,6 +30,17 @@ describe('shopifyAuth', async () => { expect(data).toMatchSnapshot(); }); + it('redirect page includes per-user grant for accessMode: online', async () => { + await server.close(); + server = await createServer({accessMode: 'online'}); + + const response = await fetch(`${BASE_URL}/auth?shop=shop1`); + const data = await response.text(); + + expect(response.status).toBe(200); + expect(data).toContain('grant_options%5B%5D=per-user'); + }); + it('responds with a 400 when no shop query parameter is given', async () => { const response = await fetch(`${BASE_URL}/auth`); const data = await response.text(); @@ -59,17 +69,20 @@ describe('shopifyAuth', async () => { }); }); -function createServer(afterAuth) { +function createServer(userConfig = {}) { const app = express(); - const {auth, callback} = createShopifyAuthRoutes({ + const serverConfig = { host: 'http://myshop.myshopify.com', apiKey: 'key', secret: 'secret', scope: ['scope'], shopStore: new MemoryStrategy(), - afterAuth, - }); + accessMode: 'offline', + afterAuth: jest.fn(), + }; + + const {auth, callback} = createShopifyAuthRoutes(Object.assign({}, serverConfig, userConfig)); app.use('/auth', auth); app.use('/auth/callback', callback); diff --git a/tests/ShopifyConfig.test.js b/tests/ShopifyConfig.test.js index c7113ec..3cd4360 100644 --- a/tests/ShopifyConfig.test.js +++ b/tests/ShopifyConfig.test.js @@ -17,17 +17,18 @@ describe('ShopifyConfig', async () => { }); it('logs errors when given bad props', () => { - shopifyExpress({apiKey: 32, + shopifyExpress({ + apiKey: 32, host: { notGood: true }, secret: true, scope: 'orders', afterAuth: true, + accessMode: 'gerblable', }); expect(console.error).toBeCalled(); expect(console.error.mock.calls).toMatchSnapshot(); }); - it('does not log errors when given valid proptypes', () => { shopifyExpress({ apiKey: 'fake', diff --git a/tests/__snapshots__/ShopifyConfig.test.js.snap b/tests/__snapshots__/ShopifyConfig.test.js.snap index 08715f6..45b1f02 100644 --- a/tests/__snapshots__/ShopifyConfig.test.js.snap +++ b/tests/__snapshots__/ShopifyConfig.test.js.snap @@ -17,6 +17,9 @@ Array [ Array [ "Warning: Failed option type: Invalid option \`afterAuth\` of type \`boolean\` supplied to \`ShopifyExpress\`, expected \`function\`.", ], + Array [ + "Warning: Failed option type: Invalid option \`accessMode\` of value \`gerblable\` supplied to \`ShopifyExpress\`, expected one of [\\"offline\\",\\"online\\"].", + ], ] `; From b807e433c40ad70e6168234fcf0963652237dcdf Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Fri, 2 Mar 2018 11:21:47 -0500 Subject: [PATCH 41/62] =?UTF-8?q?=F0=9F=93=93=20add=20accessMode=20to=20th?= =?UTF-8?q?e=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index fd87f72..0f85dc4 100644 --- a/README.md +++ b/README.md @@ -29,6 +29,7 @@ const {routes, withShop} = shopifyExpress({ apiKey: SHOPIFY_APP_KEY, secret: SHOPIFY_APP_SECRET, scope: ['write_orders, write_products'], + accessMode: 'offline', afterAuth(request, response) { const { session: { accessToken, shop } } = request; // install webhooks or hook into your own app here From 49bcb89258ee5c15e3401f64137f47b6eb537336 Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Fri, 2 Mar 2018 15:35:50 -0500 Subject: [PATCH 42/62] =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F=20=20fix=20missing?= =?UTF-8?q?=20=3F=20in=20queryParam=20generation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/shopifyAuth.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/shopifyAuth.js b/routes/shopifyAuth.js index d282a7e..1d9842c 100644 --- a/routes/shopifyAuth.js +++ b/routes/shopifyAuth.js @@ -39,7 +39,7 @@ module.exports = function createShopifyAuthRoutes({ `, From ecb9e442c6c5f146feb93583e38b961941333bf4 Mon Sep 17 00:00:00 2001 From: Adam Waselnuk Date: Fri, 2 Mar 2018 16:28:05 -0500 Subject: [PATCH 43/62] Bump to node 8.1.1 --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index 8104cab..0e79152 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -8.1.0 +8.1.1 From 3ea4270da6249030cc47006379372bc360ff1883 Mon Sep 17 00:00:00 2001 From: Jamie Dwyer Date: Mon, 5 Mar 2018 14:20:50 -0500 Subject: [PATCH 44/62] Regenerated snapshots --- routes/tests/__snapshots__/shopifyAuth.test.js.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/tests/__snapshots__/shopifyAuth.test.js.snap b/routes/tests/__snapshots__/shopifyAuth.test.js.snap index 90f0a12..6dd894c 100644 --- a/routes/tests/__snapshots__/shopifyAuth.test.js.snap +++ b/routes/tests/__snapshots__/shopifyAuth.test.js.snap @@ -5,7 +5,7 @@ exports[`shopifyAuth / responds to get requests by returning a redirect page 1`] " From 3937d49f268f4749548de0a47ec67d5d9e21c135 Mon Sep 17 00:00:00 2001 From: Jamie Dwyer Date: Mon, 5 Mar 2018 15:40:13 -0500 Subject: [PATCH 45/62] First shot at adding shipit config --- RELEASING.md | 8 ++++++++ shipit.yml | 6 ++++++ 2 files changed, 14 insertions(+) create mode 100644 RELEASING.md create mode 100644 shipit.yml diff --git a/RELEASING.md b/RELEASING.md new file mode 100644 index 0000000..13f1510 --- /dev/null +++ b/RELEASING.md @@ -0,0 +1,8 @@ +1. Merge your branch into master +2. Run `npm version` which will do the following: + * write new version to package.json + * create a new commit with a commit message matching the version number + * create a new tag matching the version number +3. Push the new commit and tags to master with `git push origin master --tags` +4. Create a release on Github. Include a change log in the description +5. Deploy via Shipit \ No newline at end of file diff --git a/shipit.yml b/shipit.yml new file mode 100644 index 0000000..67d0733 --- /dev/null +++ b/shipit.yml @@ -0,0 +1,6 @@ +deploy: + override: + - rm -rf node_modules + - yarn install + - yarn run test + - npm publish From 6116acec85f5e006cd362bb3dd10d0969e6dcfa0 Mon Sep 17 00:00:00 2001 From: Jamie Dwyer Date: Tue, 6 Mar 2018 16:44:33 -0500 Subject: [PATCH 46/62] 1.0.0-alpha.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cbd8238..d232d74 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@shopify/shopify-express", - "version": "1.0.0-alpha.6", + "version": "1.0.0-alpha.7", "author": "Shopify Inc.", "description": "Get up and running quickly with Express.js and the Shopify API.", "keywords": [ From 7da14dfafba9e37de2022f2cfafb320ec41ee24d Mon Sep 17 00:00:00 2001 From: Jamie Dwyer Date: Tue, 6 Mar 2018 17:06:27 -0500 Subject: [PATCH 47/62] Update RELEASING.md --- RELEASING.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/RELEASING.md b/RELEASING.md index 13f1510..b43c86c 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -1,8 +1,8 @@ 1. Merge your branch into master -2. Run `npm version` which will do the following: +2. Run `npm version [version]` which will do the following: * write new version to package.json * create a new commit with a commit message matching the version number * create a new tag matching the version number 3. Push the new commit and tags to master with `git push origin master --tags` 4. Create a release on Github. Include a change log in the description -5. Deploy via Shipit \ No newline at end of file +5. Deploy via Shipit From 70cc358f06a61ee2b569601f3574963a434d3e39 Mon Sep 17 00:00:00 2001 From: Adam Waselnuk Date: Wed, 7 Mar 2018 09:27:11 -0500 Subject: [PATCH 48/62] Remove CHANGELOG.md --- CHANGELOG.md | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index 153b7c8..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1,6 +0,0 @@ -# Change Log - -## v1.0.0-alpha.1 (11-06-2017) - -* Initial release including `shopifyRouter`, `withShop`, and `withWebhook` - From c823cf1154aebe81ace4609918681f546f5aef14 Mon Sep 17 00:00:00 2001 From: Barry Carlyon Date: Fri, 16 Mar 2018 09:57:43 +0000 Subject: [PATCH 49/62] Redis Strategy does not account for the redis key not existing --- strategies/RedisStrategy.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/strategies/RedisStrategy.js b/strategies/RedisStrategy.js index 41b658a..80ee8d1 100644 --- a/strategies/RedisStrategy.js +++ b/strategies/RedisStrategy.js @@ -22,7 +22,11 @@ module.exports = class RedisStrategy { return done(err); } - done(null, shopData); + if (shopData) { + done(null, shopData); + } else { + done(null, {}); + } }); } }; From 523cdf598b3437033110731ba988943ce348e345 Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Tue, 20 Mar 2018 13:28:53 -0400 Subject: [PATCH 50/62] =?UTF-8?q?=F0=9F=97=92=EF=B8=8F=20Clean=20up=20READ?= =?UTF-8?q?ME?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 0f85dc4..feb0ca1 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ const {routes, withShop} = shopifyExpress({ }, }); -// mounts '/auth' and '/api' off of '/' +// mounts '/auth' and '/api' off of '/shopify' app.use('/shopify', routes); // shields myAppMiddleware from being accessed without session @@ -173,7 +173,7 @@ It is possible to use auth without a session key on your request, but not recomm This library handles body parsing on it's own for webhooks. If you're using webhooks you should make sure to follow express best-practices by only adding your body parsing middleware to specific routes that need it. **Good** -``` +```javascript app.use('/some-route', bodyParser.json(), myHandler); app.use('/webhook', withWebhook(myWebhookHandler)); @@ -181,7 +181,7 @@ This library handles body parsing on it's own for webhooks. If you're using webh ``` **Bad** -``` +```javascript app.use(bodyParser.json()); app.use('/some-route', myHandler); From ce6beb73f659d868dde7dd5f31193da47affd6c3 Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Tue, 20 Mar 2018 11:47:45 -0400 Subject: [PATCH 51/62] =?UTF-8?q?=F0=9F=8E=A8=20refactor=20strategies=20to?= =?UTF-8?q?=20use=20promises?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- middleware/webhooks.js | 24 ++++++++++------------- package.json | 2 +- routes/shopifyAuth.js | 14 ++++++------- strategies/MemoryStrategy.js | 14 +++++-------- strategies/RedisStrategy.js | 18 ++++++++--------- strategies/SQLStrategy.js | 38 +++++++++++++----------------------- yarn.lock | 19 +++++++++++++++++- 7 files changed, 64 insertions(+), 65 deletions(-) diff --git a/middleware/webhooks.js b/middleware/webhooks.js index 3c8b5be..f6ed4c2 100644 --- a/middleware/webhooks.js +++ b/middleware/webhooks.js @@ -3,7 +3,7 @@ const getRawBody = require('raw-body'); module.exports = function configureWithWebhook({ secret, shopStore }) { return function createWebhookHandler(onVerified) { - return async function withWebhook(request, response) { + return async function withWebhook(request, response, next) { const { body: data } = request; const hmac = request.get('X-Shopify-Hmac-Sha256'); const topic = request.get('X-Shopify-Topic'); @@ -22,22 +22,18 @@ module.exports = function configureWithWebhook({ secret, shopStore }) { return; } - shopStore.getShop({ shop: shopDomain }, (error, { accessToken }) => { - if (error) { - response.status(401).send(); - onVerified(new Error("Couldn't fetch credentials for shop")); - return; - } + const {accessToken} = await shopStore.getShop({ shop: shopDomain }); - request.body = rawBody.toString('utf8'); - request.webhook = { topic, shopDomain, accessToken }; + request.body = rawBody.toString('utf8'); + request.webhook = { topic, shopDomain, accessToken }; - response.status(200).send(); + response.status(200).send(); - onVerified(null, request); - }); - } catch(error) { - response.send(error); + onVerified(null, request); + } catch (error) { + response.status(401).send(); + onVerified(new Error("Unable to verify request HMAC")); + return; } }; } diff --git a/package.json b/package.json index d232d74..9fb16ac 100644 --- a/package.json +++ b/package.json @@ -34,11 +34,11 @@ "connect-redis": "^3.3.0", "express": "^4.16.2", "express-session": "^1.15.3", + "handy-redis": "^1.3.0", "knex": "^0.13.0", "node-fetch": "^1.7.3", "prop-types": "^15.6.1", "raw-body": "^2.3.2", - "redis": "^2.7.1", "sqlite3": "^3.1.9", "url": "^0.11.0" }, diff --git a/routes/shopifyAuth.js b/routes/shopifyAuth.js index 1d9842c..2d3ada1 100644 --- a/routes/shopifyAuth.js +++ b/routes/shopifyAuth.js @@ -49,7 +49,7 @@ module.exports = function createShopifyAuthRoutes({ // Users are redirected here after clicking `Install`. // The redirect from Shopify contains the authorization_code query parameter, // which the app exchanges for an access token - async callback(request, response) { + async callback(request, response, next) { const { query } = request; const { code, hmac, shop } = query; @@ -87,13 +87,10 @@ module.exports = function createShopifyAuthRoutes({ }); const responseBody = await remoteResponse.json(); - const accessToken = responseBody.access_token; - shopStore.storeShop({ accessToken, shop }, (err, token) => { - if (err) { - console.error('🔴 Error storing shop access token', err); - } + try { + const {token} = await shopStore.storeShop({ accessToken, shop }) if (request.session) { request.session.accessToken = accessToken; @@ -103,7 +100,10 @@ module.exports = function createShopifyAuthRoutes({ } afterAuth(request, response); - }); + } catch (error) { + console.error('🔴 Error storing shop access token', error); + next(error); + } } }; }; diff --git a/strategies/MemoryStrategy.js b/strategies/MemoryStrategy.js index b5769e3..99be6d1 100644 --- a/strategies/MemoryStrategy.js +++ b/strategies/MemoryStrategy.js @@ -3,17 +3,13 @@ module.exports = class MemoryStrategy { this.store = {}; } - storeShop({ shop, accessToken, data = {} }, done) { - this.store[shop] = Object.assign( - {}, - { accessToken }, - data - ); + async storeShop({ shop, accessToken }) { + this.store[shop] = {accessToken}; - return done(null, accessToken); + return {accessToken}; } - getShop({ shop }, done) { - return done(null, this.store[shop]); + async getShop({ shop }) { + return this.store[shop]; } }; diff --git a/strategies/RedisStrategy.js b/strategies/RedisStrategy.js index 80ee8d1..20229df 100644 --- a/strategies/RedisStrategy.js +++ b/strategies/RedisStrategy.js @@ -1,21 +1,17 @@ -const Redis = require('redis'); +const Redis = require('handy-redis'); module.exports = class RedisStrategy { constructor(redisConfig) { this.client = Redis.createClient(redisConfig); } - storeShop({ shop, accessToken, data = {} }, done) { - const shopData = Object.assign({}, { accessToken }, data); - this.client.hmset(shop, shopData, err => { - if (err) { - done(err); - } + async storeShop({ shop, accessToken }) { + await this.client.hmset(shop, {accessToken}) - done(null, shopData); - }); + return {accessToken}; } +<<<<<<< HEAD getShop({ shop }, done) { this.client.hgetall(shop, (err, shopData) => { if (err) { @@ -28,5 +24,9 @@ module.exports = class RedisStrategy { done(null, {}); } }); +======= + async getShop({ shop }) { + return await this.client.hgetall(shop) || {}; +>>>>>>> 🎨 refactor strategies to use promises } }; diff --git a/strategies/SQLStrategy.js b/strategies/SQLStrategy.js index 8da19e8..cfc0561 100644 --- a/strategies/SQLStrategy.js +++ b/strategies/SQLStrategy.js @@ -14,33 +14,23 @@ module.exports = class SQLStrategy { } initialize() { - return this.knex.schema - .createTableIfNotExists('shops', table => { - table.increments('id'); - table.string('shopify_domain'); - table.string('access_token'); - table.unique('shopify_domain'); - }) - .catch(error => { - console.log(error); - }); + return this.knex.schema.createTableIfNotExists('shops', table => { + table.increments('id'); + table.string('shopify_domain'); + table.string('access_token'); + table.unique('shopify_domain'); + }); } - storeShop({ shop, accessToken, data = {} }, done) { - this.knex - .raw( - `INSERT OR IGNORE INTO shops (shopify_domain, access_token) VALUES ('${shop}', '${accessToken}')` - ) - .then(result => { - return done(null, accessToken); - }); + async storeShop({ shop, accessToken }) { + await this.knex.raw( + `INSERT OR IGNORE INTO shops (shopify_domain, access_token) VALUES ('${shop}', '${accessToken}')` + ); + + return {accessToken}; } - getShop({ shop }, done) { - this.knex('shops') - .where('shopify_domain', shop) - .then(result => { - return done(null, result); - }); + getShop({ shop }) { + return this.knex('shops').where('shopify_domain', shop) } }; diff --git a/yarn.lock b/yarn.lock index 67f9b80..14fa052 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,6 +2,16 @@ # yarn lockfile v1 +"@types/node@*": + version "9.4.7" + resolved "https://registry.yarnpkg.com/@types/node/-/node-9.4.7.tgz#57d81cd98719df2c9de118f2d5f3b1120dcd7275" + +"@types/redis@^0.12.36": + version "0.12.36" + resolved "https://registry.yarnpkg.com/@types/redis/-/redis-0.12.36.tgz#61df652a269af425fe2e9ee95cdfa175577b4c3b" + dependencies: + "@types/node" "*" + abab@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e" @@ -1199,6 +1209,13 @@ handlebars@^4.0.3: optionalDependencies: uglify-js "^2.6" +handy-redis@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/handy-redis/-/handy-redis-1.3.0.tgz#c2dbc38da09f9fe3a20c36d4bc5f459ec865123c" + dependencies: + "@types/redis" "^0.12.36" + redis "^2.8.0" + har-schema@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" @@ -2710,7 +2727,7 @@ redis-parser@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.6.0.tgz#52ed09dacac108f1a631c07e9b69941e7a19504b" -redis@^2.1.0, redis@^2.7.1: +redis@^2.1.0, redis@^2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/redis/-/redis-2.8.0.tgz#202288e3f58c49f6079d97af7a10e1303ae14b02" dependencies: From 1f6dd42b14776bc0ac050b931ebfbd08d9bf7024 Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Tue, 20 Mar 2018 11:48:03 -0400 Subject: [PATCH 52/62] =?UTF-8?q?=F0=9F=97=92=EF=B8=8F=20update=20readme?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index feb0ca1..2ae4a62 100644 --- a/README.md +++ b/README.md @@ -132,10 +132,9 @@ If you do not have a table already created for your store, you can generate one ```javascript class Strategy { // shop refers to the shop's domain name - getShop({ shop }, done)) + getShop({ shop }): Promise<{accessToken: string}> // shop refers to the shop's domain name - // data can by any serializable object - storeShop({ shop, accessToken, data }, done) + storeShop({ shop, accessToken }): Promise<{accessToken: string}> } ``` From 3da56c56ad5888180cea6f5328a9c0677b0f33c8 Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Mon, 26 Mar 2018 10:44:23 -0400 Subject: [PATCH 53/62] =?UTF-8?q?=F0=9F=91=8C=20manual=20promise=20wrapper?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.json | 1 - strategies/RedisStrategy.js | 25 ++++++++----------------- yarn.lock | 19 +------------------ 3 files changed, 9 insertions(+), 36 deletions(-) diff --git a/package.json b/package.json index 9fb16ac..fc45f01 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,6 @@ "connect-redis": "^3.3.0", "express": "^4.16.2", "express-session": "^1.15.3", - "handy-redis": "^1.3.0", "knex": "^0.13.0", "node-fetch": "^1.7.3", "prop-types": "^15.6.1", diff --git a/strategies/RedisStrategy.js b/strategies/RedisStrategy.js index 20229df..080169c 100644 --- a/strategies/RedisStrategy.js +++ b/strategies/RedisStrategy.js @@ -1,8 +1,14 @@ -const Redis = require('handy-redis'); +const util = require('util'); +const redis = require('redis'); module.exports = class RedisStrategy { constructor(redisConfig) { - this.client = Redis.createClient(redisConfig); + const client = redis.createClient(redisConfig); + + this.client = { + hgetall: util.promisify(client.hgetall).bind(client), + hmset: util.promisify(client.hmset).bind(client), + } } async storeShop({ shop, accessToken }) { @@ -11,22 +17,7 @@ module.exports = class RedisStrategy { return {accessToken}; } -<<<<<<< HEAD - getShop({ shop }, done) { - this.client.hgetall(shop, (err, shopData) => { - if (err) { - return done(err); - } - - if (shopData) { - done(null, shopData); - } else { - done(null, {}); - } - }); -======= async getShop({ shop }) { return await this.client.hgetall(shop) || {}; ->>>>>>> 🎨 refactor strategies to use promises } }; diff --git a/yarn.lock b/yarn.lock index 14fa052..3160a6a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,16 +2,6 @@ # yarn lockfile v1 -"@types/node@*": - version "9.4.7" - resolved "https://registry.yarnpkg.com/@types/node/-/node-9.4.7.tgz#57d81cd98719df2c9de118f2d5f3b1120dcd7275" - -"@types/redis@^0.12.36": - version "0.12.36" - resolved "https://registry.yarnpkg.com/@types/redis/-/redis-0.12.36.tgz#61df652a269af425fe2e9ee95cdfa175577b4c3b" - dependencies: - "@types/node" "*" - abab@^1.0.3: version "1.0.4" resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e" @@ -1209,13 +1199,6 @@ handlebars@^4.0.3: optionalDependencies: uglify-js "^2.6" -handy-redis@^1.3.0: - version "1.3.0" - resolved "https://registry.yarnpkg.com/handy-redis/-/handy-redis-1.3.0.tgz#c2dbc38da09f9fe3a20c36d4bc5f459ec865123c" - dependencies: - "@types/redis" "^0.12.36" - redis "^2.8.0" - har-schema@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/har-schema/-/har-schema-1.0.5.tgz#d263135f43307c02c602afc8fe95970c0151369e" @@ -2727,7 +2710,7 @@ redis-parser@^2.6.0: version "2.6.0" resolved "https://registry.yarnpkg.com/redis-parser/-/redis-parser-2.6.0.tgz#52ed09dacac108f1a631c07e9b69941e7a19504b" -redis@^2.1.0, redis@^2.8.0: +redis@^2.1.0: version "2.8.0" resolved "https://registry.yarnpkg.com/redis/-/redis-2.8.0.tgz#202288e3f58c49f6079d97af7a10e1303ae14b02" dependencies: From 11e2d1d67132397030f3078507f9eb51c91449ff Mon Sep 17 00:00:00 2001 From: Kaelig Deloumeau-Prigent Date: Mon, 2 Apr 2018 16:25:46 -0700 Subject: [PATCH 54/62] Delete shipit.yml --- shipit.yml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 shipit.yml diff --git a/shipit.yml b/shipit.yml deleted file mode 100644 index 67d0733..0000000 --- a/shipit.yml +++ /dev/null @@ -1,6 +0,0 @@ -deploy: - override: - - rm -rf node_modules - - yarn install - - yarn run test - - npm publish From e5b8c0680c9b78e4c7927d3b6157eddbe9811dd3 Mon Sep 17 00:00:00 2001 From: Kaelig Deloumeau-Prigent Date: Mon, 2 Apr 2018 16:27:29 -0700 Subject: [PATCH 55/62] Add publishConfig.access to package.json --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index fc45f01..84c1e94 100644 --- a/package.json +++ b/package.json @@ -29,6 +29,9 @@ "prettier:check" ] }, + "publishConfig": { + "access": "public" + }, "dependencies": { "body-parser": "^1.18.2", "connect-redis": "^3.3.0", From 988734e4b5cf33fae2e968512dc6eb17b6db59cd Mon Sep 17 00:00:00 2001 From: Mallory Allen Date: Thu, 30 Aug 2018 11:16:22 -0400 Subject: [PATCH 56/62] =?UTF-8?q?=F0=9F=97=92=EF=B8=8F=20document=20deprec?= =?UTF-8?q?ation=20of=20the=20package?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 2ae4a62..23d0162 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,16 @@ -# shopify-express +## Deprecation -A small set of abstractions that will help you quickly build an Express.js app that consumes the Shopify API. +:exclamation: **This project is deprecated**. This means Shopify will not be maintaining it going forward. If you are interested in building a Shopify app using first party tools then check out our other libraries: + +* [@shopify/koa-shopify-auth](https://github.com/Shopify/quilt/tree/master/packages/koa-shopify-auth) +* [@shopify/koa-shopify-graphql-proxy](https://github.com/Shopify/quilt/blob/master/packages/koa-shopify-graphql-proxy/README.md) +* [shopify_app](https://github.com/Shopify/shopify_app) -:exclamation: **This project is currently in alpha status**. This means that the API could change at any time. It also means that your feedback will have a big impact on how the project evolves, so please feel free to [open issues](https://github.com/shopify/shopify-express/issues) if there is something you would like to see added. +These are all used internally and written against technologies we use for our own applications. Of course, if you wish to continue using Express, feel free to fork this codebase and continue it as you wish. +# shopify-express + +A small set of abstractions that will help you quickly build an Express.js app that consumes the Shopify API. ## Example From c936472408275f831dcfe8cef334d98cd10fc333 Mon Sep 17 00:00:00 2001 From: Mike Ragalie Date: Mon, 3 Sep 2018 15:12:16 -0400 Subject: [PATCH 57/62] Handle ITP2 correctly --- middleware/withShop.js | 4 + package.json | 1 + routes/index.js | 7 +- routes/shopifyAuth.js | 457 +++++++++++++++++- .../__snapshots__/shopifyAuth.test.js.snap | 20 +- routes/tests/shopifyAuth.test.js | 56 ++- yarn.lock | 7 + 7 files changed, 515 insertions(+), 37 deletions(-) diff --git a/middleware/withShop.js b/middleware/withShop.js index 4f69cbe..0456c37 100644 --- a/middleware/withShop.js +++ b/middleware/withShop.js @@ -1,3 +1,5 @@ +const TEST_COOKIE_NAME = 'shopifyTestCookie'; + module.exports = function withShop({ authBaseUrl } = {}) { return function verifyRequest(request, response, next) { const { query: { shop }, session, baseUrl } = request; @@ -7,6 +9,8 @@ module.exports = function withShop({ authBaseUrl } = {}) { return; } + response.cookie(TEST_COOKIE_NAME, '1'); + if (shop) { response.redirect(`${authBaseUrl || baseUrl}/auth?shop=${shop}`); return; diff --git a/package.json b/package.json index 84c1e94..b7e68d5 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "dependencies": { "body-parser": "^1.18.2", "connect-redis": "^3.3.0", + "cookie-parser": "^1.4.3", "express": "^4.16.2", "express-session": "^1.15.3", "knex": "^0.13.0", diff --git a/routes/index.js b/routes/index.js index 3bde606..7db0762 100644 --- a/routes/index.js +++ b/routes/index.js @@ -1,5 +1,6 @@ const express = require('express'); const bodyParser = require('body-parser'); +const cookieParser = require('cookie-parser') const createShopifyAuthRoutes = require('./shopifyAuth'); const shopifyApiProxy = require('./shopifyApiProxy'); @@ -7,10 +8,12 @@ const shopifyApiProxy = require('./shopifyApiProxy'); module.exports = function createRouter(shopifyConfig) { const router = express.Router(); const rawParser = bodyParser.raw({ type: '*/*' }); - const {auth, callback} = createShopifyAuthRoutes(shopifyConfig) + const simpleCookieParser = cookieParser(); + const {auth, callback, enableCookies} = createShopifyAuthRoutes(shopifyConfig) router.use('/auth/callback', callback); - router.use('/auth', auth); + router.use('/auth/enable_cookies', enableCookies); + router.use('/auth', simpleCookieParser, auth); router.use( '/api', rawParser, diff --git a/routes/shopifyAuth.js b/routes/shopifyAuth.js index 2d3ada1..a6c7b3e 100644 --- a/routes/shopifyAuth.js +++ b/routes/shopifyAuth.js @@ -2,6 +2,9 @@ const querystring = require('querystring'); const crypto = require('crypto'); const fetch = require('node-fetch'); +const TEST_COOKIE_NAME = 'shopifyTestCookie'; +const TOP_LEVEL_OAUTH_COOKIE_NAME = 'shopifyTopLevelOAuth'; + module.exports = function createShopifyAuthRoutes({ host, apiKey, @@ -21,6 +24,13 @@ module.exports = function createShopifyAuthRoutes({ return response.status(400).send('Expected a shop query parameter'); } + if (!request.cookies[TEST_COOKIE_NAME]) { + // This is to avoid a redirect loop if the app doesn't use verifyRequest or set the test cookie elsewhere. + response.cookie(TEST_COOKIE_NAME, '1'); + topLevelRedirect(response, `${host}${baseUrl}/enable_cookies?${querystring.stringify({shop})}`); + return; + } + const redirectTo = `https://${shop}/admin/oauth/authorize`; const redirectParams = { @@ -34,16 +44,13 @@ module.exports = function createShopifyAuthRoutes({ redirectParams['grant_options[]'] = 'per-user'; } - response.send( - ` - - - - - `, - ); + if (!request.cookies[TOP_LEVEL_OAUTH_COOKIE_NAME]) { + response.cookie(TOP_LEVEL_OAUTH_COOKIE_NAME, '1'); + topLevelRedirect(response, `${redirectTo}?${querystring.stringify(redirectParams)}`); + return; + } + + response.redirect(`${redirectTo}?${querystring.stringify(redirectParams)}`); }, // Users are redirected here after clicking `Install`. @@ -104,6 +111,436 @@ module.exports = function createShopifyAuthRoutes({ console.error('🔴 Error storing shop access token', error); next(error); } + }, + + enableCookies(request, response) { + const HEADING = 'Enable cookies'; + const BODY = 'You must manually enable cookies in this browser in order to use this app within Shopify.' + const FOOTER = `Cookies let the app authenticate you by temporarily storing your preferences and personal +information. They expire after 30 days.` + const ACTION = 'Enable cookies'; + + const { query, baseUrl } = request; + const { shop } = query; + + if (shop == null) { + return response.status(400).send('Expected a shop query parameter'); + } + + response.send(` + + + + + + Redirecting… + + + + +
+
+
+
+
+
+
+
+
+

${HEADING}

+
+
+

${BODY}

+
+
+

${FOOTER}

+
+
+
+
+
+
+ +
+
+
+
+
+
+
+
+
+ +`); } }; }; + +function topLevelRedirect(response, url) { + response.send( + ` + + + + + `, + ); +} diff --git a/routes/tests/__snapshots__/shopifyAuth.test.js.snap b/routes/tests/__snapshots__/shopifyAuth.test.js.snap index 6dd894c..36a7e14 100644 --- a/routes/tests/__snapshots__/shopifyAuth.test.js.snap +++ b/routes/tests/__snapshots__/shopifyAuth.test.js.snap @@ -1,14 +1,14 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`shopifyAuth / responds to get requests by returning a redirect page 1`] = ` +exports[`shopifyAuth / responds with a 400 when no shop query parameter is given 1`] = `"Expected a shop query parameter"`; + +exports[`shopifyAuth / with cookie access but without a prior top-level attempt responds to get requests by returning a redirect page 1`] = ` " - - - - - " + + + + + " `; - -exports[`shopifyAuth / responds with a 400 when no shop query parameter is given 1`] = `"Expected a shop query parameter"`; diff --git a/routes/tests/shopifyAuth.test.js b/routes/tests/shopifyAuth.test.js index adc0b6c..81feaf1 100644 --- a/routes/tests/shopifyAuth.test.js +++ b/routes/tests/shopifyAuth.test.js @@ -2,6 +2,7 @@ const findFreePort = require('find-free-port') const fetch = require('node-fetch'); const http = require('http'); const express = require('express'); +const cookieParser = require('cookie-parser') const { MemoryStrategy } = require('../../strategies'); const createShopifyAuthRoutes = require('../shopifyAuth'); @@ -22,23 +23,48 @@ describe('shopifyAuth', async () => { }); describe('/', () => { - it('responds to get requests by returning a redirect page', async () => { - const response = await fetch(`${BASE_URL}/auth?shop=shop1`); - const data = await response.text(); - - expect(response.status).toBe(200); - expect(data).toMatchSnapshot(); + describe('without cookie access', () => { + it('top-level redirects to the enable_cookies page', async () => { + const response = await fetch(`${BASE_URL}/auth?shop=shop1`); + const data = await response.text(); + + expect(response.status).toBe(200); + expect(data).toContain(`window.top.location.href = "${BASE_URL}/auth/enable_cookies?shop=shop1"`); + }); }); - it('redirect page includes per-user grant for accessMode: online', async () => { - await server.close(); - server = await createServer({accessMode: 'online'}); + describe('with cookie access but without a prior top-level attempt', () => { + it('responds to get requests by returning a redirect page', async () => { + const headers = {cookie: 'shopifyTestCookie=1;'}; + const response = await fetch(`${BASE_URL}/auth?shop=shop1`, {headers}); + const data = await response.text(); - const response = await fetch(`${BASE_URL}/auth?shop=shop1`); - const data = await response.text(); + expect(response.status).toBe(200); + expect(data).toMatchSnapshot(); + }); + + it('redirect page includes per-user grant for accessMode: online', async () => { + await server.close(); + server = await createServer({accessMode: 'online'}); + + const headers = {cookie: 'shopifyTestCookie=1;'}; + const response = await fetch(`${BASE_URL}/auth?shop=shop1`, {headers}); + const data = await response.text(); + + expect(response.status).toBe(200); + expect(data).toContain('grant_options%5B%5D=per-user'); + }); + }); + + describe('with cookie access and a prior top-level attempt', () => { + it('redirects directly to the grant page', async () => { + const headers = {cookie: 'shopifyTestCookie=1; shopifyTopLevelOAuth=1;'}; + const response = await fetch(`${BASE_URL}/auth?shop=shop1`, {headers, redirect: 'manual'}); + const data = await response.text(); - expect(response.status).toBe(200); - expect(data).toContain('grant_options%5B%5D=per-user'); + expect(response.status).toBe(302); + expect(response.headers.get('location')).toContain('https://shop1/admin/oauth/authorize'); + }); }); it('responds with a 400 when no shop query parameter is given', async () => { @@ -73,7 +99,7 @@ function createServer(userConfig = {}) { const app = express(); const serverConfig = { - host: 'http://myshop.myshopify.com', + host: BASE_URL, apiKey: 'key', secret: 'secret', scope: ['scope'], @@ -84,7 +110,7 @@ function createServer(userConfig = {}) { const {auth, callback} = createShopifyAuthRoutes(Object.assign({}, serverConfig, userConfig)); - app.use('/auth', auth); + app.use('/auth', cookieParser(), auth); app.use('/auth/callback', callback); server = http.createServer(app); diff --git a/yarn.lock b/yarn.lock index 3160a6a..cc9b09c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -560,6 +560,13 @@ convert-source-map@^1.4.0, convert-source-map@^1.5.0: version "1.5.1" resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-1.5.1.tgz#b8278097b9bc229365de5c62cf5fcaed8b5599e5" +cookie-parser@^1.4.3: + version "1.4.3" + resolved "https://registry.yarnpkg.com/cookie-parser/-/cookie-parser-1.4.3.tgz#0fe31fa19d000b95f4aadf1f53fdc2b8a203baa5" + dependencies: + cookie "0.3.1" + cookie-signature "1.0.6" + cookie-signature@1.0.6: version "1.0.6" resolved "https://registry.yarnpkg.com/cookie-signature/-/cookie-signature-1.0.6.tgz#e303a882b342cc3ee8ca513a79999734dab3ae2c" From dc84accbdb414a9fc5c3e53632b92b627d9b4dc0 Mon Sep 17 00:00:00 2001 From: Mike Ragalie Date: Tue, 4 Sep 2018 07:54:54 -0400 Subject: [PATCH 58/62] Clear top-level cookie afterward This way if something fails in the top-level OAuth we can recover by retrying. --- routes/shopifyAuth.js | 1 + routes/tests/shopifyAuth.test.js | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/routes/shopifyAuth.js b/routes/shopifyAuth.js index a6c7b3e..8cc7306 100644 --- a/routes/shopifyAuth.js +++ b/routes/shopifyAuth.js @@ -50,6 +50,7 @@ module.exports = function createShopifyAuthRoutes({ return; } + response.clearCookie(TOP_LEVEL_OAUTH_COOKIE_NAME); response.redirect(`${redirectTo}?${querystring.stringify(redirectParams)}`); }, diff --git a/routes/tests/shopifyAuth.test.js b/routes/tests/shopifyAuth.test.js index 81feaf1..84eb97e 100644 --- a/routes/tests/shopifyAuth.test.js +++ b/routes/tests/shopifyAuth.test.js @@ -57,13 +57,14 @@ describe('shopifyAuth', async () => { }); describe('with cookie access and a prior top-level attempt', () => { - it('redirects directly to the grant page', async () => { + it('redirects directly to the grant page and removes top-level cookie', async () => { const headers = {cookie: 'shopifyTestCookie=1; shopifyTopLevelOAuth=1;'}; const response = await fetch(`${BASE_URL}/auth?shop=shop1`, {headers, redirect: 'manual'}); const data = await response.text(); expect(response.status).toBe(302); expect(response.headers.get('location')).toContain('https://shop1/admin/oauth/authorize'); + expect(response.headers.get('cookie')).toBe(null); }); }); From e9fce7161c83571ed89a465f67c1b3711f01ce6a Mon Sep 17 00:00:00 2001 From: Mike Ragalie Date: Tue, 4 Sep 2018 23:36:23 -0400 Subject: [PATCH 59/62] Extract test cookie name to constant --- constants.js | 1 + middleware/withShop.js | 2 +- routes/shopifyAuth.js | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 constants.js diff --git a/constants.js b/constants.js new file mode 100644 index 0000000..e7aec91 --- /dev/null +++ b/constants.js @@ -0,0 +1 @@ +module.exports.TEST_COOKIE_NAME = 'shopifyTestCookie'; diff --git a/middleware/withShop.js b/middleware/withShop.js index 0456c37..c30b533 100644 --- a/middleware/withShop.js +++ b/middleware/withShop.js @@ -1,4 +1,4 @@ -const TEST_COOKIE_NAME = 'shopifyTestCookie'; +const {TEST_COOKIE_NAME} = require('../constants'); module.exports = function withShop({ authBaseUrl } = {}) { return function verifyRequest(request, response, next) { diff --git a/routes/shopifyAuth.js b/routes/shopifyAuth.js index 8cc7306..617f649 100644 --- a/routes/shopifyAuth.js +++ b/routes/shopifyAuth.js @@ -2,7 +2,8 @@ const querystring = require('querystring'); const crypto = require('crypto'); const fetch = require('node-fetch'); -const TEST_COOKIE_NAME = 'shopifyTestCookie'; +const {TEST_COOKIE_NAME} = require('../constants'); + const TOP_LEVEL_OAUTH_COOKIE_NAME = 'shopifyTopLevelOAuth'; module.exports = function createShopifyAuthRoutes({ From 74d6509299e0ef3a916a6cf867e2c5231774fddc Mon Sep 17 00:00:00 2001 From: Mike Ragalie Date: Tue, 4 Sep 2018 23:51:10 -0400 Subject: [PATCH 60/62] Don't display prompt for pos and mobile ITP doesn't apply to WebKitView --- routes/shopifyAuth.js | 44 ++++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/routes/shopifyAuth.js b/routes/shopifyAuth.js index 617f649..b364084 100644 --- a/routes/shopifyAuth.js +++ b/routes/shopifyAuth.js @@ -475,22 +475,36 @@ information. They expire after 30 days.` window.apiKey = "${apiKey}"; window.shopOrigin = "https://${shop}"; - function setCookieAndRedirect() { - document.cookie = "shopify.cookies_persist=true"; - window.location.href = window.shopOrigin + "/admin/apps/" + window.apiKey; - } - - document.addEventListener("DOMContentLoaded", function() { - if (document.hasStorageAccess) { - var itpContent = document.querySelector('#CookiePartitionPrompt'); - itpContent.style.display = 'block'; - - var button = document.querySelector('#AcceptCookies'); - button.addEventListener('click', setCookieAndRedirect); - } else { - setCookieAndRedirect(); + (function() { + function setCookieAndRedirect() { + document.cookie = "shopify.cookies_persist=true"; + window.location.href = window.shopOrigin + "/admin/apps/" + window.apiKey; + } + + function shouldDisplayPrompt() { + if (navigator.userAgent.indexOf('com.jadedpixel.pos') !== -1) { + return false; + } + + if (navigator.userAgent.indexOf('Shopify Mobile/iOS') !== -1) { + return false; + } + + return Boolean(document.hasStorageAccess); } - }); + + document.addEventListener("DOMContentLoaded", function() { + if (shouldDisplayPrompt()) { + var itpContent = document.querySelector('#CookiePartitionPrompt'); + itpContent.style.display = 'block'; + + var button = document.querySelector('#AcceptCookies'); + button.addEventListener('click', setCookieAndRedirect); + } else { + setCookieAndRedirect(); + } + }); + })(); From eccac0bcb5724fbd6339eb8c530842a69cb81cbe Mon Sep 17 00:00:00 2001 From: Mike Ragalie Date: Wed, 5 Sep 2018 08:52:20 -0400 Subject: [PATCH 61/62] Clear top-level oauth cookie on success If we later lose the session, we want to start the oauth process over from the beginning. --- constants.js | 1 + middleware/withShop.js | 3 ++- routes/shopifyAuth.js | 4 +--- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/constants.js b/constants.js index e7aec91..3f0765b 100644 --- a/constants.js +++ b/constants.js @@ -1 +1,2 @@ module.exports.TEST_COOKIE_NAME = 'shopifyTestCookie'; +module.exports.TOP_LEVEL_OAUTH_COOKIE_NAME = 'shopifyTopLevelOAuth'; diff --git a/middleware/withShop.js b/middleware/withShop.js index c30b533..e550a7f 100644 --- a/middleware/withShop.js +++ b/middleware/withShop.js @@ -1,10 +1,11 @@ -const {TEST_COOKIE_NAME} = require('../constants'); +const {TEST_COOKIE_NAME, TOP_LEVEL_OAUTH_COOKIE_NAME} = require('../constants'); module.exports = function withShop({ authBaseUrl } = {}) { return function verifyRequest(request, response, next) { const { query: { shop }, session, baseUrl } = request; if (session && session.accessToken) { + response.cookie(TOP_LEVEL_OAUTH_COOKIE_NAME); next(); return; } diff --git a/routes/shopifyAuth.js b/routes/shopifyAuth.js index b364084..317ab68 100644 --- a/routes/shopifyAuth.js +++ b/routes/shopifyAuth.js @@ -2,9 +2,7 @@ const querystring = require('querystring'); const crypto = require('crypto'); const fetch = require('node-fetch'); -const {TEST_COOKIE_NAME} = require('../constants'); - -const TOP_LEVEL_OAUTH_COOKIE_NAME = 'shopifyTopLevelOAuth'; +const {TEST_COOKIE_NAME, TOP_LEVEL_OAUTH_COOKIE_NAME} = require('../constants'); module.exports = function createShopifyAuthRoutes({ host, From dd512278e75e264597f2d12bbe68670daf2910d7 Mon Sep 17 00:00:00 2001 From: Mike Ragalie Date: Wed, 5 Sep 2018 12:54:06 -0400 Subject: [PATCH 62/62] 1.0.0-alpha.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index b7e68d5..5f81345 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@shopify/shopify-express", - "version": "1.0.0-alpha.7", + "version": "1.0.0-alpha.8", "author": "Shopify Inc.", "description": "Get up and running quickly with Express.js and the Shopify API.", "keywords": [