ruby_shopify_app 1.0.0

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.
Files changed (186) hide show
  1. checksums.yaml +7 -0
  2. data/.babelrc +5 -0
  3. data/.github/CODEOWNERS +2 -0
  4. data/.github/ISSUE_TEMPLATE/bug-report.md +63 -0
  5. data/.github/ISSUE_TEMPLATE/config.yml +1 -0
  6. data/.github/ISSUE_TEMPLATE/feature-request.md +33 -0
  7. data/.github/PULL_REQUEST_TEMPLATE.md +22 -0
  8. data/.github/probots.yml +2 -0
  9. data/.github/workflows/build.yml +40 -0
  10. data/.github/workflows/release.yml +24 -0
  11. data/.github/workflows/rubocop.yml +22 -0
  12. data/.gitignore +14 -0
  13. data/.nvmrc +1 -0
  14. data/.rubocop.yml +18 -0
  15. data/.ruby-version +1 -0
  16. data/CHANGELOG-OLD.md +643 -0
  17. data/CHANGELOG.md +6 -0
  18. data/CONTRIBUTING.md +81 -0
  19. data/Gemfile +11 -0
  20. data/Gemfile.lock +280 -0
  21. data/LICENSE +19 -0
  22. data/README.md +132 -0
  23. data/Rakefile +7 -0
  24. data/SECURITY.md +59 -0
  25. data/app/assets/images/storage_access.svg +1 -0
  26. data/app/assets/javascripts/shopify_app/app_bridge_2.0.12.js +10 -0
  27. data/app/assets/javascripts/shopify_app/app_bridge_redirect.js +22 -0
  28. data/app/assets/javascripts/shopify_app/enable_cookies.js +3 -0
  29. data/app/assets/javascripts/shopify_app/itp_helper.js +40 -0
  30. data/app/assets/javascripts/shopify_app/partition_cookies.js +8 -0
  31. data/app/assets/javascripts/shopify_app/post_redirect.js +9 -0
  32. data/app/assets/javascripts/shopify_app/redirect.js +31 -0
  33. data/app/assets/javascripts/shopify_app/request_storage_access.js +3 -0
  34. data/app/assets/javascripts/shopify_app/storage_access.js +148 -0
  35. data/app/assets/javascripts/shopify_app/storage_access_redirect.js +17 -0
  36. data/app/assets/javascripts/shopify_app/top_level.js +2 -0
  37. data/app/assets/javascripts/shopify_app/top_level_interaction.js +11 -0
  38. data/app/controllers/concerns/shopify_app/authenticated.rb +16 -0
  39. data/app/controllers/concerns/shopify_app/ensure_authenticated_links.rb +39 -0
  40. data/app/controllers/concerns/shopify_app/require_known_shop.rb +40 -0
  41. data/app/controllers/concerns/shopify_app/shop_access_scopes_verification.rb +32 -0
  42. data/app/controllers/shopify_app/authenticated_controller.rb +8 -0
  43. data/app/controllers/shopify_app/callback_controller.rb +195 -0
  44. data/app/controllers/shopify_app/extension_verification_controller.rb +15 -0
  45. data/app/controllers/shopify_app/sessions_controller.rb +202 -0
  46. data/app/controllers/shopify_app/webhooks_controller.rb +36 -0
  47. data/app/views/shopify_app/partials/_button_styles.html.erb +109 -0
  48. data/app/views/shopify_app/partials/_card_styles.html.erb +33 -0
  49. data/app/views/shopify_app/partials/_empty_state_styles.html.erb +98 -0
  50. data/app/views/shopify_app/partials/_form_styles.html.erb +56 -0
  51. data/app/views/shopify_app/partials/_layout_styles.html.erb +182 -0
  52. data/app/views/shopify_app/partials/_typography_styles.html.erb +35 -0
  53. data/app/views/shopify_app/sessions/enable_cookies.html.erb +70 -0
  54. data/app/views/shopify_app/sessions/new.html.erb +51 -0
  55. data/app/views/shopify_app/sessions/request_storage_access.html.erb +68 -0
  56. data/app/views/shopify_app/sessions/top_level_interaction.html.erb +63 -0
  57. data/app/views/shopify_app/shared/post_redirect_to_auth_shopify.html.erb +13 -0
  58. data/app/views/shopify_app/shared/redirect.html.erb +23 -0
  59. data/config/locales/cs.yml +23 -0
  60. data/config/locales/da.yml +20 -0
  61. data/config/locales/de.yml +22 -0
  62. data/config/locales/en.yml +15 -0
  63. data/config/locales/es.yml +22 -0
  64. data/config/locales/fi.yml +20 -0
  65. data/config/locales/fr.yml +23 -0
  66. data/config/locales/it.yml +21 -0
  67. data/config/locales/ja.yml +17 -0
  68. data/config/locales/ko.yml +19 -0
  69. data/config/locales/nb.yml +21 -0
  70. data/config/locales/nl.yml +21 -0
  71. data/config/locales/pl.yml +21 -0
  72. data/config/locales/pt-BR.yml +21 -0
  73. data/config/locales/pt-PT.yml +22 -0
  74. data/config/locales/sv.yml +21 -0
  75. data/config/locales/th.yml +20 -0
  76. data/config/locales/tr.yml +22 -0
  77. data/config/locales/vi.yml +22 -0
  78. data/config/locales/zh-CN.yml +16 -0
  79. data/config/locales/zh-TW.yml +16 -0
  80. data/config/routes.rb +23 -0
  81. data/docs/Quickstart.md +31 -0
  82. data/docs/Releasing.md +21 -0
  83. data/docs/Troubleshooting.md +159 -0
  84. data/docs/Upgrading.md +132 -0
  85. data/docs/shopify_app/authentication.md +124 -0
  86. data/docs/shopify_app/engine.md +82 -0
  87. data/docs/shopify_app/generators.md +127 -0
  88. data/docs/shopify_app/handling-access-scopes-changes.md +24 -0
  89. data/docs/shopify_app/script-tags.md +28 -0
  90. data/docs/shopify_app/session-repository.md +88 -0
  91. data/docs/shopify_app/testing.md +38 -0
  92. data/docs/shopify_app/webhooks.md +72 -0
  93. data/images/app-proxy-screenshot.png +0 -0
  94. data/karma.conf.js +44 -0
  95. data/lib/generators/shopify_app/add_after_authenticate_job/add_after_authenticate_job_generator.rb +47 -0
  96. data/lib/generators/shopify_app/add_after_authenticate_job/templates/after_authenticate_job.rb +11 -0
  97. data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +40 -0
  98. data/lib/generators/shopify_app/add_marketing_activity_extension/templates/marketing_activities_controller.rb +62 -0
  99. data/lib/generators/shopify_app/add_webhook/add_webhook_generator.rb +69 -0
  100. data/lib/generators/shopify_app/add_webhook/templates/webhook_job.rb.tt +13 -0
  101. data/lib/generators/shopify_app/app_proxy_controller/app_proxy_controller_generator.rb +26 -0
  102. data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_controller.rb +8 -0
  103. data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_route.rb +11 -0
  104. data/lib/generators/shopify_app/app_proxy_controller/templates/index.html.erb +19 -0
  105. data/lib/generators/shopify_app/authenticated_controller/authenticated_controller_generator.rb +15 -0
  106. data/lib/generators/shopify_app/authenticated_controller/templates/authenticated_controller.rb +5 -0
  107. data/lib/generators/shopify_app/controllers/controllers_generator.rb +30 -0
  108. data/lib/generators/shopify_app/home_controller/home_controller_generator.rb +53 -0
  109. data/lib/generators/shopify_app/home_controller/templates/home_controller.rb +18 -0
  110. data/lib/generators/shopify_app/home_controller/templates/index.html.erb +75 -0
  111. data/lib/generators/shopify_app/home_controller/templates/unauthenticated_home_controller.rb +12 -0
  112. data/lib/generators/shopify_app/install/install_generator.rb +121 -0
  113. data/lib/generators/shopify_app/install/templates/_flash_messages.html.erb +3 -0
  114. data/lib/generators/shopify_app/install/templates/embedded_app.html.erb +44 -0
  115. data/lib/generators/shopify_app/install/templates/flash_messages.js +24 -0
  116. data/lib/generators/shopify_app/install/templates/omniauth.rb +4 -0
  117. data/lib/generators/shopify_app/install/templates/session_store.rb +4 -0
  118. data/lib/generators/shopify_app/install/templates/shopify_app.js +15 -0
  119. data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +25 -0
  120. data/lib/generators/shopify_app/install/templates/shopify_app_importmap.js +13 -0
  121. data/lib/generators/shopify_app/install/templates/shopify_app_index.js +2 -0
  122. data/lib/generators/shopify_app/install/templates/shopify_provider.rb.tt +8 -0
  123. data/lib/generators/shopify_app/install/templates/user_agent.rb +6 -0
  124. data/lib/generators/shopify_app/products_controller/products_controller_generator.rb +19 -0
  125. data/lib/generators/shopify_app/products_controller/templates/products_controller.rb +8 -0
  126. data/lib/generators/shopify_app/rotate_shopify_token_job/rotate_shopify_token_job_generator.rb +16 -0
  127. data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token.rake +17 -0
  128. data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token_job.rb +42 -0
  129. data/lib/generators/shopify_app/routes/routes_generator.rb +32 -0
  130. data/lib/generators/shopify_app/routes/templates/routes.rb +12 -0
  131. data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +70 -0
  132. data/lib/generators/shopify_app/shop_model/templates/db/migrate/add_shop_access_scopes_column.erb +5 -0
  133. data/lib/generators/shopify_app/shop_model/templates/db/migrate/create_shops.erb +15 -0
  134. data/lib/generators/shopify_app/shop_model/templates/shop.rb +8 -0
  135. data/lib/generators/shopify_app/shop_model/templates/shops.yml +3 -0
  136. data/lib/generators/shopify_app/shopify_app_generator.rb +18 -0
  137. data/lib/generators/shopify_app/user_model/templates/db/migrate/add_user_access_scopes_column.erb +5 -0
  138. data/lib/generators/shopify_app/user_model/templates/db/migrate/create_users.erb +16 -0
  139. data/lib/generators/shopify_app/user_model/templates/user.rb +8 -0
  140. data/lib/generators/shopify_app/user_model/templates/users.yml +4 -0
  141. data/lib/generators/shopify_app/user_model/user_model_generator.rb +70 -0
  142. data/lib/generators/shopify_app/views/views_generator.rb +30 -0
  143. data/lib/shopify_app/access_scopes/noop_strategy.rb +13 -0
  144. data/lib/shopify_app/access_scopes/shop_strategy.rb +24 -0
  145. data/lib/shopify_app/access_scopes/user_strategy.rb +41 -0
  146. data/lib/shopify_app/configuration.rb +119 -0
  147. data/lib/shopify_app/controller_concerns/app_proxy_verification.rb +38 -0
  148. data/lib/shopify_app/controller_concerns/csrf_protection.rb +15 -0
  149. data/lib/shopify_app/controller_concerns/embedded_app.rb +20 -0
  150. data/lib/shopify_app/controller_concerns/itp.rb +45 -0
  151. data/lib/shopify_app/controller_concerns/localization.rb +23 -0
  152. data/lib/shopify_app/controller_concerns/login_protection.rb +259 -0
  153. data/lib/shopify_app/controller_concerns/payload_verification.rb +24 -0
  154. data/lib/shopify_app/controller_concerns/webhook_verification.rb +23 -0
  155. data/lib/shopify_app/engine.rb +47 -0
  156. data/lib/shopify_app/jobs/scripttags_manager_job.rb +16 -0
  157. data/lib/shopify_app/jobs/webhooks_manager_job.rb +16 -0
  158. data/lib/shopify_app/managers/scripttags_manager.rb +78 -0
  159. data/lib/shopify_app/managers/webhooks_manager.rb +62 -0
  160. data/lib/shopify_app/middleware/jwt_middleware.rb +43 -0
  161. data/lib/shopify_app/middleware/same_site_cookie_middleware.rb +34 -0
  162. data/lib/shopify_app/omniauth/omniauth_configuration.rb +64 -0
  163. data/lib/shopify_app/session/in_memory_session_store.rb +31 -0
  164. data/lib/shopify_app/session/in_memory_shop_session_store.rb +16 -0
  165. data/lib/shopify_app/session/in_memory_user_session_store.rb +16 -0
  166. data/lib/shopify_app/session/jwt.rb +67 -0
  167. data/lib/shopify_app/session/null_user_session_store.rb +22 -0
  168. data/lib/shopify_app/session/session_repository.rb +56 -0
  169. data/lib/shopify_app/session/session_storage.rb +20 -0
  170. data/lib/shopify_app/session/shop_session_storage.rb +42 -0
  171. data/lib/shopify_app/session/shop_session_storage_with_scopes.rb +58 -0
  172. data/lib/shopify_app/session/user_session_storage.rb +42 -0
  173. data/lib/shopify_app/session/user_session_storage_with_scopes.rb +58 -0
  174. data/lib/shopify_app/test_helpers/all.rb +2 -0
  175. data/lib/shopify_app/test_helpers/webhook_verification_helper.rb +17 -0
  176. data/lib/shopify_app/utils.rb +37 -0
  177. data/lib/shopify_app/version.rb +4 -0
  178. data/lib/shopify_app.rb +80 -0
  179. data/package.json +27 -0
  180. data/service.yml +4 -0
  181. data/shipit.rubygems.yml +4 -0
  182. data/shopify_app.gemspec +39 -0
  183. data/translation.yml +7 -0
  184. data/webpack.config.js +24 -0
  185. data/yarn.lock +5230 -0
  186. metadata +465 -0
