shopify_app 13.0.0 → 13.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +13 -6
  3. data/CHANGELOG.md +10 -0
  4. data/Gemfile +3 -0
  5. data/README.md +32 -1
  6. data/Rakefile +1 -0
  7. data/app/controllers/shopify_app/authenticated_controller.rb +1 -0
  8. data/app/controllers/shopify_app/callback_controller.rb +1 -1
  9. data/app/controllers/shopify_app/sessions_controller.rb +8 -5
  10. data/app/controllers/shopify_app/webhooks_controller.rb +6 -5
  11. data/config/locales/fi.yml +1 -1
  12. data/config/routes.rb +1 -0
  13. data/lib/generators/shopify_app/add_after_authenticate_job/add_after_authenticate_job_generator.rb +5 -3
  14. data/lib/generators/shopify_app/add_after_authenticate_job/templates/after_authenticate_job.rb +1 -0
  15. data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +2 -1
  16. data/lib/generators/shopify_app/add_marketing_activity_extension/templates/marketing_activities_controller.rb +4 -4
  17. data/lib/generators/shopify_app/add_webhook/add_webhook_generator.rb +5 -4
  18. data/lib/generators/shopify_app/add_webhook/templates/webhook_job.rb +5 -0
  19. data/lib/generators/shopify_app/app_proxy_controller/app_proxy_controller_generator.rb +4 -3
  20. data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_controller.rb +3 -3
  21. data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_route.rb +10 -9
  22. data/lib/generators/shopify_app/controllers/controllers_generator.rb +1 -0
  23. data/lib/generators/shopify_app/home_controller/home_controller_generator.rb +4 -3
  24. data/lib/generators/shopify_app/install/install_generator.rb +9 -8
  25. data/lib/generators/shopify_app/install/templates/omniauth.rb +2 -1
  26. data/lib/generators/shopify_app/install/templates/user_agent.rb +2 -1
  27. data/lib/generators/shopify_app/routes/routes_generator.rb +1 -0
  28. data/lib/generators/shopify_app/routes/templates/routes.rb +10 -9
  29. data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +5 -4
  30. data/lib/generators/shopify_app/shop_model/templates/shop.rb +1 -0
  31. data/lib/generators/shopify_app/shopify_app_generator.rb +4 -3
  32. data/lib/generators/shopify_app/user_model/templates/user.rb +1 -0
  33. data/lib/generators/shopify_app/user_model/user_model_generator.rb +5 -4
  34. data/lib/generators/shopify_app/views/views_generator.rb +1 -0
  35. data/lib/shopify_app.rb +7 -4
  36. data/lib/shopify_app/configuration.rb +15 -8
  37. data/lib/shopify_app/controller_concerns/app_proxy_verification.rb +3 -2
  38. data/lib/shopify_app/controller_concerns/embedded_app.rb +3 -2
  39. data/lib/shopify_app/controller_concerns/localization.rb +1 -0
  40. data/lib/shopify_app/controller_concerns/login_protection.rb +46 -11
  41. data/lib/shopify_app/controller_concerns/webhook_verification.rb +2 -1
  42. data/lib/shopify_app/engine.rb +1 -0
  43. data/lib/shopify_app/jobs/scripttags_manager_job.rb +1 -1
  44. data/lib/shopify_app/jobs/webhooks_manager_job.rb +1 -1
  45. data/lib/shopify_app/managers/scripttags_manager.rb +4 -3
  46. data/lib/shopify_app/managers/webhooks_manager.rb +4 -3
  47. data/lib/shopify_app/middleware/same_site_cookie_middleware.rb +2 -1
  48. data/lib/shopify_app/session/in_memory_session_store.rb +7 -3
  49. data/lib/shopify_app/session/in_memory_shop_session_store.rb +10 -0
  50. data/lib/shopify_app/session/in_memory_user_session_store.rb +10 -0
  51. data/lib/shopify_app/session/jwt.rb +48 -0
  52. data/lib/shopify_app/session/null_user_session_store.rb +22 -0
  53. data/lib/shopify_app/session/session_repository.rb +13 -16
  54. data/lib/shopify_app/session/session_storage.rb +1 -0
  55. data/lib/shopify_app/session/shop_session_storage.rb +21 -9
  56. data/lib/shopify_app/session/user_session_storage.rb +19 -8
  57. data/lib/shopify_app/test_helpers/all.rb +1 -0
  58. data/lib/shopify_app/test_helpers/webhook_verification_helper.rb +16 -0
  59. data/lib/shopify_app/utils.rb +6 -5
  60. data/lib/shopify_app/version.rb +2 -1
  61. data/package-lock.json +4 -4
  62. data/package.json +1 -1
  63. data/shopify_app.gemspec +8 -4
  64. data/yarn.lock +3 -3
  65. metadata +22 -3
