shopify_app 13.2.0 → 20.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (167) hide show
  1. checksums.yaml +4 -4
  2. data/.github/CODEOWNERS +1 -0
  3. data/.github/ISSUE_TEMPLATE/bug-report.md +63 -0
  4. data/.github/ISSUE_TEMPLATE/config.yml +1 -0
  5. data/.github/ISSUE_TEMPLATE/feature-request.md +33 -0
  6. data/.github/PULL_REQUEST_TEMPLATE.md +22 -0
  7. data/.github/workflows/build.yml +40 -0
  8. data/.github/workflows/cla.yml +22 -0
  9. data/.github/workflows/close-waiting-for-response-issues.yml +20 -0
  10. data/.github/workflows/release.yml +24 -0
  11. data/.github/workflows/remove-labels-on-activity.yml +16 -0
  12. data/.github/workflows/rubocop.yml +22 -0
  13. data/.github/workflows/stale.yml +31 -0
  14. data/.gitignore +1 -2
  15. data/.nvmrc +1 -1
  16. data/.rubocop.yml +2 -0
  17. data/.ruby-version +1 -1
  18. data/CHANGELOG.md +221 -0
  19. data/CONTRIBUTING.md +81 -0
  20. data/Gemfile +5 -2
  21. data/Gemfile.lock +248 -0
  22. data/README.md +74 -563
  23. data/Rakefile +4 -3
  24. data/SECURITY.md +59 -0
  25. data/app/assets/images/storage_access.svg +1 -2
  26. data/app/assets/javascripts/shopify_app/app_bridge_3.1.1.js +10 -0
  27. data/app/assets/javascripts/shopify_app/app_bridge_redirect.js +22 -0
  28. data/app/assets/javascripts/shopify_app/app_bridge_utils_3.1.1.js +1 -0
  29. data/app/assets/javascripts/shopify_app/post_redirect.js +9 -0
  30. data/app/assets/javascripts/shopify_app/redirect.js +10 -14
  31. data/app/assets/javascripts/shopify_app/storage_access.js +5 -10
  32. data/app/assets/javascripts/shopify_app/top_level_interaction.js +1 -1
  33. data/app/controllers/concerns/shopify_app/authenticated.rb +4 -0
  34. data/app/controllers/concerns/shopify_app/ensure_authenticated_links.rb +39 -0
  35. data/app/controllers/concerns/shopify_app/require_known_shop.rb +48 -0
  36. data/app/controllers/concerns/shopify_app/shop_access_scopes_verification.rb +40 -0
  37. data/app/controllers/shopify_app/authenticated_controller.rb +1 -0
  38. data/app/controllers/shopify_app/callback_controller.rb +56 -77
  39. data/app/controllers/shopify_app/extension_verification_controller.rb +2 -7
  40. data/app/controllers/shopify_app/sessions_controller.rb +33 -117
  41. data/app/controllers/shopify_app/webhooks_controller.rb +5 -26
  42. data/app/views/shopify_app/partials/_button_styles.html.erb +41 -36
  43. data/app/views/shopify_app/partials/_card_styles.html.erb +3 -3
  44. data/app/views/shopify_app/partials/_empty_state_styles.html.erb +28 -59
  45. data/app/views/shopify_app/partials/_form_styles.html.erb +56 -0
  46. data/app/views/shopify_app/partials/_layout_styles.html.erb +16 -1
  47. data/app/views/shopify_app/partials/_typography_styles.html.erb +6 -6
  48. data/app/views/shopify_app/sessions/enable_cookies.html.erb +2 -7
  49. data/app/views/shopify_app/sessions/new.html.erb +38 -110
  50. data/app/views/shopify_app/sessions/request_storage_access.html.erb +12 -12
  51. data/app/views/shopify_app/sessions/top_level_interaction.html.erb +21 -22
  52. data/app/views/shopify_app/shared/post_redirect_to_auth_shopify.html.erb +13 -0
  53. data/app/views/shopify_app/shared/redirect.html.erb +2 -2
  54. data/config/locales/de.yml +11 -11
  55. data/config/locales/ja.yml +4 -4
  56. data/config/locales/nl.yml +2 -2
  57. data/config/locales/th.yml +4 -4
  58. data/config/locales/vi.yml +22 -0
  59. data/config/locales/zh-CN.yml +2 -2
  60. data/config/routes.rb +20 -12
  61. data/docs/Quickstart.md +19 -83
  62. data/docs/Releasing.md +18 -15
  63. data/docs/Troubleshooting.md +140 -5
  64. data/docs/Upgrading.md +247 -0
  65. data/docs/shopify_app/authentication.md +128 -0
  66. data/docs/shopify_app/content-security-policy.md +10 -0
  67. data/docs/shopify_app/engine.md +82 -0
  68. data/docs/shopify_app/generators.md +127 -0
  69. data/docs/shopify_app/handling-access-scopes-changes.md +24 -0
  70. data/docs/shopify_app/script-tags.md +28 -0
  71. data/docs/shopify_app/session-repository.md +88 -0
  72. data/docs/shopify_app/testing.md +38 -0
  73. data/docs/shopify_app/webhooks.md +72 -0
  74. data/karma.conf.js +1 -1
  75. data/lib/generators/shopify_app/add_after_authenticate_job/add_after_authenticate_job_generator.rb +10 -9
  76. data/lib/generators/shopify_app/add_after_authenticate_job/templates/after_authenticate_job.rb +1 -0
  77. data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +4 -3
  78. data/lib/generators/shopify_app/add_webhook/add_webhook_generator.rb +15 -14
  79. data/lib/generators/shopify_app/add_webhook/templates/webhook_job.rb.tt +9 -1
  80. data/lib/generators/shopify_app/app_proxy_controller/app_proxy_controller_generator.rb +7 -6
  81. data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_controller.rb +2 -1
  82. data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_route.rb +1 -1
  83. data/lib/generators/shopify_app/authenticated_controller/authenticated_controller_generator.rb +4 -4
  84. data/lib/generators/shopify_app/controllers/controllers_generator.rb +5 -4
  85. data/lib/generators/shopify_app/home_controller/home_controller_generator.rb +27 -4
  86. data/lib/generators/shopify_app/home_controller/templates/home_controller.rb +12 -2
  87. data/lib/generators/shopify_app/home_controller/templates/index.html.erb +74 -16
  88. data/lib/generators/shopify_app/home_controller/templates/unauthenticated_home_controller.rb +16 -0
  89. data/lib/generators/shopify_app/install/install_generator.rb +52 -40
  90. data/lib/generators/shopify_app/install/templates/embedded_app.html.erb +5 -2
  91. data/lib/generators/shopify_app/install/templates/flash_messages.js +0 -2
  92. data/lib/generators/shopify_app/install/templates/session_store.rb +2 -1
  93. data/lib/generators/shopify_app/install/templates/shopify_app.js +1 -1
  94. data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +43 -5
  95. data/lib/generators/shopify_app/install/templates/shopify_app_importmap.js +13 -0
  96. data/lib/generators/shopify_app/products_controller/products_controller_generator.rb +19 -0
  97. data/lib/generators/shopify_app/products_controller/templates/products_controller.rb +8 -0
  98. data/lib/generators/shopify_app/rotate_shopify_token_job/rotate_shopify_token_job_generator.rb +4 -4
  99. data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token.rake +1 -0
  100. data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token_job.rb +1 -1
  101. data/lib/generators/shopify_app/routes/routes_generator.rb +6 -5
  102. data/lib/generators/shopify_app/routes/templates/routes.rb +5 -5
  103. data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +35 -7
  104. data/lib/generators/shopify_app/shop_model/templates/db/migrate/add_shop_access_scopes_column.erb +5 -0
  105. data/lib/generators/shopify_app/shop_model/templates/shop.rb +2 -1
  106. data/lib/generators/shopify_app/shopify_app_generator.rb +4 -3
  107. data/lib/generators/shopify_app/user_model/templates/db/migrate/add_user_access_scopes_column.erb +5 -0
  108. data/lib/generators/shopify_app/user_model/templates/user.rb +2 -1
  109. data/lib/generators/shopify_app/user_model/user_model_generator.rb +35 -7
  110. data/lib/generators/shopify_app/views/views_generator.rb +5 -4
  111. data/lib/shopify_app/access_scopes/noop_strategy.rb +13 -0
  112. data/lib/shopify_app/access_scopes/shop_strategy.rb +24 -0
  113. data/lib/shopify_app/access_scopes/user_strategy.rb +41 -0
  114. data/lib/shopify_app/configuration.rb +58 -11
  115. data/lib/shopify_app/controller_concerns/app_proxy_verification.rb +4 -4
  116. data/lib/shopify_app/controller_concerns/csrf_protection.rb +16 -0
  117. data/lib/shopify_app/controller_concerns/embedded_app.rb +6 -3
  118. data/lib/shopify_app/controller_concerns/ensure_billing.rb +243 -0
  119. data/lib/shopify_app/controller_concerns/frame_ancestors.rb +16 -0
  120. data/lib/shopify_app/controller_concerns/itp.rb +3 -3
  121. data/lib/shopify_app/controller_concerns/localization.rb +1 -0
  122. data/lib/shopify_app/controller_concerns/login_protection.rb +105 -90
  123. data/lib/shopify_app/controller_concerns/payload_verification.rb +25 -0
  124. data/lib/shopify_app/controller_concerns/redirect_for_embedded.rb +36 -0
  125. data/lib/shopify_app/controller_concerns/sanitized_params.rb +36 -0
  126. data/lib/shopify_app/controller_concerns/webhook_verification.rb +3 -18
  127. data/lib/shopify_app/engine.rb +26 -11
  128. data/lib/shopify_app/errors.rb +34 -0
  129. data/lib/shopify_app/jobs/scripttags_manager_job.rb +2 -2
  130. data/lib/shopify_app/jobs/webhooks_manager_job.rb +4 -5
  131. data/lib/shopify_app/managers/scripttags_manager.rb +12 -6
  132. data/lib/shopify_app/managers/webhooks_manager.rb +62 -42
  133. data/lib/shopify_app/middleware/jwt_middleware.rb +6 -3
  134. data/lib/shopify_app/session/in_memory_session_store.rb +2 -3
  135. data/lib/shopify_app/session/in_memory_shop_session_store.rb +10 -7
  136. data/lib/shopify_app/session/in_memory_user_session_store.rb +10 -7
  137. data/lib/shopify_app/session/jwt.rb +19 -16
  138. data/lib/shopify_app/session/null_user_session_store.rb +2 -1
  139. data/lib/shopify_app/session/session_repository.rb +40 -2
  140. data/lib/shopify_app/session/session_storage.rb +4 -6
  141. data/lib/shopify_app/session/shop_session_storage.rb +6 -6
  142. data/lib/shopify_app/session/shop_session_storage_with_scopes.rb +57 -0
  143. data/lib/shopify_app/session/user_session_storage.rb +20 -7
  144. data/lib/shopify_app/session/user_session_storage_with_scopes.rb +71 -0
  145. data/lib/shopify_app/test_helpers/all.rb +2 -1
  146. data/lib/shopify_app/test_helpers/webhook_verification_helper.rb +4 -3
  147. data/lib/shopify_app/utils.rb +14 -7
  148. data/lib/shopify_app/version.rb +2 -1
  149. data/lib/shopify_app.rb +52 -29
  150. data/package.json +7 -8
  151. data/service.yml +1 -5
  152. data/shopify_app.gemspec +22 -20
  153. data/translation.yml +1 -1
  154. data/yarn.lock +2173 -2206
  155. metadata +110 -56
  156. data/.github/ISSUE_TEMPLATE.md +0 -14
  157. data/.github/probots.yml +0 -2
  158. data/.travis.yml +0 -28
  159. data/config/locales/hi.yml +0 -23
  160. data/config/locales/ms.yml +0 -22
  161. data/docs/install-on-dev-shop.png +0 -0
  162. data/docs/test-your-app.png +0 -0
  163. data/lib/generators/shopify_app/install/templates/omniauth.rb +0 -3
  164. data/lib/generators/shopify_app/install/templates/shopify_provider.rb +0 -20
  165. data/lib/generators/shopify_app/install/templates/user_agent.rb +0 -6
  166. data/lib/shopify_app/middleware/same_site_cookie_middleware.rb +0 -34
  167. data/package-lock.json +0 -7245
