shopify_app 12.0.0 → 13.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +39 -0
  3. data/README.md +67 -20
  4. data/app/assets/javascripts/shopify_app/itp_helper.js +6 -6
  5. data/app/assets/javascripts/shopify_app/storage_access.js +35 -6
  6. data/app/controllers/concerns/shopify_app/authenticated.rb +1 -1
  7. data/app/controllers/shopify_app/callback_controller.rb +14 -10
  8. data/app/controllers/shopify_app/sessions_controller.rb +49 -13
  9. data/app/views/shopify_app/sessions/enable_cookies.html.erb +1 -1
  10. data/app/views/shopify_app/sessions/request_storage_access.html.erb +1 -1
  11. data/config/locales/pt-BR.yml +1 -1
  12. data/lib/generators/shopify_app/add_marketing_activity_extension/templates/marketing_activities_controller.rb +1 -5
  13. data/lib/generators/shopify_app/home_controller/templates/index.html.erb +1 -1
  14. data/lib/generators/shopify_app/install/templates/embedded_app.html.erb +1 -1
  15. data/lib/generators/shopify_app/install/templates/shopify_app.rb +1 -1
  16. data/lib/generators/shopify_app/install/templates/shopify_provider.rb +1 -1
  17. data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +1 -1
  18. data/lib/generators/shopify_app/shop_model/templates/shop.rb +1 -1
  19. data/lib/generators/shopify_app/user_model/templates/user.rb +1 -1
  20. data/lib/generators/shopify_app/user_model/user_model_generator.rb +1 -1
  21. data/lib/shopify_app/configuration.rb +12 -9
  22. data/lib/shopify_app/controller_concerns/login_protection.rb +39 -22
  23. data/lib/shopify_app/engine.rb +1 -1
  24. data/lib/shopify_app/middleware/same_site_cookie_middleware.rb +18 -45
  25. data/lib/shopify_app/session/in_memory_shop_session_store.rb +4 -0
  26. data/lib/shopify_app/session/in_memory_user_session_store.rb +4 -0
  27. data/lib/shopify_app/session/session_repository.rb +38 -13
  28. data/lib/shopify_app/session/session_storage.rb +0 -10
  29. data/lib/shopify_app/session/{storage_strategies/shop_storage_strategy.rb → shop_session_storage.rb} +9 -2
  30. data/lib/shopify_app/session/{storage_strategies/user_storage_strategy.rb → user_session_storage.rb} +10 -3
  31. data/lib/shopify_app/version.rb +1 -1
  32. data/lib/shopify_app.rb +4 -2
  33. data/package-lock.json +1228 -1207
  34. data/package.json +2 -2
  35. data/shopify_app.gemspec +4 -4
  36. metadata +12 -10
@@ -32,7 +32,7 @@
32
32
  myshopifyUrl: "https://#{current_shopify_domain}",
33
33
  hasStorageAccessUrl: "#{has_storage_access_url}",
34
34
  doesNotHaveStorageAccessUrl: "#{does_not_have_storage_access_url}",
35
- appHomeUrl: "#{app_home_url}"
35
+ appTargetUrl: "#{app_target_url}"
36
36
  },
37
37
  },
38
38
  )
@@ -24,7 +24,7 @@
24
24
  myshopifyUrl: "https://#{current_shopify_domain}",
25
25
  hasStorageAccessUrl: "#{has_storage_access_url}",
26
26
  doesNotHaveStorageAccessUrl: "#{does_not_have_storage_access_url}",
27
- appHomeUrl: "#{app_home_url}"
27
+ appTargetUrl: "#{app_target_url}"
28
28
  },
29
29
  },
30
30
  )
@@ -4,7 +4,7 @@ pt-BR:
4
4
  could_not_log_in: Não foi possível fazer login na Shopify store
5
5
  invalid_shop_url: Domínio de loja inválido
6
6
  enable_cookies_heading: Habilitar cookies de %{app}
7
- enable_cookies_body: Você deve habilitar manualmente os cookies neste navegador
7
+ enable_cookies_body: Você precisa habilitar manualmente os cookies neste navegador
8
8
  para usar %{app} dentro da Shopify.
9
9
  enable_cookies_footer: Os cookies permitem que o app o autentique armazenando temporariamente
10
10
  suas preferências e dados pessoais. Eles expiram depois de 30 dias.
@@ -3,11 +3,7 @@
3
3
  class MarketingActivitiesController < ShopifyApp::ExtensionVerificationController
