shopify_app 21.2.0 → 21.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/build.yml +7 -8
  3. data/.github/workflows/stale.yml +1 -0
  4. data/.spin/rails/prepare-application +8 -0
  5. data/CHANGELOG.md +15 -0
  6. data/Gemfile +1 -0
  7. data/Gemfile.lock +106 -91
  8. data/README.md +19 -15
  9. data/SECURITY.md +1 -1
  10. data/app/controllers/concerns/shopify_app/authenticated.rb +4 -9
  11. data/app/controllers/concerns/shopify_app/ensure_authenticated_links.rb +3 -2
  12. data/app/controllers/concerns/shopify_app/ensure_has_session.rb +19 -0
  13. data/app/controllers/concerns/shopify_app/ensure_installed.rb +62 -0
  14. data/app/controllers/concerns/shopify_app/require_known_shop.rb +3 -38
  15. data/app/controllers/shopify_app/authenticated_controller.rb +1 -1
  16. data/app/controllers/shopify_app/callback_controller.rb +64 -27
  17. data/app/controllers/shopify_app/extension_verification_controller.rb +4 -1
  18. data/app/controllers/shopify_app/sessions_controller.rb +11 -2
  19. data/config/locales/ja.yml +1 -1
  20. data/docs/Troubleshooting.md +38 -2
  21. data/docs/Upgrading.md +40 -32
  22. data/docs/shopify_app/controller-concerns.md +48 -0
  23. data/docs/shopify_app/logging.md +21 -0
  24. data/docs/shopify_app/webhooks.md +13 -0
  25. data/lib/generators/shopify_app/add_app_uninstalled_job/add_app_uninstalled_job_generator.rb +15 -0
  26. data/lib/generators/shopify_app/add_app_uninstalled_job/templates/app_uninstalled_job.rb.tt +22 -0
  27. data/lib/generators/shopify_app/add_gdpr_jobs/add_gdpr_jobs_generator.rb +23 -0
  28. data/lib/generators/shopify_app/add_gdpr_jobs/templates/customers_data_request_job.rb.tt +22 -0
  29. data/lib/generators/shopify_app/add_gdpr_jobs/templates/customers_redact_job.rb.tt +22 -0
  30. data/lib/generators/shopify_app/add_gdpr_jobs/templates/shop_redact_job.rb.tt +22 -0
  31. data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +1 -0
  32. data/lib/generators/shopify_app/add_marketing_activity_extension/templates/marketing_activities_controller.rb +2 -1
  33. data/lib/generators/shopify_app/add_webhook/templates/webhook_job.rb.tt +2 -1
  34. data/lib/generators/shopify_app/authenticated_controller/templates/authenticated_controller.rb +1 -1
  35. data/lib/generators/shopify_app/home_controller/templates/index.html.erb +1 -1
  36. data/lib/generators/shopify_app/home_controller/templates/unauthenticated_home_controller.rb +1 -1
  37. data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +8 -2
  38. data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token_job.rb +1 -1
  39. data/lib/generators/shopify_app/shopify_app_generator.rb +2 -0
  40. data/lib/shopify_app/access_scopes/noop_strategy.rb +4 -0
  41. data/lib/shopify_app/access_scopes/user_strategy.rb +5 -0
  42. data/lib/shopify_app/configuration.rb +11 -0
  43. data/lib/shopify_app/controller_concerns/ensure_billing.rb +3 -0
  44. data/lib/shopify_app/controller_concerns/itp.rb +5 -0
  45. data/lib/shopify_app/controller_concerns/login_protection.rb +52 -13
  46. data/lib/shopify_app/controller_concerns/redirect_for_embedded.rb +4 -1
  47. data/lib/shopify_app/controller_concerns/webhook_verification.rb +4 -1
  48. data/lib/shopify_app/logger.rb +28 -0
  49. data/lib/shopify_app/managers/scripttags_manager.rb +1 -0
  50. data/lib/shopify_app/managers/webhooks_manager.rb +6 -0
  51. data/lib/shopify_app/session/jwt.rb +1 -1
  52. data/lib/shopify_app/session/session_repository.rb +15 -4
  53. data/lib/shopify_app/version.rb +1 -1
  54. data/lib/shopify_app.rb +2 -0
  55. data/shopify_app.gemspec +2 -1
  56. data/yarn.lock +5 -5
  57. metadata +30 -4
@@ -3,46 +3,11 @@
3
3
  module ShopifyApp
4
4
  module RequireKnownShop
5
5
  extend ActiveSupport::Concern
6
- include ShopifyApp::RedirectForEmbedded
6
+ include ShopifyApp::EnsureInstalled
7
7
 
