shopify-sinatra-app 0.8.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e65e3d915b446765b3c72cbe5e67cc296aca66b5833a494ba78c8dbe6fb1ad33
4
- data.tar.gz: 22aabb580d8abf787149e05ae6f1c32d15015960ebbfa3be028b39242b4fe48c
3
+ metadata.gz: 46049e2afcd7dce3a1d3f115ba665a731d4fcb1b5344dcc9ff84649cfd30beda
4
+ data.tar.gz: 2ba425471b1cbb5e40f91178d646d8c0dd4da456321518a0772afb7ec4359058
5
5
  SHA512:
6
- metadata.gz: cca5d08f7d50b3bb3a4772a0fa4a5bac1d51bb2354dc2b54c57828ecfb38f85ce0eab8cc9055446ff35ba8975753d73b10b6b4a1fd24776952358bfe64fb39c8
7
- data.tar.gz: b21f1e83888d7f67856429d0c7f914cfd64a6b04c95a492443bd9c57495efb1c7bda651a94bd4e2db1cf4266f4a3c8befc70986bd093aa377db14d5073d7ace1
6
+ metadata.gz: c7223a2223ba1a71865bdbf05ef3d20585723da858cc99b2ac3872c13c29a3cb6db4e03502a8412f329393e6458e4d2c8038355abcd81247992bd44cf4ff8826
7
+ data.tar.gz: 81f547ba807a56f66802482746ff9a7d6d4ccacf7c9c5d3650491e3cabde7280fed22ce576b42a901f0ce5a57f36cef355108ac12f82223aeca33b9dc8ca1ac3
@@ -0,0 +1,15 @@
1
+ version: 2.1
2
+ orbs:
3
+ ruby: circleci/ruby@0.1.2
4
+
5
+ jobs:
6
+ build:
7
+ docker:
8
+ - image: circleci/ruby:2.6.6
9
+ executor: ruby/default
10
+ steps:
11
+ - checkout
12
+ - ruby/bundle-install
13
+ - run:
14
+ name: Tests
15
+ command: ./bin/ci.sh
@@ -0,0 +1,14 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: bundler
4
+ directory: "/"
5
+ schedule:
6
+ interval: daily
7
+ time: "09:00"
8
+ timezone: America/Toronto
9
+ open-pull-requests-limit: 3
10
+ ignore:
11
+ - dependency-name: omniauth
12
+ versions:
13
+ - 2.0.2
14
+ - 2.0.3
data/CHANGELOG CHANGED
@@ -1,3 +1,35 @@
1
+ 1.0.0
2
+ -----
3
+ * Update to omniauth 2.0.4 and add rack protection authenticity token
4
+ * All forms will need an authenticity_token param added like in the login view
5
+ * Refactored the provided routes to be simpler. Now only login and logout (no more install)
6
+ * shopify_webhook now defines the route because it needs to also configure ignoring csrf for the route
7
+ * refactored other internal methods to simplify and reduce surface area
8
+ * Removed heroku rake tasks. They are out of date and don't encourage best practises
9
+
10
+ 0.12.0
11
+ ------
12
+ * Update to use the Shopify AppBridge instead of the ESDK
13
+ * This change is mostly to generated files so you'll need to apply those updates
14
+ to your own versions.
15
+ * shop_origin no longer includes protocol
16
+ * return_to re-worked to function with the AppBridge
17
+
18
+ 0.11.0
19
+ ------
20
+ * remove rack-flash3 use sinatra-flash instead
21
+ * remove a duplicate config of sessions that was breaking the same_site fix
22
+ * remove a runtime dependency that didn't end up being used for the same_site fix but was added anyways
23
+ * update ruby and rake versions
24
+
25
+ 0.10.0
26
+ ------
27
+ * Add the api_version to settings and update to 2019-07
28
+
29
+ 0.9.0
30
+ -----
31
+ * set secure and and same_site options on the session cookie. Fixes auth with the upcoming chrome 80 release
32
+
1
33
  0.8.0
2
34
  -----
3
35
  * Shopify updated the way sessions are created: https://github.com/Shopify/shopify_api/blob/master/README.md#-breaking-change-notice-for-version-700-, updating code to reflect these changes.
data/README.md CHANGED
@@ -58,7 +58,10 @@ This will create a new skeleton shopify-sinatra-app. The generator will create s
58
58
 
59
59
  ### Setting the app to use your Shopify API credentials
60
60
 
61
- You'll need to create a Shopify Partner Account and a new application. You can make an account [here](http://www.shopify.ca/partners) and see this [tutorial](http://docs.shopify.com/api/the-basics/getting-started) for creating a new application. This app uses the default redirect_uri from omniauth `<your domain>/auth/shopify/callback` so set it accordingly when creating your app.
61
+ You'll need to create a Shopify Partner Account and a new application. You can make an account [here](http://www.shopify.ca/partners) and see this [tutorial](http://docs.shopify.com/api/the-basics/getting-started) for creating a new application. The requires 2 redirects configured:
62
+
63
+ * the default redirect_uri from omniauth `<your domain>/auth/shopify/callback`
64
+ * and `<your domain>/login`
62
65
 