4
4
  def preload_form_data
5
5
  preload_data = {
6
- "form_data": {
7
- "budget": {
8
- "currency": "USD",
9
- }
10
- }
6
+ "form_data": {}
11
7
  }
12
8
  render(json: preload_data, status: :ok)
13
9
  end
@@ -2,7 +2,7 @@
2
2
 
3
3
  <ul>
4
4
  <% @products.each do |product| %>
5
- <li><%= link_to product.title, "https://#{@shop_session.domain}/admin/products/#{product.id}", target: "_top" %></li>
5
+ <li><%= link_to product.title, "https://#{@current_shopify_session.domain}/admin/products/#{product.id}", target: "_top" %></li>
6
6
  <% end %>
7
7
  </ul>
8
8
 
@@ -28,7 +28,7 @@
28
28
 
29
29
  <%= content_tag(:div, nil, id: 'shopify-app-init', data: {
30
30
  api_key: ShopifyApp.configuration.api_key,
31
- shop_origin: (@shop_session.domain if @shop_session),
31
+ shop_origin: (@current_shopify_session.domain if @current_shopify_session),
32
32
  debug: Rails.env.development?
33
33
  } ) %>
34
34
 
@@ -8,7 +8,7 @@ ShopifyApp.configure do |config|
8
8
  config.embedded_app = <%= embedded_app? %>
9
9
  config.after_authenticate_job = false
10
10
  config.api_version = "<%= @api_version %>"
11
- config.session_repository = 'ShopifyApp::InMemorySessionStore'
11
+ config.shop_session_repository = 'ShopifyApp::InMemoryShopSessionStore'
12
12
  end
13
13
 
14
14
  # ShopifyApp::Utils.fetch_known_api_versions # Uncomment to fetch known api versions from shopify servers on boot
@@ -4,7 +4,6 @@ provider :shopify,
4
4
  ShopifyApp.configuration.api_key,
5
5
  ShopifyApp.configuration.secret,
6
6
  scope: ShopifyApp.configuration.scope,
7
- per_user_permissions: ShopifyApp.configuration.per_user_tokens,
8
7
  setup: lambda { |env|
9
8
  strategy = env['omniauth.strategy']
10
9
 
@@ -17,4 +16,5 @@ provider :shopify,
17
16
 
18
17
  strategy.options[:client_options][:site] = shop
19
18
  strategy.options[:old_client_secret] = ShopifyApp.configuration.old_secret
19
+ strategy.options[:per_user_permissions] = strategy.session[:user_tokens]
20
20
  }
@@ -16,7 +16,7 @@ module ShopifyApp
16
16
  end
17
17
 
18
18
  def update_shopify_app_initializer
19
- gsub_file 'config/initializers/shopify_app.rb', 'ShopifyApp::InMemorySessionStore', 'Shop'
19
+ gsub_file 'config/initializers/shopify_app.rb', 'ShopifyApp::InMemoryShopSessionStore', 'Shop'
20
20
  end
21
21
 
22
22
  def create_shop_fixtures
@@ -1,5 +1,5 @@
1
1
  class Shop < ActiveRecord::Base
2
- include ShopifyApp::SessionStorage
2
+ include ShopifyApp::ShopSessionStorage
3
3
 
4
4
  def api_version
5
5
  ShopifyApp.configuration.api_version
@@ -1,5 +1,5 @@
1
1
  class User < ActiveRecord::Base
2
- include ShopifyApp::SessionStorage
2
+ include ShopifyApp::UserSessionStorage
3
3
 
4
4
  def api_version
5
5
  ShopifyApp.configuration.api_version
@@ -16,7 +16,7 @@ module ShopifyApp
16
16
  end
17
17
 
18
18
  def update_shopify_app_initializer
19
- gsub_file 'config/initializers/shopify_app.rb', 'ShopifyApp::InMemorySessionStore', 'User'
19
+ gsub_file 'config/initializers/shopify_app.rb', 'ShopifyApp::InMemoryUserSessionStore', 'User'
20
20
  end
21
21
 
22
22
  def create_user_fixtures
@@ -5,7 +5,7 @@ module ShopifyApp
5
5
  # for the app in your Shopify Partners page. Change your settings in
6
6
  # `config/initializers/shopify_app.rb`
7
7
  attr_accessor :application_name
8
- attr_accessor :api_key
8
+ attr_accessor :api_key
9
9
  attr_accessor :secret
