├── public └── favicon.ico ├── utils ├── req.js ├── error.js └── fetch-helpers.js ├── easydb ├── package.json ├── .gitignore └── dbviewer.js ├── css └── app.css ├── .gitignore ├── server ├── auth.js ├── api-helpers.js ├── db.js └── shopify-server.js ├── pages ├── api │ ├── validate-token.js │ ├── shopify │ │ ├── install-auth-url.js │ │ └── install-granted.js │ └── debug.js ├── app │ ├── index.js │ ├── login.js │ └── shopify-welcome.js ├── debug.js └── index.js ├── ReadMe.md ├── components ├── shop-url.js ├── with │ ├── app-auth.js │ └── app-layout.js └── nav.js ├── next.config.js ├── package.json └── client └── shopify-client.js /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/chrisandrewca/shopify-nextjs/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /utils/req.js: -------------------------------------------------------------------------------- 1 | export const redirect = (res, code, path) => { 2 | res.writeHeader(code, { Location: path }); 3 | res.end(); 4 | } -------------------------------------------------------------------------------- /easydb/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dbviewer", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "start": "node dbviewer.js" 7 | }, 8 | "dependencies": { 9 | "easydb-io": "^2.0.0" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /css/app.css: -------------------------------------------------------------------------------- 1 | .shop-url { 2 | width: 35rem; 3 | margin-bottom: 1rem; 4 | font: 400 1.6rem "ShopifySans", -apple-system, BlinkMacSystemFont, San Francisco, Roboto, Segoe UI, Helvetica Neue, sans-serif; 5 | } 6 | 7 | .shop-url label { 8 | font-size: 1.8rem; 9 | } 10 | 11 | .shop-url .Polaris-TextField__Input { 12 | padding: 1.6rem; 13 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | .env* 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /easydb/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | .env* 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /server/auth.js: -------------------------------------------------------------------------------- 1 | import jwt from 'jsonwebtoken' 2 | import { log } from '../utils/error' 3 | 4 | export const generateJwt = async (payload) => { 5 | const token = jwt.sign(payload, process.env.FOR_SERVER_CODE_JWT_SECRET, { expiresIn: 60, algorithm: 'HS256' }); 6 | await log('generated jwt', { payload, token }); 7 | return token; 8 | } 9 | 10 | export const validateJwt = async (token) => { 11 | const valid = jwt.verify(token, process.env.FOR_SERVER_CODE_JWT_SECRET); 12 | await log('validated jwt', { token, valid }); 13 | return valid; 14 | } -------------------------------------------------------------------------------- /pages/api/validate-token.js: -------------------------------------------------------------------------------- 1 | import { parseCookies } from 'nookies' 2 | import { allGood, runApi } from "../../server/api-helpers" 3 | import { tryParse } from '../../utils/error' 4 | import { validateJwt } from '../../server/auth' 5 | 6 | export const GET = async (req, res) => { 7 | const cookies = parseCookies({ req }); 8 | const { token } = tryParse(cookies['user']); 9 | const authorized = await validateJwt(token); 10 | allGood(res, { authorized }); 11 | } 12 | 13 | export default async (req, res) => { 14 | await runApi(req, res, { GET }); 15 | } -------------------------------------------------------------------------------- /pages/api/shopify/install-auth-url.js: -------------------------------------------------------------------------------- 1 | import { getAuthUrl } from "../../../server/shopify-server" 2 | import { allGood, badRequest, runApi } from "../../../server/api-helpers" 3 | 4 | export const GET = async (req, res) => { 5 | const { shopUrl } = req.query; 6 | const authUrl = await getAuthUrl(shopUrl); 7 | if (!authUrl) { 8 | return badRequest(res, 'Invalid shop url'); 9 | } 10 | 11 | allGood(res, { authUrl, apiKey: process.env.FOR_SERVER_CODE_SHOPIFY_API_KEY }); 12 | } 13 | 14 | export default async (req, res) => { 15 | await runApi(req, res, { GET }); 16 | } -------------------------------------------------------------------------------- /pages/api/debug.js: -------------------------------------------------------------------------------- 1 | export default async (req, res) => { 2 | console.info(`BASE_URL: ${process.env.BASE_URL}`); 3 | console.info(`SHOPIFY_REDIRECT_URL: ${process.env.SHOPIFY_REDIRECT_URL}`); 4 | console.info(`FOR_SERVER_CODE_SHOPIFY_API_KEY: ${process.env.FOR_SERVER_CODE_SHOPIFY_API_KEY}`); 5 | console.info(`FOR_SERVER_CODE_SHOPIFY_API_SECRET: ${process.env.FOR_SERVER_CODE_SHOPIFY_API_SECRET}`); 6 | console.info(`typeof window ${typeof window}`); 7 | 8 | res.status(200).json({ 9 | BASE_URL: process.env.BASE_URL, 10 | SHOPIFY_REDIRECT_URL: process.env.SHOPIFY_REDIRECT_URL 11 | }); 12 | } -------------------------------------------------------------------------------- /ReadMe.md: -------------------------------------------------------------------------------- 1 | ### Configuration 2 | Create .\\.env with 3 | 4 | BASE_URL= 5 | SHOPIFY_REDIRECT_URL= 6 | FOR_SERVER_CODE_COOKIE_DOMAIN= 7 | FOR_SERVER_CODE_SHOPIFY_API_KEY= 8 | FOR_SERVER_CODE_SHOPIFY_API_SECRET= 9 | FOR_SERVER_CODE_JWT_SECRET= 10 | 11 | Create .\\.env.local with 12 | 13 | SRDBG=TRUE 14 | 15 | Create 1-click, no-login db at [www.easydb.io](https://www.easydb.io). Put API keys into .\\server\\db.js. 16 | 17 | ### Run 18 | 1. Setup your shopify app in the partner dashboard, you'll need a public facing https url 19 | 2. Fill out the configuration 20 | 3. `npm install` 21 | 4. `npm run dev` -------------------------------------------------------------------------------- /easydb/dbviewer.js: -------------------------------------------------------------------------------- 1 | const EasyDB = require('easydb-io/bundle.js')({ 2 | database: '', 3 | token: '' 4 | }) 5 | 6 | const db = EasyDB; 7 | 8 | // Use a callback 9 | // db.put('myKey', {some: 'data'}, err => console.info(err)) 10 | // db.get('myKey', (err, value) => console.info(value, err)) 11 | // db.delete('myKey', err => console.info(err)) 12 | // db.list((err, value) => console.info(value, err)) 13 | 14 | // Or, async/await 15 | (async () => { 16 | let value, values 17 | // value = await db.put('myKey', {some: 'data'}) 18 | // value = await db.get('myKey') 19 | // value = await db.delete('myKey') 20 | values = await db.list() 21 | console.info(values); 22 | })() -------------------------------------------------------------------------------- /components/shop-url.js: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { Button, Form, TextField } from '@shopify/polaris' 3 | 4 | const ShopUrl = ({ onSubmit }) => { 5 | const [shopUrl, setShopUrl] = useState(''); 6 | 7 | return (
8 |
onSubmit(shopUrl)}> 9 |
10 | 17 |
18 | 19 |
20 |
21 | ) 22 | } 23 | 24 | export default ShopUrl 25 | -------------------------------------------------------------------------------- /pages/app/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import withAppLayout from '../../components/with/app-layout' 3 | 4 | const AppHome = (props) => ( 5 |
6 |
7 |

{props.title}

8 |
{JSON.stringify(props)}
9 | 23 |
24 |
25 | ) 26 | 27 | export default withAppLayout(AppHome); -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | //https://www.leighhalliday.com/secrets-env-vars-nextjs-now 2 | //const nextEnv = require('next-env'); 3 | //const dotenvLoad = require('dotenv-load'); 4 | //dotenvLoad(); 5 | //const withNextEnv = nextEnv(); 6 | 7 | const withBundleAnalyzer = require('@next/bundle-analyzer')({ 8 | enabled: process.env.ANALYZE === 'true' 9 | }); 10 | const withCSS = require('@zeit/next-css'); 11 | 12 | module.exports = withBundleAnalyzer( 13 | withCSS({ 14 | webpack(config, options) { 15 | // Fixes npm packages that depend on `fs` module 16 | config.node = { 17 | fs: 'empty' 18 | } 19 | return config 20 | }, 21 | target: 'serverless', 22 | // WARNING: exposed client side 23 | env: { 24 | BASE_URL: process.env.BASE_URL, 25 | SHOPIFY_REDIRECT_URL: process.env.SHOPIFY_REDIRECT_URL, 26 | SNJS_DEBUG: process.env.SNJS_DEBUG 27 | } 28 | })); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shopify-nextjs", 3 | "version": "0.1.0", 4 | "scripts": { 5 | "debug": "dotenv-load cross-env NODE_OPTIONS=--inspect next", 6 | "dev": "dotenv-load next", 7 | "build": "next build", 8 | "start": "next start", 9 | "analyze": "cross-env ANALYZE=true npm run build" 10 | }, 11 | "dependencies": { 12 | "@next/bundle-analyzer": "^9.1.4", 13 | "@shopify/app-bridge-react": "^1.12.0", 14 | "@shopify/polaris": "^4.9.0", 15 | "@zeit/next-css": "^1.0.1", 16 | "easydb-io": "^2.0.0", 17 | "isomorphic-unfetch": "^3.0.0", 18 | "jsonwebtoken": "^8.5.1", 19 | "next": "9.1.4", 20 | "next-env": "^1.1.0", 21 | "nookies": "^2.0.8", 22 | "query-string": "^6.9.0", 23 | "react": "16.12.0", 24 | "react-dom": "16.12.0", 25 | "uuid": "^3.3.3" 26 | }, 27 | "devDependencies": { 28 | "cross-env": "^6.0.3", 29 | "dotenv-load": "^2.0.0" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pages/api/shopify/install-granted.js: -------------------------------------------------------------------------------- 1 | import { setCookie } from 'nookies'; 2 | import { addShopifyUser } from '../../../server/shopify-server' 3 | import { internalError, runApi } from '../../../server/api-helpers' 4 | import { redirect } from '../../../utils/req' 5 | import { log, tryStringify } from '../../../utils/error' 6 | 7 | const GET = async (req, res) => { 8 | const { token, user } = await addShopifyUser(req.query); 9 | if (!token || !user) { 10 | return internalError(res); 11 | } 12 | 13 | const cookie = tryStringify({ token, shopUrl: user.shopUrl }); 14 | setCookie({ res }, 'user', cookie, { 15 | maxAge: 60 * 60, 16 | path: '/', 17 | domain: process.env.FOR_SERVER_CODE_COOKIE_DOMAIN 18 | }); 19 | 20 | await log('set cookie', { headers: res.getHeaders() }); 21 | redirect(res, 303, '/app/shopify-welcome'); 22 | } 23 | 24 | export default async (req, res) => { 25 | await runApi(req, res, { GET }); 26 | } -------------------------------------------------------------------------------- /components/with/app-auth.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { tryGetApi, setHeaders } from '../../utils/fetch-helpers' 3 | 4 | export default (Component, redirectUrl = null) => { 5 | const wrapper = (props) => { 6 | return ( 7 | 8 | ) 9 | } 10 | 11 | wrapper.getInitialProps = async (ctx) => { 12 | const { authorized } = await validateToken(ctx); 13 | 14 | if (authorized) { 15 | const props = Component.getInitialProps && 16 | (await Component.getInitialProps(ctx)); 17 | return { ...props }; 18 | } else { 19 | if (!redirectUrl) { 20 | redirectUrl = '/app/login'; 21 | } 22 | ctx.res.writeHead(303, { Location: redirectUrl }); 23 | ctx.res.end(); 24 | } 25 | } 26 | 27 | return wrapper; 28 | } 29 | 30 | const validateToken = async ({ req }) => { 31 | const init = setHeaders({ cookie: req.headers.cookie }); 32 | return await tryGetApi('/api/validate-token', init); 33 | } -------------------------------------------------------------------------------- /client/shopify-client.js: -------------------------------------------------------------------------------- 1 | import { Redirect } from '@shopify/app-bridge/actions' 2 | import { createApp } from '@shopify/app-bridge' 3 | import { setQuery, tryGetApi } from '../utils/fetch-helpers'; 4 | import { log, tryOrLog } from '../utils/error'; 5 | 6 | export const redirectToShopifyAuth = async (shopUrl) => { 7 | const { authUrl, apiKey } = (await getInstallAuthUrl(shopUrl)); 8 | if (!authUrl) { 9 | await log('Invalid shopify authorize url', { authUrl, shopUrl }); 10 | return; 11 | } 12 | 13 | if (window.top === window.self) { 14 | window.location.assign(authUrl); 15 | } else { 16 | tryOrLog(() => { 17 | const app = createApp({ 18 | apiKey: apiKey, 19 | shopOrigin: shopUrl 20 | }); 21 | Redirect.create(app).dispatch(Redirect.Action.ADMIN_PATH, authUrl); 22 | }); 23 | } 24 | } 25 | 26 | const getInstallAuthUrl = async (shopUrl) => { 27 | const init = setQuery({ shopUrl }); 28 | return await tryGetApi('/api/shopify/install-auth-url', init); 29 | } -------------------------------------------------------------------------------- /utils/error.js: -------------------------------------------------------------------------------- 1 | export const log = async (msg, ...objs) => { 2 | if (process.env.SNJS_DEBUG) { 3 | objs = await Promise.all( 4 | objs.map(o => tryStringify(o)) 5 | ); 6 | 7 | if (typeof window === 'undefined') { 8 | console.debug(); 9 | console.groupCollapsed(`[[ ${msg} ]]`, ...objs || 'trace'); 10 | console.trace(); 11 | console.groupEnd(); 12 | console.debug(); 13 | } else { 14 | console.groupCollapsed(msg, ...objs || 'trace'); 15 | console.trace(); 16 | console.groupEnd(); 17 | } 18 | } // TODO production log 19 | } 20 | 21 | export const tryParse = (payload) => 22 | tryOrLog(() => JSON.parse(payload)) || {} 23 | 24 | export const tryStringify = (obj) => 25 | tryOrLog(() => JSON.stringify(obj)) || '' 26 | 27 | export const tryOrLog = (func, msg = '') => { 28 | try { 29 | return func(); 30 | } catch (error) { 31 | log(`${msg} ${error.message}`, error); 32 | return null; 33 | } 34 | } 35 | 36 | export const tryOrLogAsync = async (promise, msg = '') => { 37 | try { 38 | return await promise; 39 | } catch (error) { 40 | log(`${msg} ${error.message}`, error); 41 | return null; 42 | } 43 | } -------------------------------------------------------------------------------- /pages/app/login.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import ShopUrl from '../../components/shop-url' 3 | import { redirectToShopifyAuth } from '../../client/shopify-client' 4 | import withAppLayout from '../../components/with/app-layout' 5 | 6 | const AppLogin = () => { 7 | const handleShopUrl = async (shopUrl) => { 8 | await redirectToShopifyAuth(shopUrl); 9 | } 10 | 11 | return ( 12 |
13 |

