Skip to content
This repository was archived by the owner on Jul 14, 2022. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
87 commits
Select commit Hold shift + click to select a range
aa2bacd
don't expect body parser to be used
kevinhughes27 Nov 24, 2017
9e4d333
configure bodyParser.raw for the api proxy
kevinhughes27 Nov 27, 2017
870c2d1
Merge pull request #23 from Shopify/dont-expect-body-parser
kevinhughes27 Nov 27, 2017
4b3f282
v1.0.0-alpha.4
marutypes Nov 27, 2017
65c7e58
➕ add husky
marutypes Nov 28, 2017
b6acab5
👌 pretty command doesn't autofix
marutypes Nov 28, 2017
f49396e
✨ lint on commit
marutypes Nov 28, 2017
9067292
💅 pretty
marutypes Nov 28, 2017
20ff91d
👌 consolidate prettier commands
marutypes Nov 28, 2017
528d0ad
✨ configurable SQL strategy
marutypes Nov 28, 2017
1b7baaf
🗒️ update README
marutypes Nov 28, 2017
34eb4f3
Merge pull request #24 from Shopify/add-husky
marutypes Nov 28, 2017
e01a874
👌 change whitelist to blacklist
marutypes Nov 28, 2017
9ea77fe
✅ add tests to be filled out in subsequent pr
marutypes Nov 28, 2017
bc7735f
👌 remove unnecessary prefix to routes
marutypes Nov 28, 2017
91e9316
👌 add fulfillment services
marutypes Nov 28, 2017
4673002
👌 polish readme
marutypes Nov 28, 2017
93150f0
Merge pull request #27 from Shopify/configurable-sql-strategy
marutypes Nov 28, 2017
a428c16
Merge pull request #28 from Shopify/change-whitelist-to-blacklist
marutypes Nov 28, 2017
f788926
1.0.0-alpha.5
marutypes Nov 28, 2017
e12b571
🐛 fix usage of fetch with no polyfill
marutypes Dec 4, 2017
fcd44f1
Correct typo in README
extrablind Dec 5, 2017
c2ca747
➕ add node-fetch
marutypes Dec 7, 2017
1bce52c
✅ test api proxy
marutypes Dec 7, 2017
305fdf5
💅 refactor auth tests
marutypes Dec 7, 2017
fadd515
👌 remove smiley face from inline fixture
marutypes Dec 8, 2017
1c11883
Merge pull request #30 from extrablind/patch-2
marutypes Dec 8, 2017
e98a1cf
Merge pull request #31 from Shopify/fix/node-fetch-and-api-proxy-tests
marutypes Dec 11, 2017
6258029
Use correct secret and don't assume bodyParser.json
jamiemtdwyer Dec 12, 2017
9052678
Clean up webhook interface and further improvements
jamiemtdwyer Dec 16, 2017
0c010b6
Merge pull request #32 from Shopify/fix-webhook-middleware
jamiemtdwyer Dec 18, 2017
0960060
v1.0.0-alpha.6
jamiemtdwyer Jan 9, 2018
a2a8322
➕ add findFreePort and 🛠️ fix CI
marutypes Jan 13, 2018
e053f4d
Merge pull request #37 from Shopify/fix/CI
marutypes Jan 16, 2018
320d9d0
Fix missing fetch require
marekweb Jan 30, 2018
4c60a5b
Make correction to database field name in README
tomsouthall Feb 26, 2018
cfeb77c
Merge pull request #42 from marekweb/master
marutypes Feb 28, 2018
590effe
Merge pull request #46 from tomsouthall/patch-1
marutypes Feb 28, 2018
93963ab
➕✅ add prop-types for validating config objects
marutypes Mar 1, 2018
1702666
🛠️ add sane error when shop and / or accesstoken are not on session
marutypes Mar 1, 2018
d8777b9
👌 warn when no session is present on request context but dont break
marutypes Mar 1, 2018
353eef1
📓 update README
marutypes Mar 1, 2018
d2ae503
👌 error when session is null in shopifyApiProxy
marutypes Mar 1, 2018
866e774
Merge pull request #50 from Shopify/fix/session-shenanigans
marutypes Mar 1, 2018
71ed758
Merge pull request #49 from Shopify/enhancement/validate-config
marutypes Mar 1, 2018
2df84d8
🛠️ fix prop-types definition for shop store
marutypes Mar 1, 2018
52a5f77
✅ update snapshots
marutypes Mar 1, 2018
30a4a2e
Merge pull request #51 from Shopify/patch-wrong-casing-1
marutypes Mar 2, 2018
0b3ab6d
🛠️ use baseUrl to generate callback url
marutypes Mar 2, 2018
23c3f73
📓 update README
marutypes Mar 2, 2018
24e57e5
✅ update tests
marutypes Mar 2, 2018
86c54dd
🗒️ tweak readme
marutypes Mar 2, 2018
482175f
Merge pull request #53 from Shopify/fix/callbacks-use-router-baseUrl
marutypes Mar 2, 2018
0371f87
✨ add accessMode option to config and generate redirect urls based on it
marutypes Mar 2, 2018
b807e43
📓 add accessMode to the README
marutypes Mar 2, 2018
ad3edeb
Merge pull request #54 from Shopify/enhancement/add-accessModes
marutypes Mar 2, 2018
49bcb89
🛠️ fix missing ? in queryParam generation
marutypes Mar 2, 2018
ecb9e44
Bump to node 8.1.1
AWaselnuk Mar 2, 2018
3ea4270
Regenerated snapshots
jamiemtdwyer Mar 5, 2018
745b22a
Merge pull request #56 from Shopify/fix-auth-snapshot
jamiemtdwyer Mar 5, 2018
3937d49
First shot at adding shipit config
jamiemtdwyer Mar 5, 2018
08da4d5
Merge pull request #57 from Shopify/add-shipit-config
jamiemtdwyer Mar 6, 2018
6116ace
1.0.0-alpha.7
jamiemtdwyer Mar 6, 2018
7da14df
Update RELEASING.md
jamiemtdwyer Mar 6, 2018
70cc358
Remove CHANGELOG.md
AWaselnuk Mar 7, 2018
32c4e72
Merge pull request #59 from Shopify/remove-changelog
AWaselnuk Mar 7, 2018
c823cf1
Redis Strategy does not account for the redis key not existing
Mar 16, 2018
e127690
Merge pull request #66 from BarryCarlyon/blankredis
marutypes Mar 20, 2018
523cdf5
🗒️ Clean up README
marutypes Mar 20, 2018
8ed2cb3
Merge pull request #69 from Shopify/TheMallen-patch-1
jamiemtdwyer Mar 20, 2018
ce6beb7
🎨 refactor strategies to use promises
marutypes Mar 20, 2018
1f6dd42
🗒️ update readme
marutypes Mar 20, 2018
3da56c5
👌 manual promise wrapper
marutypes Mar 26, 2018
f7c05b6
Merge pull request #68 from Shopify/refactor/promisify-strategies
marutypes Mar 28, 2018
11e2d1d
Delete shipit.yml
kaelig Apr 2, 2018
e5b8c06
Add publishConfig.access to package.json
kaelig Apr 2, 2018
b443c2d
Merge pull request #71 from Shopify/delete-shipit
kaelig Apr 3, 2018
82c4977
Merge pull request #72 from Shopify/publishconfig
kaelig Apr 3, 2018
988734e
🗒️ document deprecation of the package
Aug 30, 2018
c936472
Handle ITP2 correctly
ragalie Sep 3, 2018
dc84acc
Clear top-level cookie afterward
ragalie Sep 4, 2018
e9fce71
Extract test cookie name to constant
ragalie Sep 5, 2018
74d6509
Don't display prompt for pos and mobile
ragalie Sep 5, 2018
eccac0b
Clear top-level oauth cookie on success
ragalie Sep 5, 2018
4c86406
Merge pull request #108 from Shopify/itp2-fixes
ragalie Sep 5, 2018
dd51227
1.0.0-alpha.8
ragalie Sep 5, 2018
a4016ea
Merge pull request #106 from Shopify/deprecate
marutypes Sep 6, 2018
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
8.1.0
8.1.1
6 changes: 0 additions & 6 deletions CHANGELOG.md