10
10
  attr_accessor :old_secret
11
11
  attr_accessor :scope
@@ -14,9 +14,8 @@ module ShopifyApp
14
14
  attr_accessor :webhooks
15
15
  attr_accessor :scripttags
16
16
  attr_accessor :after_authenticate_job
17
- attr_reader :session_repository
18
- attr_accessor :per_user_tokens
19
- alias_method :per_user_tokens?, :per_user_tokens
17
+ attr_reader :shop_session_repository
18
+ attr_reader :user_session_repository
20
19
  attr_accessor :api_version
21
20
 
22
21
  # customise urls
@@ -44,7 +43,6 @@ module ShopifyApp
44
43
  @myshopify_domain = 'myshopify.com'
45
44
  @scripttags_manager_queue_name = Rails.application.config.active_job.queue_name
46
45
  @webhooks_manager_queue_name = Rails.application.config.active_job.queue_name
47
- @per_user_tokens = false
48
46
  @disable_webpacker = ENV['SHOPIFY_APP_DISABLE_WEBPACKER'].present?
49
47
  end
50
48
 
@@ -52,9 +50,14 @@ module ShopifyApp
52
50
  @login_url || File.join(@root_url, 'login')
53
51
  end
54
52
 
55
- def session_repository=(klass)
56
- @session_repository = klass
57
- ShopifyApp::SessionRepository.storage = klass
53
+ def user_session_repository=(klass)
54
+ @user_session_repository = klass
55
+ ShopifyApp::SessionRepository.user_storage = klass
56
+ end
57
+
58
+ def shop_session_repository=(klass)
59
+ @shop_session_repository = klass
60
+ ShopifyApp::SessionRepository.shop_storage = klass
58
61
  end
59
62
 
60
63
  def has_webhooks?
@@ -66,7 +69,7 @@ module ShopifyApp
66
69
  end
67
70
 
68
71
  def enable_same_site_none
69
- @enable_same_site_none.nil? ? embedded_app? : @enable_same_site_none
72
+ !Rails.env.test? && (@enable_same_site_none.nil? ? embedded_app? : @enable_same_site_none)
70
73
  end
71
74
  end
72
75
 
@@ -14,44 +14,48 @@ module ShopifyApp
14
14
  rescue_from ActiveResource::UnauthorizedAccess, :with => :close_session
15
15
  end
16
16
 
17
- def shopify_session
18
- return redirect_to_login unless shop_session
17
+ def activate_shopify_session
18
+ return redirect_to_login if current_shopify_session.blank?
19
19
  clear_top_level_oauth_cookie
20
20
 
21
21
  begin
22
- ShopifyAPI::Base.activate_session(shop_session)
22
+ ShopifyAPI::Base.activate_session(current_shopify_session)
23
23
  yield
24
24
  ensure
25
25
  ShopifyAPI::Base.clear_session
26
26
  end
27
27
  end
28
28
 
29
- def shop_session
30
- if ShopifyApp.configuration.per_user_tokens?
31
- return unless session[:shopify_user]
32
- @shop_session ||= ShopifyApp::SessionRepository.retrieve(session[:shopify_user]['id'])
29
+ def current_shopify_session
30
+ if session[:user_id].present?
31
+ @current_shopify_session ||= user_session
33
32
  else
34
- return unless session[:shopify]
35
- @shop_session ||= ShopifyApp::SessionRepository.retrieve(session[:shopify])
33
+ @current_shopify_session ||= shop_session
36
34
  end
37
35
  end
38
36
 
37
+ def user_session
38
+ return if session[:user_id].blank?
39
+ ShopifyApp::SessionRepository.retrieve_user_session(session[:user_id])
40
+ end
41
+
42
+ def shop_session
43
+ return if session[:shop_id].blank?
44
+ ShopifyApp::SessionRepository.retrieve_shop_session(session[:shop_id])
45
+ end
46
+
39
47
  def login_again_if_different_user_or_shop
40
- if ShopifyApp.configuration.per_user_tokens?
41
- valid_session_data = session[:user_session].present? && params[:session].present? # session data was sent/stored correctly
42
- sessions_do_not_match = session[:user_session] != params[:session] # current user is different from stored user
48
+ if session[:user_session].present? && params[:session].present? # session data was sent/stored correctly
49
+ clear_session = session[:user_session] != params[:session] # current user is different from stored user
43
50
 