Connect with your Shopify account

14 | 15 |
16 | 17 |
18 | 39 |
40 | ); 41 | } 42 | 43 | export default withAppLayout(AppLogin) -------------------------------------------------------------------------------- /components/nav.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | const links = [ 4 | { href: 'https://zeit.co/now', label: 'ZEIT' }, 5 | { href: 'https://github.com/zeit/next.js', label: 'GitHub' }, 6 | ].map(link => { 7 | link.key = `nav-link-${link.href}-${link.label}` 8 | return link 9 | }) 10 | 11 | const Nav = () => ( 12 | 51 | ) 52 | 53 | export default Nav 54 | -------------------------------------------------------------------------------- /pages/debug.js: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { tryGetApi } from '../utils/fetch-helpers'; 3 | 4 | const Debug = (props) => { 5 | console.info(`BASE_URL: ${process.env.BASE_URL}`); 6 | console.info(`SHOPIFY_REDIRECT_URL: ${process.env.SHOPIFY_REDIRECT_URL}`); 7 | 8 | const [debug, setDebug] = useState(''); 9 | 10 | const handleGetDebug = async (event) => { 11 | event.preventDefault(); 12 | const debug = await tryGetApi('api/debug'); 13 | console.info('Debug.getInitialProps'); 14 | console.info(debug); 15 | setDebug(JSON.stringify(debug)); 16 | } 17 | 18 | return ( 19 | <> 20 |
21 |

via procces.env

22 |

BASE_URL: {process.env.BASE_URL}

23 |

SHOPIFY_REDIRECT_URL: {process.env.SHOPIFY_REDIRECT_URL}

24 |
25 |
26 |

via getInitialProps

27 |
{props.debug}
28 |
29 |
30 |

31 |
{debug}
32 |
33 | 34 | ); 35 | } 36 | 37 | Debug.getInitialProps = async () => { 38 | const debug = await tryGetApi('api/debug'); 39 | console.info('Debug.getInitialProps'); 40 | console.info(debug); 41 | return { debug: JSON.stringify(debug) }; 42 | } 43 | 44 | export default Debug -------------------------------------------------------------------------------- /server/api-helpers.js: -------------------------------------------------------------------------------- 1 | import { log, tryOrLogAsync } from "../utils/error"; 2 | 3 | /* 4 | * API middleware 5 | */ 6 | // const prewareByVerb = { 7 | // DELETE: [], 8 | // GET: [], 9 | // POST: [], 10 | // PUT: [] 11 | // } 12 | 13 | /* 14 | * API responses 15 | */ 16 | export const badRequest = (res, error) => { 17 | res.status(400).json({ error }); 18 | } 19 | 20 | export const internalError = (res, error = 'Internal error.') => { 21 | res.status(500).json({ error }); 22 | } 23 | 24 | export const allGood = (res, obj) => { 25 | res.status(200).json(obj); 26 | } 27 | 28 | /* 29 | * API helpers 30 | */ 31 | const unhandled = async (req, res) => { 32 | // TODO error handling 33 | internalError(res); 34 | } 35 | 36 | const handlerByVerb = { 37 | DELETE: unhandled, 38 | GET: unhandled, 39 | POST: unhandled, 40 | PUT: unhandled 41 | } 42 | 43 | export const runApi = async (req, res, handlers) => { // preware=null) => { 44 | handlers = { ...handlerByVerb, ...handlers }; 45 | 46 | const run = handlers[req.method]; 47 | // preware = preware 48 | // ? preware[req.method] 49 | // : prewareByVerb[req.method]; 50 | 51 | // req.mware_results = {}; 52 | // for (const pre of preware) { 53 | // if ((await pre(req, res))) { 54 | // continue; 55 | // } 56 | // break; 57 | // } 58 | 59 | await log('run api', { method: req.method, url: req.url }); 60 | const _null = await tryOrLogAsync(run(req, res)); 61 | 62 | if (_null === null) { 63 | internalError(res); 64 | } 65 | } -------------------------------------------------------------------------------- /pages/app/shopify-welcome.js: -------------------------------------------------------------------------------- 1 | import withAppAuth from '../../components/with/app-auth' 2 | import withAppLayout from '../../components/with/app-layout' 3 | 4 | const ShopifyWelcome = (props) => { 5 | return ( 6 |
7 |