8
8
  included do
9
- before_action :check_shop_domain
10
- before_action :check_shop_known
11
- end
12
-
13
- def current_shopify_domain
14
- return if params[:shop].blank?
15
-
16
- @shopify_domain ||= ShopifyApp::Utils.sanitize_shop_domain(params[:shop])
17
- end
18
-
19
- private
20
-
21
- def check_shop_domain
22
- redirect_to(ShopifyApp.configuration.login_url) unless current_shopify_domain
23
- end
24
-
25
- def check_shop_known
26
- @shop = SessionRepository.retrieve_shop_session_by_shopify_domain(current_shopify_domain)
27
- unless @shop
28
- if embedded_param?
29
- redirect_for_embedded
30
- else
31
- redirect_to(shop_login)
32
- end
33
- end
34
- end
35
-
36
- def shop_login
37
- url = URI(ShopifyApp.configuration.login_url)
38
-
39
- url.query = URI.encode_www_form(
40
- shop: params[:shop],
41
- host: params[:host],
42
- return_to: request.fullpath,
43
- )
44
-
45
- url.to_s
9
+ ShopifyApp::Logger.deprecated("RequireKnownShop has been replaced by EnsureInstalled."\
10
+ " Please use the EnsureInstalled controller concern for the same behavior", "22.0.0")
46
11
  end
47
12
  end
48
13
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  module ShopifyApp
4
4
  class AuthenticatedController < ActionController::Base
5
- include ShopifyApp::Authenticated
5
+ include ShopifyApp::EnsureHasSession
6
6
 
7
7
  protect_from_forgery with: :exception
8
8
  end
@@ -8,49 +8,86 @@ module ShopifyApp
8
8
 
9
9
  def callback
10
10
  begin
11
- filtered_params = request.parameters.symbolize_keys.slice(:code, :shop, :timestamp, :state, :host, :hmac)
12
-
13
- auth_result = ShopifyAPI::Auth::Oauth.validate_auth_callback(
14
- cookies: {
15
- ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME =>
16
- cookies.encrypted[ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME],
17
- },
18
- auth_query: ShopifyAPI::Auth::Oauth::AuthQuery.new(**filtered_params),
19
- )
20
- rescue
11
+ api_session, cookie = validated_auth_objects
12
+ rescue => error
13
+ deprecate_callback_rescue(error) unless error.class.module_parent == ShopifyAPI::Errors
21
14
  return respond_with_error
22
15
  end
23
16
 
24
- cookies.encrypted[auth_result[:cookie].name] = {
25
- expires: auth_result[:cookie].expires,
26
- secure: true,
27
- http_only: true,
28
- value: auth_result[:cookie].value,
29
- }
17
+ save_session(api_session) if api_session
18
+ update_rails_cookie(api_session, cookie)
30
19
 
31
- session[:shopify_user_id] = auth_result[:session].associated_user.id if auth_result[:session].online?
20
+ return respond_with_user_token_flow if start_user_token_flow?(api_session)
32
21
 
33
- if start_user_token_flow?(auth_result[:session])
34
- return respond_with_user_token_flow
35
- end
22
+ perform_post_authenticate_jobs(api_session)
23
+ redirect_to_app if check_billing(api_session)
24
+ end
36
25
 
37
- perform_post_authenticate_jobs(auth_result[:session])
38
- has_payment = check_billing(auth_result[:session])
26
+ private
39
27
 
40
- respond_successfully if has_payment
28
+ def deprecate_callback_rescue(error)
29
+ message = <<~EOS
30
+ An error of type #{error.class} was rescued. This is not part of `ShopifyAPI::Errors`, which could indicate a
31
+ bug in your app, or a bug in the shopify_app gem. Future versions of the gem may re-raise this error rather
32
+ than rescuing it.
33
+ EOS
34
+ ShopifyApp::Logger.deprecated(message, "22.0.0")
41
35
  end
42
36
 