@@ -1,6 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  class WebhooksManagerJob < ActiveJob::Base
3
-
4
4
  queue_as do
5
5
  ShopifyApp.configuration.webhooks_manager_queue_name
6
6
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  class ScripttagsManager
3
4
  class CreationFailed < StandardError; end
@@ -43,7 +44,7 @@ module ShopifyApp
43
44
  def destroy_scripttags
44
45
  scripttags = expanded_scripttags
45
46
  ShopifyAPI::ScriptTag.all.each do |tag|
46
- ShopifyAPI::ScriptTag.delete(tag.id) if is_required_scripttag?(scripttags, tag)
47
+ ShopifyAPI::ScriptTag.delete(tag.id) if required_scripttag?(scripttags, tag)
47
48
  end
48
49
 
49
50
  @current_scripttags = nil
@@ -55,8 +56,8 @@ module ShopifyApp
55
56
  self.class.build_src(required_scripttags, shop_domain)
56
57
  end
57
58
 
58
- def is_required_scripttag?(scripttags, tag)
59
- scripttags.map{ |w| w[:src] }.include? tag.src
59
+ def required_scripttag?(scripttags, tag)
60
+ scripttags.map { |w| w[:src] }.include?(tag.src)
60
61
  end
61
62
 
62
63
  def create_scripttag(attributes)
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  class WebhooksManager
3
4
  class CreationFailed < StandardError; end
@@ -31,7 +32,7 @@ module ShopifyApp
31
32
 
32
33
  def destroy_webhooks
33
34
  ShopifyAPI::Webhook.all.to_a.each do |webhook|
34
- ShopifyAPI::Webhook.delete(webhook.id) if is_required_webhook?(webhook)
35
+ ShopifyAPI::Webhook.delete(webhook.id) if required_webhook?(webhook)
35
36
  end
36
37
 
37
38
  @current_webhooks = nil
@@ -39,8 +40,8 @@ module ShopifyApp
39
40
 
40
41
  private
41
42
 
42
- def is_required_webhook?(webhook)
43
- required_webhooks.map{ |w| w[:address] }.include? webhook.address
43
+ def required_webhook?(webhook)
44
+ required_webhooks.map { |w| w[:address] }.include?(webhook.address)
44
45
  end
45
46
 
46
47
  def create_webhook(attributes)
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  class SameSiteCookieMiddleware
3
4
  COOKIE_SEPARATOR = "\n"
@@ -19,7 +20,7 @@ module ShopifyApp
19
20
  .split(COOKIE_SEPARATOR)
20
21
  .compact
21
22
  .map do |cookie|
22
- cookie << '; Secure' if not cookie =~ /;\s*secure/i
23
+ cookie << '; Secure' unless cookie =~ /;\s*secure/i
23
24
  cookie << '; SameSite=None' unless cookie =~ /;\s*samesite=/i
24
25
  cookie
25
26
  end
@@ -1,4 +1,7 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
3
+ # rubocop:disable Style/ClassVars
4
+ # Class var repo is needed here in order to share data between the 2 child classes.
2
5
  class InMemorySessionStore
3
6
  class EnvironmentError < StandardError; end
4
7
 
@@ -6,7 +9,7 @@ module ShopifyApp
6
9
  repo[id]
7
10
  end
8
11
 
9
- def self.store(session, *args)
12
+ def self.store(session, *_args)
10
13
  id = SecureRandom.uuid
11
14
  repo[id] = session
12
15
  id
@@ -18,10 +21,11 @@ module ShopifyApp
18
21
 
19
22
  def self.repo
20
23
  if Rails.env.production?