@@ -1,62 +1,82 @@
1
1
  # frozen_string_literal: true
2
+
3
+ require "uri"
4
+
2
5
  module ShopifyApp
3
6
  class WebhooksManager
4
- class CreationFailed < StandardError; end
5
-
6
- def self.queue(shop_domain, shop_token, webhooks)
7
- ShopifyApp::WebhooksManagerJob.perform_later(
8
- shop_domain: shop_domain,
9
- shop_token: shop_token,
10
- webhooks: webhooks
11
- )
12
- end
7
+ class << self
8
+ def queue(shop_domain, shop_token)
9
+ ShopifyApp::WebhooksManagerJob.perform_later(
10
+ shop_domain: shop_domain,
11
+ shop_token: shop_token
12
+ )
13
+ end
13
14
 
14
- attr_reader :required_webhooks
15
+ def create_webhooks(session:)
16
+ return unless ShopifyApp.configuration.has_webhooks?
15
17
 
16
- def initialize(webhooks)
17
- @required_webhooks = webhooks
18
- end
18
+ ShopifyAPI::Webhooks::Registry.register_all(session: session)
19
+ end
19
20
 
20
- def recreate_webhooks!
21
- destroy_webhooks
22
- create_webhooks
23
- end
21
+ def recreate_webhooks!(session:)
22
+ destroy_webhooks
23
+ return unless ShopifyApp.configuration.has_webhooks?
24
24
 