43
- private
37
+ def save_session(api_session)
38
+ ShopifyApp::SessionRepository.store_session(api_session)
39
+ end
40
+
41
+ def validated_auth_objects
42
+ filtered_params = request.parameters.symbolize_keys.slice(:code, :shop, :timestamp, :state, :host, :hmac)
43
+
44
+ oauth_payload = ShopifyAPI::Auth::Oauth.validate_auth_callback(
45
+ cookies: {
46
+ ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME =>
47
+ cookies.encrypted[ShopifyAPI::Auth::Oauth::SessionCookie::SESSION_COOKIE_NAME],
48
+ },
49
+ auth_query: ShopifyAPI::Auth::Oauth::AuthQuery.new(**filtered_params),
50
+ )
51
+ api_session = oauth_payload.dig(:session)
52
+ cookie = oauth_payload.dig(:cookie)
53
+
54
+ [api_session, cookie]
55
+ end
56
+
57
+ def update_rails_cookie(api_session, cookie)
58
+ if cookie.value.present?
59
+ cookies.encrypted[cookie.name] = {
60
+ expires: cookie.expires,
61
+ secure: true,
62
+ http_only: true,
63
+ value: cookie.value,
64
+ }
65
+ end
66
+
67
+ session[:shopify_user_id] = api_session.associated_user.id if api_session.online?
68
+ ShopifyApp::Logger.debug("Saving Shopify user ID to cookie")
69
+ end
44
70
 
45
- def respond_successfully
71
+ def redirect_to_app
46
72
  if ShopifyAPI::Context.embedded?
47
- return_to = session.delete(:return_to) || ""
48
- redirect_to(ShopifyAPI::Auth.embedded_app_url(params[:host]) + return_to, allow_other_host: true)
73
+ return_to = "#{decoded_host}#{session.delete(:return_to)}"
74
+ return_to = ShopifyApp.configuration.root_url if deduced_phishing_attack?
75
+ redirect_to(return_to, allow_other_host: true)
49
76
  else
50
77
  redirect_to(return_address)
51
78
  end
52
79
  end
53
80
 
81
+ def decoded_host
82
+ @decoded_host ||= ShopifyAPI::Auth.embedded_app_url(params[:host])
83
+ end
84
+
85
+ # host param doesn't match the configured myshopify_domain
86
+ def deduced_phishing_attack?
87
+ sanitized_host = ShopifyApp::Utils.sanitize_shop_domain(URI(decoded_host).host)
88
+ sanitized_host.nil?
89
+ end
90
+
54
91
  def respond_with_error
55
92
  flash[:error] = I18n.t("could_not_log_in")
56
93
  redirect_to(login_url_with_optional_shop)
@@ -9,7 +9,10 @@ module ShopifyApp
9
9
  private
10
10
 
11
11
  def verify_request
12
- head(:unauthorized) unless hmac_valid?(request.body.read)
12
+ unless hmac_valid?(request.body.read)
13
+ head(:unauthorized)
14
+ ShopifyApp::Logger.debug("Extension verification failed due to invalid HMAC")
15
+ end
13
16
  end
14
17
  end
15
18
  end
@@ -27,6 +27,8 @@ module ShopifyApp
27
27
  def destroy
28
28
  reset_session
29
29
  flash[:notice] = I18n.t(".logged_out")
30
+ ShopifyApp::Logger.debug("Session destroyed")
31
+ ShopifyApp::Logger.debug("Redirecting to #{login_url_with_optional_shop}")
30
32
  redirect_to(login_url_with_optional_shop)
31
33
  end
32
34
 
@@ -38,6 +40,7 @@ module ShopifyApp
38
40
  copy_return_to_param_to_session
39
41
 
40
42
  if embedded_redirect_url?
43
+ ShopifyApp::Logger.debug("Embedded URL within / authenticate")
41
44
  if embedded_param?
42
45
  redirect_for_embedded
43
46
  else
@@ -52,6 +55,7 @@ module ShopifyApp
52
55
 
53
56
  def start_oauth
54
57
  callback_url = ShopifyApp.configuration.login_callback_url.gsub(%r{^/}, "")
58
+ ShopifyApp::Logger.debug("Starting OAuth with the following callback URL: #{callback_url}")
55
59
 
56
60
  auth_attributes = ShopifyAPI::Auth::Oauth.begin_auth(
57
61
  shop: sanitized_shop_name,
@@ -65,7 +69,10 @@ module ShopifyApp
65
69
  value: auth_attributes[:cookie].value,
66
70
  }
67
71
 
68
- redirect_to(auth_attributes[:auth_route], allow_other_host: true)
72
+ auth_route = auth_attributes[:auth_route]
73
+
74
+ ShopifyApp::Logger.debug("Redirecting to auth_route - #{auth_route}")
75
+ redirect_to(auth_route, allow_other_host: true)
69
76
  end
70
77
 
71
78
  def validate_shop_presence
@@ -94,7 +101,9 @@ module ShopifyApp
94
101
  end
95
102
 
96
103
  def redirect_auth_to_top_level
97
- fullpage_redirect_to(login_url_with_optional_shop(top_level: true))
104
+ url = login_url_with_optional_shop(top_level: true)
105
+ ShopifyApp::Logger.debug("Redirecting to top level - #{url}")
106
+ fullpage_redirect_to(url)
98
107
  end
