diff --git a/src/appmixer/shopify/CustomerDataRequest.js b/src/appmixer/shopify/CustomerDataRequest.js new file mode 100644 index 000000000..040e074d0 --- /dev/null +++ b/src/appmixer/shopify/CustomerDataRequest.js @@ -0,0 +1,26 @@ +module.exports = context => { + + class CustomerDataRequest extends context.db.Model { + + static get collection() { + return 'customerDataRequests'; + } + + static get idProperty() { + return 'id'; + } + + static get properties() { + + return [ + 'id', + 'request', + 'created' + ]; + } + } + + CustomerDataRequest.createSettersAndGetters(); + + return CustomerDataRequest; +}; diff --git a/src/appmixer/shopify/CustomerRedactRequest.js b/src/appmixer/shopify/CustomerRedactRequest.js new file mode 100644 index 000000000..0ae7a5870 --- /dev/null +++ b/src/appmixer/shopify/CustomerRedactRequest.js @@ -0,0 +1,26 @@ +module.exports = context => { + + class CustomerRedactRequest extends context.db.Model { + + static get collection() { + return 'customerRedactsRequests'; + } + + static get idProperty() { + return 'id'; + } + + static get properties() { + + return [ + 'id', + 'request', + 'created' + ]; + } + } + + CustomerRedactRequest.createSettersAndGetters(); + + return CustomerRedactRequest; +}; diff --git a/src/appmixer/shopify/ShopRedactRequest.js b/src/appmixer/shopify/ShopRedactRequest.js new file mode 100644 index 000000000..76f36ce6d --- /dev/null +++ b/src/appmixer/shopify/ShopRedactRequest.js @@ -0,0 +1,27 @@ +module.exports = context => { + + class ShopRedactRequest extends context.db.Model { + + static get collection() { + return 'shopRedactRequests'; + } + + static get idProperty() { + return 'id'; + } + + static get properties() { + + return [ + 'id', + 'request', + 'created' + ]; + } + } + + ShopRedactRequest.createSettersAndGetters(); + + return ShopRedactRequest; +}; + diff --git a/src/appmixer/shopify/package.json b/src/appmixer/shopify/package.json index 0aedc280a..71608a739 100644 --- a/src/appmixer/shopify/package.json +++ b/src/appmixer/shopify/package.json @@ -3,7 +3,8 @@ "version": "0.1.0", "author": "Jimoh Damilola ", "dependencies": { - "shopify-api-node": "3.12.1", + "crypto": "1.0.1", + "shopify-api-node": "3.13.1", "shopify-token": "4.0.0" } } diff --git a/src/appmixer/shopify/plugin.js b/src/appmixer/shopify/plugin.js new file mode 100644 index 000000000..1058a9dd9 --- /dev/null +++ b/src/appmixer/shopify/plugin.js @@ -0,0 +1,22 @@ +module.exports = async function(context) { + + const lock = await context.lock('shopify-plugin-init'); + + let secret; + try { + secret = await context.service.stateGet('secret'); + if (!secret) { + + await require('./CustomerDataRequest')(context).createIndex({ status: 1 }); + await require('./CustomerRedactRequest')(context).createIndex({ status: 1 }); + await require('./ShopRedactRequest')(context).createIndex({ status: 1 }); + + secret = require('crypto').randomBytes(128).toString('base64'); + await context.service.stateSet('secret', secret); + } + } finally { + lock.unlock(); + } + + require('./routes')(context); +}; diff --git a/src/appmixer/shopify/routes.js b/src/appmixer/shopify/routes.js new file mode 100644 index 000000000..301c2397a --- /dev/null +++ b/src/appmixer/shopify/routes.js @@ -0,0 +1,145 @@ +const crypto = require('crypto'); +const auth = require('./auth'); + +module.exports = function(context) { + + const CustomerDataRequest = require('./CustomerDataRequest')(context); + const CustomerRedactRequest = require('./CustomerRedactRequest')(context); + const ShopRedactRequest = require('./ShopRedactRequest')(context); + + /** + * Shopify install link, Shopify calls this link to install the app: + * https://api.qa.appmixer.com/plugins/appmixer/shopify/install?shop=appmixer.myshopify.com + * Set the base url (https://api.qa.appmixer.com/plugins/appmixer/shopify) in the Shopify app settings. + */ + context.http.router.register({ + method: 'GET', + path: '/install', + options: { + handler: (req, h) => { + + const { shop } = req.query; + const clientId = encodeURIComponent(context.config.clientId); + const redirectUrl = encodeURIComponent(context.config.appStoreInstallRedirectUri); + const scopes = encodeURIComponent(auth.definition.scope.join(',')); + + const authUrl = `https://${shop}/admin/oauth/authorize?client_id=${clientId}&redirect_uri=${redirectUrl}&scope=${scopes}`; + + return h.redirect(authUrl); + }, + auth: false + } + }); + + context.http.router.register({ + method: 'POST', + path: '/customers/data_request', + options: { + handler: (req) => { + + verifyHmac(req, context.config.clientSecret); + + const { payload } = req; + + return new CustomerDataRequest().populate({ + request: payload, + created: new Date() + }).save(); + }, + auth: false + } + }); + + context.http.router.register({ + method: 'POST', + path: '/customers/redact', + options: { + handler: (req) => { + + verifyHmac(req, context.config.clientSecret); + + const { payload } = req; + + return new CustomerRedactRequest().populate({ + request: payload, + created: new Date() + }).save(); + }, + auth: false + } + }); + + context.http.router.register({ + method: 'POST', + path: '/shop/redact', + options: { + handler: (req) => { + + verifyHmac(req, context.config.clientSecret); + + const { payload } = req; + + return new ShopRedactRequest().populate({ + request: payload, + created: new Date() + }).save(); + }, + auth: false + } + }); + + context.http.router.register({ + method: 'GET', + path: '/customers/data_request', + options: { + handler: () => { + return CustomerDataRequest.find() || []; + } + } + }); + + context.http.router.register({ + method: 'GET', + path: '/customers/redact', + options: { + handler: () => { + return CustomerRedactRequest.find() || []; + } + } + }); + + context.http.router.register({ + method: 'GET', + path: '/shop/redact', + options: { + handler: () => { + return ShopRedactRequest.find() || []; + } + } + }); + + const getSignature = function(data, secret) { + return crypto + .createHmac('sha256', secret) + .update(data) + .digest('base64'); + + }; + + /** + * verify HMAC token from Shopify + * https://shopify.dev/docs/apps/webhooks/configuration/https#step-5-verify-the-webhook + * @param req + * @param secret + */ + const verifyHmac = function(req, secret) { + + const expected = req.headers['x-shopify-hmac-sha256']; + const current = getSignature(JSON.stringify(req.payload), secret); + + if (current !== expected) { + throw context.http.HttpError.unauthorized('Invalid HMAC. ' + current + ' ' + expected); + } + }; +}; +