25
- def create_webhooks
26
- return unless required_webhooks.present?
25
+ add_registrations
26
+ ShopifyAPI::Webhooks::Registry.register_all(session: session)
27
+ end
27
28
 
28
- required_webhooks.each do |webhook|
29
- create_webhook(webhook) unless webhook_exists?(webhook[:topic])
29
+ def destroy_webhooks
30
+ return unless ShopifyApp.configuration.has_webhooks?
31
+
32
+ ShopifyApp.configuration.webhooks.each do |attributes|
33
+ ShopifyAPI::Webhooks::Registry.unregister(topic: attributes[:topic])
34
+ end
30
35
  end
31
- end
32
36
 
33
- def destroy_webhooks
34
- ShopifyAPI::Webhook.all.to_a.each do |webhook|
35
- ShopifyAPI::Webhook.delete(webhook.id) if required_webhook?(webhook)
37
+ def add_registrations
38
+ return unless ShopifyApp.configuration.has_webhooks?
39
+
40
+ ShopifyApp.configuration.webhooks.each do |attributes|
41
+ webhook_path = path(attributes)
42
+
43
+ ShopifyAPI::Webhooks::Registry.add_registration(
44
+ topic: attributes[:topic],
45
+ delivery_method: attributes[:delivery_method] || :http,
46
+ path: webhook_path,
47
+ handler: webhook_job_klass(webhook_path),
48
+ fields: attributes[:fields]
49
+ )
50
+ end
36
51
  end
