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,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module Authenticated
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ include ShopifyApp::Localization
9
+ include ShopifyApp::LoginProtection
10
+ include ShopifyApp::CsrfProtection
11
+ include ShopifyApp::EmbeddedApp
12
+ before_action :login_again_if_different_user_or_shop
13
+ around_action :activate_shopify_session
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module EnsureAuthenticatedLinks
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ before_action :redirect_to_splash_page, if: :missing_expected_jwt?
9
+ end
10
+
11
+ private
12
+
13
+ def splash_page
14
+ splash_page_with_params(
15
+ return_to: request.fullpath,
16
+ shop: current_shopify_domain,
17
+ host: params[:host]
18
+ )
19
+ end
20
+
21
+ def splash_page_with_params(params)
22
+ uri = URI(root_path)
23
+ uri.query = params.compact.to_query
24
+ uri.to_s
25
+ end
26
+
27
+ def redirect_to_splash_page
28
+ redirect_to(splash_page)
29
+ rescue ShopifyApp::LoginProtection::ShopifyDomainNotFound => error
30
+ Rails.logger.warn("[ShopifyApp::EnsureAuthenticatedLinks] Redirecting to login: [#{error.class}] "\
31
+ "Could not determine current shop domain")
32
+ redirect_to(ShopifyApp.configuration.login_url)
33
+ end
34
+
35
+ def missing_expected_jwt?
36
+ jwt_shopify_domain.blank?
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module RequireKnownShop
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ before_action :check_shop_domain
9
+ before_action :check_shop_known
10
+ end
11
+
12
+ def current_shopify_domain
13
+ return if params[:shop].blank?
14
+ @shopify_domain ||= ShopifyApp::Utils.sanitize_shop_domain(params[:shop])
15
+ end
16
+
17
+ private
18
+
19
+ def check_shop_domain
20
+ redirect_to(ShopifyApp.configuration.login_url) unless current_shopify_domain
21
+ end
22
+
23
+ def check_shop_known
24
+ @shop = SessionRepository.retrieve_shop_session_by_shopify_domain(current_shopify_domain)
25
+ redirect_to(shop_login) unless @shop
26
+ end
27
+
28
+ def shop_login
29
+ url = URI(ShopifyApp.configuration.login_url)
30
+
31
+ url.query = URI.encode_www_form(
32
+ shop: params[:shop],
33
+ host: params[:host],
34
+ return_to: request.fullpath,
35
+ )
36
+
37
+ url.to_s
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module ShopAccessScopesVerification
5
+ extend ActiveSupport::Concern
6
+
7
+ included do
8
+ before_action :login_on_scope_changes
9
+ end
10
+
11
+ protected
12
+
13
+ def login_on_scope_changes
14
+ redirect_to(shop_login) if scopes_mismatch?
15
+ end
16
+
17
+ private
18
+
19
+ def scopes_mismatch?
20
+ ShopifyApp.configuration.shop_access_scopes_strategy.update_access_scopes?(current_shopify_domain)
21
+ end
22
+
23
+ def current_shopify_domain
24
+ return if params[:shop].blank?
25
+ ShopifyApp::Utils.sanitize_shop_domain(params[:shop])
26
+ end
27
+
28
+ def shop_login
29
+ ShopifyApp::Utils.shop_login_url(shop: params[:shop], host: params[:host], return_to: request.fullpath)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+ module ShopifyApp
3
+ class AuthenticatedController < ActionController::Base
4
+ include ShopifyApp::Authenticated
5
+
6
+ protect_from_forgery with: :exception
7
+ end
8
+ end
@@ -0,0 +1,195 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ # Performs login after OAuth completes
5
+ class CallbackController < ActionController::Base
6
+ include ShopifyApp::LoginProtection
7
+
8
+ def callback
9
+ return respond_with_error if invalid_request?
10
+
11
+ store_access_token_and_build_session
12
+
13
+ if start_user_token_flow?
14
+ return respond_with_user_token_flow
15
+ end
16
+
17
+ perform_post_authenticate_jobs
18
+
19
+ respond_successfully
20
+ end
21
+
22
+ private
23
+
24
+ def respond_successfully
25
+ if jwt_request?
26
+ head(:ok)
27
+ else
28
+ redirect_to(return_address)
29
+ end
30
+ end
31
+
32
+ def respond_with_user_token_flow
33
+ redirect_to(login_url_with_optional_shop)
34
+ end
35
+
36
+ def store_access_token_and_build_session
37
+ if native_browser_request?
38
+ reset_session_options
39
+ end
40
+ set_shopify_session
41
+ end
42
+
43
+ def invalid_request?
44
+ return true unless auth_hash
45
+
46
+ jwt_request? && !valid_jwt_auth?
47
+ end
48
+
49
+ def native_browser_request?
50
+ !jwt_request?
51
+ end
52
+
53
+ def perform_post_authenticate_jobs
54
+ install_webhooks
55
+ install_scripttags
56
+ perform_after_authenticate_job
57
+ end
58
+
59
+ def respond_with_error
60
+ if jwt_request?
61
+ head(:unauthorized)
62
+ else
63
+ flash[:error] = I18n.t('could_not_log_in')
64
+ redirect_to(login_url_with_optional_shop)
65
+ end
66
+ end
67
+
68
+ # Override user_session_by_cookie from LoginProtection to bypass allow_cookie_authentication
69
+ # setting check because session cookies are justified at top level
70
+ def user_session_by_cookie
71
+ return unless session[:user_id].present?
72
+ ShopifyApp::SessionRepository.retrieve_user_session(session[:user_id])
73
+ end
74
+
75
+ def start_user_token_flow?
76
+ if jwt_request?
77
+ false
78
+ else
79
+ return false unless ShopifyApp::SessionRepository.user_storage.present?
80
+ update_user_access_scopes?
81
+ end
82
+ end
83
+
84
+ def update_user_access_scopes?
85
+ return true if user_session.blank?
86
+ user_access_scopes_strategy.update_access_scopes?(user_id: session[:user_id])
87
+ end
88
+
89
+ def user_access_scopes_strategy
90
+ ShopifyApp.configuration.user_access_scopes_strategy
91
+ end
92
+
93
+ def jwt_request?
94
+ jwt_shopify_domain || jwt_shopify_user_id
95
+ end
96
+
97
+ def valid_jwt_auth?
98
+ auth_hash && jwt_shopify_domain == shop_name && jwt_shopify_user_id == associated_user_id
99
+ end
100
+
101
+ def auth_hash
102
+ request.env['omniauth.auth']
103
+ end
104
+
105
+ def shop_name
106
+ auth_hash.uid
107
+ end
108
+
109
+ def offline_access_token
110
+ ShopifyApp::SessionRepository.retrieve_shop_session_by_shopify_domain(shop_name)&.token
111
+ end
112
+
113
+ def online_access_token
114
+ ShopifyApp::SessionRepository.retrieve_user_session_by_shopify_user_id(associated_user_id)&.token
115
+ end
116
+
117
+ def associated_user
118
+ return unless auth_hash.dig('extra', 'associated_user').present?
119
+
120
+ auth_hash['extra']['associated_user'].merge('scope' => auth_hash['extra']['associated_user_scope'])
121
+ end
122
+
123
+ def associated_user_id
124
+ associated_user && associated_user['id']
125
+ end
126
+
127
+ def token
128
+ auth_hash['credentials']['token']
129
+ end
130
+
131
+ def access_scopes
132
+ auth_hash.dig('extra', 'scope')
133
+ end
134
+
135
+ def reset_session_options
136
+ request.session_options[:renew] = true
137
+ session.delete(:_csrf_token)
138
+ end
139
+
140
+ def set_shopify_session
141
+ session_store = ShopifyAPI::Session.new(
142
+ domain: shop_name,
143
+ token: token,
144
+ api_version: ShopifyApp.configuration.api_version,
145
+ access_scopes: access_scopes
146
+ )
147
+
148
+ session[:shopify_user] = associated_user
149
+ if session[:shopify_user].present?
150
+ session[:shop_id] = nil if shop_session && shop_session.domain != shop_name
151
+ session[:user_id] = ShopifyApp::SessionRepository.store_user_session(session_store, associated_user)
152
+ else
153
+ session[:shop_id] = ShopifyApp::SessionRepository.store_shop_session(session_store)
154
+ session[:user_id] = nil if user_session && user_session.domain != shop_name
155
+ end
156
+ session[:shopify_domain] = shop_name
157
+ session[:user_session] = auth_hash&.extra&.session
158
+ end
159
+
160
+ def install_webhooks
161
+ return unless ShopifyApp.configuration.has_webhooks?
162
+
163
+ WebhooksManager.queue(
164
+ shop_name,
165
+ offline_access_token || online_access_token,
166
+ ShopifyApp.configuration.webhooks
167
+ )
168
+ end
169
+
170
+ def install_scripttags
171
+ return unless ShopifyApp.configuration.has_scripttags?
172
+
173
+ ScripttagsManager.queue(
174
+ shop_name,
175
+ offline_access_token || online_access_token,
176
+ ShopifyApp.configuration.scripttags
177
+ )
178
+ end
179
+
180
+ def perform_after_authenticate_job
181
+ config = ShopifyApp.configuration.after_authenticate_job
182
+
183
+ return unless config && config[:job].present?
184
+
185
+ job = config[:job]
186
+ job = job.constantize if job.is_a?(String)
187
+
188
+ if config[:inline] == true
189
+ job.perform_now(shop_domain: session[:shopify_domain])
190
+ else
191
+ job.perform_later(shop_domain: session[:shopify_domain])
192
+ end
193
+ end
194
+ end
195
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ class ExtensionVerificationController < ActionController::Base
5
+ include ShopifyApp::PayloadVerification
6
+ protect_from_forgery with: :null_session
7
+ before_action :verify_request
8
+
9
+ private
10
+
11
+ def verify_request
12
+ head(:unauthorized) unless hmac_valid?(request.body.read)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,202 @@
1
+ # frozen_string_literal: true
2
+ module ShopifyApp
3
+ class SessionsController < ActionController::Base
4
+ include ShopifyApp::LoginProtection
5
+
6
+ layout false, only: :new
7
+
8
+ after_action only: [:new, :create] do |controller|
9
+ controller.response.headers.except!('X-Frame-Options')
10
+ end
11
+
12
+ def new
13
+ authenticate if sanitized_shop_name.present?
14
+ end
15
+
16
+ def create
17
+ authenticate
18
+ end
19
+
20
+ def enable_cookies
21
+ return unless validate_shop_presence
22
+
23
+ render(:enable_cookies, layout: false, locals: {
24
+ does_not_have_storage_access_url: top_level_interaction_path(
25
+ shop: sanitized_shop_name,
26
+ host: host,
27
+ return_to: params[:return_to]
28
+ ),
29
+ has_storage_access_url: login_url_with_optional_shop(top_level: true),
30
+ app_target_url: granted_storage_access_path(
31
+ shop: sanitized_shop_name,
32
+ host: host,
33
+ return_to: params[:return_to]
34
+ ),
35
+ current_shopify_domain: current_shopify_domain,
36
+ })
37
+ end
38
+
39
+ def top_level_interaction
40
+ @url = login_url_with_optional_shop(top_level: true)
41
+ validate_shop_presence
42
+ end
43
+
44
+ def granted_storage_access
45
+ return unless validate_shop_presence
46
+
47
+ session['shopify.granted_storage_access'] = true
48
+
49
+ copy_return_to_param_to_session
50
+
51
+ redirect_to(return_address_with_params({ shop: @shop }))
52
+ end
53
+
54
+ def destroy
55
+ reset_session
56
+ flash[:notice] = I18n.t('.logged_out')
57
+ redirect_to(login_url_with_optional_shop)
58
+ end
59
+
60
+ private
61
+
62
+ def authenticate
63
+ return render_invalid_shop_error unless sanitized_shop_name.present?
64
+ session['shopify.omniauth_params'] = { shop: sanitized_shop_name }
65
+
66
+ copy_return_to_param_to_session
67
+
68
+ set_user_tokens_option
69
+
70
+ if user_agent_can_partition_cookies
71
+ authenticate_with_partitioning
72
+ else
73
+ authenticate_normally
74
+ end
75
+ end
76
+
77
+ def authenticate_normally
78
+ if request_storage_access?
79
+ redirect_to_request_storage_access
80
+ elsif authenticate_in_context?
81
+ authenticate_in_context
82
+ else
83
+ authenticate_at_top_level
84
+ end
85
+ end
86
+
87
+ def authenticate_with_partitioning
88
+ if session['shopify.cookies_persist']
89
+ clear_top_level_oauth_cookie
90
+ authenticate_in_context
91
+ else
92
+ set_top_level_oauth_cookie
93
+ enable_cookie_access
94
+ end
95
+ end
96
+
97
+ # Override shop_session_by_cookie from LoginProtection to bypass allow_cookie_authentication
98
+ # setting check because session cookies are justified at top level
99
+ def shop_session_by_cookie
100
+ return unless session[:shop_id].present?
101
+ ShopifyApp::SessionRepository.retrieve_shop_session(session[:shop_id])
102
+ end
103
+
104
+ # rubocop:disable Lint/SuppressedException
105
+ def set_user_tokens_option
106
+ current_shop_session = shop_session
107
+
108
+ if current_shop_session.blank?
109
+ session[:user_tokens] = false
110
+ return
111
+ end
112
+
113
+ session[:user_tokens] = ShopifyApp::SessionRepository.user_storage.present?
114
+
115
+ ShopifyAPI::Session.temp(
116
+ domain: current_shop_session.domain,
117
+ token: current_shop_session.token,
118
+ api_version: current_shop_session.api_version
119
+ ) do
120
+ ShopifyAPI::Metafield.find(:token_validity_bogus_check)
121
+ end
122
+ rescue ActiveResource::UnauthorizedAccess
123
+ session[:user_tokens] = false
124
+ rescue StandardError
125
+ end
126
+ # rubocop:enable Lint/SuppressedException
127
+
128
+ def validate_shop_presence
129
+ @shop = sanitized_shop_name
130
+ unless @shop
131
+ render_invalid_shop_error
132
+ return false
133
+ end
134
+
135
+ true
136
+ end
137
+
138
+ def copy_return_to_param_to_session
139
+ session[:return_to] = RedirectSafely.make_safe(params[:return_to], '/') if params[:return_to]
140
+ end
141
+
142
+ def render_invalid_shop_error
143
+ flash[:error] = I18n.t('invalid_shop_url')
144
+ redirect_to(return_address)
145
+ end
146
+
147
+ def enable_cookie_access
148
+ fullpage_redirect_to(enable_cookies_path(
149
+ shop: sanitized_shop_name,
150
+ host: host,
151
+ return_to: session[:return_to]
152
+ ))
153
+ end
154
+
155
+ def authenticate_in_context
156
+ post_redirect_to_auth_shopify
157
+ end
158
+
159
+ def post_redirect_to_auth_shopify
160
+ render('shopify_app/shared/post_redirect_to_auth_shopify', layout: false)
161
+ end
162
+
163
+ def authenticate_at_top_level
164
+ fullpage_redirect_to(login_url_with_optional_shop(top_level: true))
165
+ end
166
+
167
+ def authenticate_in_context?
168
+ return true unless ShopifyApp.configuration.embedded_app?
169
+ params[:top_level]
170
+ end
171
+
172
+ def request_storage_access?
173
+ return false unless ShopifyApp.configuration.embedded_app?
174
+ return false if params[:top_level]
175
+ return false if user_agent_is_mobile
176
+ return false if user_agent_is_pos
177
+
178
+ !session['shopify.granted_storage_access']
179
+ end
180
+
181
+ def redirect_to_request_storage_access
182
+ render(
183
+ :request_storage_access,
184
+ layout: false,
185
+ locals: {
186
+ does_not_have_storage_access_url: top_level_interaction_path(
187
+ shop: sanitized_shop_name,
188
+ host: host,
189
+ return_to: session[:return_to]
190
+ ),
191
+ has_storage_access_url: login_url_with_optional_shop(top_level: true),
192
+ app_target_url: granted_storage_access_path(
193
+ shop: sanitized_shop_name,
194
+ host: host,
195
+ return_to: session[:return_to]
196
+ ),
197
+ current_shopify_domain: current_shopify_domain,
198
+ }
199
+ )
200
+ end
201
+ end
202
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+ module ShopifyApp
3
+ class MissingWebhookJobError < StandardError; end
4
+
5
+ class WebhooksController < ActionController::Base
6
+ include ShopifyApp::WebhookVerification
7
+
8
+ def receive
9
+ params.permit!
10
+ webhook_job_klass.perform_later(shop_domain: shop_domain, webhook: webhook_params.to_h)
11
+ head(:ok)
12
+ end
13
+
14
+ private
15
+
16
+ def webhook_params
17
+ params.except(:controller, :action, :type)
18
+ end
19
+
20
+ def webhook_job_klass
21
+ webhook_job_klass_name.safe_constantize || raise(ShopifyApp::MissingWebhookJobError)
22
+ end
23
+
24
+ def webhook_job_klass_name(type = webhook_type)
25
+ [webhook_namespace, "#{type}_job"].compact.join('/').classify
26
+ end
27
+
28
+ def webhook_type
29
+ params[:type]
30
+ end
31
+
32
+ def webhook_namespace
33
+ ShopifyApp.configuration.webhook_jobs_namespace
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,109 @@
1
+ <style>
2
+ .Polaris-Button {
3
+ position:relative;
4
+ display:-webkit-inline-box;
5
+ display:-ms-inline-flexbox;
6
+ display:inline-flex;
7
+ -webkit-box-align:center;
8
+ -ms-flex-align:center;
9
+ align-items:center;
10
+ -webkit-box-pack:center;
11
+ -ms-flex-pack:center;
12
+ justify-content:center;
13
+ min-height:3.6rem;
14
+ min-width:3.6rem;
15
+ margin:0;
16
+ padding:0.7rem 1.6rem;
17
+ background-color:#ffffff;
18
+ border:1px solid #babfc3;
19
+ border-top-color: #c9cccf;
20
+ border-bottom-color: #babfc4;
21
+ box-shadow:0 1px 0 0 rgba(0, 0, 0, 0.05);
22
+ border-radius:4px;
23
+ line-height:1;
24
+ color:#202223;
25
+ text-align:center;
26
+ cursor:pointer;
27
+ -webkit-user-select:none;
28
+ -moz-user-select:none;
29
+ -ms-user-select:none;
30
+ user-select:none;
31
+ text-decoration:none;
32
+ transition-property:background, border, box-shadow;
33
+ transition-duration:100ms;
34
+ transition-timing-function:cubic-bezier(0.64, 0, 0.35, 1);
35
+ }
36
+
37
+ .Polaris-Button:hover {
38
+ background-color:#f6f6f7;
39
+ }
40
+
41
+ .Polaris-Button:focus {
42
+ outline:0;
43
+ }
44
+
45
+ .Polaris-Button:focus:after {
46
+ box-shadow:0 0 0 .2rem #448fff;
47
+ }
48
+
49
+ .Polaris-Button:after {
50
+ content:'';
51
+ position:absolute;
52
+ z-index:1;
53
+ top:-.2rem;
54
+ right:-.2rem;
55
+ bottom:-.2rem;
56
+ left:-.2rem;
57
+ display:block;
58
+ pointer-events:none;
59
+ box-shadow:0 0 0 -.2rem #448fff;
60
+ transition:box-shadow 100ms cubic-bezier(0.64, 0, 0.35, 1);
61
+ border-radius:5px;
62
+ }
63
+
64
+ .Polaris-Button:active {
65
+ background-color:#f1f2f3);
66
+ }
67
+
68
+ .Polaris-Button__Content {
69
+ font-size:1.4rem;
70
+ font-weight:500;
71
+ line-height:1.6rem;
72
+ text-transform:initial;
73
+ letter-spacing:initial;
74
+ position:relative;
75
+ display:-webkit-box;
76
+ display:-ms-flexbox;
77
+ display:flex;
78
+ -webkit-box-pack:center;
79
+ -ms-flex-pack:center;
80
+ justify-content:center;
81
+ -webkit-box-align:center;
82
+ -ms-flex-align:center;
83
+ align-items:center;
84
+ min-width:1px;
85
+ min-height:1px;
86
+ }
87
+
88
+ .Polaris-Button--primary {
89
+ background-color:#008060;
90
+ border-color:transparent;
91
+ border-width:0;
92
+ color:white;
93
+ }
94
+
95
+ .Polaris-Button--primary:hover {
96
+ background-color:#006e52;
97
+ border-color:transparent;
98
+ color:white;
99
+ }
100
+
101
+ .Polaris-Button--primary:active {
102
+ background-color:#005e46;
103
+ border-color:transparent;
104
+ }
105
+
106
+ .Polaris-Button--sizeLarge {
107
+ padding:1.1rem 2.4rem;
108
+ }
109
+ </style>