63
66
  Note - The shopify-sinatra-app creates an embedded app! You need change the embedded setting to `enabled` in the [Shopify Partner area](https://app.shopify.com/services/partners/api_clients) for your app. If you don't want your app to be embedded then remove the related code in `layout/application.erb` and delete the `layout/_top_bar.erb` file and the references to it in the other views.
64
67
 
@@ -87,13 +90,11 @@ get '/products.json' do
87
90
  end
88
91
  ```
89
92
 
90
- **shopify_webhook** - This method is for an endpoint that receives a webhook from Shopify. Webhooks are a great way to keep your app in sync with a shop's data without polling. You can read more about webhooks [here](http://docs.shopify.com/api/tutorials/using-webhooks). This method also takes a block and yields the `shop_name` and `webhook_body` as a hash (note only works for json webhooks, don't use xml). Here is an example that listens to an order creation webhook:
93
+ **shopify_webhook** - This method defines a `post` endpoint that receives a webhook from Shopify. Webhooks are a great way to keep your app in sync with a shop's data without polling. You can read more about webhooks [here](http://docs.shopify.com/api/tutorials/using-webhooks). This method also takes a block and yields the `shop_name` and `webhook_body` as a hash (note only works for json webhooks, don't use xml). Here is an example that listens to an order creation webhook:
91
94
 
92
95
  ```ruby
93
- post '/order.json' do
94
- shopify_webhook do |shop_name, webhook_data|
95
- # do something with the data
96
- end
96
+ shopify_webhook('/order.json') do |shop_name, webhook_data|
97
+ # do something with the data
97
98
  end
98
99
  ```
99
100
 
@@ -107,14 +108,11 @@ ShopifyAPI::Base.activate_session(api_session)
107
108
 
108
109
  It's impossible to control the flow of webhooks to your app from Shopify especially if a larger store installs your app or if a shop has a flash sale. To prevent your app from getting overloaded with webhook requests it is best practise to process webhooks in a background queue and return a `200` to Shopify immediately. Ruby has several good background job frameworks that work with Sinatra including [Sidekiq](https://github.com/mperham/sidekiq) and [Resque](https://github.com/resque/resque).
109
110
 
110
-
111
111
  **after_shopify_auth** - This is a private method provided with the framework that gets called whenever the app is authorized. You should fill this method in with anything you need to initialize, for example webhooks and services on Shopify or any other database models you have created specific to a shop. Note that this method will be called anytime the auth flow is completed so this method should be idempotent (running it twice has the same effect as running it once).
112
112
 
113
- **logout** - This method clears the current session
114
-
115
113
  shopify-sinatra-app includes sinatra/activerecord for creating models that can be persisted in the database. You might want to read more about sinatra/activerecord and the methods it makes available to you: [https://github.com/janko-m/sinatra-activerecord](https://github.com/janko-m/sinatra-activerecord)
116
114
 
117
- shopify-sinatra-app also includes `rack-flash3` and the flash messages are forwarded to the Shopify Embedded App SDK (see the code in `views/layouts/application.erb`). Flash messages are useful for signalling to your users that a request was successful without changing the page. The following is an example of how to use a flash message in a route:
115
+ shopify-sinatra-app also includes `sinatra-flash` and the flash messages are forwarded to the Shopify Embedded App SDK (see the code in `views/layouts/application.erb`). Flash messages are useful for signalling to your users that a request was successful without changing the page. The following is an example of how to use a flash message in a route:
118
116
 
119
117
  ```ruby
120
118
  post '/flash_message' do
@@ -128,17 +126,17 @@ note - a flash must be followed by a redirect or it won't work!
128
126
 
129
127
  Developing
130
128
  ----------
131
- The embedded app sdk won't load non https content so you'll need to use a forwarding service like [ngrok](https://ngrok.com/) or [forwardhq](https://forwardhq.com/). Set your application url in the [Shopify Partner area](https://app.shopify.com/services/partners/api_clients) to your forwarded url. However The redirect_uri should still be `http://localhost:4567/auth/shopify/callback` which will allow you to install your app on a live shop while running it locally.
129
+ The embedded app sdk won't load non https content so you'll need to use a real domain or a forwarding service like [ngrok](https://ngrok.com/). Set your application url in the [Shopify Partner area](https://app.shopify.com/services/partners/api_clients) to your forwarded url and set the redirect_uri to your forwarded url + `/auth/shopify/callback` which will allow you to install your app on a live shop while running it locally.
132
130
 
133
- To run the app locally we use `foreman` which comes with the [Heroku Toolbelt](https://devcenter.heroku.com/articles/quickstart). Foreman handles running our application and setting our credentials as environment variables. To run the application type:
131
+ To run the app locally we use [overmind](https://github.com/DarthSim/overmind) a tool for running multiple process and setting our credentials as environment variables. To run the application run:
134
132
 
135
133
  ```
136
- PORT=4567 foreman run web
134
+ overmind start
137
135
  ```
138
136
 
139
- Note - we use `foreman run ...` not `foreman start ...` because we only want to start the single process that is our app. This means if you add a debugger in your app it will trigger properly in the command line when the debugger is hit. If you don't have any debuggers feel free to use `foreman start -p 4567`.
137
+ To connect to a single process to use a debugger/break point use `overmind connect <process>`
140
138
 
141
- To debug your app simply add `require 'byebug'` at the top and then type `byebug` where you would like to drop into an interactive session. You may also want to try out [Pry](http://pryrepl.org/).
139
+ To debug your app add `require 'byebug'` at the top and then add `byebug` to your code where you would like to drop into an interactive session. You may also want to try out [Pry](http://pryrepl.org/).
142
140
 
143
141
  If you are testing webhooks locally make sure they also go through the forwarded url and not `localhost`.
144
142
 
@@ -158,60 +156,6 @@ bundle exec rake test
158
156
  Checkout the contents of the `app_test.rb` file and the `test_helper.rb` and modify them as you add functionality to your app. You can also check the tests of other apps using this framework to see more about how to write tests for your own app.
159
157
 
160
158
 
161
- Deploying
162
- ---------
163
-
164
- This template was created with deploying to Heroku in mind. Heroku is a cloud based app hosting provider that makes it easy to get an application into a product environment.
165
-
166
- Before you can get started with Heroku you need to create a git repo for you application:
167
-
168
- ```
169
- git init
170
- git add .
171
- git commit -m "initial commit"
172
- ```
173
-
174
- Now you can create a new heroku application. Download the [Heroku Toolbelt](https://devcenter.heroku.com/articles/quickstart) and run the following command to create a new application:
175
-
176
- ```
177
- heroku apps:create <your new app name>
178
- ```
179
-
180
- You will also need to add the following (free) add-ons to your new Heroku app:
181
-
182
- ```
183
- heroku addons:add heroku-postgresql
184
- ```
185
-
186
- Now we can deploy the new application to Heroku. Deploying to Heroku is as simple as pushing the code using git:
187
-
188
- ```
189
- git push heroku master
190
- ```
191
-
192
- A `rake deploy2heroku` command is included in the generated Rakefile which does just this.
193
-
194
- Now that our application is deployed we need to run `rake db:migrate` to initialize our database on Heroku. To do this run:
195
-
196
- ```
197
- heroku run rake db:migrate
198
- ```
199
-
200
- We also need to set our environment variables on Heroku. The environment variables are stored in `.env` and are not tracked by git. This is to protect your credentials in the case of a source control breach. Heroku provides a command to set environment variables: `heroku config:set VAR=foo`. In the generated Rakefile there is a helper method that will properly set all the variables in your `.env` file:
201
-
202
- ```
203
- rake creds2heroku
204
- ```
205
-
206
- and make sure you have at least 1 dyno for web:
207
-
208
- ```
209
- heroku scale web=1
210
- ```
211
-
212
- Make sure you set your shopify apps url to your Heroku app url (and make sure to use the `https` version or else the Embedded App SDK won't work) in the Shopify Partner area https://app.shopify.com/services/partners/api_clients.
213
-
214
-
215
159
  Apps using this framework
216
160
  -------------------------
217
161
 
@@ -1,10 +1,10 @@
1
1
  #!/bin/bash
2
2
 
3
+ set -e
4
+
3
5
  cd example
4
6
 
5
7
  bundle install
6
8
  bundle exec rake db:migrate
7
9
  bundle exec rake test:prepare
8
10
  bundle exec rake test
9
-
10
- cd ../../..
data/example/Gemfile CHANGED
@@ -1,9 +1,9 @@
1
1
  source 'https://rubygems.org'
2
- ruby '2.5.3'
2
+ ruby '2.6.6'
3
3
 
4
4
  gem 'shopify-sinatra-app', path: '../'
5
5
  gem 'sinatra-activerecord'
6
- gem 'rack-flash3', require: 'rack-flash'
6
+ gem 'sinatra-flash'
7
7
 
8
8
  group :production do
9
9
  gem 'pg'
@@ -15,9 +15,7 @@ group :development, :test do
15
15
  end
16
16
 
17
17
  group :development do
18
- gem 'rake'
19
- gem 'foreman'
20
- gem 'dotenv'
18
+ gem 'rake', '>= 12.3.3'
21
19
  end
22
20
 
23
21
  group :test do
data/example/Rakefile CHANGED
@@ -2,24 +2,6 @@ require 'sinatra/activerecord/rake'
2
2
  require 'rake/testtask'
3
3
  require './src/app'
4
4
 
5
- task :creds2heroku do
6
- Bundler.with_clean_env do
7
- File.readlines('.env').each do |var|
8
- pipe = IO.popen("heroku config:set #{var}")
9
- while (line = pipe.gets)
10
- print line
11
- end
12
- end
13
- end
14
- end
15
-
16
- task :deploy2heroku do
17
- pipe = IO.popen('git push heroku master --force')
18
- while (line = pipe.gets)
19
- print line
20
- end
21
- end
22
-
23
5
  namespace :test do
24
6
  task :prepare do
25
7
  `RACK_ENV=test rake db:create`
data/example/config.ru CHANGED
@@ -1,7 +1,3 @@
1
- if Gem::Specification.find_all_by_name('dotenv').any?
2
- require 'dotenv'
3
- Dotenv.load
4
- end
5
-
6
1
  require './src/app'
2
+ SinatraApp.set :bind, '0.0.0.0'
7
3
  SinatraApp.run!
data/example/db/schema.rb CHANGED
@@ -2,11 +2,11 @@
2
2
  # of editing this file, please use the migrations feature of Active Record to
3
3
  # incrementally modify your database, and then regenerate this schema definition.
4
4
  #
5
- # Note that this schema.rb definition is the authoritative source for your
6
- # database schema. If you need to create the application database on another
7
- # system, you should be using db:schema:load, not running all the migrations
8
- # from scratch. The latter is a flawed and unsustainable approach (the more migrations
9
- # you'll amass, the slower it'll run and the greater likelihood for issues).
5
+ # This file is the source Rails uses to define your schema when running `bin/rails
6
+ # db:schema:load`. When creating a new database, `bin/rails db:schema:load` tends to
7
+ # be faster and is potentially less error prone than running all of your
8
+ # migrations from scratch. Old migrations may fail to apply correctly if those
9
+ # migrations use external dependencies or application code.
10
10
  #
11
11
  # It's strongly recommended that you check this file into your version control system.
12
12
 
data/example/src/app.rb CHANGED
@@ -1,7 +1,9 @@
1
1
  require 'sinatra/shopify-sinatra-app'
2
+ require 'sinatra/flash'
2
3
 
3
4
  class SinatraApp < Sinatra::Base
4
5
  register Sinatra::Shopify
6
+ register Sinatra::Flash
5
7
 
6
8
  # set the scope that your app needs, read more here:
7
9
  # http://docs.shopify.com/api/tutorials/oauth
@@ -21,10 +23,8 @@ class SinatraApp < Sinatra::Base
21
23
  # this endpoint recieves the uninstall webhook
22
24
  # and cleans up data, add to this endpoint as your app
23
25
  # stores more data.
24
- post '/uninstall' do
25
- shopify_webhook do |shop_name, params|
26
- Shop.find_by(name: shop_name).destroy
27
- end
26
+ shopify_webhook '/uninstall' do |shop_name, params|
27
+ Shop.find_by(name: shop_name).destroy
28
28
  end
29
29
 
30
30
  private
@@ -23,8 +23,9 @@ class AppTest < Minitest::Test
23
23
 
24
24
  def test_root_with_session
25
25
  set_session
26
- fake 'https://testshop.myshopify.com/admin/shop.json', body: {myshopify_domain: @shop_name}.to_json
27
- fake 'https://testshop.myshopify.com/admin/products.json?limit=10', body: '{}'
26
+ api_url = "https://testshop.myshopify.com/admin/api/#{app.settings.api_version}"
27
+ fake "#{api_url}/shop.json", body: {myshopify_domain: @shop_name}.to_json
28
+ fake "#{api_url}/products.json?limit=10", body: '{}'
28
29
  get '/'
29
30
  assert last_response.ok?
30
31
  end
@@ -41,18 +42,18 @@ class AppTest < Minitest::Test
41
42
  def test_root_without_session_redirects_to_install
42
43
  get '/'
43
44
  assert_equal 302, last_response.status
44
- assert_equal 'http://example.org/install', last_response.location
45
+ assert_equal 'http://example.org/login', last_response.location
45
46
  end
46
47
 
47
48
  def test_root_with_shop_redirects_to_auth
48
49
  get '/?shop=othertestshop.myshopify.com'
49
- assert_match '/auth/shopify?shop=othertestshop.myshopify.com', last_response.body
50
+ assert_equal 'http://example.org/login?shop=othertestshop.myshopify.com', last_response.location
50
51
  end
51
52
 
52
53
  def test_root_with_session_and_new_shop_redirects_to_auth
53
54
  set_session
54
55
  get '/?shop=othertestshop.myshopify.com'
55
- assert_match '/auth/shopify?shop=othertestshop.myshopify.com', last_response.body
56
+ assert_equal 'http://example.org/login?shop=othertestshop.myshopify.com', last_response.location
56
57
  end
57
58
 
58
59
  def test_root_rescues_UnauthorizedAccess_clears_session_and_redirects
@@ -1,11 +1,25 @@
1
1
  <script type="text/javascript">
2
- ShopifyApp.ready(function(){
3
- <% if flash[:notice] %>
4
- ShopifyApp.flashNotice("<%= flash[:notice] %>");
5
- <% end %>
6
-
7
- <% if flash[:error] %>
8
- ShopifyApp.flashError("<%= flash[:error] %>");
9
- <% end %>
10
- });
2
+ var AppBridge = window['app-bridge'];
3
+
4
+ var actions = AppBridge.actions;
5
+ var Toast = actions.Toast;
6
+
7
+ <% if flash[:notice] %>
8
+ var notice = Toast.create(app, {
9
+ message: "<%= flash[:notice] %>",
10
+ duration: 5000
11
+ });
12
+
13
+ notice.dispatch(Toast.Action.SHOW);
14
+ <% end %>
15
+
16
+ <% if flash[:error] %>
17
+ var notice = Toast.create(app, {
18
+ message: "<%= flash[:error] %>",
19
+ duration: 5000,
20
+ isError: true,
21
+ });
22
+
23
+ notice.dispatch(Toast.Action.SHOW);
24
+ <% end %>
11
25
  </script>
@@ -1,7 +1,10 @@
1
1
  <script type="text/javascript">
2
- ShopifyApp.ready(function(){
3
- ShopifyApp.Bar.initialize({
4
- icon: '<%= "#{base_url}/icon.png" %>'
5
- });
2
+ var AppBridge = window['app-bridge'];
3
+
4
+ var actions = AppBridge.actions;
5
+ var TitleBar = actions.TitleBar;
6
+
7
+ var titleBar = TitleBar.create(app, {
8
+ icon: '<%= "#{base_url}/icon.png" %>'
6
9
  });
7
10
  </script>
@@ -7,3 +7,5 @@
7
7
  <li><a href=<%="https://#{@shop.myshopify_domain}/admin/products/#{product.id}"%> target="_blank"> <%= product.id %> </a></li>
8
8
  <% end %>
9
9
  </ul>
10
+
11
+ <a href="/logout">Logout</a>
@@ -1,12 +1,14 @@
1
1
  <!DOCTYPE html>
2
2
  <html lang="en">
3
3
  <head>
4
- <script src="https://cdn.shopify.com/s/assets/external/app.js"></script>
4
+ <script src="https://unpkg.com/@shopify/app-bridge"></script>
5
5
  <script type="text/javascript">
6
- ShopifyApp.init({
6
+ var AppBridge = window['app-bridge'];
7
+ var createApp = AppBridge.default;
8
+
9
+ var app = createApp({
7
10
  apiKey: "<%= SinatraApp.settings.api_key %>",
8
11
  shopOrigin: "<%= shop_origin %>",
9
- debug: true
10
12
  });
11
13
  </script>
12
14
  <link href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" rel="stylesheet" integrity="sha256-7s5uDGW3AHqw6xtJmNNtr+OBRJUlgkNJEo78P4b0yRw= sha512-nNo+yCHEyn0smMxSswnf/OnX6/KwJuZTlNZBjauKhTK0c+zT+q5JOCx0UFhXQ6rJR9jg6Es8gPuD2uZcYDLqSw==" crossorigin="anonymous">
@@ -28,13 +28,15 @@
28
28
  <div style="margin-top: 30px"></div>
29
29
 
30
30
  <div class="container">
31
- <form role="form" class="form-large" action="/login" method="post">
32
- <div class="input-group form-wide">
33
- <input class="form-control" type="url" name="shop" placeholder="Shop URL">
34
- <span class="input-group-btn">
35
- <input class="btn" type="submit" value="Install App" />
36
- </span>
37
- </div>
31
+ <form id="login" role="form" class="form-large" action="/auth/shopify" method="post">
32
+ <input type="hidden" name="authenticity_token" value="<%= env['rack.session'][:csrf] %>" />
33
+ <div class="input-group form-wide">
34
+ <input class="form-control" name="shop" value="<%= params['shop'] %>" placeholder="your-shop-url.myshopify.com">
35
+ <span class="input-group-btn">
36
+ <input class="btn" type="submit" value="Login" />
37
+ </span>
38
+ </div>
38
39
  </form>
39
40
  </div>
40
- </body>
41
+ </body>
42
+
@@ -1,7 +1,6 @@
1
1
  require 'sinatra/base'
2
2
  require 'sinatra/activerecord'
3
3
 
4
- require 'rack-flash'
5
4
  require 'attr_encrypted'
6
5
  require 'active_support/all'
7
6
 
@@ -12,18 +11,13 @@ module Sinatra
12
11
  module Shopify
13
12
  module Methods
14
13
 
15
- # designed to be overriden
14
+ # designed to be overridden
16
15
  def after_shopify_auth
17
16
  end
18
17
 
19
- def logout
20
- session.delete(:shopify)
21
- session.clear
22
- end
23
-
24
- # for the esdk initializer
18
+ # for the app bridge initializer
25
19
  def shop_origin
26
- "https://#{session[:shopify][:shop]}"
20
+ "#{session[:shopify][:shop]}"
27
21
  end
28
22
 
29
23
  def shopify_session(&blk)
@@ -32,35 +26,42 @@ module Sinatra
32
26
 
33
27
  if no_session?
34
28
  authenticate(return_to, return_params)
29
+
35
30
  elsif different_shop?
36
- logout
31
+ clear_session
37
32
  authenticate(return_to, return_params)
33
+
38
34
  else
39
35
  shop_name = session[:shopify][:shop]
40
36
  token = session[:shopify][:token]
41
37
  activate_shopify_api(shop_name, token)
42
38
  yield shop_name
43
39
  end
40
+
44
41
  rescue ActiveResource::UnauthorizedAccess
45
- clear_session shop_name
46
- redirect request.path
47
- end
42
+ clear_session
48
43
 
49
- def shopify_webhook(&blk)
50
- return unless verify_shopify_webhook
51
- shop_name = request.env['HTTP_X_SHOPIFY_SHOP_DOMAIN']
52
- webhook_body = ActiveSupport::JSON.decode(request.body.read.to_s)
53
- yield shop_name, webhook_body
54
- status 200
44
+ shop = Shop.find_by(name: shop_name)
45
+ shop.token = nil
46
+ shop.save
47
+
48
+ redirect request.path
55
49
  end
56
50
 
57
51
  private
58
52
 
59
- def request_protocol
60
- request.secure? ? 'https' : 'http'
53
+ def authenticate(return_to = '/', return_params = nil)
54
+ session[:return_params] = return_params if return_params
55
+
56
+ if shop_name = sanitized_shop_param(params)
57
+ redirect "/login?shop=#{shop_name}"
58
+ else
59
+ redirect '/login'
60
+ end
61
61
  end
62
62
 
63
63
  def base_url
64
+ request_protocol = request.secure? ? 'https' : 'http'
64
65
  "#{request_protocol}://#{request.env['HTTP_HOST']}"
65
66
  end
66
67
 
@@ -69,66 +70,28 @@ module Sinatra
69
70
  end
70
71
 
71
72
  def different_shop?
72
- params[:shop].present? && session[:shopify][:shop] != sanitize_shop_param(params)
73
+ params[:shop].present? && session[:shopify][:shop] != sanitized_shop_param(params)
73
74
  end
74
75
 
75
- def authenticate(return_to = '/', return_params = nil)
76
- if shop_name = sanitized_shop_name
77
- session[:return_params] = return_params if return_params
78
- redirect_url = "/auth/shopify?shop=#{shop_name}&return_to=#{base_url}#{return_to}"
79
- redirect_javascript redirect_url
80
- else
81
- redirect '/install'
82
- end
76
+ def clear_session
77
+ session.delete(:shopify)
78
+ session.clear
83
79
  end
84
80
 
85
81
  def activate_shopify_api(shop_name, token)
86
- api_session = ShopifyAPI::Session.new(domain: shop_name, token: token, api_version: '2019-04')
82
+ api_session = ShopifyAPI::Session.new(domain: shop_name, token: token, api_version: settings.api_version)
87
83
  ShopifyAPI::Base.activate_session(api_session)
88
84
  end
89
85
 
90
- def clear_session(shop_name)
91
- logout
92
- shop = Shop.find_by(name: shop_name)
93
- shop.token = nil
94
- shop.save
95
- end
96
-
97
- def redirect_javascript(url)
98
- erb %(
99
- <!DOCTYPE html>
100
- <html lang="en">
101
- <head>
102
- <meta charset="utf-8" />
103
- <base target="_top">
104
- <title>Redirecting…</title>
105
-
106
- <script type='text/javascript'>
107
- // If the current window is the 'parent', change the URL by setting location.href
108
- if (window.top == window.self) {
109
- window.top.location.href = #{url.to_json};
110
-
111
- // If the current window is the 'child', change the parent's URL with postMessage
112
- } else {
113
- message = JSON.stringify({
114
- message: 'Shopify.API.remoteRedirect',
115
- data: { location: window.location.origin + #{url.to_json} }
116
- });
117
- window.parent.postMessage(message, 'https://#{sanitized_shop_name}');
118
- }
119
- </script>
120
- </head>
121
- <body>
122
- </body>
123
- </html>
124
- ), layout: false
125
- end
126
-
127
- def sanitized_shop_name
128
- @sanitized_shop_name ||= sanitize_shop_param(params)
86
+ def receive_webhook(&blk)
87
+ return unless verify_shopify_webhook
88
+ shop_name = request.env['HTTP_X_SHOPIFY_SHOP_DOMAIN']
89
+ webhook_body = ActiveSupport::JSON.decode(request.body.read.to_s)
90
+ yield shop_name, webhook_body
91
+ status 200
129
92
  end
130
93
 
131
- def sanitize_shop_param(params)
94
+ def sanitized_shop_param(params)
132
95
  return unless params[:shop].present?
133
96
  name = params[:shop].to_s.strip
134
97
  name += '.myshopify.com' if !name.include?('myshopify.com') && !name.include?('.')
@@ -148,50 +111,73 @@ module Sinatra
148
111
  if calculated_hmac == request.env['HTTP_X_SHOPIFY_HMAC_SHA256']
149
112
  true
150
113
  else
151
- puts 'Shopify Webhook verifictation failed!'
114
+ puts 'Shopify Webhook verification failed!'
152
115
  false
153
116
  end
154
117
  end
155
118
  end
156
119
 
120
+ def shopify_webhook(route, &blk)
121
+ settings.webhook_routes << route
122
+ post(route) do
123
+ receive_webhook(&blk)
124
+ end
125
+ end
126
+
157
127
  def self.registered(app)
158
128
  app.helpers Shopify::Methods
159
129
  app.register Sinatra::ActiveRecordExtension
160
130
 
161
131
  app.set :database_file, File.expand_path('config/database.yml')
132
+
133
+ app.set :erb, layout: :'layouts/application'
162
134
  app.set :views, File.expand_path('views')
163
135
  app.set :public_folder, File.expand_path('public')
164
- app.set :erb, layout: :'layouts/application'
165
- app.set :protection, except: :frame_options
166
-
167
- app.enable :sessions
168
136
  app.enable :inline_templates
169
137
 
138
+ app.set :protection, except: :frame_options
139
+
140
+ app.set :api_version, '2019-07'
170
141
  app.set :scope, 'read_products, read_orders'
171
142
 
172
143
  app.set :api_key, ENV['SHOPIFY_API_KEY']
173
144
  app.set :shared_secret, ENV['SHOPIFY_SHARED_SECRET']
174
145
  app.set :secret, ENV['SECRET']
175
146
 
176
- app.use Rack::Flash, sweep: true
147
+ # csrf needs to be disabled for webhook routes
148
+ app.set :webhook_routes, ['/uninstall']
149
+
150
+ # add support for put/patch/delete
177
151
  app.use Rack::MethodOverride
152
+
178
153
  app.use Rack::Session::Cookie, key: 'rack.session',
179
154
  path: '/',
155
+ secure: true,
156
+ same_site: 'None',
180
157
  secret: app.settings.secret,
181
158
  expire_after: 60 * 30 # half an hour in seconds
182
159
 
160
+ app.use Rack::Protection::AuthenticityToken, allow_if: lambda { |env|
161
+ app.settings.webhook_routes.include?(env["PATH_INFO"])
162
+ }
163
+
164
+ OmniAuth.config.allowed_request_methods = [:post]
165
+
183
166
  app.use OmniAuth::Builder do
184
167
  provider :shopify,
185
- app.settings.api_key,
186
- app.settings.shared_secret,
168
+ app.settings.api_key,
169
+ app.settings.shared_secret,
170
+ scope: app.settings.scope,
171
+ setup: lambda { |env|
172
+ shop = if env['REQUEST_METHOD'] == 'POST'
173
+ env['rack.request.form_hash']['shop']
174
+ else
175
+ Rack::Utils.parse_query(env['QUERY_STRING'])['shop']
176
+ end
187
177
 
188
- scope: app.settings.scope,
189
-
190
- setup: lambda { |env|
191
- params = Rack::Utils.parse_query(env['QUERY_STRING'])
192
- site_url = "https://#{params['shop']}"
193
- env['omniauth.strategy'].options[:client_options][:site] = site_url
194
- }
178
+ site_url = "https://#{shop}"
179
+ env['omniauth.strategy'].options[:client_options][:site] = site_url
180
+ }
195
181
  end
196
182
 
197
183
  ShopifyAPI::Session.setup(
@@ -199,21 +185,13 @@ module Sinatra
199
185
  secret: app.settings.shared_secret
200
186
  )
201
187
 
202
- app.get '/install' do
203
- if params[:shop].present?
204
- authenticate
205
- else
206
- erb :install, layout: false
207
- end
208
- end
209
-
210
- app.post '/login' do
211
- authenticate
188
+ app.get '/login' do
189
+ erb :login, layout: false
212
190
  end
213
191
 
214
192
  app.get '/logout' do
215
- logout
216
- redirect '/install'
193
+ clear_session
194
+ redirect '/login'
217
195
  end
218
196
 
219
197
  app.get '/auth/shopify/callback' do
@@ -231,10 +209,10 @@ module Sinatra
231
209
 
232
210
  after_shopify_auth()
233
211
 
234
- return_to = env['omniauth.params']['return_to']
235
212
  return_params = session[:return_params]
236
213
  session.delete(:return_params)
237
214
 
215
+ return_to = '/'
238
216
  return_to += "?#{return_params.to_query}" if return_params.present?
239
217
 
240
218
  redirect return_to
@@ -1,6 +1,6 @@
1
1
  Gem::Specification.new do |s|
2
2
  s.name = 'shopify-sinatra-app'
3
- s.version = '0.8.0'
3
+ s.version = '1.0.0'
4
4
 
5
5
  s.summary = 'A classy shopify app'
6
6
  s.description = 'A Sinatra extension for building Shopify Apps. Akin to the shopify_app gem but for Sinatra'
@@ -13,16 +13,16 @@ Gem::Specification.new do |s|
13
13
  s.files = `git ls-files`.split("\n")
14
14
  s.executables << 'shopify-sinatra-app-generator'
15
15
 
16
- s.add_runtime_dependency 'sinatra', '~> 2.0.2'
16
+ s.add_runtime_dependency 'sinatra', '>= 2.0.2', '< 2.2.0'
17
17
  s.add_runtime_dependency 'sinatra-activerecord', '~> 2.0.9'
18
- s.add_runtime_dependency 'rack-flash3', '~> 1.0.5'
19
18
  s.add_runtime_dependency 'activesupport'
20
19
  s.add_runtime_dependency 'attr_encrypted', '~> 3.1.0'
21
20
 
22
- s.add_runtime_dependency 'shopify_api', '~> 7.0.1'
23
- s.add_runtime_dependency 'omniauth-shopify-oauth2'
21
+ s.add_runtime_dependency 'shopify_api', '>= 7.0.1', '< 9.4.0'
22
+ s.add_runtime_dependency 'omniauth-shopify-oauth2', '>= 2.3.2'
23
+ s.add_runtime_dependency 'omniauth', '>= 2.0.4'
24
24
 
25
- s.add_development_dependency 'rake'
25
+ s.add_development_dependency 'rake', '>= 12.3.3'
26
26
  s.add_development_dependency 'sqlite3'
27
27
  s.add_development_dependency 'minitest'
28
28
  s.add_development_dependency 'rack-test'
metadata CHANGED
@@ -1,29 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shopify-sinatra-app
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Hughes
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2019-04-26 00:00:00.000000000 Z
11
+ date: 2021-06-20 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: sinatra
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - "~>"
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: 2.0.2
20
+ - - "<"
21
+ - !ruby/object:Gem::Version
22
+ version: 2.2.0
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
- - - "~>"
27
+ - - ">="
25
28
  - !ruby/object:Gem::Version
26
29
  version: 2.0.2
30
+ - - "<"
31
+ - !ruby/object:Gem::Version
32
+ version: 2.2.0
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: sinatra-activerecord
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -38,20 +44,6 @@ dependencies:
38
44
  - - "~>"
39
45
  - !ruby/object:Gem::Version
40
46
  version: 2.0.9
41
- - !ruby/object:Gem::Dependency
42
- name: rack-flash3
43
- requirement: !ruby/object:Gem::Requirement
44
- requirements:
45
- - - "~>"
46
- - !ruby/object:Gem::Version
47
- version: 1.0.5
48
- type: :runtime
49
- prerelease: false
50
- version_requirements: !ruby/object:Gem::Requirement
51
- requirements:
52
- - - "~>"
53
- - !ruby/object:Gem::Version
54
- version: 1.0.5
55
47
  - !ruby/object:Gem::Dependency
56
48
  name: activesupport
57
49
  requirement: !ruby/object:Gem::Requirement
@@ -84,44 +76,64 @@ dependencies:
84
76
  name: shopify_api
85
77
  requirement: !ruby/object:Gem::Requirement
86
78
  requirements:
87
- - - "~>"
79
+ - - ">="
88
80
  - !ruby/object:Gem::Version
89
81
  version: 7.0.1
82
+ - - "<"
83
+ - !ruby/object:Gem::Version
84
+ version: 9.4.0
90
85
  type: :runtime
91
86
  prerelease: false
92
87
  version_requirements: !ruby/object:Gem::Requirement
93
88
  requirements:
94
- - - "~>"
89
+ - - ">="
95
90
  - !ruby/object:Gem::Version
96
91
  version: 7.0.1
92
+ - - "<"
93
+ - !ruby/object:Gem::Version
94
+ version: 9.4.0
97
95
  - !ruby/object:Gem::Dependency
98
96
  name: omniauth-shopify-oauth2
99
97
  requirement: !ruby/object:Gem::Requirement
100
98
  requirements:
101
99
  - - ">="
102
100
  - !ruby/object:Gem::Version
103
- version: '0'
101
+ version: 2.3.2
104
102
  type: :runtime
105
103
  prerelease: false
106
104
  version_requirements: !ruby/object:Gem::Requirement
107
105
  requirements:
108
106
  - - ">="
109
107
  - !ruby/object:Gem::Version
110
- version: '0'
108
+ version: 2.3.2
109
+ - !ruby/object:Gem::Dependency
110
+ name: omniauth
111
+ requirement: !ruby/object:Gem::Requirement
112
+ requirements:
113
+ - - ">="
114
+ - !ruby/object:Gem::Version
115
+ version: 2.0.4
116
+ type: :runtime
117
+ prerelease: false
118
+ version_requirements: !ruby/object:Gem::Requirement
119
+ requirements:
120
+ - - ">="
121
+ - !ruby/object:Gem::Version
122
+ version: 2.0.4
111
123
  - !ruby/object:Gem::Dependency
112
124
  name: rake
113
125
  requirement: !ruby/object:Gem::Requirement
114
126
  requirements:
115
127
  - - ">="
116
128
  - !ruby/object:Gem::Version
117
- version: '0'
129
+ version: 12.3.3
118
130
  type: :development
119
131
  prerelease: false
120
132
  version_requirements: !ruby/object:Gem::Requirement
121
133
  requirements:
122
134
  - - ">="
123
135
  - !ruby/object:Gem::Version
124
- version: '0'
136
+ version: 12.3.3
125
137
  - !ruby/object:Gem::Dependency
126
138
  name: sqlite3
127
139
  requirement: !ruby/object:Gem::Requirement
@@ -200,12 +212,14 @@ executables:
200
212
  extensions: []
201
213
  extra_rdoc_files: []
202
214
  files:
215
+ - ".circleci/config.yml"
216
+ - ".github/dependabot.yml"
203
217
  - ".gitignore"
204
- - ".travis.yml"
205
218
  - CHANGELOG
206
219
  - Gemfile
207
220
  - LICENSE
208
221
  - README.md
222
+ - bin/ci.sh
209
223
  - bin/shopify-sinatra-app-generator
210
224
  - example/.gitignore
211
225
  - example/Gemfile
@@ -226,11 +240,10 @@ files:
226
240
  - example/views/_flash_messages.erb
227
241
  - example/views/_top_bar.erb
228
242
  - example/views/home.erb
229
- - example/views/install.erb
230
243
  - example/views/layouts/application.erb
244
+ - example/views/login.erb
231
245
  - lib/sinatra/shopify-sinatra-app.rb
232
246
  - shopify-sinatra-app.gemspec
233
- - test.sh
234
247
  homepage: https://github.com/kevinhughes27/shopify-sinatra-app/
235
248
  licenses:
236
249
  - MIT
@@ -250,8 +263,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
250
263
  - !ruby/object:Gem::Version
251
264
  version: '0'
252
265
  requirements: []
253
- rubyforge_project:
254
- rubygems_version: 2.7.6
266
+ rubygems_version: 3.0.3
255
267
  signing_key:
256
268
  specification_version: 4
257
269
  summary: A classy shopify app
data/.travis.yml DELETED
@@ -1,13 +0,0 @@
1
- language: ruby
2
-
3
- rvm:
4
- - 2.5.3
5
-
6
- gemfile: example/Gemfile
7
-
8
- script: "./test.sh"
9
-
10
- notifications:
11
- email:
12
- on_success: never
13
- on_failure: never