21
- raise EnvironmentError.new("Cannot use InMemorySessionStore in a Production environment. \
22
- Please initialize ShopifyApp with a model that can store and retrieve sessions")
24
+ raise EnvironmentError, "Cannot use InMemorySessionStore in a Production environment. \
25
+ Please initialize ShopifyApp with a model that can store and retrieve sessions"
23
26
  end
24
27
  @@repo ||= {}
25
28
  end
26
29
  end
30
+ # rubocop:enable Style/ClassVars
27
31
  end
@@ -1,4 +1,14 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  class InMemoryShopSessionStore < InMemorySessionStore
4
+ def self.store(session, *args)
5
+ id = super
6
+ repo[session.domain] = session
7
+ id
8
+ end
9
+
10
+ def self.retrieve_by_shopify_domain(shopify_domain)
11
+ repo[shopify_domain]
12
+ end
3
13
  end
4
14
  end
@@ -1,4 +1,14 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  class InMemoryUserSessionStore < InMemorySessionStore
4
+ def self.store(session, user)
5
+ id = super
6
+ repo[user.shopify_user_id] = session
7
+ id
8
+ end
9
+
10
+ def self.retrieve_by_shopify_user_id(user_id)
11
+ repo[user_id]
12
+ end
3
13
  end
4
14
  end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+ module ShopifyApp
3
+ class JWT
4
+ def initialize(token)
5
+ @token = token
6
+ set_payload
7
+ end
8
+
9
+ def shopify_domain
10
+ payload && dest_host
11
+ end
12
+
13
+ def shopify_user_id
14
+ payload && payload['sub']
15
+ end
16
+
17
+ private
18
+
19
+ def payload
20
+ return unless @payload
21
+ return unless dest_host
22
+ return unless dest_host == iss_host
23
+ return unless @payload['aud'] == ShopifyApp.configuration.api_key
24
+
25
+ @payload
26
+ end
27
+
28
+ def set_payload
29
+ @payload, _ = parse_token_data(ShopifyApp.configuration.secret)
30
+ configuration_old_secret = @payload && ShopifyApp.configuration
31
+ @payload, _ = parse_token_data(ShopifyApp.configuration.old_secret) unless configuration_old_secret
32
+ end
33
+
34
+ def parse_token_data(secret)
35
+ ::JWT.decode(@token, secret, true, { algorithm: 'HS256' })
36
+ rescue ::JWT::DecodeError, ::JWT::VerificationError, ::JWT::ExpiredSignature, ::JWT::ImmatureSignature
37
+ nil
38
+ end
39
+
40
+ def dest_host
41
+ @payload && ShopifyApp::Utils.sanitize_shop_domain(@payload['dest'])
42
+ end
43
+
44
+ def iss_host
45
+ @payload && ShopifyApp::Utils.sanitize_shop_domain(@payload['iss'])
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+ module ShopifyApp
3
+ class NullUserSessionStore
4
+ class << self
5
+ def retrieve(_)
6
+ nil
7
+ end
8
+
9
+ def store(_, _)
10
+ raise SessionRepository::ConfigurationError, 'user_storage is not configured'
11
+ end
12
+
13
+ def retrieve_by_shopify_user_id(_)
14
+ nil
15
+ end
16
+
17
+ def blank?
18
+ true
19
+ end
20
+ end
21
+ end
22
+ end
@@ -1,23 +1,12 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  class SessionRepository
3
4
  class ConfigurationError < StandardError; end
4
5
 
5
6
  class << self
6
- def shop_storage=(storage)
7
- @shop_storage = storage
7
+ attr_writer :shop_storage
8
8
 
9
- unless storage.nil? || self.shop_storage.respond_to?(:store) && self.shop_storage.respond_to?(:retrieve)
10
- raise ArgumentError, "shop storage must respond to :store and :retrieve"
11
- end
12
- end
13
-
14
- def user_storage=(storage)
15
- @user_storage = storage
16
-
17
- unless storage.nil? || self.user_storage.respond_to?(:store) && self.user_storage.respond_to?(:retrieve)
18
- raise ArgumentError, "user storage must respond to :store and :retrieve"
19
- end
20
- end
9
+ attr_writer :user_storage
21
10
 
22
11
  def retrieve_shop_session(id)
23
12
  shop_storage.retrieve(id)
@@ -27,6 +16,14 @@ module ShopifyApp
27
16
  user_storage.retrieve(id)
28
17
  end
29
18
 
19
+ def retrieve_shop_session_by_shopify_domain(shopify_domain)
20
+ shop_storage.retrieve_by_shopify_domain(shopify_domain)
21
+ end
22
+
23
+ def retrieve_user_session_by_shopify_user_id(user_id)
24
+ user_storage.retrieve_by_shopify_user_id(user_id)
25
+ end
26
+
30
27
  def store_shop_session(session)
31
28
  shop_storage.store(session)
32
29
  end
@@ -36,7 +33,7 @@ module ShopifyApp
36
33
  end
37
34
 
38
35
  def shop_storage
39
- load_shop_storage || raise(ConfigurationError.new("ShopifySessionRepository.shop_storage is not configured!"))
36
+ load_shop_storage || raise(ConfigurationError, "ShopifySessionRepository.shop_storage is not configured!")
40
37
  end
41
38
 
42
39
  def user_storage
@@ -51,7 +48,7 @@ module ShopifyApp
51
48
  end
52
49
 
53
50
  def load_user_storage
54
- return unless @user_storage
51
+ return NullUserSessionStore unless @user_storage
55
52
  @user_storage.respond_to?(:safe_constantize) ? @user_storage.safe_constantize : @user_storage
56
53
  end
57
54
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  module SessionStorage
3
4
  extend ActiveSupport::Concern
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  module ShopSessionStorage
3
4
  extend ActiveSupport::Concern
@@ -8,7 +9,7 @@ module ShopifyApp
8
9
  end
9
10
 
10
11
  class_methods do
11
- def store(auth_session, *args)
12
+ def store(auth_session, *_args)
12
13
  shop = find_or_initialize_by(shopify_domain: auth_session.domain)
13
14
  shop.shopify_token = auth_session.token
14
15
  shop.save!
@@ -16,14 +17,25 @@ module ShopifyApp
16
17
  end
17
18
 
18
19
  def retrieve(id)
19
- return unless id
20
- if shop = self.find_by(id: id)
21
- ShopifyAPI::Session.new(
22
- domain: shop.shopify_domain,
23
- token: shop.shopify_token,
24
- api_version: shop.api_version
25
- )
26
- end
20
+ shop = find_by(id: id)
21
+ construct_session(shop)
22
+ end
23
+
24
+ def retrieve_by_shopify_domain(domain)
25
+ shop = find_by(shopify_domain: domain)
26
+ construct_session(shop)
27
+ end
28
+
29
+ private
30
+
31
+ def construct_session(shop)
32
+ return unless shop
33
+
34
+ ShopifyAPI::Session.new(
35
+ domain: shop.shopify_domain,
36
+ token: shop.shopify_token,
37
+ api_version: shop.api_version,
38
+ )
27
39
  end
28
40
  end
29
41
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  module UserSessionStorage
3
4
  extend ActiveSupport::Concern
@@ -17,14 +18,24 @@ module ShopifyApp
17
18
  end
18
19
 
19
20
  def retrieve(id)
20
- return unless id
21
- if user = find_by(id: id)
22
- ShopifyAPI::Session.new(
23
- domain: user.shopify_domain,
24
- token: user.shopify_token,
25
- api_version: user.api_version
26
- )
27
- end
21
+ user = find_by(id: id)
22
+ construct_session(user)
23
+ end
24
+
25
+ def retrieve_by_shopify_user_id(user_id)
26
+ user = find_by(shopify_user_id: user_id)
27
+ construct_session(user)
28
+ end
29
+
30
+ private
31
+
32
+ def construct_session(user)
33
+ return unless user
34
+ ShopifyAPI::Session.new(
35
+ domain: user.shopify_domain,
36
+ token: user.shopify_token,
37
+ api_version: user.api_version,
38
+ )
28
39
  end
29
40
  end
30
41
  end
@@ -0,0 +1 @@
1
+ require 'shopify_app/test_helpers/webhook_verification_helper'
@@ -0,0 +1,16 @@
1
+ module ShopifyApp
2
+ module TestHelpers
3
+ module WebhookVerificationHelper
4
+ def authorized_webhook_verification_headers!(params = {})
5
+ digest = OpenSSL::Digest.new('sha256')
6
+ secret = ShopifyApp.configuration.secret
7
+ valid_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, secret, params.to_query)).strip
8
+ @request.headers['HTTP_X_SHOPIFY_HMAC_SHA256'] = valid_hmac
9
+ end
10
+
11
+ def unauthorized_webhook_verification_headers!(params = {})
12
+ @request.headers['HTTP_X_SHOPIFY_HMAC_SHA256'] = "invalid_hmac"
13
+ end
14
+ end
15
+ end
16
+ end
@@ -1,13 +1,14 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
3
  module Utils
