shopify-sinatra-app 0.11.0 → 1.0.1

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: 06c208a5e6a5d5b7f496276e1aedea79466f8d7abc6082afdb7cff0b88223d49
4
- data.tar.gz: 18439bd6c8f38719b02b7afdd1ec7559bf1b016f97fcca41d473739c85708042
3
+ metadata.gz: bd601f8ec2bc981f54dcda8ce30a7b373e42f0b3db148d400a58f5642dfbfbcc
4
+ data.tar.gz: 5a9d30b5cf9ccca7883fb4a7329cdf54bd6ee9a18d5f248137193a90bde27749
5
5
  SHA512:
6
- metadata.gz: fbd6555480c19155bb742b423e27df1f53aacdb3779134fdc8b18294926611f863f43abed6a2393e4994247381cc21653c64a4171786063da5f022b1aef52f2d
7
- data.tar.gz: bae80dff8e96cc0dd29febed95b8a2336a944cab4b397803f9ad816b4c93f6996e7cc094e605bb5e3d88a12fbbbb2cf1e516d81f7575cfc7eec68dd50ea8afe1
6
+ metadata.gz: 01f6a06bcd48313f281a0f59eed41d87dc71da586f095d7192667d67af8cc37ad5b6e18846a2c9bb88298b46d5f240290784edf54e3ca6ca1d69dff5932e4662
7
+ data.tar.gz: 49de0b0f1982746dedab5792515a9d27dd777abb74363016db794fa92ce5a365addcb527c6e549920aee93d094e2dcdfcd2209ab5b898a32af26a2ceb1f39bd9
@@ -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,5 +1,26 @@
1
+ 1.0.1
2
+ -----
3
+ * drop sinatra version requirement from gemspec
4
+
5
+ 1.0.0
6
+ -----
7
+ * Update to omniauth 2.0.4 and add rack protection authenticity token
8
+ * All forms will need an authenticity_token param added like in the login view
9
+ * Refactored the provided routes to be simpler. Now only login and logout (no more install)
10
+ * shopify_webhook now defines the route because it needs to also configure ignoring csrf for the route
11
+ * refactored other internal methods to simplify and reduce surface area
12
+ * Removed heroku rake tasks. They are out of date and don't encourage best practises
13
+
14
+ 0.12.0
15
+ ------
16
+ * Update to use the Shopify AppBridge instead of the ESDK
17
+ * This change is mostly to generated files so you'll need to apply those updates
18
+ to your own versions.
19
+ * shop_origin no longer includes protocol
20
+ * return_to re-worked to function with the AppBridge
21
+
1
22
  0.11.0
2
- ----------
23
+ ------
3
24
  * remove rack-flash3 use sinatra-flash instead
4
25
  * remove a duplicate config of sessions that was breaking the same_site fix
5
26
  * remove a runtime dependency that didn't end up being used for the same_site fix but was added anyways
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 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.
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,13 +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
- EXIT_CODE=$?
10
-
11
- cd ../../..
12
-
13
- exit $EXIT_CODE
data/example/Gemfile CHANGED
@@ -1,5 +1,5 @@
1
1
  source 'https://rubygems.org'
2
- ruby '2.6.3'
2
+ ruby '2.6.6'
3
3
 
4
4
  gem 'shopify-sinatra-app', path: '../'
5
5
  gem 'sinatra-activerecord'
@@ -16,8 +16,6 @@ end
16
16
 
17
17
  group :development do
18
18
  gem 'rake', '>= 12.3.3'
19
- gem 'foreman'
20
- gem 'dotenv'
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,8 +2,8 @@
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
- # This file is the source Rails uses to define your schema when running `rails
6
- # db:schema:load`. When creating a new database, `rails db:schema:load` tends to
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
7
  # be faster and is potentially less error prone than running all of your
8
8
  # migrations from scratch. Old migrations may fail to apply correctly if those
9
9
  # migrations use external dependencies or application code.
data/example/src/app.rb CHANGED
@@ -23,10 +23,8 @@ class SinatraApp < Sinatra::Base
23
23
  # this endpoint recieves the uninstall webhook
24
24
  # and cleans up data, add to this endpoint as your app
25
25
  # stores more data.
26
- post '/uninstall' do
27
- shopify_webhook do |shop_name, params|
28
- Shop.find_by(name: shop_name).destroy
29
- end
26
+ shopify_webhook '/uninstall' do |shop_name, params|
27
+ Shop.find_by(name: shop_name).destroy
30
28
  end
31
29
 
32
30
  private