37
52
 
38
- @current_webhooks = nil
39
- end
53
+ private
40
54
 
41
- private
55
+ def path(webhook_attributes)
56
+ path = webhook_attributes[:path]
57
+ address = webhook_attributes[:address]
58
+ uri = URI(address) if address
42
59
 
43
- def required_webhook?(webhook)
44
- required_webhooks.map { |w| w[:address] }.include?(webhook.address)
45
- end
60
+ if path.present?
61
+ path
62
+ elsif uri&.path&.present?
63
+ uri.path
64
+ else
65
+ raise ::ShopifyApp::MissingWebhookJobError,
66
+ "The :path attribute is required for webhook registration."
67
+ end
68
+ end
46
69
 
47
- def create_webhook(attributes)
48
- attributes.reverse_merge!(format: 'json')
49
- webhook = ShopifyAPI::Webhook.create(attributes)
50
- raise CreationFailed, webhook.errors.full_messages.to_sentence unless webhook.persisted?
51
- webhook
52
- end
70
+ def webhook_job_klass(path)
71
+ webhook_job_klass_name(path).safe_constantize || raise(::ShopifyApp::MissingWebhookJobError)
72
+ end
53
73
 
54
- def webhook_exists?(topic)
55
- current_webhooks[topic]
56
- end
74
+ def webhook_job_klass_name(path)
75
+ job_file_name = Pathname(path.to_s).basename
57
76
 
58
- def current_webhooks
59
- @current_webhooks ||= ShopifyAPI::Webhook.all.to_a.index_by(&:topic)
77
+ [ShopifyApp.configuration.webhook_jobs_namespace,
78
+ "#{job_file_name}_job",].compact.join("/").classify
79
+ end
60
80
  end
61
81
  end
62
82
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module ShopifyApp
2
4
  class JWTMiddleware
3
5
  TOKEN_REGEX = /^Bearer\s+(.*?)$/
@@ -23,7 +25,7 @@ module ShopifyApp
23
25
  end
24
26
 
25
27
  def authorization_header(env)
26
- env['HTTP_AUTHORIZATION']
28
+ env["HTTP_AUTHORIZATION"]
27
29
  end
28
30
 
29
31
  def extract_token(env)
@@ -34,8 +36,9 @@ module ShopifyApp
34
36
  def set_env_variables(token, env)
35
37
  jwt = ShopifyApp::JWT.new(token)
36
38
 
37
- env['jwt.shopify_domain'] = jwt.shopify_domain
38
- env['jwt.shopify_user_id'] = jwt.shopify_user_id
39
+ env["jwt.shopify_domain"] = jwt.shopify_domain
40
+ env["jwt.shopify_user_id"] = jwt.shopify_user_id
41
+ env["jwt.expire_at"] = jwt.expire_at
39
42
  end
40
43
  end
41
44
  end
@@ -1,10 +1,9 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ShopifyApp
3
4
  # rubocop:disable Style/ClassVars
4
5
  # Class var repo is needed here in order to share data between the 2 child classes.
5
6
  class InMemorySessionStore
6
- class EnvironmentError < StandardError; end
7
-
8
7
  def self.retrieve(id)