This file was deleted.

121 changes: 104 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
# 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

```javascript
const express = require('express');
const shopifyExpress = require('@shopify/shopify-express');
const session = require('express-session');

const app = express();

Expand All @@ -20,20 +28,27 @@ const {
NODE_ENV,
} = process.env;

const shopify = shopifyExpress({
// session is necessary for api proxy and auth verification
app.use(session({secret: SHOPIFY_APP_SECRET}));

const {routes, withShop} = shopifyExpress({
host: SHOPIFY_APP_HOST,
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
return response.redirect('/');
},
});

// mounts '/auth/shopify' and '/api' off of '/'
app.use('/', shopify.routes);
// mounts '/auth' and '/api' off of '/shopify'
app.use('/shopify', routes);

// shields myAppMiddleware from being accessed without session
app.use('/myApp', withShop({authBaseUrl: '/shopify'}), myAppMiddleware)
```

## Shopify routes
Expand Down Expand Up @@ -62,30 +77,71 @@ 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 MemoryStrategy(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(redisConfig),
...restOfConfig,
});
```

#### 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 RedisStrategy(),
shopStore: new SQLStrategy(knexConfig),
...restOfConfig,
});
```

### Custom Strategy
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.

`shopifyExpress` takes a `shopStore` parameter. This can be any javascript class matching the following interface:
#### Custom Strategy

