shopify_app 11.3.2 → 11.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/.travis.yml +0 -2
  3. data/CHANGELOG.md +24 -0
  4. data/README.md +100 -102
  5. data/app/controllers/concerns/shopify_app/authenticated.rb +1 -1
  6. data/app/controllers/shopify_app/callback_controller.rb +8 -2
  7. data/app/controllers/shopify_app/extension_verification_controller.rb +20 -0
  8. data/config/locales/nl.yml +7 -7
  9. data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +39 -0
  10. data/lib/generators/shopify_app/add_marketing_activity_extension/templates/marketing_activities_controller.rb +66 -0
  11. data/lib/generators/shopify_app/install/install_generator.rb +0 -4
  12. data/lib/generators/shopify_app/install/templates/shopify_app.rb +1 -1
  13. data/lib/generators/shopify_app/install/templates/shopify_provider.rb +1 -0
  14. data/lib/generators/shopify_app/user_model/templates/db/migrate/create_users.erb +16 -0
  15. data/lib/generators/shopify_app/user_model/templates/user.rb +7 -0
  16. data/lib/generators/shopify_app/user_model/templates/users.yml +4 -0
  17. data/lib/generators/shopify_app/user_model/user_model_generator.rb +38 -0
  18. data/lib/shopify_app.rb +5 -0
  19. data/lib/shopify_app/configuration.rb +13 -8
  20. data/lib/shopify_app/controller_concerns/login_protection.rb +22 -3
  21. data/lib/shopify_app/engine.rb +4 -0
  22. data/lib/shopify_app/middleware/same_site_cookie_middleware.rb +60 -0
  23. data/lib/shopify_app/session/in_memory_session_store.rb +1 -1
  24. data/lib/shopify_app/session/session_repository.rb +2 -2
  25. data/lib/shopify_app/session/session_storage.rb +14 -15
  26. data/lib/shopify_app/session/storage_strategies/shop_storage_strategy.rb +24 -0
  27. data/lib/shopify_app/session/storage_strategies/user_storage_strategy.rb +26 -0
  28. data/lib/shopify_app/version.rb +1 -1
  29. data/package-lock.json +33 -35
  30. data/package.json +3 -2
  31. data/service.yml +1 -1
  32. data/shopify_app.gemspec +4 -2
  33. data/yarn.lock +14 -14
  34. metadata +49 -11
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ class MarketingActivitiesController < ShopifyApp::ExtensionVerificationController
4
+ def preload_form_data
5
+ preload_data = {
6
+ "form_data": {
7
+ "budget": {
8
+ "currency": "USD",
9
+ }
10
+ }
11
+ }
12
+ render(json: preload_data, status: :ok)
13
+ end
14
+
15
+ def update
16
+ render(json: {}, status: :accepted)
17
+ end
18
+
19
+ def pause
20
+ render(json: {}, status: :accepted)
21
+ end
22
+
23
+ def resume
24
+ render(json: {}, status: :accepted)
25
+ end
26
+
27
+ def delete
28
+ render(json: {}, status: :accepted)
29
+ end
30
+
31
+ def preview
32
+ placeholder_img = "https://cdn.shopify.com/s/files/1/0533/2089/files/placeholder-images-image_small.png"
33
+ preview_response = {
34
+ "desktop": {
35
+ "preview_url": placeholder_img,
36
+ "content_type": "text/html",
37
+ "width": 360,
38
+ "height": 200
39
+ },
40
+ "mobile": {
41
+ "preview_url": placeholder_img,
42
+ "content_type": "text/html",
43
+ "width": 360,
44
+ "height": 200
45
+ }
46
+ }
47
+ render(json: preview_response, status: :ok)
48
+ end
49
+
50
+ def create
51
+ render(json: {}, status: :ok)
52
+ end
53
+
54
+ def republish
55
+ render(json: {}, status: :accepted)
56
+ end
57
+
58
+ def errors
59
+ request_id = params[:request_id]
60
+ message = params[:message]
61
+
62
+ Rails.logger.info("[Marketing Activity App Error Feedback] Request id: #{request_id}, message: #{message}")
63
+
64
+ render(json: {}, status: :ok)
65
+ end
66
+ end
@@ -11,10 +11,6 @@ module ShopifyApp
11
11
  class_option :embedded, type: :string, default: 'true'