9
8
  repo[id]
10
9
  end
@@ -21,7 +20,7 @@ module ShopifyApp
21
20
 
22
21
  def self.repo
23
22
  if Rails.env.production?
24
- raise EnvironmentError, "Cannot use InMemorySessionStore in a Production environment. \
23
+ raise ::ShopifyApp::EnvironmentError, "Cannot use InMemorySessionStore in a Production environment. \
25
24
  Please initialize ShopifyApp with a model that can store and retrieve sessions"
26
25
  end
27
26
  @@repo ||= {}
@@ -1,14 +1,17 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ShopifyApp
3
4
  class InMemoryShopSessionStore < InMemorySessionStore
4
- def self.store(session, *args)
5
- id = super
6
- repo[session.domain] = session
7
- id
8
- end
5
+ class << self
6
+ def store(session, *args)
7
+ id = super
8
+ repo[session.shop] = session
9
+ id
10
+ end
9
11
 
10
- def self.retrieve_by_shopify_domain(shopify_domain)
11
- repo[shopify_domain]
12
+ def retrieve_by_shopify_domain(shopify_domain)
13
+ repo[shopify_domain]
14
+ end
12
15
  end
13
16
  end
14
17
  end
@@ -1,14 +1,17 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ShopifyApp
3
4
  class InMemoryUserSessionStore < InMemorySessionStore
4
- def self.store(session, user)
5
- id = super
6
- repo[user.shopify_user_id] = session
7
- id
8
- end
5
+ class << self
6
+ def store(session, user)
7
+ id = super
8
+ repo[user.shopify_user_id] = session
9
+ id
10
+ end
9
11
 
10
- def self.retrieve_by_shopify_user_id(user_id)
11
- repo[user_id]
12
+ def retrieve_by_shopify_user_id(user_id)
13
+ repo[user_id]
14
+ end
12
15
  end
13
16
  end
14
17
  end
@@ -1,18 +1,15 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ShopifyApp
3
4
  class JWT
4
- class InvalidDestinationError < StandardError; end
5
- class MismatchedHostsError < StandardError; end
6
- class InvalidAudienceError < StandardError; end
7
-
8
5
  WARN_EXCEPTIONS = [
9
6
  ::JWT::DecodeError,
10
7
  ::JWT::ExpiredSignature,
11
8
  ::JWT::ImmatureSignature,
12
9
  ::JWT::VerificationError,
13
- InvalidAudienceError,
14
- InvalidDestinationError,
15
- MismatchedHostsError,
10
+ ::ShopifyApp::InvalidAudienceError,
11
+ ::ShopifyApp::InvalidDestinationError,
12
+ ::ShopifyApp::MismatchedHostsError,
16
13
  ]
17
14
 
18
15
  def initialize(token)
@@ -21,11 +18,15 @@ module ShopifyApp
21
18
  end
22
19
 
23
20
  def shopify_domain
24
- @payload && ShopifyApp::Utils.sanitize_shop_domain(@payload['dest'])
21
+ @payload && ShopifyApp::Utils.sanitize_shop_domain(@payload["dest"])
25
22
  end
26
23
 
27
24
  def shopify_user_id
28
- @payload && @payload['sub']
25
+ @payload["sub"].to_i if @payload && @payload["sub"]
26
+ end
27
+
28
+ def expire_at
29
+ @payload["exp"].to_i if @payload && @payload["exp"]
29
30
  end
30
31
 
31
32
  private
@@ -39,21 +40,23 @@ module ShopifyApp
39
40
  end
40
41
 
41
42
  def parse_token_data(secret, old_secret)
42
- ::JWT.decode(@token, secret, true, { algorithm: 'HS256' })
43
+ ::JWT.decode(@token, secret, true, { algorithm: "HS256" })
43
44
  rescue ::JWT::VerificationError
44
45
  raise unless old_secret
45
46
 
46
- ::JWT.decode(@token, old_secret, true, { algorithm: 'HS256' })
47
+ ::JWT.decode(@token, old_secret, true, { algorithm: "HS256" })
47
48
  end
48
49
 
49
50
  def validate_payload(payload)
50
- dest_host = ShopifyApp::Utils.sanitize_shop_domain(payload['dest'])
51
- iss_host = ShopifyApp::Utils.sanitize_shop_domain(payload['iss'])
51
+ dest_host = ShopifyApp::Utils.sanitize_shop_domain(payload["dest"])
52
+ iss_host = ShopifyApp::Utils.sanitize_shop_domain(payload["iss"])
52
53
  api_key = ShopifyApp.configuration.api_key
53
54
 