3
-
4
4
  def self.sanitize_shop_domain(shop_domain)
5
+ myshopify_domain = ShopifyApp.configuration.myshopify_domain
5
6
  name = shop_domain.to_s.downcase.strip
6
- name += ".#{ShopifyApp.configuration.myshopify_domain}" if !name.include?("#{ShopifyApp.configuration.myshopify_domain}") && !name.include?(".")
7
+ name += ".#{myshopify_domain}" if !name.include?(myshopify_domain.to_s) && !name.include?(".")
7
8
  name.sub!(%r|https?://|, '')
8
9
 
9
10
  u = URI("http://#{name}")
10
- u.host if u.host&.match(/^[a-z0-9][a-z0-9\-]*[a-z0-9]\.#{Regexp.escape(ShopifyApp.configuration.myshopify_domain)}$/)
11
+ u.host if u.host&.match(/^[a-z0-9][a-z0-9\-]*[a-z0-9]\.#{Regexp.escape(myshopify_domain)}$/)
11
12
  rescue URI::InvalidURIError
12
13
  nil
13
14
  end
@@ -16,8 +17,8 @@ module ShopifyApp
16
17
  Rails.logger.info("[ShopifyAPI::ApiVersion] Fetching known Admin API Versions from Shopify...")
17
18
  ShopifyAPI::ApiVersion.fetch_known_versions
18
19
  Rails.logger.info("[ShopifyAPI::ApiVersion] Known API Versions: #{ShopifyAPI::ApiVersion.versions.keys}")
19
- rescue ActiveResource::ConnectionError
20
- logger.error( "[ShopifyAPI::ApiVersion] Unable to fetch api_versions from Shopify")
20
+ rescue ActiveResource::ConnectionError
21
+ logger.error("[ShopifyAPI::ApiVersion] Unable to fetch api_versions from Shopify")
21
22
  end
22
23
  end
23
24
  end
@@ -1,3 +1,4 @@
1
+ # frozen_string_literal: true
1
2
  module ShopifyApp
2
- VERSION = '13.0.0'.freeze
3
+ VERSION = '13.0.1'
3
4
  end
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shopify_app",
3
- "version": "12.0.2",
3
+ "version": "13.0.0",
4
4
  "lockfileVersion": 1,
5
5
  "requires": true,
6
6
  "dependencies": {
@@ -1332,9 +1332,9 @@
1332
1332
  }
1333
1333
  },
1334
1334
  "acorn": {
1335
- "version": "6.3.0",
1336
- "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.3.0.tgz",
1337
- "integrity": "sha512-/czfa8BwS88b9gWQVhc8eknunSA2DoJpJyTQkhheIf5E48u1N0R4q/YxxsAeqRrmK9TQ/uYfgLDfZo91UlANIA==",
1335
+ "version": "6.4.1",
1336
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz",
1337
+ "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==",
1338
1338
  "dev": true
1339
1339
  },
1340
1340
  "after": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "shopify_app",
3
- "version": "13.0.0",
3
+ "version": "13.0.1",
4
4
  "repository": "git@github.com:Shopify/shopify_app.git",
5
5
  "author": "Shopify",
6
6
  "license": "MIT",
@@ -1,4 +1,5 @@
1
- $LOAD_PATH.push File.expand_path('../lib', __FILE__)
1
+ # frozen_string_literal: true
2
+ $LOAD_PATH.push(File.expand_path('../lib', __FILE__))
2
3
  require "shopify_app/version"
3
4
 
4
5
  Gem::Specification.new do |s|
@@ -6,14 +7,17 @@ Gem::Specification.new do |s|
6
7
  s.version = ShopifyApp::VERSION
7
8
  s.platform = Gem::Platform::RUBY
8
9
  s.author = "Shopify"
9
- s.summary = %q{This gem is used to get quickly started with the Shopify API}
10
+ s.summary = 'This gem is used to get quickly started with the Shopify API'
10
11
 
11
12
  s.required_ruby_version = ">= 2.3.1"
12
13
 
14
+ s.metadata['allowed_push_host'] = 'https://rubygems.org'
15
+
13
16
  s.add_runtime_dependency('browser_sniffer', '~> 1.2.0')
14
17
  s.add_runtime_dependency('rails', '> 5.2.1')
15
18
  s.add_runtime_dependency('shopify_api', '~> 9.0.2')
16
19
  s.add_runtime_dependency('omniauth-shopify-oauth2', '~> 2.2.2')
20
+ s.add_runtime_dependency('jwt', '~> 2.2.1')
17
21
 
18
22
  s.add_development_dependency('rake')
19
23
  s.add_development_dependency('byebug')
@@ -26,7 +30,7 @@ Gem::Specification.new do |s|
26
30
  s.add_development_dependency('mocha')
27
31
  s.add_development_dependency('webmock')
28
32
 
29
- s.files = `git ls-files`.split("\n").reject { |f| f.match(%r{^(test|example)/}) }
30
- s.test_files = `git ls-files -- {test}/*`.split("\n")
33
+ s.files = %x(git ls-files).split("\n").reject { |f| f.match(%r{^(test|example)/}) }
34
+ s.test_files = %x(git ls-files -- {test}/*).split("\n")
31
35
  s.require_paths = ["lib"]
32
36
  end