ruby_shopify_app 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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>