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 +4 -4
- data/.circleci/config.yml +15 -0
- data/.github/dependabot.yml +14 -0
- data/CHANGELOG +32 -0
- data/README.md +13 -69
- data/{test.sh → bin/ci.sh} +2 -2
- data/example/Gemfile +3 -5
- data/example/Rakefile +0 -18
- data/example/config.ru +1 -5
- data/example/db/schema.rb +5 -5
- data/example/src/app.rb +4 -4
- data/example/test/app_test.rb +6 -5
- data/example/views/_flash_messages.erb +23 -9
- data/example/views/_top_bar.erb +7 -4
- data/example/views/home.erb +2 -0
- data/example/views/layouts/application.erb +5 -3
- data/example/views/{install.erb → login.erb} +10 -8
- data/lib/sinatra/shopify-sinatra-app.rb +77 -99
- data/shopify-sinatra-app.gemspec +6 -6
- metadata +41 -29
- data/.travis.yml +0 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 46049e2afcd7dce3a1d3f115ba665a731d4fcb1b5344dcc9ff84649cfd30beda
|
4
|
+
data.tar.gz: 2ba425471b1cbb5e40f91178d646d8c0dd4da456321518a0772afb7ec4359058
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: c7223a2223ba1a71865bdbf05ef3d20585723da858cc99b2ac3872c13c29a3cb6db4e03502a8412f329393e6458e4d2c8038355abcd81247992bd44cf4ff8826
|
7
|
+
data.tar.gz: 81f547ba807a56f66802482746ff9a7d6d4ccacf7c9c5d3650491e3cabde7280fed22ce576b42a901f0ce5a57f36cef355108ac12f82223aeca33b9dc8ca1ac3
|
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.
|
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
|
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
|
-
|
94
|
-
|
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 `
|
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/)
|
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
|
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
|
-
|
134
|
+
overmind start
|
137
135
|
```
|
138
136
|
|
139
|
-
|
137
|
+
To connect to a single process to use a debugger/break point use `overmind connect <process>`
|
140
138
|
|
141
|
-
To debug your app
|
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
|
|
data/{test.sh → bin/ci.sh}
RENAMED
data/example/Gemfile
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
source 'https://rubygems.org'
|
2
|
-
ruby '2.
|
2
|
+
ruby '2.6.6'
|
3
3
|
|
4
4
|
gem 'shopify-sinatra-app', path: '../'
|
5
5
|
gem 'sinatra-activerecord'
|
6
|
-
gem '
|
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
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
|
-
#
|
6
|
-
#
|
7
|
-
#
|
8
|
-
# from scratch.
|
9
|
-
#
|
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
|
-
|
25
|
-
|
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
|
data/example/test/app_test.rb
CHANGED
@@ -23,8 +23,9 @@ class AppTest < Minitest::Test
|
|
23
23
|
|
24
24
|
def test_root_with_session
|
25
25
|
set_session
|
26
|
-
|
27
|
-
fake
|
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/
|
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
|
-
|
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
|
-
|
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
|
-
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
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>
|
data/example/views/_top_bar.erb
CHANGED
@@ -1,7 +1,10 @@
|
|
1
1
|
<script type="text/javascript">
|
2
|
-
|
3
|
-
|
4
|
-
|
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>
|
data/example/views/home.erb
CHANGED
@@ -1,12 +1,14 @@
|
|
1
1
|
<!DOCTYPE html>
|
2
2
|
<html lang="en">
|
3
3
|
<head>
|
4
|
-
<script src="https://
|
4
|
+
<script src="https://unpkg.com/@shopify/app-bridge"></script>
|
5
5
|
<script type="text/javascript">
|
6
|
-
|
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="/
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
</
|
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
|
14
|
+
# designed to be overridden
|
16
15
|
def after_shopify_auth
|
17
16
|
end
|
18
17
|
|
19
|
-
|
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
|
-
"
|
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
|
-
|
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
|
46
|
-
redirect request.path
|
47
|
-
end
|
42
|
+
clear_session
|
48
43
|
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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
|
60
|
-
|
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] !=
|
73
|
+
params[:shop].present? && session[:shopify][:shop] != sanitized_shop_param(params)
|
73
74
|
end
|
74
75
|
|
75
|
-
def
|
76
|
-
|
77
|
-
|
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:
|
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
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
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
|
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
|
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
|
-
|
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
|
-
|
186
|
-
|
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
|
-
|
189
|
-
|
190
|
-
|
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 '/
|
203
|
-
|
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
|
-
|
216
|
-
redirect '/
|
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
|
data/shopify-sinatra-app.gemspec
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
Gem::Specification.new do |s|
|
2
2
|
s.name = 'shopify-sinatra-app'
|
3
|
-
s.version = '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', '
|
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', '
|
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.
|
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:
|
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:
|
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:
|
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:
|
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:
|
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
|
-
|
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
|