├── .gitignore ├── README.md ├── app.js ├── dev-settings.json ├── package.json ├── public └── stylesheets │ └── style.css ├── routes ├── index.js └── shopify_auth.js └── views ├── app_view.jade ├── escape_iframe.jade └── layout.jade /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | */target/* 3 | .classpath 4 | .project 5 | .settings/ 6 | target/ 7 | .externalToolBuilders/ 8 | .idea 9 | .idea/* 10 | *.iml 11 | node_modules 12 | npm-debug.log -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Bare-bones Shopify Embedded App in Node.js 2 | ========================================== 3 | 4 | For further information, see [An Embedded Shopify App With Node.js](http://blog.codezuki.com/blog/2014/02/10/shopify-nodejs/). -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Shopify Embedded App. skeleton. 3 | * 4 | * Copyright 2014 Richard Bremner 5 | * richard@codezuki.com 6 | */ 7 | 8 | var bodyParser = require('body-parser'), 9 | cookieParser = require('cookie-parser'), 10 | cookieSession = require('cookie-session'), 11 | express = require('express'), 12 | routes = require('./routes'), 13 | shopifyAuth = require('./routes/shopify_auth'), 14 | path = require('path'), 15 | nconf = require('nconf'), 16 | morgan = require('morgan'); 17 | 18 | //load settings from environment config 19 | nconf.argv().env().file({ 20 | file: (process.env.NODE_ENV || 'dev') + '-settings.json' 21 | }); 22 | exports.nconf = nconf; 23 | 24 | //configure express 25 | var app = express(); 26 | 27 | //log all requests 28 | app.use(morgan('combined')); 29 | 30 | //support json and url encoded requests 31 | app.use(bodyParser.json()); 32 | app.use(bodyParser.urlencoded({ extended: false })); 33 | 34 | //setup encrypted session cookies 35 | app.use(cookieParser()); 36 | app.use(cookieSession({ 37 | secret: "--express-session-encryption-key--" 38 | })); 39 | 40 | //statically serve from the 'public' folder 41 | app.use(express.static(path.join(__dirname, 'public'))); 42 | 43 | //use jade templating engine for view rendering 44 | app.set('view engine', 'jade'); 45 | 46 | //use the environment's port if specified 47 | app.set('port', process.env.PORT || 3000); 48 | 49 | var appAuth = new shopifyAuth.AppAuth(); 50 | 51 | //configure routes 52 | app.get('/', routes.index); 53 | app.get('/auth_app', appAuth.initAuth); 54 | app.get('/escape_iframe', appAuth.escapeIframe); 55 | app.get('/auth_code', appAuth.getCode); 56 | app.get('/auth_token', appAuth.getAccessToken); 57 | app.get('/render_app', routes.renderApp); 58 | 59 | app.listen(app.get('port'), function() { 60 | console.log('Listening on port ' + app.get('port')); 61 | }); -------------------------------------------------------------------------------- /dev-settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "oauth": { 3 | "api_key": "", 4 | "client_secret": "", 5 | "redirect_url": "http://localhost:3000/auth_token", 6 | "scope": "read_products" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "application-name", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node app.js" 7 | }, 8 | "dependencies": { 9 | "body-parser": "~1.14.0", 10 | "cookie-parser": "~1.4.0", 11 | "cookie-session": "~1.2.0", 12 | "express": "~4.13.3", 13 | "jade": "*", 14 | "oauth": "~0.9.14", 15 | "nconf": "~0.8.0", 16 | "morgan": "~1.6.1" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /public/stylesheets/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | padding: 50px; 3 | font: 14px "Lucida Grande", Helvetica, Arial, sans-serif; 4 | } 5 | 6 | a { 7 | color: #00B7FF; 8 | } -------------------------------------------------------------------------------- /routes/index.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Shopify Embedded App. skeleton. 3 | * 4 | * Copyright 2014 Richard Bremner 5 | * richard@codezuki.com 6 | */ 7 | 8 | var app = require('../app'), 9 | url = require("url"), 10 | querystring = require('querystring'); 11 | 12 | /* 13 | * Get / 14 | * 15 | * if we already have an access token then 16 | * redirect to render the app, otherwise 17 | * redirect to app authorisation. 18 | */ 19 | exports.index = function(req, res){ 20 | if (!req.session.oauth_access_token) { 21 | var parsedUrl = url.parse(req.originalUrl, true); 22 | if (parsedUrl.query && parsedUrl.query.shop) { 23 | req.session.shopUrl = 'https://' + parsedUrl.query.shop; 24 | } 25 | 26 | res.redirect("/auth_app"); 27 | } 28 | else { 29 | res.redirect("/render_app"); 30 | } 31 | }; 32 | 33 | /* 34 | * Get /render_app 35 | * 36 | * render the main app view 37 | */ 38 | exports.renderApp = function(req, res){ 39 | res.render('app_view', { 40 | title: 'My App Title', 41 | apiKey: app.nconf.get('oauth:api_key'), 42 | shopUrl: req.session.shopUrl 43 | }); 44 | }; -------------------------------------------------------------------------------- /routes/shopify_auth.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Shopify Embedded App. skeleton. 3 | * 4 | * Copyright 2014 Richard Bremner 5 | * richard@codezuki.com 6 | */ 7 | 8 | var app = require('../app'), 9 | OAuth = require('oauth').OAuth2, 10 | url = require("url"); 11 | 12 | exports.AppAuth = function() { 13 | var self = this; 14 | 15 | /* 16 | * Get /auth_app 17 | * 18 | * initiates the Shopify App authorisation 19 | */ 20 | this.initAuth = function(req, res){ 21 | if (!req.session.oauth_access_token) { 22 | res.redirect("/escape_iframe"); 23 | } else { 24 | res.redirect("/render_app"); 25 | } 26 | }; 27 | 28 | /* 29 | * Get /escape_iframe 30 | * 31 | * renders a view that contains javascript 32 | * which will change the browser top window 33 | * location to start the oauth process 34 | * 35 | * See http://docs.shopify.com/embedded-app-sdk/getting-started#oauth 36 | */ 37 | this.escapeIframe = function(req, res) { 38 | res.render('escape_iframe'); 39 | }; 40 | 41 | /* 42 | * Get /auth_code 43 | * 44 | * gets the temporary token which we can exchange 45 | * for a permanent token. User may be prompted to accept 46 | * the scope being requested 47 | */ 48 | this.getCode = function(req, res) { 49 | var redirectUrl = self.OAuth(req.session.shopUrl).getAuthorizeUrl({ 50 | redirect_uri : app.nconf.get('oauth:redirect_url'), 51 | scope: app.nconf.get('oauth:scope') 52 | }); 53 | res.redirect(redirectUrl); 54 | }; 55 | 56 | /* 57 | * Get /auth_token 58 | * 59 | * get the permanent access token which is valid 60 | * for the lifetime of the app install, it does 61 | * not expire 62 | */ 63 | this.getAccessToken = function(req, res) { 64 | var parsedUrl= url.parse(req.originalUrl, true); 65 | 66 | self.OAuth(req.session.shopUrl).getOAuthAccessToken( 67 | parsedUrl.query.code, {}, 68 | function(error, access_token, refresh_token) { 69 | if (error) { 70 | res.send(500); 71 | return; 72 | } else { 73 | req.session.oauth_access_token = access_token; 74 | res.redirect("/render_app"); 75 | } 76 | } 77 | ); 78 | }; 79 | 80 | this.OAuth = function(shopUrl) { 81 | return new OAuth( 82 | app.nconf.get('oauth:api_key'), 83 | app.nconf.get('oauth:client_secret'), 84 | shopUrl, 85 | "/admin/oauth/authorize", 86 | "/admin/oauth/access_token"); 87 | } 88 | } -------------------------------------------------------------------------------- /views/app_view.jade: -------------------------------------------------------------------------------- 1 | extends layout 2 | 3 | block content 4 | script. 5 | ShopifyApp.ready(function(){ 6 | ShopifyApp.Bar.loadingOff(); 7 | ShopifyApp.Bar.initialize({ 8 | title: '#{title}' 9 | }); 10 | }); 11 | 12 | h1 My App View 13 | p hello world 14 | -------------------------------------------------------------------------------- /views/escape_iframe.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | script. 5 | window.top.location.href = '/auth_code'; -------------------------------------------------------------------------------- /views/layout.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html 3 | head 4 | link(rel='stylesheet', href='/stylesheets/style.css') 5 | script(src='https://cdn.shopify.com/s/assets/external/app.js') 6 | script. 7 | ShopifyApp.init({ 8 | apiKey: '#{apiKey}', 9 | shopOrigin: '#{shopUrl}' 10 | }); 11 | 12 | body 13 | block content --------------------------------------------------------------------------------