sinatra-embedded-shopify-app 0.5.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,275 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'sinatra/base'
4
+ require 'sinatra/activerecord'
5
+ require 'active_support/all'
6
+ require 'shopify_api'
7
+ require 'omniauth-shopify-oauth2'
8
+
9
+ # Sinatra
10
+ module Sinatra
11
+ # Shopify
12
+ module Shopify
13
+ # Methods
14
+ module Methods
15
+ # designed to be overridden
16
+ def after_shopify_auth; end
17
+
18
+ # for the app bridge initializer
19
+ def shop_host
20
+ (session[:shopify][:host]).to_s
21
+ end
22
+
23
+ def shopify_session
24
+ return_to = request.path
25
+ return_params = request.params
26
+
27
+ if no_session?
28
+ authenticate(return_to, return_params)
29
+
30
+ elsif different_shop?
31
+ clear_session
32
+ authenticate(return_to, return_params)
33
+
34
+ else
35
+ shop_name = session[:shopify][:shop]
36
+ token = session[:shopify][:token]
37
+ activate_shopify_api(shop_name, token)
38
+ yield shop_name
39
+ end
40
+ rescue ActiveResource::UnauthorizedAccess
41
+ clear_session
42
+
43
+ shop = Shop.find_by(shopify_domain: shop_name)
44
+ shop.shopify_token = nil
45
+ shop.save
46
+
47
+ redirect request.path
48
+ end
49
+
50
+ private
51
+
52
+ def authenticate(_return_to = '/', return_params = nil)
53
+ session[:return_params] = return_params if return_params
54
+
55
+ if (shop_name = sanitized_shop_param(params))
56
+ redirect "/login?shop=#{shop_name}"
57
+ else
58
+ redirect '/login'
59
+ end
60
+ end
61
+
62
+ def base_url
63
+ request_protocol = request.secure? ? 'https' : 'http'
64
+ "#{request_protocol}://#{request.env['HTTP_HOST']}"
65
+ end
66
+
67
+ def no_session?
68
+ !session.key?(:shopify)
69
+ end
70
+
71
+ def different_shop?
72
+ params[:shop].present? && session[:shopify][:shop] != sanitized_shop_param(params)
73
+ end
74
+
75
+ def clear_session
76
+ session.delete(:shopify)
77
+ session.clear
78
+ end
79
+
80
+ def activate_shopify_api(shop_name, token)
81
+ api_session = ShopifyAPI::Session.new(domain: shop_name, token: token, api_version: settings.api_version)
82
+ ShopifyAPI::Base.activate_session(api_session)
83
+ end
84
+
85
+ def receive_webhook
86
+ return unless verify_shopify_webhook
87
+
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
92
+ end
93
+
94
+ def sanitized_shop_param(params)
95
+ return unless params[:shop].present?
96
+
97
+ name = params[:shop].to_s.strip
98
+ name += '.myshopify.com' if !name.include?('myshopify.com') && !name.include?('.')
99
+ name.gsub!('https://', '')
100
+ name.gsub!('http://', '')
101
+
102
+ u = URI("http://#{name}")
103
+ u.host.ends_with?('.myshopify.com') ? u.host : nil
104
+ end
105
+
106
+ def verify_shopify_webhook
107
+ data = request.body.read.to_s
108
+ digest = OpenSSL::Digest.new('sha256')
109
+ calculated_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, settings.shared_secret, data)).strip
110
+ request.body.rewind
111
+
112
+ if calculated_hmac == request.env['HTTP_X_SHOPIFY_HMAC_SHA256']
113
+ true
114
+ else
115
+ puts 'Shopify Webhook verification failed!'
116
+ false
117
+ end
118
+ end
119
+ end
120
+
121
+ # needs to be dynamic to incude the current shop
122
+ class ContentSecurityPolicy < Rack::Protection::Base
123
+ def csp_policy(env)
124
+ "frame-ancestors #{current_shop(env)} https://admin.shopify.com;"
125
+ end
126
+
127
+ def call(env)
128
+ status, headers, body = @app.call(env)
129
+ header = 'Content-Security-Policy'
130
+ headers[header] ||= csp_policy(env) if html? headers
131
+ [status, headers, body]
132
+ end
133
+
134
+ private
135
+
136
+ def current_shop(env)
137
+ s = session(env)
138
+ if s.has_key?('return_params')
139
+ "https://#{s['return_params']['shop']}"
140
+ elsif s.has_key?(:shopify)
141
+ "https://#{s[:shopify][:shop]}"
142
+ end
143
+ end
144
+
145
+ def html?(headers)
146
+ return false unless (header = headers.detect { |k, _v| k.downcase == 'content-type' })
147
+
148
+ options[:html_types].include? header.last[%r{^\w+/\w+}]
149
+ end
150
+ end
151
+
152
+ def shopify_webhook(route, &blk)
153
+ settings.webhook_routes << route
154
+ post(route) do
155
+ receive_webhook(&blk)
156
+ end
157
+ end
158
+
159
+ def self.registered(app)
160
+ app.helpers Shopify::Methods
161
+ app.register Sinatra::ActiveRecordExtension
162
+
163
+ app.set :database_file, File.expand_path('config/database.yml')
164
+
165
+ app.set :erb, layout: :'layouts/application'
166
+ app.set :views, File.expand_path('views')
167
+ app.set :public_folder, File.expand_path('public')
168
+ app.enable :inline_templates
169
+
170
+ app.set :protection, except: :frame_options
171
+
172
+ app.set :api_version, '2019-07'
173
+ app.set :scope, 'read_products, read_orders'
174
+
175
+ app.set :api_key, ENV['SHOPIFY_API_KEY']
176
+ app.set :shared_secret, ENV['SHOPIFY_SHARED_SECRET']
177
+ app.set :secret, ENV['SECRET']
178
+
179
+ # csrf needs to be disabled for webhook routes
180
+ app.set :webhook_routes, ['/uninstall']
181
+
182
+ # add support for put/patch/delete
183
+ app.use Rack::MethodOverride
184
+
185
+ app.use Rack::Session::Cookie, key: 'rack.session',
186
+ path: '/',
187
+ secure: true,
188
+ same_site: 'None',
189
+ secret: app.settings.secret,
190
+ expire_after: 60 * 30 # half an hour in seconds
191
+
192
+ app.use Shopify::ContentSecurityPolicy
193
+
194
+ app.use Rack::Protection::AuthenticityToken, allow_if: lambda { |env|
195
+ app.settings.webhook_routes.include?(env['PATH_INFO'])
196
+ }
197
+
198
+ OmniAuth.config.allowed_request_methods = [:post]
199
+
200
+ app.use OmniAuth::Builder do
201
+ provider :shopify,
202
+ app.settings.api_key,
203
+ app.settings.shared_secret,
204
+ scope: app.settings.scope,
205
+ setup: lambda { |env|
206
+ shop = if env['REQUEST_METHOD'] == 'POST'
207
+ env['rack.request.form_hash']['shop']
208
+ else
209
+ Rack::Utils.parse_query(env['QUERY_STRING'])['shop']
210
+ end
211
+
212
+ site_url = "https://#{shop}"
213
+ env['omniauth.strategy'].options[:client_options][:site] = site_url
214
+ }
215
+ end
216
+
217
+ ShopifyAPI::Session.setup(
218
+ api_key: app.settings.api_key,
219
+ secret: app.settings.shared_secret
220
+ )
221
+
222
+ app.get '/login' do
223
+ erb :login, layout: false
224
+ end
225
+
226
+ app.get '/logout' do
227
+ clear_session
228
+ redirect '/login'
229
+ end
230
+
231
+ app.get '/auth/shopify/callback' do
232
+ shop_name = params['shop']
233
+ token = request.env['omniauth.auth']['credentials']['token']
234
+ host = params['host']
235
+
236
+ shop = Shop.find_or_initialize_by(shopify_domain: shop_name)
237
+ shop.shopify_token = token
238
+ shop.save!
239
+
240
+ session[:shopify] = {
241
+ shop: shop_name,
242
+ host: host,
243
+ token: token
244
+ }
245
+
246
+ after_shopify_auth
247
+
248
+ return_params = session[:return_params]
249
+ session.delete(:return_params)
250
+
251
+ return_to = '/'
252
+ return_to += "?#{return_params.to_query}" if return_params.present?
253
+
254
+ redirect return_to
255
+ end
256
+
257
+ app.get '/auth/failure' do
258
+ erb "<h1>Authentication Failed:</h1>
259
+ <h3>message:<h3> <pre>#{params}</pre>", layout: false
260
+ end
261
+ end
262
+ end
263
+
264
+ register Shopify
265
+ end
266
+
267
+ # Shop
268
+ class Shop < ActiveRecord::Base
269
+ def self.secret
270
+ @secret ||= ENV['SECRET']
271
+ end
272
+
273
+ validates_presence_of :shopify_domain
274
+ validates_presence_of :shopify_token, on: :create
275
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = 'sinatra-embedded-shopify-app'
5
+ s.version = '0.5.16'
6
+
7
+ s.summary = 'A classy shopify embeded app'
8
+ s.description = 'A Sinatra extension for building Shopify Embedded Apps. Akin to the shopify_app gem but for Sinatra'
9
+
10
+ s.authors = ['Whisker Side']
11
+ s.email = 'whiskerside@gmail.com'
12
+ s.homepage = 'https://github.com/whiskerside/sinatra-embedded-shopify-app/'
13
+ s.license = 'MIT'
14
+
15
+ s.files = `git ls-files`.split("\n")
16
+ s.executables << 'sinatra-embedded-shopify-app-generator'
17
+
18
+ s.add_runtime_dependency 'activesupport'
19
+ s.add_runtime_dependency 'sinatra', '>= 2.2', '< 3.1'
20
+ s.add_runtime_dependency 'sinatra-activerecord', '~> 2.0.9'
21
+
22
+ s.add_runtime_dependency 'omniauth', '~> 2.0', '>= 2.0.4'
23
+ s.add_runtime_dependency 'omniauth-shopify-oauth2', '~> 2.3', '>= 2.3.2'
24
+ s.add_runtime_dependency 'shopify_api', '9.5.1'
25
+
26
+ s.add_development_dependency 'fakeweb'
27
+ s.add_development_dependency 'minitest'
28
+ s.add_development_dependency 'mocha'
29
+ s.add_development_dependency 'rack-test', '~> 2.0.2'
30
+ s.add_development_dependency 'rake', '~> 12.3', '>= 12.3.3'
31
+ s.add_development_dependency 'sqlite3', '~> 1.6.2'
32
+ end
metadata ADDED
@@ -0,0 +1,272 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sinatra-embedded-shopify-app
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.16
5
+ platform: ruby
6
+ authors:
7
+ - Whisker Side
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2023-05-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: sinatra
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '2.2'
34
+ - - "<"
35
+ - !ruby/object:Gem::Version
36
+ version: '3.1'
37
+ type: :runtime
38
+ prerelease: false
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '2.2'
44
+ - - "<"
45
+ - !ruby/object:Gem::Version
46
+ version: '3.1'
47
+ - !ruby/object:Gem::Dependency
48
+ name: sinatra-activerecord
49
+ requirement: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 2.0.9
54
+ type: :runtime
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ requirements:
58
+ - - "~>"
59
+ - !ruby/object:Gem::Version
60
+ version: 2.0.9
61
+ - !ruby/object:Gem::Dependency
62
+ name: omniauth
63
+ requirement: !ruby/object:Gem::Requirement
64
+ requirements:
65
+ - - "~>"
66
+ - !ruby/object:Gem::Version
67
+ version: '2.0'
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: 2.0.4
71
+ type: :runtime
72
+ prerelease: false
73
+ version_requirements: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - "~>"
76
+ - !ruby/object:Gem::Version
77
+ version: '2.0'
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: 2.0.4
81
+ - !ruby/object:Gem::Dependency
82
+ name: omniauth-shopify-oauth2
83
+ requirement: !ruby/object:Gem::Requirement
84
+ requirements:
85
+ - - "~>"
86
+ - !ruby/object:Gem::Version
87
+ version: '2.3'
88
+ - - ">="
89
+ - !ruby/object:Gem::Version
90
+ version: 2.3.2
91
+ type: :runtime
92
+ prerelease: false
93
+ version_requirements: !ruby/object:Gem::Requirement
94
+ requirements:
95
+ - - "~>"
96
+ - !ruby/object:Gem::Version
97
+ version: '2.3'
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: 2.3.2
101
+ - !ruby/object:Gem::Dependency
102
+ name: shopify_api
103
+ requirement: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - '='
106
+ - !ruby/object:Gem::Version
107
+ version: 9.5.1
108
+ type: :runtime
109
+ prerelease: false
110
+ version_requirements: !ruby/object:Gem::Requirement
111
+ requirements:
112
+ - - '='
113
+ - !ruby/object:Gem::Version
114
+ version: 9.5.1
115
+ - !ruby/object:Gem::Dependency
116
+ name: fakeweb
117
+ requirement: !ruby/object:Gem::Requirement
118
+ requirements:
119
+ - - ">="
120
+ - !ruby/object:Gem::Version
121
+ version: '0'
122
+ type: :development
123
+ prerelease: false
124
+ version_requirements: !ruby/object:Gem::Requirement
125
+ requirements:
126
+ - - ">="
127
+ - !ruby/object:Gem::Version
128
+ version: '0'
129
+ - !ruby/object:Gem::Dependency
130
+ name: minitest
131
+ requirement: !ruby/object:Gem::Requirement
132
+ requirements:
133
+ - - ">="
134
+ - !ruby/object:Gem::Version
135
+ version: '0'
136
+ type: :development
137
+ prerelease: false
138
+ version_requirements: !ruby/object:Gem::Requirement
139
+ requirements:
140
+ - - ">="
141
+ - !ruby/object:Gem::Version
142
+ version: '0'
143
+ - !ruby/object:Gem::Dependency
144
+ name: mocha
145
+ requirement: !ruby/object:Gem::Requirement
146
+ requirements:
147
+ - - ">="
148
+ - !ruby/object:Gem::Version
149
+ version: '0'
150
+ type: :development
151
+ prerelease: false
152
+ version_requirements: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ version: '0'
157
+ - !ruby/object:Gem::Dependency
158
+ name: rack-test
159
+ requirement: !ruby/object:Gem::Requirement
160
+ requirements:
161
+ - - "~>"
162
+ - !ruby/object:Gem::Version
163
+ version: 2.0.2
164
+ type: :development
165
+ prerelease: false
166
+ version_requirements: !ruby/object:Gem::Requirement
167
+ requirements:
168
+ - - "~>"
169
+ - !ruby/object:Gem::Version
170
+ version: 2.0.2
171
+ - !ruby/object:Gem::Dependency
172
+ name: rake
173
+ requirement: !ruby/object:Gem::Requirement
174
+ requirements:
175
+ - - "~>"
176
+ - !ruby/object:Gem::Version
177
+ version: '12.3'
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: 12.3.3
181
+ type: :development
182
+ prerelease: false
183
+ version_requirements: !ruby/object:Gem::Requirement
184
+ requirements:
185
+ - - "~>"
186
+ - !ruby/object:Gem::Version
187
+ version: '12.3'
188
+ - - ">="
189
+ - !ruby/object:Gem::Version
190
+ version: 12.3.3
191
+ - !ruby/object:Gem::Dependency
192
+ name: sqlite3
193
+ requirement: !ruby/object:Gem::Requirement
194
+ requirements:
195
+ - - "~>"
196
+ - !ruby/object:Gem::Version
197
+ version: 1.6.2
198
+ type: :development
199
+ prerelease: false
200
+ version_requirements: !ruby/object:Gem::Requirement
201
+ requirements:
202
+ - - "~>"
203
+ - !ruby/object:Gem::Version
204
+ version: 1.6.2
205
+ description: A Sinatra extension for building Shopify Embedded Apps. Akin to the shopify_app
206
+ gem but for Sinatra
207
+ email: whiskerside@gmail.com
208
+ executables:
209
+ - sinatra-embedded-shopify-app-generator
210
+ extensions: []
211
+ extra_rdoc_files: []
212
+ files:
213
+ - ".circleci/config.yml"
214
+ - ".github/dependabot.yml"
215
+ - ".gitignore"
216
+ - ".ruby-version"
217
+ - CHANGELOG
218
+ - Gemfile
219
+ - Gemfile.lock
220
+ - LICENSE
221
+ - README.md
222
+ - bin/ci.sh
223
+ - bin/sinatra-embedded-shopify-app-generator
224
+ - example/.gitignore
225
+ - example/.ruby-version
226
+ - example/Gemfile
227
+ - example/Gemfile.lock
228
+ - example/Procfile
229
+ - example/README.md
230
+ - example/Rakefile
231
+ - example/config.ru
232
+ - example/config/database.yml
233
+ - example/db/migrate/20140413221328_create_shops.rb
234
+ - example/db/migrate/20140414042317_add_index_to_shops.rb
235
+ - example/db/schema.rb
236
+ - example/db/seeds.rb
237
+ - example/public/icon.png
238
+ - example/public/legend.gif
239
+ - example/src/app.rb
240
+ - example/test/app_test.rb
241
+ - example/test/test_helper.rb
242
+ - example/views/_flash_messages.erb
243
+ - example/views/_top_bar.erb
244
+ - example/views/home.erb
245
+ - example/views/layouts/application.erb
246
+ - example/views/login.erb
247
+ - lib/sinatra/sinatra-embedded-shopify-app.rb
248
+ - sinatra-embedded-shopify-app.gemspec
249
+ homepage: https://github.com/whiskerside/sinatra-embedded-shopify-app/
250
+ licenses:
251
+ - MIT
252
+ metadata: {}
253
+ post_install_message:
254
+ rdoc_options: []
255
+ require_paths:
256
+ - lib
257
+ required_ruby_version: !ruby/object:Gem::Requirement
258
+ requirements:
259
+ - - ">="
260
+ - !ruby/object:Gem::Version
261
+ version: '0'
262
+ required_rubygems_version: !ruby/object:Gem::Requirement
263
+ requirements:
264
+ - - ">="
265
+ - !ruby/object:Gem::Version
266
+ version: '0'
267
+ requirements: []
268
+ rubygems_version: 3.1.6
269
+ signing_key:
270
+ specification_version: 4
271
+ summary: A classy shopify embeded app
272
+ test_files: []