54
- raise InvalidAudienceError, "'aud' claim does not match api_key" unless payload['aud'] == api_key
55
- raise InvalidDestinationError, "'dest' claim host not a valid shopify host" unless dest_host
56
- raise MismatchedHostsError, "'dest' claim host does not match 'iss' claim host" unless dest_host == iss_host
55
+ raise ::ShopifyApp::InvalidAudienceError,
56
+ "'aud' claim does not match api_key" unless payload["aud"] == api_key
57
+ raise ::ShopifyApp::InvalidDestinationError, "'dest' claim host not a valid shopify host" unless dest_host
58
+ raise ::ShopifyApp::MismatchedHostsError,
59
+ "'dest' claim host does not match 'iss' claim host" unless dest_host == iss_host
57
60
 
58
61
  payload
59
62
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ShopifyApp
3
4
  class NullUserSessionStore
4
5
  class << self
@@ -7,7 +8,7 @@ module ShopifyApp
7
8
  end
8
9
 
9
10
  def store(_, _)
10
- raise SessionRepository::ConfigurationError, 'user_storage is not configured'
11
+ raise ::ShopifyApp::ConfigurationError, "user_storage is not configured"
11
12
  end
12
13
 
13
14
  def retrieve_by_shopify_user_id(_)
@@ -1,7 +1,8 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ShopifyApp
3
4
  class SessionRepository
4
- class ConfigurationError < StandardError; end
5
+ extend ShopifyAPI::Auth::SessionStorage
5
6
 
6
7
  class << self
7
8
  attr_writer :shop_storage
@@ -33,22 +34,59 @@ module ShopifyApp
33
34
  end
34
35
 
35
36
  def shop_storage
36
- load_shop_storage || raise(ConfigurationError, "ShopifySessionRepository.shop_storage is not configured!")
37
+ load_shop_storage || raise(::ShopifyApp::ConfigurationError,
38
+ "ShopifySessionRepository.shop_storage is not configured!")
37
39
  end
38
40
 
39
41
  def user_storage
40
42
  load_user_storage
41
43
  end
42
44
 
45
+ # ShopifyAPI::Auth::SessionStorage override
46
+ def store_session(session)
47
+ if session.online?
48
+ user_storage.store(session, session.associated_user)
49
+ else
50
+ shop_storage.store(session)
51
+ end
52
+ end
53
+
54
+ # ShopifyAPI::Auth::SessionStorage override
55
+ def load_session(id)
56
+ match = id.match(/^offline_(.*)/)
57
+ if match
58
+ retrieve_shop_session_by_shopify_domain(match[1])
59
+ else
60
+ retrieve_user_session_by_shopify_user_id(id.split("_").last)
61
+ end
62
+ end
63
+
64
+ # ShopifyAPI::Auth::SessionStorage override
65
+ def delete_session(id)
66
+ match = id.match(/^offline_(.*)/)
67
+
68
+ record = if match
69
+ Shop.find_by(shopify_domain: match[1])
70
+ else
71
+ User.find_by(shopify_user_id: id.split("_").last)
72
+ end
73
+
74
+ record.destroy
75
+
76
+ true
77
+ end
78
+
43
79
  private
44
80
 
45
81
  def load_shop_storage
46
82
  return unless @shop_storage
83
+
47
84
  @shop_storage.respond_to?(:safe_constantize) ? @shop_storage.safe_constantize : @shop_storage
48
85
  end
49
86
 
50
87
  def load_user_storage
51
88
  return NullUserSessionStore unless @user_storage
89
+
52
90
  @user_storage.respond_to?(:safe_constantize) ? @user_storage.safe_constantize : @user_storage
53
91
  end
54
92
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ShopifyApp
3
4
  module SessionStorage
4
5
  extend ActiveSupport::Concern
@@ -9,12 +10,9 @@ module ShopifyApp
9
10
  end
10
11
 
11
12
  def with_shopify_session(&block)
12
- ShopifyAPI::Session.temp(
13
- domain: shopify_domain,
14
- token: shopify_token,
15
- api_version: api_version,
16
- &block
17
- )
13
+ ShopifyAPI::Auth::Session.temp(shop: shopify_domain, access_token: shopify_token) do
14
+ yield block
15
+ end
18
16
  end
19
17
  end
20
18
  end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ShopifyApp
3
4
  module ShopSessionStorage
4
5
  extend ActiveSupport::Concern
@@ -10,8 +11,8 @@ module ShopifyApp
10
11
 
11
12
  class_methods do
12
13
  def store(auth_session, *_args)
13
- shop = find_or_initialize_by(shopify_domain: auth_session.domain)
14
- shop.shopify_token = auth_session.token
14
+ shop = find_or_initialize_by(shopify_domain: auth_session.shop)
15
+ shop.shopify_token = auth_session.access_token
15
16
  shop.save!