99
108
  end
100
109
  end
@@ -8,7 +8,7 @@ ja:
8
8
  enable_cookies_footer: Cookieを使用すると、各種設定や個人情報を一時的に保存することで、アプリ認証を受けることができます。30日後に有効期限が切れます。
9
9
  enable_cookies_action: Cookieを有効にする
10
10
  top_level_interaction_heading: お使いのブラウザを更新する必要があります%{app}
11
- top_level_interaction_body: Shopifyがアプリを開けるように、ブラウザーはCookieにアクセスするための%{app}のようなアプリが必要です。
11
+ top_level_interaction_body: Shopifyがアプリを開けるように、ブラウザはCookieにアクセスするための%{app}のようなアプリが必要です。
12
12
  top_level_interaction_action: 続ける
13
13
  request_storage_access_heading: "%{app}はCookieへのアクセス許可が必要です"
14
14
  request_storage_access_body: Cookieを使用すると、個人情報を一時的に保存することで、アプリ認証を受けることができます。[続ける]
@@ -3,7 +3,7 @@
3
3
  #### Table of contents
4
4
 
5
5
  [Generators](#generators)
6
- * [The `shopify_app:install` generator hangs](#the-shopifyappinstall-generator-hangs)
6
+ * [The `shopify_app:install` generator hangs](#the-shopify_appinstall-generator-hangs)
7
7
 
8
8
  [Rails](#rails)
9
9
  * [Known issues with Rails `v6.1`](#known-issues-with-rails-v61)
@@ -18,6 +18,8 @@
18
18
  * [My app can't make requests to the Shopify API](#my-app-cant-make-requests-to-the-shopify-api)
19
19
  * [I'm stuck in a redirect loop after OAuth](#im-stuck-in-a-redirect-loop-after-oauth)
20
20
 
21
+ [Debugging Tips](#debugging-tips)
22
+
21
23
  ## Generators
22
24
 
23
25
  ### The shopify_app:install generator hangs
@@ -143,9 +145,43 @@ X-Shopify-API-Request-Failure-Unauthorized: true
143
145
 
144
146
  Then, use the [Shopify App Bridge Redirect](https://shopify.dev/tools/app-bridge/actions/navigation/redirect) action to redirect your app frontend to the app login URL if this header is set.
145
147
 
146
-
147
148
  ### I'm stuck in a redirect loop after OAuth
148
149
 
149
150
  In previous versions of `ShopifyApp::Authenticated` controller concern, App Bridge embedded apps were able to include the `Authenticated` controller concern in the `HomeController` and other embedded controllers. This is no longer supported due to browsers blocking 3rd party cookies to increase privacy. App Bridge 3 is needed to handle all embedded sessions.
150
151
 
151
152
  For more details on how to handle embeded sessions, refer to [the session token documentation](https://shopify.dev/apps/auth/oauth/session-tokens).
153
+
154
+ ### `redirect_uri is not whitelisted`
155
+
156
+ * Ensure you have set the `HOST` environment variable to match your host's URL, e.g. `http://localhost:3000` or `https://my-host-name.trycloudflare.com`.
157
+ * Update the app's URL and whitelisted URLs in App Setup on https://partners.shopify.com
158
+
159
+ ### `This app can’t load due to an issue with browser cookies`
160
+
161
+ This can be caused by an infinite redirect due to a coding error
162
+ To investigate the cause, you can add a breakpoint or logging to the `rescue` clause of `ShopifyApp::CallbackController`.
163
+
164
+ One possible cause is that for XHR requests, the `Authenticated` concern should be used, rather than `RequireKnownShop`.
165
+ See below for further details.
166
+
167
+ ## Controller Concerns
168
+ ### Authenticated vs RequireKnownShop
169
+ The gem heavily relies on the `current_shopify_domain` helper to contextualize a request to a given Shopify shop. This helper is set in different and conflicting ways if the request is authenticated or not.
170
+
171
+ Because of these conflicting approaches the `Authenticated` (for use in authenticated requests) and `RequireKnownShop` (for use in unauthenticated requests) controller concerns must *never* be included within the same controller.
172
+
173
+ #### Authenticated Requests
174
+ For authenticated requests, use the [`Authenticated` controller concern](https://github.com/Shopify/shopify_app/blob/main/app/controllers/concerns/shopify_app/authenticated.rb). The `current_shopify_domain` is set from the JWT for these requests.
175
+
176
+ #### Unauthenticated Requests
177
+ For unauthenticated requests, use the [`RequireKnownShop` controller concern](https://github.com/Shopify/shopify_app/blob/main/app/controllers/concerns/shopify_app/require_known_shop.rb). The `current_shopify_domain` is set from the query string parameters that are passed.
178
+
179
+ ## Debugging Tips
180
+
181
+ If you do run into issues with the gem there are two useful techniques to apply: Adding log statements, and using an interactive debugger, such as `pry`.
182
+
183
+ You can temporarily add log statements or debugger calls to the `shopify_app` or `shopify-api-ruby` gems:
184
+ * You can modify a gem using [`bundle open`](https://boringrails.com/tips/bundle-open-debug-gems)
185
+ * Alternatively, you can your modify your `Gemfile` to use local locally checked out gems with the the [`path` option](https://bundler.io/man/gemfile.5.html).
186
+
187
+ Note that if you make changes to a gem, you will need to restart the app for the changes to be applied.
data/docs/Upgrading.md CHANGED
@@ -4,6 +4,12 @@ This file documents important changes needed to upgrade your app's Shopify App v
4
4
 
5
5
  #### Table of contents
6
6
 
7
+ [General Advice](#general-advice)
8
+
9
+ [Unreleased](#unreleased)
10
+
11
+ [Upgrading to `v20.3.0`](#upgrading-to-v2030)
12
+
7
13
  [Upgrading to `v20.2.0`](#upgrading-to-v2020)
8
14
 
9
15
  [Upgrading to `v20.1.0`](#upgrading-to-v2010)
@@ -20,7 +26,29 @@ This file documents important changes needed to upgrade your app's Shopify App v
20
26
 
21
27
  [Upgrading from `v8.6` to `v9.0.0`](#upgrading-from-v86-to-v900)
22
28
 
29
+ ## General Advice
30
+
31
+ Although we strive to make upgrades as smooth as possible, some effort may be required to stay up to date with the latest changes to `shopify_app`.
32
+
33
+ We strongly recommend you avoid 'monkeypatching' any existing code from `ShopifyApp`, e.g. by inheriting from `ShopifyApp` and then overriding particular methods. This can result in difficult upgrades. If your app does so, you will need to carefully check the gem's internal changes when upgrading.
34
+
35
+ If you need to upgrade by more than one major version (e.g. from v18 to v20), we recommend doing one at a time. Deploy each into production to help to detect problems earlier.
36
+
37
+ We also recommend the use of a staging site which matches your production environment as closely as possible.
38
+
39
+ If you do run into issues, we recommend looking at our [debugging tips.](https://github.com/Shopify/shopify_app/blob/main/docs/Troubleshooting.md#debugging-tips)
40
+
41
+ ## Upgrading to 21.3.0
42
+ The `Itp` controller concern has been removed from `LoginProtection` which is included by the `Authenticated` controller concern.
43
+ If any of your controllers are dependant on methods from `Itp` then you can include `ShopifyApp::Itp` directly.
44
+ You may notice a deprecation notice saying, `Itp will be removed in an upcoming version`.
45
+ This is because we intend on removing `Itp` completely in `v22.0.0`, but this will work in the meantime.
46
+
47
+ ## Upgrading to `v20.3.0`
48
+ Calling `LoginProtection#current_shopify_domain` will no longer raise an error if there is no active session. It will now return a nil value. The internal behavior of raising an error on OAuth redirect is still in place, however. If you were calling `current_shopify_domain` in authenticated actions and expecting an error if nil, you'll need to do a presence check and raise that error within your app.
49
+
23
50
  ## Upgrading to `v20.2.0`
51
+
24
52
  All custom errors defined inline within the `ShopifyApp` gem have been moved to `lib/shopify_app/errors.rb`.
25
53
 
26
54
  - If you rescue any errors defined in this gem, you will need to rename them to match their new namespacing.
@@ -36,8 +64,11 @@ Note that the following steps are *optional* and only apply to **embedded** appl
36
64
 
37
65
  ## Upgrading to `v19.0.0`
38
66
 
39
- This update moves API authentication logic from this gem to the [`shopify_api`](https://github.com/Shopify/shopify-api-ruby)
40
- gem.
67
+ There are several major changes in this release:
68
+
69
+ * A change of strategy regarding sessions: Due to security changes with browsers, support for cookie based sessions was dropped. JWT is now the only supported method for managing sessions.
70
+ * As part of that change, this update moves API authentication logic from this gem to the [`shopify_api`](https://github.com/Shopify/shopify-api-ruby) gem.
71
+ * Previously the `shopify_api` gem relied on `ActiveResource`, an outdated library which was [removed](https://github.com/rails/rails/commit/f1637bf2bb00490203503fbd943b73406e043d1d) from Rails in 2012. v10 of `shopify_api` has a replacement approach which aims to provide a similar syntax, but changes will be necessary.
41
72
 
42
73
  ### High-level process
43
74
 
@@ -48,18 +79,20 @@ gem.
48
79
  - Remove `allow_jwt_authentication=` and `allow_cookie_authentication=` invocations from
49
80
  `config/initializers/shopify_app.rb` as the decision logic for which authentication method to use is now handled
50
81
  internally by the `shopify_api` gem, using the `ShopifyAPI::Context.embedded_app` setting.
51
- - `v19.0.0` updates the `shopify_api` dependency to `10.0.0`. This version of `shopify_api` has breaking changes. See
52
- the documentation for addressing these breaking changes on GitHub [here](https://github.com/Shopify/shopify-api-ruby#breaking-change-notice-for-version-1000).
82
+ - [Follow the guidance for upgrading `shopify-api-ruby`](https://github.com/Shopify/shopify-api-ruby#breaking-change-notice-for-version-1000).
53
83
 
54
84
  ### Specific cases
55
85
 
56
- #### Shopify user id in session
86
+ #### Shopify user ID in session
57
87
 
58
88
  Previously, we set the entire app user object in the `session` object.
59
89
  As of v19, since we no longer save the app user to the session (but only the shopify user id), we now store it as `session[:shopify_user_id]`. Please make sure to update any references to that object.
60
90
 
61
91
  #### Webhook Jobs
62
92
 
93
+ It is assumed that you have an ActiveJob implementation configured for `perform_later`, e.g. Sidekiq.
94
+ Ensure your jobs inherit from `ApplicationJob` or `ActiveJob::Base`.
95
+
63
96
  Add a new `handle` method to existing webhook jobs to go through the updated `shopify_api` gem.
64
97
 
65
98
  ```ruby
@@ -95,32 +128,7 @@ Shopify API session, or `nil` if no such session is available.
95
128
 
96
129
  #### Setting up `ShopifyAPI::Context`
97
130
 
98
- The `shopify_app` initializer must configure the `ShopifyAPI::Context`. The Rails generator will
99
- generate a block in the `shopify_app` initializer. To do so manually, ensure the following is
100
- part of the `after_initialize` block in `shopify_app.rb`.
101
-
102
- ```ruby
103
- Rails.application.config.after_initialize do
104
- if ShopifyApp.configuration.api_key.present? && ShopifyApp.configuration.secret.present?
105
- ShopifyAPI::Context.setup(
106
- api_key: ShopifyApp.configuration.api_key,
107
- api_secret_key: ShopifyApp.configuration.secret,
108
- old_api_secret_key: ShopifyApp.configuration.old_secret,
109
- api_version: ShopifyApp.configuration.api_version,
110
- host_name: URI(ENV.fetch('HOST', '')).host || '',
111
- scope: ShopifyApp.configuration.scope,
112
- is_private: !ENV.fetch('SHOPIFY_APP_PRIVATE_SHOP', '').empty?,
113
- is_embedded: ShopifyApp.configuration.embedded_app,
114
- session_storage: ShopifyApp::SessionRepository,
115
- logger: Rails.logger,
116
- private_shop: ENV.fetch('SHOPIFY_APP_PRIVATE_SHOP', nil),
117
- user_agent_prefix: "ShopifyApp/#{ShopifyApp::VERSION}"
118
- )
119
-
120
- ShopifyApp::WebhooksManager.add_registrations
121
- end
122
- end
123
- ```
131
+ The `shopify_app` initializer must configure the `ShopifyAPI::Context`. The Rails generator will generate a block in the `shopify_app` initializer. To do so manually, you can refer to `after_initialize` block in the [template](https://github.com/Shopify/shopify_app/blob/main/lib/generators/shopify_app/install/templates/shopify_app.rb.tt).
124
132
 
125
133
  ## Upgrading to `v18.1.2`
126
134
 
@@ -128,7 +136,7 @@ Version 18.1.2 replaces the deprecated EASDK redirect with an App Bridge 2 redir
128
136
 
129
137
  ## Upgrading to `v17.2.0`
130
138
 
131
- ### Different SameSite cookie attribute behaviour
139
+ ### Different SameSite cookie attribute behavior
132
140
 
133
141
  To support Rails `v6.1`, the [`SameSiteCookieMiddleware`](/lib/shopify_app/middleware/same_site_cookie_middleware.rb) was updated to configure cookies to `SameSite=None` if the app is embedded. Before this release, cookies were configured to `SameSite=None` only if this attribute had not previously been set before.
134
142
 
@@ -0,0 +1,48 @@
1
+ # Controller Concerns
2
+
3
+ The following controller concerns are designed to be public and can be included in your controllers. Concerns defined in `lib/shopify_app/controller_concerns` are designed to be private and are not meant to be included directly into your controllers.
4
+
5
+ ## Authenticated
6
+ Designed for controllers that are designed to handle authenticated actions by ensuring there is a valid session for the request.
7
+
8
+ In addition to session management, this concern will also handle localization, CSRF protection, embedded app settings, and billing enforcement.
9
+
10
+ #### LoginProtection - Session Management
11
+ This concern will setup and teardown the session around the action. If the session cannot be setup for the requested shop the request will be redirected to login.
12
+
13
+ The concern will load sessions depending on your app's configuration:
14
+
15
+ **Embedded apps**
16
+
17
+ Cookies are not available for embedded apps because it loads in an iframe, so this concern will load the session from the request's `Authorization` header containing a session token, which can be set using [App Bridge](https://shopify.dev/apps/tools/app-bridge).
18
+
19
+ Learn more about [using `authenticatedFetch`](https://shopify.dev/apps/auth/oauth/session-tokens/getting-started#step-2-authenticate-your-requests) to create session tokens and authenticate your requests.
20
+
21
+ **Non-embedded apps**
22
+
23
+ Since cookies are available, the concern will load the session directly from them, so you can make regular `fetch` requests on your front-end.
24
+
25
+ #### Localization
26
+ I18n localization is saved to the session for consistent translations for the session.
27
+
28
+ #### CSRFProtection
29
+ Implements Rails' [protect_from_forgery](https://api.rubyonrails.org/classes/ActionController/RequestForgeryProtection/ClassMethods.html#method-i-protect_from_forgery) unless a valid session token is found for the request.
30
+
31
+ #### EmbeddedApp
32
+ If your ShopifyApp configuration has the `embedded_app` config set to true, [P3P header](https://www.w3.org/P3P/) and [content security policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) are handled for you.
33
+
34
+ #### EnsureBilling
35
+ If billing is enabled for the app, the active payment for the session is queried and enforced if needed. If billing is required the user will be redirected to a page requesting payment.
36
+
37
+ ## EnsureAuthenticatedLinks
38
+ Designed to be more of a lightweight session concern specifically for XHR requests. Where `Authenticated` does far more than just session management, this concern will redirect to the splash page of the app if no active session was found.
39
+
40
+ ## RequireKnownShop
41
+ Designed to handle unauthenticated requests for *embedded apps*. If you are non-embedded app, we recommend using `Authenticated` concern instead of this one.
42
+
43
+ Rather than using the JWT to determine the requested shop of the request, the `shop` name param is taken from the query string that Shopify Admin provides.
44
+
45
+ If the shop session cannot be found for the provided `shop` in the query string, the request will be redirected to login or the `embedded_redirect_url`.
46
+
47
+ ## ShopAccessScopesVerification
48
+ If scopes for the session don't match configuration of scopes defined in `config/initializers/shopify_app.rb` the user will be redirected to login or the `embedded_redirect_url`
@@ -0,0 +1,21 @@
1
+ # Logging
2
+
3
+ ## Log Levels
4
+
5
+ There are four log levels with `error` being the most severe.
6
+
7
+ 1. Debug
8
+ 2. Info
9
+ 3. Warn
10
+ 4. Error
11
+
12
+ ## Configuration
13
+
14
+ The logging is controlled by the `log_level` configuration setting.
15
+ The default log level is `:info`.
16
+ You can disable all logs by setting this to `:off`.
17
+
18
+ ## Upgrading
19
+
20
+ For a newly-generated app, the `shopify_app` initializer will contain the `log_level` setting.
21
+ If you are upgrading from a previous version of the `shopify_app` gem then you will need to add this manually, otherwise it will default to `:info`.
@@ -3,6 +3,7 @@
3
3
  #### Table of contents
4
4
 
5
5
  [Manage webhooks using `ShopifyApp::WebhooksManager`](#manage-webhooks-using-shopifyappwebhooksmanager)
6
+ [Mandatory GDPR Webhooks](#mandatory-gdpr-webhooks)
6
7
 
7
8
  ## Manage webhooks using `ShopifyApp::WebhooksManager`
8
9
 
@@ -70,3 +71,15 @@ rails g shopify_app:add_webhook --topic carts/update --path webhooks/carts_updat
70
71
  ```
71
72
 
72
73
  Where `--topic` is the topic and `--path` is the path the webhook should be sent to.
74
+
75
+ ## Mandatory GDPR Webhooks
76
+
77
+ We have three mandatory GDPR webhooks
78
+
79
+ 1. `customers/data_request`
80
+ 2. `customer/redact`
81
+ 3. `shop/redact`
82
+
83
+ The `generate shopify_app` command generated three job templates corresponding to all three of these webhooks.
84
+ To pass our approval process you will need to set these webhooks in your partner dashboard.
85
+ You can read more about that [here](https://shopify.dev/apps/webhooks/configuration/mandatory-webhooks).
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module ShopifyApp
6
+ module Generators
7
+ class AddAppUninstalledJobGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("../templates", __FILE__)
9
+
10
+ def create_job
11
+ template("app_uninstalled_job.rb", "app/jobs/app_uninstalled_job.rb")
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,22 @@
1
+ class AppUninstalledJob < ActiveJob::Base
2
+ extend ShopifyAPI::Webhooks::Handler
3
+
4
+ class << self
5
+ def handle(topic:, shop:, body:)
6
+ perform_later(topic: topic, shop_domain: shop, webhook: body)
7
+ end
8
+ end
9
+
10
+ def perform(topic:, shop_domain:, webhook:)
11
+ shop = Shop.find_by(shopify_domain: shop_domain)
12
+
13
+ if shop.nil?
14
+ logger.error("#{self.class} failed: cannot find shop with domain '#{shop_domain}'")
15
+
16
+ raise ActiveRecord::RecordNotFound, "Shop Not Found"
17
+ end
18
+
19
+ logger.info("#{self.class} started for shop '#{shop_domain}'")
20
+ shop.destroy
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rails/generators/base"
4
+
5
+ module ShopifyApp
6
+ module Generators
7
+ class AddGdprJobsGenerator < Rails::Generators::Base
8
+ source_root File.expand_path("../templates", __FILE__)
9
+
10
+ def add_customer_data_request_job
11
+ template("customers_data_request_job.rb", "app/jobs/customers_data_request_job.rb")
12
+ end
13
+
14
+ def add_shop_redact_job
15
+ template("shop_redact_job.rb", "app/jobs/shop_redact_job.rb")
16
+ end
17
+
18
+ def add_customer_redact_job
19
+ template("customers_redact_job.rb", "app/jobs/customers_redact_job.rb")
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,22 @@
1
+ class CustomersDataRequestJob < ActiveJob::Base
2
+ extend ShopifyAPI::Webhooks::Handler
3
+
4
+ class << self
5
+ def handle(topic:, shop:, body:)
6
+ perform_later(topic: topic, shop_domain: shop, webhook: body)
7
+ end
8
+ end
9
+
10
+ def perform(topic:, shop_domain:, webhook:)
11
+ shop = Shop.find_by(shopify_domain: shop_domain)
12
+
13
+ if shop.nil?
14
+ logger.error("#{self.class} failed: cannot find shop with domain '#{shop_domain}'")
15
+
16
+ raise ActiveRecord::RecordNotFound, "Shop Not Found"
17
+ end
18
+
19
+ shop.with_shopify_session do
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ class CustomersRedactJob < ActiveJob::Base
2
+ extend ShopifyAPI::Webhooks::Handler
3
+
4
+ class << self
5
+ def handle(topic:, shop:, body:)
6
+ perform_later(topic: topic, shop_domain: shop, webhook: body)
7
+ end
8
+ end
9
+
10
+ def perform(topic:, shop_domain:, webhook:)
11
+ shop = Shop.find_by(shopify_domain: shop_domain)
12
+
13
+ if shop.nil?
14
+ logger.error("#{self.class} failed: cannot find shop with domain '#{shop_domain}'")
15
+
16
+ raise ActiveRecord::RecordNotFound, "Shop Not Found"
17
+ end
18
+
19
+ shop.with_shopify_session do
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,22 @@
1
+ class ShopRedactJob < ActiveJob::Base
2
+ extend ShopifyAPI::Webhooks::Handler
3
+
4
+ class << self
5
+ def handle(topic:, shop:, body:)
6
+ perform_later(topic: topic, shop_domain: shop, webhook: body)
7
+ end
8
+ end
9
+
10
+ def perform(topic:, shop_domain:, webhook:)
11
+ shop = Shop.find_by(shopify_domain: shop_domain)
12
+
13
+ if shop.nil?
14
+ logger.error("#{self.class} failed: cannot find shop with domain '#{shop_domain}'")
15
+
16
+ raise ActiveRecord::RecordNotFound, "Shop Not Found"
17
+ end
18
+
19
+ shop.with_shopify_session do
20
+ end
21
+ end
22
+ end