@@ -42,18 +42,18 @@ class AppTest < Minitest::Test
42
42
  def test_root_without_session_redirects_to_install
43
43
  get '/'
44
44
  assert_equal 302, last_response.status
45
- assert_equal 'http://example.org/install', last_response.location
45
+ assert_equal 'http://example.org/login', last_response.location
46
46
  end
47
47
 
48
48
  def test_root_with_shop_redirects_to_auth
49
49
  get '/?shop=othertestshop.myshopify.com'
50
- 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
51
51
  end
52
52
 
53
53
  def test_root_with_session_and_new_shop_redirects_to_auth
54
54
  set_session
55
55
  get '/?shop=othertestshop.myshopify.com'
56
- 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
57
57
  end
58
58
 
59
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
+
@@ -11,18 +11,13 @@ module Sinatra
11
11
  module Shopify
12
12
  module Methods
13
13
 
14
- # designed to be overriden
14
+ # designed to be overridden
15
15
  def after_shopify_auth
16
16
  end
17
17
 
18
- def logout
19
- session.delete(:shopify)
20
- session.clear
21
- end
22
-
23
- # for the esdk initializer
18
+ # for the app bridge initializer
24
19
  def shop_origin
25
- "https://#{session[:shopify][:shop]}"
20
+ "#{session[:shopify][:shop]}"
26
21
  end
27
22
 
28
23
  def shopify_session(&blk)
@@ -31,35 +26,42 @@ module Sinatra
31
26
 
32
27
  if no_session?
33
28
  authenticate(return_to, return_params)
29
+
34
30
  elsif different_shop?
35
- logout
31
+ clear_session
36
32
  authenticate(return_to, return_params)
33
+
37
34
  else
38
35
  shop_name = session[:shopify][:shop]
39
36
  token = session[:shopify][:token]
40
37
  activate_shopify_api(shop_name, token)
41
38
  yield shop_name
42
39
  end
40
+
43
41
  rescue ActiveResource::UnauthorizedAccess
44
- clear_session shop_name
45
- redirect request.path
46
- end
42
+ clear_session
47
43
 
48
- def shopify_webhook(&blk)
49
- return unless verify_shopify_webhook
50
- shop_name = request.env['HTTP_X_SHOPIFY_SHOP_DOMAIN']
51
- webhook_body = ActiveSupport::JSON.decode(request.body.read.to_s)
52
- yield shop_name, webhook_body
53
- status 200
44
+ shop = Shop.find_by(name: shop_name)
45
+ shop.token = nil
46
+ shop.save
47
+
48
+ redirect request.path
54
49
  end
55
50
 
56
51
  private
57
52
 
58
- def request_protocol
59
- 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
60
61
  end
61
62
 
62
63
  def base_url
64
+ request_protocol = request.secure? ? 'https' : 'http'
63
65
  "#{request_protocol}://#{request.env['HTTP_HOST']}"
64
66
  end
65
67
 
@@ -68,17 +70,12 @@ module Sinatra
68
70
  end
69
71
 
70
72
  def different_shop?
71
- params[:shop].present? && session[:shopify][:shop] != sanitize_shop_param(params)
73
+ params[:shop].present? && session[:shopify][:shop] != sanitized_shop_param(params)
72
74
  end
73
75
 
74
- def authenticate(return_to = '/', return_params = nil)
75
- if shop_name = sanitized_shop_name
76
- session[:return_params] = return_params if return_params
77
- redirect_url = "/auth/shopify?shop=#{shop_name}&return_to=#{base_url}#{return_to}"
78
- redirect_javascript redirect_url
79
- else
80
- redirect '/install'
81
- end
76
+ def clear_session
77
+ session.delete(:shopify)
78
+ session.clear
82
79
  end
83
80
 
84
81
  def activate_shopify_api(shop_name, token)
@@ -86,48 +83,15 @@ module Sinatra
86
83
  ShopifyAPI::Base.activate_session(api_session)
87
84
  end
88
85
 