16
17
  shop.id
17
18
  end
@@ -31,10 +32,9 @@ module ShopifyApp
31
32
  def construct_session(shop)
32
33
  return unless shop
33
34
 
34
- ShopifyAPI::Session.new(
35
- domain: shop.shopify_domain,
36
- token: shop.shopify_token,
37
- api_version: shop.api_version,
35
+ ShopifyAPI::Auth::Session.new(
36
+ shop: shop.shopify_domain,
37
+ access_token: shop.shopify_token
38
38
  )
39
39
  end
40
40
  end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module ShopSessionStorageWithScopes
5
+ extend ActiveSupport::Concern
6
+ include ::ShopifyApp::SessionStorage
7
+
8
+ included do
9
+ validates :shopify_domain, presence: true, uniqueness: { case_sensitive: false }
10
+ end
11
+
12
+ class_methods do
13
+ def store(auth_session, *_args)
14
+ shop = find_or_initialize_by(shopify_domain: auth_session.shop)
15
+ shop.shopify_token = auth_session.access_token
16
+ shop.access_scopes = auth_session.scope.to_s
17
+
18
+ shop.save!
19
+ shop.id
20
+ end
21
+
22
+ def retrieve(id)
23
+ shop = find_by(id: id)
24
+ construct_session(shop)
25
+ end
26
+
27
+ def retrieve_by_shopify_domain(domain)
28
+ shop = find_by(shopify_domain: domain)
29
+ construct_session(shop)
30
+ end
31
+
32
+ private
33
+
34
+ def construct_session(shop)
35
+ return unless shop
36
+
37
+ ShopifyAPI::Auth::Session.new(
38
+ shop: shop.shopify_domain,
39
+ access_token: shop.shopify_token,
40
+ scope: shop.access_scopes
41
+ )
42
+ end
43
+ end
44
+
45
+ def access_scopes=(scopes)
46
+ super(scopes)
47
+ rescue NotImplementedError, NoMethodError
48
+ raise NotImplementedError, "#access_scopes= must be defined to handle storing access scopes: #{scopes}"
49
+ end
50
+
51
+ def access_scopes
52
+ super
53
+ rescue NotImplementedError, NoMethodError
54
+ raise NotImplementedError, "#access_scopes= must be defined to hook into stored access scopes"
55
+ end
56
+ end
57
+ end
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ShopifyApp
3
4
  module UserSessionStorage
4
5
  extend ActiveSupport::Concern
@@ -10,9 +11,9 @@ module ShopifyApp
10
11
 
11
12
  class_methods do
12
13
  def store(auth_session, user)
13
- user = find_or_initialize_by(shopify_user_id: user[:id])
14
- user.shopify_token = auth_session.token
15
- user.shopify_domain = auth_session.domain
14
+ user = find_or_initialize_by(shopify_user_id: user.id)
15
+ user.shopify_token = auth_session.access_token
16
+ user.shopify_domain = auth_session.shop
16
17
  user.save!
17
18
  user.id
18
19
  end
@@ -31,10 +32,22 @@ module ShopifyApp
31
32
 
32
33
  def construct_session(user)
33
34
  return unless user
34
- ShopifyAPI::Session.new(
35
- domain: user.shopify_domain,
36
- token: user.shopify_token,
37
- api_version: user.api_version,
35
+
36
+ associated_user = ShopifyAPI::Auth::AssociatedUser.new(
37
+ id: user.shopify_user_id,
38
+ first_name: "",
39
+ last_name: "",
40
+ email: "",
41
+ email_verified: false,
42
+ account_owner: false,
43
+ locale: "",
44
+ collaborator: false
45
+ )
46
+
47
+ ShopifyAPI::Auth::Session.new(
48
+ shop: user.shopify_domain,
49
+ access_token: user.shopify_token,
50
+ associated_user: associated_user
38
51
  )
39
52
  end
40
53
  end
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module UserSessionStorageWithScopes
5
+ extend ActiveSupport::Concern
6
+ include ::ShopifyApp::SessionStorage
7
+
8
+ included do
9
+ validates :shopify_domain, presence: true
10
+ end
11
+
12
+ class_methods do
13
+ def store(auth_session, user)
14
+ user = find_or_initialize_by(shopify_user_id: user.id)
15
+ user.shopify_token = auth_session.access_token
16
+ user.shopify_domain = auth_session.shop
17
+ user.access_scopes = auth_session.scope.to_s
18
+
19
+ user.save!
20
+ user.id
21
+ end
22
+
23
+ def retrieve(id)
24
+ user = find_by(id: id)
25
+ construct_session(user)
26
+ end
27
+
28
+ def retrieve_by_shopify_user_id(user_id)
29
+ user = find_by(shopify_user_id: user_id)
30
+ construct_session(user)
31
+ end
32
+
33
+ private
34
+
35
+ def construct_session(user)
36
+ return unless user
37
+
38
+ associated_user = ShopifyAPI::Auth::AssociatedUser.new(
39
+ id: user.shopify_user_id,
40
+ first_name: "",
41
+ last_name: "",
42
+ email: "",
43
+ email_verified: false,
44
+ account_owner: false,
45
+ locale: "",
46
+ collaborator: false
47
+ )
48
+
49
+ ShopifyAPI::Auth::Session.new(
50
+ shop: user.shopify_domain,
51
+ access_token: user.shopify_token,
52
+ scope: user.access_scopes,
53
+ associated_user_scope: user.access_scopes,
54
+ associated_user: associated_user
55
+ )
56
+ end
57
+ end
58
+
59
+ def access_scopes=(scopes)
60
+ super(scopes)
61
+ rescue NotImplementedError, NoMethodError
62
+ raise NotImplementedError, "#access_scopes= must be defined to handle storing access scopes: #{scopes}"
63
+ end
64
+
65
+ def access_scopes
66
+ super
67
+ rescue NotImplementedError, NoMethodError
68
+ raise NotImplementedError, "#access_scopes= must be defined to hook into stored access scopes"
69
+ end
70
+ end
71
+ end
@@ -1,2 +1,3 @@
1
1
  # frozen_string_literal: true
2
- require 'shopify_app/test_helpers/webhook_verification_helper'
2
+
3
+ require "shopify_app/test_helpers/webhook_verification_helper"
@@ -1,16 +1,17 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ShopifyApp
3
4
  module TestHelpers
4
5
  module WebhookVerificationHelper
5
6
  def authorized_webhook_verification_headers!(params = {})
6
- digest = OpenSSL::Digest.new('sha256')
7
+ digest = OpenSSL::Digest.new("sha256")
7
8
  secret = ShopifyApp.configuration.secret
8
9
  valid_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, secret, params.to_query)).strip