Welcome!

8 |
9 | 10 |

Start →

11 |

Thank you for your purchase.

12 |
13 |
14 | 57 |
58 | ); 59 | } 60 | 61 | export default 62 | withAppAuth( 63 | withAppLayout( 64 | ShopifyWelcome)); -------------------------------------------------------------------------------- /components/with/app-layout.js: -------------------------------------------------------------------------------- 1 | // css order of application to override polaris styles 2 | import polaris from '@shopify/polaris/styles.css' 3 | import appcss from '../../css/app.css' 4 | 5 | import React from 'react' 6 | import Head from 'next/head' 7 | import { AppProvider } from '@shopify/polaris' 8 | import { Provider } from '@shopify/app-bridge-react' 9 | import { parseCookies } from 'nookies' 10 | 11 | import Nav from '../nav' 12 | import { tryParse } from '../../utils/error' 13 | 14 | export default (Component) => { 15 | const wrapper = (props) => { 16 | 17 | const withShopMarkup = ( 18 | //
19 |
20 | 21 | 27 | 28 | 29 | 30 |
31 | ); 32 | 33 | const withoutShopMarkup = ( 34 | //
35 |
36 | 37 | 38 | 39 |
40 | ); 41 | 42 | const appMarkup = props.shopUrl 43 | ? withShopMarkup 44 | : withoutShopMarkup; 45 | 46 | return ( 47 |
48 | 49 | Shopify NextJS - Login 50 | 51 | 52 |
57 | ) 58 | } 59 | 60 | wrapper.getInitialProps = async (ctx) => { 61 | const cookies = parseCookies(ctx); 62 | const { shopUrl } = tryParse(cookies['user']); 63 | const apiKey = process.env.FOR_SERVER_CODE_SHOPIFY_API_KEY; 64 | 65 | const props = Component.getInitialProps && 66 | (await Component.getInitialProps(ctx)); 67 | return { ...props, apiKey, shopUrl }; 68 | } 69 | 70 | return wrapper; 71 | } -------------------------------------------------------------------------------- /server/db.js: -------------------------------------------------------------------------------- 1 | import easyDB from 'easydb-io' 2 | import uuidv4 from 'uuid/v4' 3 | import { log } from '../utils/error' 4 | 5 | const db = easyDB({ 6 | database: '', 7 | token: '' 8 | }) 9 | 10 | // TODO database states, review me 11 | // User has no shopUrl 12 | // User has shopUrl but has not been authorized (failed, otherwise) 13 | // User registers twice and orphans its secrets object when new user id generated 14 | 15 | export const addUser = async (query, auth) => { 16 | const previouslyRegisteredAs = await dbGet(query.shop); 17 | if (previouslyRegisteredAs) { 18 | await dbDelete(previouslyRegisteredAs.id); 19 | } 20 | 21 | try { 22 | const id = uuidv4(); 23 | const user = { 24 | id, 25 | shopUrl: query.shop 26 | }; 27 | await db.put(query.shop, user); 28 | 29 | const secrets = { 30 | id, 31 | shopifyToken: auth.access_token, 32 | shopifyScope: auth.scope 33 | }; 34 | 35 | await db.put(id, secrets); 36 | 37 | await log('created user', { user, secrets }); 38 | return user; 39 | 40 | } catch (error) { 41 | await log(`unable to create user: ${error.message}`, { 42 | error, 43 | id, 44 | user, 45 | secrets 46 | }); 47 | return null; 48 | } 49 | } 50 | 51 | export const getUser = async (shopUrl) => { 52 | try { 53 | return await db.get(shopUrl); 54 | } catch (error) { 55 | await log(`unable to get user: ${error.message}`, { error, shopUrl }); 56 | return null; 57 | } 58 | } 59 | 60 | export const getSecrets = async (userId) => { 61 | try { 62 | return await db.get(userId); 63 | } catch (error) { 64 | await log(`unable to get secrets: ${error.message}`, { error, userId }); 65 | return null; 66 | } 67 | } 68 | 69 | const dbGet = async (key) => { 70 | try { 71 | return await db.get(key); 72 | } catch (error) { 73 | await log('dbGet', error); 74 | return null; 75 | } 76 | } 77 | 78 | const dbDelete = async (key) => { 79 | try { 80 | return await db.delete(key); 81 | } catch (error) { 82 | await log('dbDelete', error); 83 | return null; 84 | } 85 | } -------------------------------------------------------------------------------- /utils/fetch-helpers.js: -------------------------------------------------------------------------------- 1 | import fetch from 'isomorphic-unfetch' 2 | import { log, tryOrLog, tryStringify } from './error' 3 | import queryString from 'query-string' 4 | 5 | export const setQuery = (query, obj = {}) => { 6 | query = tryOrLog(() => queryString.stringify(query)) || ''; 7 | query = query 8 | ? `?${query}` 9 | : ''; 10 | return { ...obj, query: `${query}` }; 11 | } 12 | 13 | export const setHeaders = (headers, obj = {}) => { 14 | headers = !!obj && obj.headers 15 | ? { ...obj.headers, ...headers } 16 | : { ...headers }; 17 | return { ...obj, headers }; 18 | } 19 | 20 | export const setMethod = (method, obj = {}) => { 21 | return { ...obj, method }; 22 | } 23 | 24 | export const setBody = (body, obj = {}) => { 25 | body = tryStringify(body); 26 | return { ...obj, body }; 27 | } 28 | 29 | export const tryPost = async (url, init) => { 30 | init = setMethod('post', 31 | setHeaders({ 'Content-Type': 'application/json' }, init) 32 | ); 33 | 34 | return await tryFetch(url, init); 35 | } 36 | 37 | export const tryPostApi = async (endpoint, payload) => { 38 | init = setMethod('post', 39 | setHeaders({ 'Content-Type': 'application/json' }, init) 40 | ); 41 | 42 | return await tryFetch(`${process.env.BASE_URL}${endpoint}`, init); 43 | } 44 | 45 | export const tryGetApi = async (endpoint, init) => { 46 | init = setMethod('get', 47 | setHeaders({ credentials: 'same-origin' }, 48 | init.query ? init : setQuery('', init) 49 | ) 50 | ); 51 | 52 | const url = `${process.env.BASE_URL}${endpoint}${init.query}`; 53 | return await tryFetch(url, init); 54 | } 55 | 56 | const tryFetch = async (url, init) => { 57 | try { 58 | await log('fetch', { url, init }); 59 | const res = await fetch(url, init); 60 | 61 | if (res.ok) { 62 | await log('response', { url: res.url, status: res.status, text: res.statusText }); 63 | return await res.json(); 64 | } else { 65 | // https://github.com/developit/unfetch#caveats 66 | const error = new Error(res.statusText); 67 | error.response = res; 68 | throw error; 69 | } 70 | } catch (error) { 71 | const { response } = error; 72 | const code = response 73 | ? response.status 74 | : 400; 75 | const message = response 76 | ? response.statusText 77 | : error.message; 78 | await log('response', { message, code }); 79 | return {}; 80 | } 81 | } -------------------------------------------------------------------------------- /server/shopify-server.js: -------------------------------------------------------------------------------- 1 | import crypto from 'crypto' 2 | import queryString from 'query-string' 3 | import { addUser } from './db' 4 | import { log } from '../utils/error' 5 | import { tryPost, setBody } from '../utils/fetch-helpers' 6 | import { generateJwt } from './auth' 7 | 8 | export const addShopifyUser = async (query) => { 9 | const isValid = await isShopifyInstallRequest(query); 10 | if (!isValid) { 11 | return null; 12 | } 13 | 14 | const body = { 15 | client_id: process.env.FOR_SERVER_CODE_SHOPIFY_API_KEY, 16 | client_secret: process.env.FOR_SERVER_CODE_SHOPIFY_API_SECRET, 17 | code: query.code 18 | }; 19 | 20 | const auth = await tryPost(`https://${query.shop}/admin/oauth/access_token`, setBody(body)); 21 | if (!auth) { 22 | return null; 23 | } 24 | 25 | const user = await addUser(query, auth); 26 | if (!user) { 27 | return null; 28 | } 29 | 30 | const token = await generateJwt({ 31 | hmac: query.hmac, 32 | code: query.code, 33 | shopUrl: query.shop 34 | }); 35 | 36 | if (!token) { 37 | return null; 38 | } 39 | 40 | return { token, user }; 41 | } 42 | 43 | // Notice: Keep as API so cache isn't involved with scope? Uhh...logging? 44 | export const getAuthUrl = async (shopUrl) => { 45 | const apiKey = process.env.FOR_SERVER_CODE_SHOPIFY_API_KEY; 46 | const redirectUri = process.env.SHOPIFY_REDIRECT_URL; 47 | // TODO nonce 48 | const permissionUrl = `/oauth/authorize?client_id=${apiKey}&scope=read_products,read_content&redirect_uri=${redirectUri}`; 49 | const authUrl = `https://${shopUrl}/admin${permissionUrl}`; 50 | await log('created', { authUrl }); 51 | return authUrl; 52 | } 53 | 54 | export const isShopifyInstallRequest = async (query) => { 55 | const result = (isValidShopifyRequest(query) 56 | && query.code); 57 | 58 | if (!result) { 59 | log(`query missing &code=XXX`, query); 60 | } 61 | 62 | return result; 63 | } 64 | 65 | const isValidShopifyRequest = async (query) => { 66 | query = { ...query }; 67 | 68 | const hmac = query.hmac; 69 | delete query.hmac; 70 | query = queryString.stringify(query); 71 | 72 | const check = crypto 73 | .createHmac('sha256', process.env.FOR_SERVER_CODE_SHOPIFY_API_SECRET) 74 | .update(query) 75 | .digest('hex'); 76 | 77 | const result = (hmac === check); 78 | 79 | if (!result) { 80 | await log('invalid hmac', { query, check, hmac }); 81 | } 82 | 83 | return result; 84 | } -------------------------------------------------------------------------------- /pages/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Head from 'next/head' 3 | import Nav from '../components/nav' 4 | 5 | const Home = () => { 6 | return ( 7 |
8 | 9 | Home 10 | 11 | 12 | 13 |
87 | ) 88 | } 89 | 90 | export default Home --------------------------------------------------------------------------------