89
- def clear_session(shop_name)
90
- logout
91
- shop = Shop.find_by(name: shop_name)
92
- shop.token = nil
93
- shop.save
94
- end
95
-
96
- def redirect_javascript(url)
97
- erb %(
98
- <!DOCTYPE html>
99
- <html lang="en">
100
- <head>
101
- <meta charset="utf-8" />
102
- <base target="_top">
103
- <title>Redirecting…</title>
104
-
105
- <script type='text/javascript'>
106
- // If the current window is the 'parent', change the URL by setting location.href
107
- if (window.top == window.self) {
108
- window.top.location.href = #{url.to_json};
109
-
110
- // If the current window is the 'child', change the parent's URL with postMessage
111
- } else {
112
- message = JSON.stringify({
113
- message: 'Shopify.API.remoteRedirect',
114
- data: { location: window.location.origin + #{url.to_json} }
115
- });
116
- window.parent.postMessage(message, 'https://#{sanitized_shop_name}');
117
- }
118
- </script>
119
- </head>
120
- <body>
121
- </body>
122
- </html>
123
- ), layout: false
124
- end
125
-
126
- def sanitized_shop_name
127
- @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
128
92
  end
129
93
 
130
- def sanitize_shop_param(params)
94
+ def sanitized_shop_param(params)
131
95
  return unless params[:shop].present?
132
96
  name = params[:shop].to_s.strip
133
97
  name += '.myshopify.com' if !name.include?('myshopify.com') && !name.include?('.')
@@ -147,21 +111,30 @@ module Sinatra
147
111
  if calculated_hmac == request.env['HTTP_X_SHOPIFY_HMAC_SHA256']
148
112
  true
149
113
  else
150
- puts 'Shopify Webhook verifictation failed!'
114
+ puts 'Shopify Webhook verification failed!'
151
115
  false
152
116
  end
153
117
  end
154
118
  end
155
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
+
156
127
  def self.registered(app)
157
128
  app.helpers Shopify::Methods
158
129
  app.register Sinatra::ActiveRecordExtension
159
- app.enable :inline_templates
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'
136
+ app.enable :inline_templates
137
+
165
138
  app.set :protection, except: :frame_options
166
139
 
167
140
  app.set :api_version, '2019-07'
@@ -171,7 +144,12 @@ module Sinatra
171
144
  app.set :shared_secret, ENV['SHOPIFY_SHARED_SECRET']
172
145
  app.set :secret, ENV['SECRET']
173
146
 
147
+ # csrf needs to be disabled for webhook routes
148
+ app.set :webhook_routes, ['/uninstall']
149
+
150
+ # add support for put/patch/delete
174
151
  app.use Rack::MethodOverride
152
+
175
153
  app.use Rack::Session::Cookie, key: 'rack.session',
176
154
  path: '/',
177
155
  secure: true,
@@ -179,18 +157,27 @@ module Sinatra
179
157
  secret: app.settings.secret,
180
158
  expire_after: 60 * 30 # half an hour in seconds
181
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
+
182
166
  app.use OmniAuth::Builder do
183
167
  provider :shopify,
184
- app.settings.api_key,
185
- 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
186
177
 
187
- scope: app.settings.scope,
188
-
189
- setup: lambda { |env|
190
- params = Rack::Utils.parse_query(env['QUERY_STRING'])
191
- site_url = "https://#{params['shop']}"
192
- env['omniauth.strategy'].options[:client_options][:site] = site_url
193
- }
178
+ site_url = "https://#{shop}"
179
+ env['omniauth.strategy'].options[:client_options][:site] = site_url
180
+ }
194
181
  end
195
182
 
196
183
  ShopifyAPI::Session.setup(
@@ -198,21 +185,13 @@ module Sinatra
198
185
  secret: app.settings.shared_secret
199
186
  )
200
187
 
201
- app.get '/install' do
202
- if params[:shop].present?
203
- authenticate
204
- else
205
- erb :install, layout: false
206
- end
207
- end
208
-
209
- app.post '/login' do
210
- authenticate
188
+ app.get '/login' do
189
+ erb :login, layout: false
211
190
  end
212
191
 
213
192
  app.get '/logout' do
214
- logout
215
- redirect '/install'
193
+ clear_session
194
+ redirect '/login'
216
195
  end
217
196
 
218
197
  app.get '/auth/shopify/callback' do
@@ -230,10 +209,10 @@ module Sinatra
230
209
 
231
210
  after_shopify_auth()
232
211
 
233
- return_to = env['omniauth.params']['return_to']
234
212
  return_params = session[:return_params]
235
213
  session.delete(:return_params)
236
214
 
215
+ return_to = '/'
237
216
  return_to += "?#{return_params.to_query}" if return_params.present?
238
217
 
239
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.11.0'
3
+ s.version = '1.0.1'
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,13 +13,14 @@ 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'
17
17
  s.add_runtime_dependency 'sinatra-activerecord', '~> 2.0.9'
18
18
  s.add_runtime_dependency 'activesupport'
19
19
  s.add_runtime_dependency 'attr_encrypted', '~> 3.1.0'
20
20
 