9
- @request.headers['HTTP_X_SHOPIFY_HMAC_SHA256'] = valid_hmac
10
+ @request.headers["HTTP_X_SHOPIFY_HMAC_SHA256"] = valid_hmac
10
11
  end
11
12
 
12
13
  def unauthorized_webhook_verification_headers!
13
- @request.headers['HTTP_X_SHOPIFY_HMAC_SHA256'] = "invalid_hmac"
14
+ @request.headers["HTTP_X_SHOPIFY_HMAC_SHA256"] = "invalid_hmac"
14
15
  end
15
16
  end
16
17
  end
@@ -1,11 +1,12 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  module ShopifyApp
3
4
  module Utils
4
5
  def self.sanitize_shop_domain(shop_domain)
5
6
  myshopify_domain = ShopifyApp.configuration.myshopify_domain
6
7
  name = shop_domain.to_s.downcase.strip
7
8
  name += ".#{myshopify_domain}" if !name.include?(myshopify_domain.to_s) && !name.include?(".")
8
- name.sub!(%r|https?://|, '')
9
+ name.sub!(%r|https?://|, "")
9
10
 
10
11
  u = URI("http://#{name}")
11
12
  u.host if u.host&.match(/^[a-z0-9][a-z0-9\-]*[a-z0-9]\.#{Regexp.escape(myshopify_domain)}$/)
@@ -13,12 +14,18 @@ module ShopifyApp
13
14
  nil
14
15
  end
15
16
 
16
- def self.fetch_known_api_versions
17
- Rails.logger.info("[ShopifyAPI::ApiVersion] Fetching known Admin API Versions from Shopify...")
18
- ShopifyAPI::ApiVersion.fetch_known_versions
19
- Rails.logger.info("[ShopifyAPI::ApiVersion] Known API Versions: #{ShopifyAPI::ApiVersion.versions.keys}")
20
- rescue ActiveResource::ConnectionError
21
- logger.error("[ShopifyAPI::ApiVersion] Unable to fetch api_versions from Shopify")
17
+ def self.shop_login_url(shop:, host:, return_to:)
18
+ return ShopifyApp.configuration.login_url unless shop
19
+
20
+ url = URI(ShopifyApp.configuration.login_url)
21
+
22
+ url.query = URI.encode_www_form(
23
+ shop: shop,
24
+ host: host,
25
+ return_to: return_to,
26
+ )
27
+
28
+ url.to_s
22
29
  end
23
30
  end
24
31
  end