44
- if valid_session_data && sessions_do_not_match
45
- clear_session = true
46
- end
47
51
  end
48
52
 
49
- if shop_session && params[:shop] && params[:shop].is_a?(String) && (shop_session.domain != params[:shop])
53
+ if current_shopify_session && params[:shop] && params[:shop].is_a?(String) && (current_shopify_session.domain != params[:shop])
50
54
  clear_session = true
51
55
  end
52
56
 
53
57
  if clear_session
54
- clear_shop_session
58
+ clear_shopify_session
55
59
  redirect_to_login
56
60
  end
57
61
  end
@@ -76,12 +80,13 @@ module ShopifyApp
76
80
  end
77
81
 
78
82
  def close_session
79
- clear_shop_session
83
+ clear_shopify_session
80
84
  redirect_to(login_url_with_optional_shop)
81
85
  end
82
86
 
83
- def clear_shop_session
84
- session[:shopify] = nil
87
+ def clear_shopify_session
88
+ session[:shop_id] = nil
89
+ session[:user_id] = nil
85
90
  session[:shopify_domain] = nil
86
91
  session[:shopify_user] = nil
87
92
  session[:user_session] = nil
@@ -100,8 +105,10 @@ module ShopifyApp
100
105
  query_params = {}
101
106
  query_params[:shop] = sanitized_params[:shop] if params[:shop].present?
102
107
 
103
- if session[:return_to] && return_to_param_required?
104
- query_params[:return_to] = session[:return_to]
108
+ return_to = session[:return_to] || params[:return_to]
109
+
110
+ if return_to.present? && return_to_param_required?
111
+ query_params[:return_to] = return_to
105
112
  end
106
113
 
107
114
  has_referer_shop_name = referer_sanitized_shop_name.present?
@@ -165,5 +172,15 @@ module ShopifyApp
165
172
  def return_address
166
173
  session.delete(:return_to) || ShopifyApp.configuration.root_url
167
174
  end
175
+
176
+ def return_address_with_params(params)
177
+ uri = URI(return_address)
178
+ uri.query = CGI.parse(uri.query.to_s)
179
+ .symbolize_keys
180
+ .transform_values { |v| v.one? ? v.first : v }
181
+ .merge(params)
182
+ .to_query
183
+ uri.to_s
184
+ end
168
185
  end
169
186
  end
@@ -14,7 +14,7 @@ module ShopifyApp
14
14
  end
15
15
 
16
16
  initializer "shopify_app.middleware" do |app|
17
- app.config.middleware.insert_before(ActionDispatch::Cookies, ShopifyApp::SameSiteCookieMiddleware)
17
+ app.config.middleware.insert_after(::Rack::Runtime, ShopifyApp::SameSiteCookieMiddleware)
18
18
  end
19
19
  end
20
20
  end
@@ -1,60 +1,33 @@
1
1
  module ShopifyApp
2
2
  class SameSiteCookieMiddleware
3
+ COOKIE_SEPARATOR = "\n"
4
+
3
5
  def initialize(app)
4
6
  @app = app
5
7
  end
6
8
 
7
9
  def call(env)
8
- _status, headers, _body = @app.call(env)
9
- ensure
10
+ status, headers, body = @app.call(env)
10
11
  user_agent = env['HTTP_USER_AGENT']
11
12
 
12
- if headers && headers['Set-Cookie'] && !SameSiteCookieMiddleware.same_site_none_incompatible?(user_agent) &&
13
- ShopifyApp.configuration.enable_same_site_none
14
-
15
- cookies = headers['Set-Cookie'].split("\n").compact
16
-
17
- cookies.each do |cookie|
18
- unless cookie.include?("; SameSite")
19
- headers['Set-Cookie'] = headers['Set-Cookie'].gsub("#{cookie}", "#{cookie}; secure; SameSite=None")
13
+ if headers && headers['Set-Cookie'] &&
14
+ BrowserSniffer.new(user_agent).same_site_none_compatible? &&
15
+ ShopifyApp.configuration.enable_same_site_none &&
16
+ Rack::Request.new(env).ssl?
17
+
18
+ set_cookies = headers['Set-Cookie']
19
+ .split(COOKIE_SEPARATOR)
20
+ .compact
21
+ .map do |cookie|
22
+ cookie << '; Secure' if not cookie =~ /;\s*secure/i
23
+ cookie << '; SameSite=None' unless cookie =~ /;\s*samesite=/i
24
+ cookie
20
25
  end