12
12
  class_option :api_version, type: :string, default: nil
13
13
 
14
- def add_dotenv_gem
15
- gem('dotenv-rails', group: [:test, :development])
16
- end
17
-
18
14
  def create_shopify_app_initializer
19
15
  @application_name = format_array_argument(options['application_name'])
20
16
  @scope = format_array_argument(options['scope'])
@@ -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.session_repository = 'ShopifyApp::InMemorySessionStore'
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,6 +4,7 @@ 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,
7
8
  setup: lambda { |env|
8
9
  strategy = env['omniauth.strategy']
9
10
 
@@ -0,0 +1,16 @@
1
+ class CreateUsers < ActiveRecord::Migration[<%= rails_migration_version %>]
2
+ def self.up
3
+ create_table :users do |t|
4
+ t.bigint :shopify_user_id, null: false
5
+ t.string :shopify_domain, null: false
6
+ t.string :shopify_token, null: false
7
+ t.timestamps
8
+ end
9
+
10
+ add_index :users, :shopify_user_id, unique: true
11
+ end
12
+
13
+ def self.down
14
+ drop_table :users
15
+ end
16
+ end
@@ -0,0 +1,7 @@
1
+ class User < ActiveRecord::Base
2
+ include ShopifyApp::SessionStorage
3
+
4
+ def api_version
5
+ ShopifyApp.configuration.api_version
6
+ end
7
+ end
@@ -0,0 +1,4 @@
1
+ regular_user:
2
+ shopify_domain: 'regular-shop.myshopify.com'
3
+ shopify_token: 'token'
4
+ shopify_user_id: 1
@@ -0,0 +1,38 @@
1
+ require 'rails/generators/base'
2
+ require 'rails/generators/active_record'
3
+
4
+ module ShopifyApp
5
+ module Generators
6
+ class UserModelGenerator < Rails::Generators::Base
7
+ include Rails::Generators::Migration
8
+ source_root File.expand_path('../templates', __FILE__)
9
+
10
+ def create_user_model
11
+ copy_file 'user.rb', 'app/models/user.rb'
12
+ end
13
+
14
+ def create_user_migration
15
+ migration_template 'db/migrate/create_users.erb', 'db/migrate/create_users.rb'
16
+ end
17
+
18
+ def update_shopify_app_initializer
19
+ gsub_file 'config/initializers/shopify_app.rb', 'ShopifyApp::InMemorySessionStore', 'User'
20
+ end
21
+
22
+ def create_user_fixtures
23
+ copy_file 'users.yml', 'test/fixtures/users.yml'
24
+ end
25
+
26
+ private
27
+
28
+ def rails_migration_version
29
+ Rails.version.match(/\d\.\d/)[0]
30
+ end
31
+
32
+ # for generating a timestamp when using `create_migration`
33
+ def self.next_migration_number(dir)
34
+ ActiveRecord::Generators::Base.next_migration_number(dir)
35
+ end
36
+ end
37
+ end
38
+ end
@@ -40,7 +40,12 @@ module ShopifyApp
40
40
  require 'shopify_app/managers/webhooks_manager'
41
41
  require 'shopify_app/managers/scripttags_manager'
42
42
 
43
+ # middleware
44
+ require 'shopify_app/middleware/same_site_cookie_middleware'
45
+
43
46
  # session
47
+ require 'shopify_app/session/storage_strategies/shop_storage_strategy'
48
+ require 'shopify_app/session/storage_strategies/user_storage_strategy'
44
49
  require 'shopify_app/session/session_storage'
45
50
  require 'shopify_app/session/session_repository'
46
51
  require 'shopify_app/session/in_memory_session_store'
@@ -14,7 +14,9 @@ module ShopifyApp
14
14
  attr_accessor :webhooks
15
15
  attr_accessor :scripttags
16
16
  attr_accessor :after_authenticate_job
17
- attr_accessor :session_repository
17
+ attr_reader :session_repository
18
+ attr_accessor :per_user_tokens
19
+ alias_method :per_user_tokens?, :per_user_tokens
18
20
  attr_accessor :api_version
19
21
 
20
22
  # customise urls
@@ -34,11 +36,15 @@ module ShopifyApp
34
36
  # allow namespacing webhook jobs
35
37
  attr_accessor :webhook_jobs_namespace
36
38
 
39
+ # allow enabling of same site none on cookies
40
+ attr_accessor :enable_same_site_none
41
+
37
42
  def initialize
38
43
  @root_url = '/'
39
44
  @myshopify_domain = 'myshopify.com'
40
45
  @scripttags_manager_queue_name = Rails.application.config.active_job.queue_name
41
46
  @webhooks_manager_queue_name = Rails.application.config.active_job.queue_name
47
+ @per_user_tokens = false
42
48
  @disable_webpacker = ENV['SHOPIFY_APP_DISABLE_WEBPACKER'].present?
43
49
  end
44
50
 
@@ -47,13 +53,8 @@ module ShopifyApp
47
53
  end
48
54
 
49
55
  def session_repository=(klass)
50
- if Rails.configuration.cache_classes
51
- ShopifyApp::SessionRepository.storage = klass
52
- else
53
- ActiveSupport::Reloader.to_prepare do
54
- ShopifyApp::SessionRepository.storage = klass
55
- end
56
- end
56
+ @session_repository = klass
57
+ ShopifyApp::SessionRepository.storage = klass
57
58
  end
58
59
 
59
60
  def has_webhooks?
@@ -63,6 +64,10 @@ module ShopifyApp
63
64
  def has_scripttags?
64
65
  scripttags.present?
65
66
  end
67
+
68
+ def enable_same_site_none
69
+ @enable_same_site_none.nil? ? embedded_app? : @enable_same_site_none
70
+ end
66
71
  end
67
72
 
68
73
  def self.configuration
@@ -27,12 +27,30 @@ module ShopifyApp
27
27
  end
28
28
 
29
29
  def shop_session
30
- return unless session[:shopify]
31
- @shop_session ||= ShopifyApp::SessionRepository.retrieve(session[:shopify])
30
+ if ShopifyApp.configuration.per_user_tokens?
31
+ return unless session[:shopify_user]
32
+ @shop_session ||= ShopifyApp::SessionRepository.retrieve(session[:shopify_user]['id'])
33
+ else
34
+ return unless session[:shopify]
35
+ @shop_session ||= ShopifyApp::SessionRepository.retrieve(session[:shopify])
36
+ end
32
37
  end
33
38
 
34
- def login_again_if_different_shop
39
+ 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
43
+
44
+ if valid_session_data && sessions_do_not_match
45
+ clear_session = true
46
+ end
47
+ end
48
+
35
49
  if shop_session && params[:shop] && params[:shop].is_a?(String) && (shop_session.domain != params[:shop])
50
+ clear_session = true
51
+ end
52
+
53
+ if clear_session
36
54
  clear_shop_session
37
55
  redirect_to_login
38
56
  end
@@ -60,6 +78,7 @@ module ShopifyApp
60
78
  session[:shopify] = nil
61
79
  session[:shopify_domain] = nil
62
80
  session[:shopify_user] = nil
81
+ session[:user_session] = nil
63
82
  end
64
83
 
65
84
  def login_url_with_optional_shop(top_level: false)
@@ -12,5 +12,9 @@ module ShopifyApp
12
12
  storage_access.svg
13
13
  ]