@@ -0,0 +1,259 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'browser_sniffer'
4
+
5
+ module ShopifyApp
6
+ module LoginProtection
7
+ extend ActiveSupport::Concern
8
+ include ShopifyApp::Itp
9
+
10
+ class ShopifyDomainNotFound < StandardError; end
11
+
12
+ class ShopifyHostNotFound < StandardError; end
13
+
14
+ included do
15
+ after_action :set_test_cookie
16
+ rescue_from ActiveResource::UnauthorizedAccess, with: :close_session
17
+ end
18
+
19
+ ACCESS_TOKEN_REQUIRED_HEADER = 'X-Shopify-API-Request-Failure-Unauthorized'
20
+
21
+ def activate_shopify_session
22
+ if user_session_expected? && user_session.blank?
23
+ signal_access_token_required
24
+ return redirect_to_login
25
+ end
26
+
27
+ return redirect_to_login if current_shopify_session.blank?
28
+
29
+ clear_top_level_oauth_cookie
30
+
31
+ begin
32
+ ShopifyAPI::Base.activate_session(current_shopify_session)
33
+ yield
34
+ ensure
35
+ ShopifyAPI::Base.clear_session
36
+ end
37
+ end
38
+
39
+ def current_shopify_session
40
+ @current_shopify_session ||= begin
41
+ user_session || shop_session
42
+ end
43
+ end
44
+
45
+ def user_session
46
+ user_session_by_jwt || user_session_by_cookie
47
+ end
48
+
49
+ def user_session_by_jwt
50
+ return unless ShopifyApp.configuration.allow_jwt_authentication
51
+ return unless jwt_shopify_user_id
52
+ ShopifyApp::SessionRepository.retrieve_user_session_by_shopify_user_id(jwt_shopify_user_id)
53
+ end
54
+
55
+ def user_session_by_cookie
56
+ return unless ShopifyApp.configuration.allow_cookie_authentication
57
+ return unless session[:user_id].present?
58
+ ShopifyApp::SessionRepository.retrieve_user_session(session[:user_id])
59
+ end
60
+
61
+ def shop_session
62
+ shop_session_by_jwt || shop_session_by_cookie
63
+ end
64
+
65
+ def shop_session_by_jwt
66
+ return unless ShopifyApp.configuration.allow_jwt_authentication
67
+ return unless jwt_shopify_domain
68
+ ShopifyApp::SessionRepository.retrieve_shop_session_by_shopify_domain(jwt_shopify_domain)
69
+ end
70
+
71
+ def shop_session_by_cookie
72
+ return unless ShopifyApp.configuration.allow_cookie_authentication
73
+ return unless session[:shop_id].present?
74
+ ShopifyApp::SessionRepository.retrieve_shop_session(session[:shop_id])
75
+ end
76
+
77
+ def login_again_if_different_user_or_shop
78
+ if session[:user_session].present? && params[:session].present? # session data was sent/stored correctly
79
+ clear_session = session[:user_session] != params[:session] # current user is different from stored user
80
+ end
81
+
82
+ if current_shopify_session &&
83
+ params[:shop] && params[:shop].is_a?(String) &&
84
+ (current_shopify_session.domain != params[:shop])
85
+ clear_session = true
86
+ end
87
+
88
+ if clear_session
89
+ clear_shopify_session
90
+ redirect_to_login
91
+ end
92
+ end
93
+
94
+ def signal_access_token_required
95
+ response.set_header(ACCESS_TOKEN_REQUIRED_HEADER, "true")
96
+ end
97
+
98
+ def jwt_expire_at
99
+ expire_at = request.env['jwt.expire_at']
100
+ return unless expire_at
101
+ expire_at - 5.seconds # 5s gap to start fetching new token in advance
102
+ end
103
+
104
+ protected
105
+
106
+ def jwt_shopify_domain
107
+ request.env['jwt.shopify_domain']
108
+ end
109
+
110
+ def jwt_shopify_user_id
111
+ request.env['jwt.shopify_user_id']
112
+ end
113
+
114
+ def host
115
+ params[:host]
116
+ end
117
+
118
+ def redirect_to_login
119
+ if request.xhr?
120
+ head(:unauthorized)
121
+ else
122
+ if request.get?
123
+ path = request.path
124
+ query = sanitized_params.to_query
125
+ else
126
+ referer = URI(request.referer || "/")
127
+ path = referer.path
128
+ query = "#{referer.query}&#{sanitized_params.to_query}"
129
+ end
130
+ session[:return_to] = query.blank? ? path.to_s : "#{path}?#{query}"
131
+ redirect_to(login_url_with_optional_shop)
132
+ end
133
+ end
134
+
135
+ def close_session
136
+ clear_shopify_session
137
+ redirect_to(login_url_with_optional_shop)
138
+ end
139
+
140
+ def clear_shopify_session
141
+ session[:shop_id] = nil
142
+ session[:user_id] = nil
143
+ session[:shopify_domain] = nil
144
+ session[:shopify_user] = nil
145
+ session[:user_session] = nil
146
+ end
147
+
148
+ def login_url_with_optional_shop(top_level: false)
149
+ url = ShopifyApp.configuration.login_url
150
+
151
+ query_params = login_url_params(top_level: top_level)
152
+
153
+ url = "#{url}?#{query_params.to_query}" if query_params.present?
154
+ url
155
+ end
156
+
157
+ def login_url_params(top_level:)
158
+ query_params = {}
159
+ query_params[:shop] = sanitized_params[:shop] if params[:shop].present?
160
+
161
+ return_to = RedirectSafely.make_safe(session[:return_to] || params[:return_to], nil)
162
+
163
+ if return_to.present? && return_to_param_required?
164
+ query_params[:return_to] = return_to
165
+ end
166
+
167
+ has_referer_shop_name = referer_sanitized_shop_name.present?
168
+
169
+ if has_referer_shop_name
170
+ query_params[:shop] ||= referer_sanitized_shop_name
171
+ end
172
+
173
+ if params[:host].present?
174
+ query_params[:host] ||= host
175
+ end
176
+
177
+ query_params[:top_level] = true if top_level
178
+ query_params
179
+ end
180
+
181
+ def return_to_param_required?
182
+ native_params = %i[shop hmac timestamp locale protocol return_to]
183
+ request.path != '/' || sanitized_params.except(*native_params).any?
184
+ end
185
+
186
+ def fullpage_redirect_to(url)
187
+ if ShopifyApp.configuration.embedded_app?
188
+ render('shopify_app/shared/redirect', layout: false,
189
+ locals: { url: url, current_shopify_domain: current_shopify_domain })
190
+ else
191
+ redirect_to(url)
192
+ end
193
+ end
194
+
195
+ def current_shopify_domain
196
+ shopify_domain = sanitized_shop_name ||
197
+ jwt_shopify_domain ||
198
+ session[:shopify_domain]
199
+
200
+ return shopify_domain if shopify_domain.present?
201
+
202
+ raise ShopifyDomainNotFound
203
+ end
204
+
205
+ def sanitized_shop_name
206
+ @sanitized_shop_name ||= sanitize_shop_param(params)
207
+ end
208
+
209
+ def referer_sanitized_shop_name
210
+ return unless request.referer.present?
211
+
212
+ @referer_sanitized_shop_name ||= begin
213
+ referer_uri = URI(request.referer)
214
+ query_params = Rack::Utils.parse_query(referer_uri.query)
215
+
216
+ sanitize_shop_param(query_params.with_indifferent_access)
217
+ end
218
+ end
219
+
220
+ def sanitize_shop_param(params)
221
+ return unless params[:shop].present?
222
+ ShopifyApp::Utils.sanitize_shop_domain(params[:shop])
223
+ end
224
+
225
+ def sanitized_params
226
+ request.query_parameters.clone.tap do |query_params|
227
+ if params[:shop].is_a?(String)
228
+ query_params[:shop] = sanitize_shop_param(params)
229
+ end
230
+ end
231
+ end
232
+
233
+ def return_address
234
+ return_address_with_params(shop: current_shopify_domain, host: host)
235
+ rescue ShopifyDomainNotFound, ShopifyHostNotFound
236
+ base_return_address
237
+ end
238
+
239
+ def base_return_address
240
+ session.delete(:return_to) || ShopifyApp.configuration.root_url
241
+ end
242
+
243
+ def return_address_with_params(params)
244
+ uri = URI(base_return_address)
245
+ uri.query = CGI.parse(uri.query.to_s)
246
+ .symbolize_keys
247
+ .transform_values { |v| v.one? ? v.first : v }
248
+ .merge(params)
249
+ .to_query
250
+ uri.to_s
251
+ end
252
+
253
+ private
254
+
255
+ def user_session_expected?
256
+ !ShopifyApp.configuration.user_session_repository.blank? && ShopifyApp::SessionRepository.user_storage.present?
257
+ end
258
+ end
259
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+ module ShopifyApp
3
+ module PayloadVerification
4
+ extend ActiveSupport::Concern
5
+
6
+ private
7
+
8
+ def shopify_hmac
9
+ request.headers['HTTP_X_SHOPIFY_HMAC_SHA256']
10
+ end
11
+
12
+ def hmac_valid?(data)
13
+ secrets = [ShopifyApp.configuration.secret, ShopifyApp.configuration.old_secret].reject(&:blank?)
14
+
15
+ secrets.any? do |secret|
16
+ digest = OpenSSL::Digest.new('sha256')
17
+ ActiveSupport::SecurityUtils.secure_compare(
18
+ shopify_hmac,
19
+ Base64.strict_encode64(OpenSSL::HMAC.digest(digest, secret, data))
20
+ )
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+ module ShopifyApp
3
+ module WebhookVerification
4
+ extend ActiveSupport::Concern
5
+ include ShopifyApp::PayloadVerification
6
+
7
+ included do
8
+ skip_before_action :verify_authenticity_token, raise: false
9
+ before_action :verify_request
10
+ end
11
+
12
+ private
13
+
14
+ def verify_request
15
+ data = request.raw_post
16
+ return head(:unauthorized) unless hmac_valid?(data)
17
+ end
18
+
19
+ def shop_domain
20
+ request.headers['HTTP_X_SHOPIFY_SHOP_DOMAIN']
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+ module ShopifyApp
3
+ module RedactJobParams
4
+ private
5
+
6
+ def args_info(job)
7
+ log_disabled_classes = %w(ShopifyApp::ScripttagsManagerJob ShopifyApp::WebhooksManagerJob)
8
+ return "" if log_disabled_classes.include?(job.class.name)
9
+ super
10
+ end
11
+ end
12
+
13
+ class Engine < Rails::Engine
14
+ engine_name 'shopify_app'
15
+ isolate_namespace ShopifyApp
16
+
17
+ initializer "shopify_app.assets.precompile" do |app|
18
+ app.config.assets.precompile += %w[
19
+ shopify_app/redirect.js
20
+ shopify_app/post_redirect.js
21
+ shopify_app/top_level.js
22
+ shopify_app/enable_cookies.js
23
+ shopify_app/request_storage_access.js
24
+ storage_access.svg
25
+ ]
26
+ end
27
+
28
+ initializer "shopify_app.middleware" do |app|
29
+ app.config.middleware.insert_after(::Rack::Runtime, ShopifyApp::SameSiteCookieMiddleware)
30
+
31
+ if ShopifyApp.configuration.allow_jwt_authentication
32
+ app.config.middleware.insert_after(ShopifyApp::SameSiteCookieMiddleware, ShopifyApp::JWTMiddleware)
33
+ end
34
+ end
35
+
36
+ initializer "shopify_app.redact_job_params" do
37
+ ActiveSupport.on_load(:active_job) do
38
+ if ActiveJob::Base.respond_to?(:log_arguments?)
39
+ WebhooksManagerJob.log_arguments = false
40
+ ScripttagsManagerJob.log_arguments = false
41
+ elsif ActiveJob::Logging::LogSubscriber.private_method_defined?(:args_info)
42
+ ActiveJob::Logging::LogSubscriber.prepend(RedactJobParams)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module ShopifyApp
3
+ class ScripttagsManagerJob < ActiveJob::Base
4
+ queue_as do
5
+ ShopifyApp.configuration.scripttags_manager_queue_name
6
+ end
7
+
8
+ def perform(shop_domain:, shop_token:, scripttags:)
9
+ api_version = ShopifyApp.configuration.api_version
10
+ ShopifyAPI::Session.temp(domain: shop_domain, token: shop_token, api_version: api_version) do
11
+ manager = ScripttagsManager.new(scripttags, shop_domain)
12
+ manager.create_scripttags
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+ module ShopifyApp
3
+ class WebhooksManagerJob < ActiveJob::Base
4
+ queue_as do
5
+ ShopifyApp.configuration.webhooks_manager_queue_name
6
+ end
7
+
8
+ def perform(shop_domain:, shop_token:, webhooks:)
9
+ api_version = ShopifyApp.configuration.api_version
10
+ ShopifyAPI::Session.temp(domain: shop_domain, token: shop_token, api_version: api_version) do
11
+ manager = WebhooksManager.new(webhooks)
12
+ manager.create_webhooks
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,78 @@
1
+ # frozen_string_literal: true
2
+ module ShopifyApp
3
+ class ScripttagsManager
4
+ class CreationFailed < StandardError; end
5
+
6
+ def self.queue(shop_domain, shop_token, scripttags)
7
+ ShopifyApp::ScripttagsManagerJob.perform_later(
8
+ shop_domain: shop_domain,
9
+ shop_token: shop_token,
10
+ # Procs cannot be serialized so we interpolate now, if necessary
11
+ scripttags: build_src(scripttags, shop_domain)
12
+ )
13
+ end
14
+
15
+ def self.build_src(scripttags, domain)
16
+ scripttags.map do |tag|
17
+ next tag unless tag[:src].respond_to?(:call)
18
+ tag = tag.dup
19
+ tag[:src] = tag[:src].call(domain)
20
+ tag
21
+ end
22
+ end
23
+
24
+ attr_reader :required_scripttags, :shop_domain
25
+
26
+ def initialize(scripttags, shop_domain)
27
+ @required_scripttags = scripttags
28
+ @shop_domain = shop_domain
29
+ end
30
+
31
+ def recreate_scripttags!
32
+ destroy_scripttags
33
+ create_scripttags
34
+ end
35
+
36
+ def create_scripttags
37
+ return unless required_scripttags.present?
38
+
39
+ expanded_scripttags.each do |scripttag|
40
+ create_scripttag(scripttag) unless scripttag_exists?(scripttag[:src])
41
+ end
42
+ end
43
+
44
+ def destroy_scripttags
45
+ scripttags = expanded_scripttags
46
+ ShopifyAPI::ScriptTag.all.each do |tag|
47
+ ShopifyAPI::ScriptTag.delete(tag.id) if required_scripttag?(scripttags, tag)
48
+ end
49
+
50
+ @current_scripttags = nil
51
+ end
52
+
53
+ private
54
+
55
+ def expanded_scripttags
56
+ self.class.build_src(required_scripttags, shop_domain)
57
+ end
58
+
59
+ def required_scripttag?(scripttags, tag)
60
+ scripttags.map { |w| w[:src] }.include?(tag.src)
61
+ end
62
+
63
+ def create_scripttag(attributes)
64
+ attributes.reverse_merge!(format: 'json')
65
+ scripttag = ShopifyAPI::ScriptTag.create(attributes)
66
+ raise CreationFailed, scripttag.errors.full_messages.to_sentence unless scripttag.persisted?
67
+ scripttag
68
+ end
69
+
70
+ def scripttag_exists?(src)
71
+ current_scripttags[src]
72
+ end
73
+
74
+ def current_scripttags
75
+ @current_scripttags ||= ShopifyAPI::ScriptTag.all.index_by(&:src)
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+ module ShopifyApp
3
+ class WebhooksManager
4
+ class CreationFailed < StandardError; end
5
+
6
+ def self.queue(shop_domain, shop_token, webhooks)
7
+ ShopifyApp::WebhooksManagerJob.perform_later(
8
+ shop_domain: shop_domain,
9
+ shop_token: shop_token,
10
+ webhooks: webhooks
11
+ )
12
+ end
13
+
14
+ attr_reader :required_webhooks
15
+
16
+ def initialize(webhooks)
17
+ @required_webhooks = webhooks
18
+ end
19
+
20
+ def recreate_webhooks!
21
+ destroy_webhooks
22
+ create_webhooks
23
+ end
24
+
25
+ def create_webhooks
26
+ return unless required_webhooks.present?
27
+
28
+ required_webhooks.each do |webhook|
29
+ create_webhook(webhook) unless webhook_exists?(webhook[:topic])
30
+ end
31
+ end
32
+
33
+ def destroy_webhooks
34
+ ShopifyAPI::Webhook.all.to_a.each do |webhook|
35
+ ShopifyAPI::Webhook.delete(webhook.id) if required_webhook?(webhook)
36
+ end
37
+
38
+ @current_webhooks = nil
39
+ end
40
+
41
+ private
42
+
43
+ def required_webhook?(webhook)
44
+ required_webhooks.map { |w| w[:address] }.include?(webhook.address)
45
+ end
46
+
47
+ def create_webhook(attributes)
48
+ attributes.reverse_merge!(format: 'json')
49
+ webhook = ShopifyAPI::Webhook.create(attributes)
50
+ raise CreationFailed, webhook.errors.full_messages.to_sentence unless webhook.persisted?
51
+ webhook
52
+ end
53
+
54
+ def webhook_exists?(topic)
55
+ current_webhooks[topic]
56
+ end
57
+
58
+ def current_webhooks
59
+ @current_webhooks ||= ShopifyAPI::Webhook.all.to_a.index_by(&:topic)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+ module ShopifyApp
3
+ class JWTMiddleware
4
+ TOKEN_REGEX = /^Bearer\s+(.*?)$/
5
+
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ return call_next(env) unless authorization_header(env)
12
+
13
+ token = extract_token(env)
14
+ return call_next(env) unless token
15
+
16
+ set_env_variables(token, env)
17
+ call_next(env)
18
+ end
19
+
20
+ private
21
+
22
+ def call_next(env)
23
+ @app.call(env)
24
+ end
25
+
26
+ def authorization_header(env)
27
+ env['HTTP_AUTHORIZATION']
28
+ end
29
+
30
+ def extract_token(env)
31
+ match = authorization_header(env).match(TOKEN_REGEX)
32
+ match && match[1]
33
+ end
34
+
35
+ def set_env_variables(token, env)
36
+ jwt = ShopifyApp::JWT.new(token)
37
+
38
+ env['jwt.shopify_domain'] = jwt.shopify_domain
39
+ env['jwt.shopify_user_id'] = jwt.shopify_user_id
40
+ env['jwt.expire_at'] = jwt.expire_at
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+ module ShopifyApp
3
+ class SameSiteCookieMiddleware
4
+ COOKIE_SEPARATOR = "\n"
5
+
6
+ def initialize(app)
7
+ @app = app
8
+ end
9
+
10
+ def call(env)
11
+ status, headers, body = @app.call(env)
12
+ user_agent = env['HTTP_USER_AGENT']
13
+
14
+ if headers && headers['Set-Cookie'] &&
15
+ BrowserSniffer.new(user_agent).same_site_none_compatible? &&
16
+ ShopifyApp.configuration.enable_same_site_none &&
17
+ Rack::Request.new(env).ssl?
18
+
19
+ set_cookies = headers['Set-Cookie']
20
+ .split(COOKIE_SEPARATOR)
21
+ .compact
22
+ .map do |cookie|
23
+ cookie << '; Secure' unless cookie =~ /;\s*secure/i
24
+ cookie << '; SameSite=None' if ShopifyApp.configuration.embedded_app?
25
+ cookie
26
+ end
27
+
28
+ headers['Set-Cookie'] = set_cookies.join(COOKIE_SEPARATOR)
29
+ end
30
+
31
+ [status, headers, body]
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ class OmniAuthConfiguration
5
+ attr_reader :strategy, :request
6
+ attr_writer :client_options_site, :scopes, :per_user_permissions
7
+
8
+ def initialize(strategy, request)
9
+ @strategy = strategy
10
+ @request = request
11
+ end
12
+
13
+ def build_options
14
+ strategy.options[:client_options][:site] = client_options_site
15
+ strategy.options[:scope] = scopes
16
+ strategy.options[:old_client_secret] = ShopifyApp.configuration.old_secret
17
+ strategy.options[:per_user_permissions] = request_online_tokens?
18
+ end
19
+
20
+ private
21
+
22
+ def request_online_tokens?
23
+ return @per_user_permissions unless @per_user_permissions.nil?
24
+ default_request_online_tokens?
25
+ end
26
+
27
+ def scopes
28
+ @scopes || default_scopes
29
+ end
30
+
31
+ def client_options_site
32
+ @client_options_site || default_client_options_site
33
+ end
34
+
35
+ def default_scopes
36
+ if request_online_tokens?
37
+ ShopifyApp.configuration.user_access_scopes
38
+ else
39
+ ShopifyApp.configuration.shop_access_scopes
40
+ end
41
+ end
42
+
43
+ def default_client_options_site
44
+ return '' unless shop_domain.present?
45
+ "https://#{shopify_auth_params[:shop]}"
46
+ end
47
+
48
+ def default_request_online_tokens?
49
+ strategy.session[:user_tokens] && !update_shop_scopes?
50
+ end
51
+
52
+ def update_shop_scopes?
53
+ ShopifyApp.configuration.shop_access_scopes_strategy.update_access_scopes?(shop_domain)
54
+ end
55
+
56
+ def shop_domain
57
+ request.params['shop'] || (shopify_auth_params && shopify_auth_params['shop'])
58
+ end
59
+
60
+ def shopify_auth_params
61
+ strategy.session['shopify.omniauth_params']&.with_indifferent_access
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ module ShopifyApp
3
+ # rubocop:disable Style/ClassVars
4
+ # Class var repo is needed here in order to share data between the 2 child classes.
5
+ class InMemorySessionStore
6
+ class EnvironmentError < StandardError; end
7
+
8
+ def self.retrieve(id)
9
+ repo[id]
10
+ end
11
+
12
+ def self.store(session, *_args)
13
+ id = SecureRandom.uuid
14
+ repo[id] = session
15
+ id
16
+ end
17
+
18
+ def self.clear
19
+ @@repo = nil
20
+ end
21
+
22
+ def self.repo
23
+ if Rails.env.production?
24
+ raise EnvironmentError, "Cannot use InMemorySessionStore in a Production environment. \
25
+ Please initialize ShopifyApp with a model that can store and retrieve sessions"
26
+ end
27
+ @@repo ||= {}
28
+ end
29
+ end
30
+ # rubocop:enable Style/ClassVars
31
+ end