21
- s.add_runtime_dependency 'shopify_api', '>= 7.0.1', '< 9.1.0'
22
- s.add_runtime_dependency 'omniauth-shopify-oauth2'
21
+ s.add_runtime_dependency 'shopify_api', '>= 7.0.1', '< 9.6'
22
+ s.add_runtime_dependency 'omniauth-shopify-oauth2', '>= 2.3.2'
23
+ s.add_runtime_dependency 'omniauth', '>= 2.0.4'
23
24
 
24
25
  s.add_development_dependency 'rake', '>= 12.3.3'
25
26
  s.add_development_dependency 'sqlite3'
metadata CHANGED
@@ -1,29 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: shopify-sinatra-app
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 1.0.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Kevin Hughes
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-03-29 00:00:00.000000000 Z
11
+ date: 2022-05-10 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
- version: 2.0.2
19
+ version: '0'
20
20
  type: :runtime
21
21
  prerelease: false
22
22
  version_requirements: !ruby/object:Gem::Requirement
23
23
  requirements:
24
- - - "~>"
24
+ - - ">="
25
25
  - !ruby/object:Gem::Version
26
- version: 2.0.2
26
+ version: '0'
27
27
  - !ruby/object:Gem::Dependency
28
28
  name: sinatra-activerecord
29
29
  requirement: !ruby/object:Gem::Requirement
@@ -75,7 +75,7 @@ dependencies:
75
75
  version: 7.0.1
76
76
  - - "<"
77
77
  - !ruby/object:Gem::Version
78
- version: 9.1.0
78
+ version: '9.6'
79
79
  type: :runtime
80
80
  prerelease: false
81
81
  version_requirements: !ruby/object:Gem::Requirement
@@ -85,21 +85,35 @@ dependencies:
85
85
  version: 7.0.1
86
86
  - - "<"
87
87
  - !ruby/object:Gem::Version
88
- version: 9.1.0
88
+ version: '9.6'
89
89
  - !ruby/object:Gem::Dependency
90
90
  name: omniauth-shopify-oauth2
91
91
  requirement: !ruby/object:Gem::Requirement
92
92
  requirements:
93
93
  - - ">="
94
94
  - !ruby/object:Gem::Version
95
- version: '0'
95
+ version: 2.3.2
96
96
  type: :runtime
97
97
  prerelease: false
98
98
  version_requirements: !ruby/object:Gem::Requirement
99
99
  requirements:
100
100
  - - ">="
101
101
  - !ruby/object:Gem::Version
102
- version: '0'
102
+ version: 2.3.2
103
+ - !ruby/object:Gem::Dependency
104
+ name: omniauth
105
+ requirement: !ruby/object:Gem::Requirement
106
+ requirements:
107
+ - - ">="
108
+ - !ruby/object:Gem::Version
109
+ version: 2.0.4
110
+ type: :runtime
111
+ prerelease: false
112
+ version_requirements: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - ">="
115
+ - !ruby/object:Gem::Version
116
+ version: 2.0.4
103
117
  - !ruby/object:Gem::Dependency
104
118
  name: rake
105
119
  requirement: !ruby/object:Gem::Requirement
@@ -192,12 +206,14 @@ executables:
192
206
  extensions: []
193
207
  extra_rdoc_files: []
194
208
  files:
209
+ - ".circleci/config.yml"
210
+ - ".github/dependabot.yml"
195
211
  - ".gitignore"
196
- - ".travis.yml"
197
212
  - CHANGELOG
198
213
  - Gemfile
199
214
  - LICENSE
200
215
  - README.md
216
+ - bin/ci.sh
201
217
  - bin/shopify-sinatra-app-generator
202
218
  - example/.gitignore
203
219
  - example/Gemfile
@@ -218,11 +234,10 @@ files:
218
234
  - example/views/_flash_messages.erb
219
235
  - example/views/_top_bar.erb
220
236
  - example/views/home.erb
221
- - example/views/install.erb
222
237
  - example/views/layouts/application.erb
238
+ - example/views/login.erb
223
239
  - lib/sinatra/shopify-sinatra-app.rb
224
240
  - shopify-sinatra-app.gemspec
225
- - test.sh
226
241
  homepage: https://github.com/kevinhughes27/shopify-sinatra-app/
227
242
  licenses:
228
243
  - MIT
data/.travis.yml DELETED
@@ -1,13 +0,0 @@
1
- language: ruby
2
-
3
- rvm:
4
- - 2.6.3
5
-
6
- gemfile: example/Gemfile
7
-
8
- script: "./test.sh"
9
-
10
- notifications:
11
- email:
12
- on_success: never
13
- on_failure: never