14
14
  end
15
+
16
+ initializer "shopify_app.middleware" do |app|
17
+ app.config.middleware.insert_before(ActionDispatch::Cookies, ShopifyApp::SameSiteCookieMiddleware)
18
+ end
15
19
  end
16
20
  end
@@ -0,0 +1,60 @@
1
+ module ShopifyApp
2
+ class SameSiteCookieMiddleware
3
+ def initialize(app)
4
+ @app = app
5
+ end
6
+
7
+ def call(env)
8
+ _status, headers, _body = @app.call(env)
9
+ ensure
10
+ user_agent = env['HTTP_USER_AGENT']
11
+
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")
20
+ end
21
+ end
22
+ end
23
+ end
24
+
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
54
+
55
+ return digits[0] > major if digits[0] != major
56
+ return digits[1] > minor if digits[1] != minor
57
+ digits[2] >= build
58
+ end
59
+ end
60
+ end
@@ -6,7 +6,7 @@ module ShopifyApp
6
6
  repo[id]
7
7
  end
8
8
 
9
- def self.store(session)
9
+ def self.store(session, *args)
10
10
  id = SecureRandom.uuid
11
11
  repo[id] = session
12
12
  id
@@ -15,8 +15,8 @@ module ShopifyApp
15
15
  storage.retrieve(id)