21
- end
22
- end
23
- end
24
26
 
25
- def self.same_site_none_incompatible?(user_agent)
26
- sniffer = BrowserSniffer.new(user_agent)
27
-
28
- webkit_same_site_bug?(sniffer) || drops_unrecognized_same_site_cookies?(sniffer)
29
- rescue
30
- true
31
- end
32
-
33
- def self.webkit_same_site_bug?(sniffer)
34
- (sniffer.os == :ios && sniffer.os_version.match?(/^([0-9]|1[12])[\.\_]/)) ||
35
- (sniffer.os == :mac && sniffer.browser == :safari && sniffer.os_version.match?(/^10[\.\_]14/))
36
- end
37
-
38
- def self.drops_unrecognized_same_site_cookies?(sniffer)
39
- (chromium_based?(sniffer) && sniffer.major_browser_version >= 51 && sniffer.major_browser_version <= 66) ||
40
- (uc_browser?(sniffer) && !uc_browser_version_at_least?(sniffer: sniffer, major: 12, minor: 13, build: 2))
41
- end
42
-
43
- def self.chromium_based?(sniffer)
44
- sniffer.browser_name.downcase.match?(/chrom(e|ium)/)
45
- end
46
-
47
- def self.uc_browser?(sniffer)
48
- sniffer.user_agent.downcase.match?(/uc\s?browser/)
49
- end
50
-
51
- def self.uc_browser_version_at_least?(sniffer:, major:, minor:, build:)
52
- digits = sniffer.browser_version.split('.').map(&:to_i)
53
- return false unless digits.count >= 3
27
+ headers['Set-Cookie'] = set_cookies.join(COOKIE_SEPARATOR)
28
+ end
54
29
 
55
- return digits[0] > major if digits[0] != major
56
- return digits[1] > minor if digits[1] != minor
57
- digits[2] >= build
30
+ [status, headers, body]
58
31
  end
59
32
  end
60
33
  end
@@ -0,0 +1,4 @@
1
+ module ShopifyApp
2
+ class InMemoryShopSessionStore < InMemorySessionStore
3
+ end
4
+ end
@@ -0,0 +1,4 @@
1
+ module ShopifyApp
2
+ class InMemoryUserSessionStore < InMemorySessionStore
3
+ end
4
+ end
@@ -3,31 +3,56 @@ module ShopifyApp
3
3
  class ConfigurationError < StandardError; end
4
4
 
5
5
  class << self
6
- def storage=(storage)
7
- @storage = storage
6
+ def shop_storage=(storage)
7
+ @shop_storage = storage
8
8
 
9
- unless storage.nil? || self.storage.respond_to?(:store) && self.storage.respond_to?(:retrieve)
10
- raise ArgumentError, "storage must respond to :store and :retrieve"
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
11
  end
12
12
  end
13
13
 
14
- def retrieve(id)
15
- storage.retrieve(id)
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
21
+
22
+ def retrieve_shop_session(id)
23
+ shop_storage.retrieve(id)
24
+ end
25
+
26
+ def retrieve_user_session(id)
27
+ user_storage.retrieve(id)
16
28
  end
17
29
 
18
- def store(session, *args)
19
- storage.store(session, *args)
30
+ def store_shop_session(session)
31
+ shop_storage.store(session)
20
32
  end
21
33
 
22
- def storage
23
- load_storage || raise(ConfigurationError.new("ShopifySessionRepository.storage is not configured!"))
34
+ def store_user_session(session, user)
35
+ user_storage.store(session, user)
36
+ end
37
+
38
+ def shop_storage
39
+ load_shop_storage || raise(ConfigurationError.new("ShopifySessionRepository.shop_storage is not configured!"))
40
+ end
41
+
42
+ def user_storage
43
+ load_user_storage
24
44
  end
25
45
 
26
46
  private
27
47
 
28
- def load_storage
29
- return unless @storage
30
- @storage.respond_to?(:safe_constantize) ? @storage.safe_constantize : @storage
48
+ def load_shop_storage
49
+ return unless @shop_storage
50
+ @shop_storage.respond_to?(:safe_constantize) ? @shop_storage.safe_constantize : @shop_storage
51
+ end
52
+
53
+ def load_user_storage
54
+ return unless @user_storage
55
+ @user_storage.respond_to?(:safe_constantize) ? @user_storage.safe_constantize : @user_storage
31
56
  end
32
57
  end
33
58
  end