`shopifyExpress` accepts any javascript class matching the following interface:

```javascript
class Strategy {
constructor(){}
// 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}>
}
```

Expand All @@ -95,20 +151,51 @@ const shopify = shopifyExpress({

### `withShop`

`app.use('/someProtectedPath', withShop, someHandler);`
`app.use('/someProtectedPath', withShop({authBaseUrl: '/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`

`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

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`.

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**
```javascript
app.use('/some-route', bodyParser.json(), myHandler);

app.use('/webhook', withWebhook(myWebhookHandler));
app.use('/', shopifyExpress.routes);
```

**Bad**
```javascript
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.
8 changes: 8 additions & 0 deletions RELEASING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
1. Merge your branch into master
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
9 changes: 9 additions & 0 deletions circle.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
machine:
node:
version: 8.1
dependencies:
override:
- yarn install --dev
test:
override:
- yarn run test:ci
2 changes: 2 additions & 0 deletions constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
module.exports.TEST_COOKIE_NAME = 'shopifyTestCookie';
module.exports.TOP_LEVEL_OAUTH_COOKIE_NAME = 'shopifyTopLevelOAuth';
23 changes: 19 additions & 4 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,27 @@
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,
accessMode: PropTypes.oneOf(['offline', 'online']),
};

const defaults = {
shopStore: new MemoryStrategy(),
accessMode: 'offline'
};

module.exports = function shopify(shopifyConfig) {
const config = Object.assign(
{shopStore: new MemoryStrategy()},
shopifyConfig,
);
PropTypes.checkPropTypes(ShopifyConfigTypes, shopifyConfig, 'option', 'ShopifyExpress');

const config = Object.assign({}, defaults, shopifyConfig);

return {
middleware: createMiddleware(config),
Expand Down
3 changes: 1 addition & 2 deletions middleware/index.js
Original file line number Diff line number Diff line change
@@ -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,
Expand Down
53 changes: 32 additions & 21 deletions middleware/webhooks.js
Original file line number Diff line number Diff line change
@@ -1,29 +1,40 @@
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, 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');

const generated_hash = crypto
.createHmac('sha256', SHOPIFY_APP_SECRET)
.update(JSON.stringify(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);
}
const {accessToken} = await shopStore.getShop({ shop: shopDomain });

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.status(401).send();
onVerified(new Error("Unable to verify request HMAC"));
return;
}
};
}
};
22 changes: 13 additions & 9 deletions middleware/withShop.js
Original file line number Diff line number Diff line change
@@ -1,19 +1,23 @@
module.exports = function withShop({ redirect } = { redirect: true }) {
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 } = request;
const { query: { shop }, session, baseUrl } = request;

if (session && session.accessToken) {
return next();
response.cookie(TOP_LEVEL_OAUTH_COOKIE_NAME);
next();
return;
}

if (shop && redirect) {
return response.redirect(`/auth/shopify?shop=${shop}`);
}
response.cookie(TEST_COOKIE_NAME, '1');

if (redirect) {
return response.redirect('/install');
if (shop) {
response.redirect(`${authBaseUrl || baseUrl}/auth?shop=${shop}`);
return;
}

return response.status(401).json('Unauthorized');
response.redirect('/install');
return;
};
};
Loading