16
16
  end
17
17
 
18
- def store(session)
19
- storage.store(session)
18
+ def store(session, *args)
19
+ storage.store(session, *args)
20
20
  end
21
21
 
22
22
  def storage
@@ -3,9 +3,12 @@ module ShopifyApp
3
3
  extend ActiveSupport::Concern
4
4
 
5
5
  included do
6
- validates :shopify_domain, presence: true, uniqueness: { case_sensitive: false }
7
6
  validates :shopify_token, presence: true
8
7
  validates :api_version, presence: true
8
+ validates :shopify_domain, presence: true,
9
+ if: Proc.new {|_| ShopifyApp.configuration.per_user_tokens? }
10
+ validates :shopify_domain, presence: true, uniqueness: { case_sensitive: false },
11
+ if: Proc.new {|_| !ShopifyApp.configuration.per_user_tokens? }
9
12
  end
10
13
 
11
14
  def with_shopify_session(&block)
@@ -18,23 +21,19 @@ module ShopifyApp
18
21
  end
19
22
 
20
23
  class_methods do
21
- def store(session)
22
- shop = find_or_initialize_by(shopify_domain: session.domain)
23
- shop.shopify_token = session.token
24
- shop.save!
25
- shop.id
24
+
25
+ def strategy_klass
26
+ ShopifyApp.configuration.per_user_tokens? ?
27
+ ShopifyApp::SessionStorage::UserStorageStrategy :
28
+ ShopifyApp::SessionStorage::ShopStorageStrategy
26
29
  end
27
30
 
28
- def retrieve(id)
29
- return unless id
31
+ def store(auth_session, user: nil)
32
+ strategy_klass.store(auth_session, user)
33
+ end
30
34
 
31
- if shop = self.find_by(id: id)
32
- ShopifyAPI::Session.new(
33
- domain: shop.shopify_domain,
34
- token: shop.shopify_token,
35
- api_version: shop.api_version
36
- )
37
- end
35
+ def retrieve(id)
36
+ strategy_klass.retrieve(id)
38
37
  end
39
38
  end
40
39
  end
@@ -0,0 +1,24 @@
1
+ module ShopifyApp
2
+ module SessionStorage
3
+ class ShopStorageStrategy
4
+
5
+ def self.store(auth_session, *args)
6
+ shop = Shop.find_or_initialize_by(shopify_domain: auth_session.domain)
7
+ shop.shopify_token = auth_session.token
8
+ shop.save!
9
+ shop.id
10
+ end
11
+
12
+ def self.retrieve(id)
13
+ return unless id
14
+ if shop = Shop.find_by(id: id)
15
+ ShopifyAPI::Session.new(
16
+ domain: shop.shopify_domain,
17
+ token: shop.shopify_token,
18
+ api_version: shop.api_version
19
+ )
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,26 @@
1
+ module ShopifyApp
2
+ module SessionStorage
3
+ class UserStorageStrategy
4
+
5
+ def self.store(auth_session, user)
6
+ user = User.find_or_initialize_by(shopify_user_id: user[:id])
7
+ user.shopify_token = auth_session.token
8
+ user.shopify_domain = auth_session.domain
9
+ user.save!
10
+ user.id
11
+ end
12
+
13
+ def self.retrieve(id)
14
+ return unless id
15
+ if user = User.find_by(shopify_user_id: id)
16
+ ShopifyAPI::Session.new(
17
+ domain: user.shopify_domain,
18
+ token: user.shopify_token,
19
+ api_version: user.api_version
20
+ )
21
+ end
22
+ end
23
+
24
+ end
25
+ end
26
+ end