shopify_app 17.0.1 → 17.1.0

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 (57) hide show
  1. checksums.yaml +4 -4
  2. data/.github/ISSUE_TEMPLATE/bug-report.md +63 -0
  3. data/.github/ISSUE_TEMPLATE/config.yml +1 -0
  4. data/.github/ISSUE_TEMPLATE/feature-request.md +33 -0
  5. data/.github/PULL_REQUEST_TEMPLATE.md +17 -1
  6. data/.github/workflows/release.yml +6 -2
  7. data/CHANGELOG.md +21 -0
  8. data/CONTRIBUTING.md +76 -0
  9. data/Gemfile.lock +63 -63
  10. data/README.md +72 -593
  11. data/app/controllers/concerns/shopify_app/ensure_authenticated_links.rb +4 -0
  12. data/app/controllers/concerns/shopify_app/shop_access_scopes_verification.rb +32 -0
  13. data/app/controllers/shopify_app/callback_controller.rb +35 -4
  14. data/app/controllers/shopify_app/sessions_controller.rb +13 -4
  15. data/docs/Quickstart.md +15 -77
  16. data/docs/Upgrading.md +110 -0
  17. data/docs/shopify_app/authentication.md +124 -0
  18. data/docs/shopify_app/engine.md +82 -0
  19. data/docs/shopify_app/generators.md +127 -0
  20. data/docs/shopify_app/handling-access-scopes-changes.md +8 -0
  21. data/docs/shopify_app/script-tags.md +28 -0
  22. data/docs/shopify_app/session-repository.md +88 -0
  23. data/docs/shopify_app/testing.md +38 -0
  24. data/docs/shopify_app/webhooks.md +72 -0
  25. data/lib/generators/shopify_app/home_controller/templates/home_controller.rb +2 -0
  26. data/lib/generators/shopify_app/home_controller/templates/unauthenticated_home_controller.rb +1 -0
  27. data/lib/generators/shopify_app/install/install_generator.rb +30 -1
  28. data/lib/generators/shopify_app/install/templates/omniauth.rb +1 -0
  29. data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +20 -14
  30. data/lib/generators/shopify_app/install/templates/shopify_provider.rb.tt +8 -0
  31. data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +27 -0
  32. data/lib/generators/shopify_app/shop_model/templates/db/migrate/add_shop_access_scopes_column.erb +5 -0
  33. data/lib/generators/shopify_app/shop_model/templates/shop.rb +1 -1
  34. data/lib/generators/shopify_app/shopify_app_generator.rb +1 -1
  35. data/lib/generators/shopify_app/user_model/templates/db/migrate/add_user_access_scopes_column.erb +5 -0
  36. data/lib/generators/shopify_app/user_model/templates/user.rb +1 -1
  37. data/lib/generators/shopify_app/user_model/user_model_generator.rb +27 -0
  38. data/lib/shopify_app.rb +10 -0
  39. data/lib/shopify_app/access_scopes/noop_strategy.rb +13 -0
  40. data/lib/shopify_app/access_scopes/shop_strategy.rb +24 -0
  41. data/lib/shopify_app/access_scopes/user_strategy.rb +41 -0
  42. data/lib/shopify_app/configuration.rb +22 -0
  43. data/lib/shopify_app/omniauth/omniauth_configuration.rb +64 -0
  44. data/lib/shopify_app/session/in_memory_shop_session_store.rb +9 -7
  45. data/lib/shopify_app/session/in_memory_user_session_store.rb +9 -7
  46. data/lib/shopify_app/session/shop_session_storage_with_scopes.rb +58 -0
  47. data/lib/shopify_app/session/user_session_storage_with_scopes.rb +58 -0
  48. data/lib/shopify_app/utils.rb +12 -0
  49. data/lib/shopify_app/version.rb +1 -1
  50. data/package.json +1 -1
  51. data/shopify_app.gemspec +1 -1
  52. data/yarn.lock +70 -101
  53. metadata +27 -8
  54. data/.github/ISSUE_TEMPLATE.md +0 -19
  55. data/docs/install-on-dev-shop.png +0 -0
  56. data/docs/test-your-app.png +0 -0
  57. data/lib/generators/shopify_app/install/templates/shopify_provider.rb +0 -20
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class HomeController < AuthenticatedController
4
+ include ShopifyApp::ShopAccessScopesVerification
5
+
4
6
  def index
5
7
  @products = ShopifyAPI::Product.find(:all, params: { limit: 10 })
6
8
  @webhooks = ShopifyAPI::Webhook.find(:all)
@@ -3,6 +3,7 @@
3
3
  class HomeController < ApplicationController
4
4
  include ShopifyApp::EmbeddedApp
5
5
  include ShopifyApp::RequireKnownShop
6
+ include ShopifyApp::ShopAccessScopesVerification
6
7
 
7
8
  def index
8
9
  @shop_origin = current_shopify_domain
@@ -30,9 +30,11 @@ module ShopifyApp
30
30
  copy_file('omniauth.rb', 'config/initializers/omniauth.rb')
31
31
  end
32
32
 
33
+ return if !Rails.env.test? && shopify_provider_exists?
34
+
33
35
  inject_into_file(
34
36
  'config/initializers/omniauth.rb',
35
- File.read(File.expand_path(find_in_source_paths('shopify_provider.rb'))),
37
+ shopify_provider_template,
36
38
  after: "Rails.application.config.middleware.use(OmniAuth::Builder) do\n"
37
39
  )
38
40
  end
@@ -72,6 +74,33 @@ module ShopifyApp
72
74
 
73
75
  private
74
76
 
77
+ def shopify_provider_exists?
78
+ File.open("config/initializers/omniauth.rb") do |file|
79
+ file.each_line do |line|
80
+ if line =~ /provider :shopify/
81
+ puts "\e[33m#{omniauth_warning}\e[0m"
82
+ return true
83
+ end
84
+ end
85
+ end
86
+ false
87
+ end
88
+
89
+ def omniauth_warning
90
+ <<~OMNIAUTH
91
+ \n[WARNING] The Shopify App generator attempted to add the following Shopify Omniauth \
92
+ provider 'config/initializers/omniauth.rb':
93
+
94
+ \e[0m#{shopify_provider_template}\e[33m
95
+
96
+ Consider updating 'config/initializers/omniauth.rb' to match the configuration above.
97
+ OMNIAUTH
98
+ end
99
+
100
+ def shopify_provider_template
101
+ File.read(File.expand_path(find_in_source_paths('shopify_provider.rb.tt')))
102
+ end
103
+
75
104
  def embedded_app?
76
105
  options['embedded'] == 'true'
77
106
  end
@@ -1,3 +1,4 @@
1
1
  # frozen_string_literal: true
2
+
2
3
  Rails.application.config.middleware.use(OmniAuth::Builder) do
3
4
  end
@@ -1,17 +1,23 @@
1
- unless defined? Rails::Generators
2
- ShopifyApp.configure do |config|
3
- config.application_name = "<%= @application_name %>"
4
- config.api_key = ENV.fetch('SHOPIFY_API_KEY', '').presence || raise('Missing SHOPIFY_API_KEY. See https://github.com/Shopify/shopify_app#api-keys')
5
- config.secret = ENV.fetch('SHOPIFY_API_SECRET', '').presence || raise('Missing SHOPIFY_API_SECRET. See https://github.com/Shopify/shopify_app#api-keys')
6
- config.old_secret = "<%= @old_secret %>"
7
- config.scope = "<%= @scope %>" # Consult this page for more scope options:
8
- # https://help.shopify.com/en/api/getting-started/authentication/oauth/scopes
9
- config.embedded_app = <%= embedded_app? %>
10
- config.after_authenticate_job = false
11
- config.api_version = "<%= @api_version %>"
12
- config.shop_session_repository = 'Shop'
13
- config.allow_jwt_authentication = <%= !with_cookie_authentication? %>
14
- config.allow_cookie_authentication = <%= with_cookie_authentication? %>
1
+ ShopifyApp.configure do |config|
2
+ config.application_name = "<%= @application_name %>"
3
+ config.old_secret = "<%= @old_secret %>"
4
+ config.scope = "<%= @scope %>" # Consult this page for more scope options:
5
+ # https://help.shopify.com/en/api/getting-started/authentication/oauth/scopes
6
+ config.embedded_app = <%= embedded_app? %>
7
+ config.after_authenticate_job = false
8
+ config.api_version = "<%= @api_version %>"
9
+ config.shop_session_repository = 'Shop'
10
+
11
+ config.reauth_on_access_scope_changes = true
12
+
13
+ config.allow_jwt_authentication = <%= !with_cookie_authentication? %>
14
+ config.allow_cookie_authentication = <%= with_cookie_authentication? %>
15
+
16
+ config.api_key = ENV.fetch('SHOPIFY_API_KEY', '').presence
17
+ config.secret = ENV.fetch('SHOPIFY_API_SECRET', '').presence
18
+ if defined? Rails::Server
19
+ raise('Missing SHOPIFY_API_KEY. See https://github.com/Shopify/shopify_app#requirements') unless config.api_key
20
+ raise('Missing SHOPIFY_API_SECRET. See https://github.com/Shopify/shopify_app#requirements') unless config.secret
15
21
  end
16
22
  end
17
23
 
@@ -0,0 +1,8 @@
1
+ provider :shopify,
2
+ ShopifyApp.configuration.api_key,
3
+ ShopifyApp.configuration.secret,
4
+ scope: ShopifyApp.configuration.scope,
5
+ setup: lambda { |env|
6
+ configuration = ShopifyApp::OmniAuthConfiguration.new(env['omniauth.strategy'], Rack::Request.new(env))
7
+ configuration.build_options
8
+ }
@@ -8,6 +8,8 @@ module ShopifyApp
8
8
  include Rails::Generators::Migration
9
9
  source_root File.expand_path('../templates', __FILE__)
10
10
 
11
+ class_option :new_shopify_cli_app, type: :boolean, default: false
12
+
11
13
  def create_shop_model
12
14
  copy_file('shop.rb', 'app/models/shop.rb')
13
15
  end
@@ -16,6 +18,27 @@ module ShopifyApp
16
18
  migration_template('db/migrate/create_shops.erb', 'db/migrate/create_shops.rb')
17
19
  end
18
20
 
21
+ def create_shop_with_access_scopes_migration
22
+ scopes_column_prompt = <<~PROMPT
23
+ It is highly recommended that apps record the access scopes granted by \
24
+ merchants during app installation. See app/models/shop.rb to modify how \
25
+ access scopes are stored and retrieved.
26
+
27
+ [WARNING] You will need to update the access_scopes accessors in the Shop model \
28
+ to allow shopify_app to store and retrieve scopes when going through OAuth.
29
+
30
+ The following migration will add an `access_scopes` column to the Shop model. \
31
+ Do you want to include this migration? [y/n]
32
+ PROMPT
33
+
34
+ if new_shopify_cli_app? || Rails.env.test? || yes?(scopes_column_prompt)
35
+ migration_template(
36
+ 'db/migrate/add_shop_access_scopes_column.erb',
37
+ 'db/migrate/add_shop_access_scopes_column.rb'
38
+ )
39
+ end
40
+ end
41
+
19
42
  def update_shopify_app_initializer
20
43
  gsub_file('config/initializers/shopify_app.rb', 'ShopifyApp::InMemoryShopSessionStore', 'Shop')
21
44
  end
@@ -26,6 +49,10 @@ module ShopifyApp
26
49
 
27
50
  private
28
51
 
52
+ def new_shopify_cli_app?
53
+ options['new_shopify_cli_app']
54
+ end
55
+
29
56
  def rails_migration_version
30
57
  Rails.version.match(/\d\.\d/)[0]
31
58
  end
@@ -0,0 +1,5 @@
1
+ class AddShopAccessScopesColumn < ActiveRecord::Migration[<%= rails_migration_version %>]
2
+ def change
3
+ add_column :shops, :access_scopes, :string
4
+ end
5
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  class Shop < ActiveRecord::Base
3
- include ShopifyApp::ShopSessionStorage
3
+ include ShopifyApp::ShopSessionStorageWithScopes
4
4
 
5
5
  def api_version
6
6
  ShopifyApp.configuration.api_version
@@ -9,7 +9,7 @@ module ShopifyApp
9
9
 
10
10
  def run_all_generators
11
11
  generate("shopify_app:install #{@opts.join(' ')}")
12
- generate("shopify_app:shop_model")
12
+ generate("shopify_app:shop_model #{@opts.join(' ')}")
13
13
  generate("shopify_app:authenticated_controller")
14
14
  generate("shopify_app:home_controller #{@opts.join(' ')}")
15
15
  end
@@ -0,0 +1,5 @@
1
+ class AddUserAccessScopesColumn < ActiveRecord::Migration[<%= rails_migration_version %>]
2
+ def change
3
+ add_column :users, :access_scopes, :string
4
+ end
5
+ end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
  class User < ActiveRecord::Base
3
- include ShopifyApp::UserSessionStorage
3
+ include ShopifyApp::UserSessionStorageWithScopes
4
4
 
5
5
  def api_version
6
6
  ShopifyApp.configuration.api_version
@@ -8,6 +8,8 @@ module ShopifyApp
8
8
  include Rails::Generators::Migration
9
9
  source_root File.expand_path('../templates', __FILE__)
10
10
 
11
+ class_option :new_shopify_cli_app, type: :boolean, default: false
12
+
11
13
  def create_user_model
12
14
  copy_file('user.rb', 'app/models/user.rb')
13
15
  end
@@ -16,6 +18,27 @@ module ShopifyApp
16
18
  migration_template('db/migrate/create_users.erb', 'db/migrate/create_users.rb')
17
19
  end
18
20
 
21
+ def create_scopes_storage_in_user_model
22
+ scopes_column_prompt = <<~PROMPT
23
+ It is highly recommended that apps record the access scopes granted by \
24
+ merchants during app installation. See app/models/user.rb to modify how \
25
+ access scopes are stored and retrieved.
26
+
27
+ [WARNING] You will need to update the access_scopes accessors in the User model \
28
+ to allow shopify_app to store and retrieve scopes when going through OAuth.
29
+
30
+ The following migration will add an `access_scopes` column to the User model. \
31
+ Do you want to include this migration? [y/n]
32
+ PROMPT
33
+
34
+ if new_shopify_cli_app? || Rails.env.test? || yes?(scopes_column_prompt)
35
+ migration_template(
36
+ 'db/migrate/add_user_access_scopes_column.erb',
37
+ 'db/migrate/add_user_access_scopes_column.rb'
38
+ )
39
+ end
40
+ end
41
+
19
42
  def update_shopify_app_initializer
20
43
  gsub_file('config/initializers/shopify_app.rb', 'ShopifyApp::InMemoryUserSessionStore', 'User')
21
44
  end
@@ -26,6 +49,10 @@ module ShopifyApp
26
49
 
27
50
  private
28
51
 
52
+ def new_shopify_cli_app?
53
+ options['new_shopify_cli_app']
54
+ end
55
+
29
56
  def rails_migration_version
30
57
  Rails.version.match(/\d\.\d/)[0]
31
58
  end
data/lib/shopify_app.rb CHANGED
@@ -57,5 +57,15 @@ module ShopifyApp
57
57
  require 'shopify_app/session/session_repository'
58
58
  require 'shopify_app/session/session_storage'
59
59
  require 'shopify_app/session/shop_session_storage'
60
+ require 'shopify_app/session/shop_session_storage_with_scopes'
60
61
  require 'shopify_app/session/user_session_storage'
62
+ require 'shopify_app/session/user_session_storage_with_scopes'
63
+
64
+ # access scopes strategies
65
+ require 'shopify_app/access_scopes/shop_strategy'
66
+ require 'shopify_app/access_scopes/user_strategy'
67
+ require 'shopify_app/access_scopes/noop_strategy'
68
+
69
+ # omniauth_configuration
70
+ require 'shopify_app/omniauth/omniauth_configuration'
61
71
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module AccessScopes
5
+ class NoopStrategy
6
+ class << self
7
+ def update_access_scopes?(*_args)
8
+ false
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module AccessScopes
5
+ class ShopStrategy
6
+ class << self
7
+ def update_access_scopes?(shop_domain)
8
+ shop_access_scopes = shop_access_scopes(shop_domain)
9
+ configuration_access_scopes != shop_access_scopes
10
+ end
11
+
12
+ private
13
+
14
+ def shop_access_scopes(shop_domain)
15
+ ShopifyApp::SessionRepository.retrieve_shop_session_by_shopify_domain(shop_domain)&.access_scopes
16
+ end
17
+
18
+ def configuration_access_scopes
19
+ ShopifyAPI::ApiAccess.new(ShopifyApp.configuration.shop_access_scopes)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ module AccessScopes
5
+ class UserStrategy
6
+ class InvalidInput < StandardError; end
7
+
8
+ class << self
9
+ def update_access_scopes?(user_id: nil, shopify_user_id: nil)
10
+ return update_access_scopes_for_user_id?(user_id) if user_id
11
+ return update_access_scopes_for_shopify_user_id?(shopify_user_id) if shopify_user_id
12
+ raise(InvalidInput, '#update_access_scopes? requires user_id or shopify_user_id parameter inputs')
13
+ end
14
+
15
+ private
16
+
17
+ def update_access_scopes_for_user_id?(user_id)
18
+ user_access_scopes = user_access_scopes_by_user_id(user_id)
19
+ configuration_access_scopes != user_access_scopes
20
+ end
21
+
22
+ def update_access_scopes_for_shopify_user_id?(shopify_user_id)
23
+ user_access_scopes = user_access_scopes_by_shopify_user_id(shopify_user_id)
24
+ configuration_access_scopes != user_access_scopes
25
+ end
26
+
27
+ def user_access_scopes_by_user_id(user_id)
28
+ ShopifyApp::SessionRepository.retrieve_user_session(user_id)&.access_scopes
29
+ end
30
+
31
+ def user_access_scopes_by_shopify_user_id(shopify_user_id)
32
+ ShopifyApp::SessionRepository.retrieve_user_session_by_shopify_user_id(shopify_user_id)&.access_scopes
33
+ end
34
+
35
+ def configuration_access_scopes
36
+ ShopifyAPI::ApiAccess.new(ShopifyApp.configuration.user_access_scopes)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -9,6 +9,8 @@ module ShopifyApp
9
9
  attr_accessor :secret
10
10
  attr_accessor :old_secret
11
11
  attr_accessor :scope
12
+ attr_writer :shop_access_scopes
13
+ attr_writer :user_access_scopes
12
14
  attr_accessor :embedded_app
13
15
  alias_method :embedded_app?, :embedded_app
14
16
  attr_accessor :webhooks
@@ -16,6 +18,8 @@ module ShopifyApp
16
18
  attr_accessor :after_authenticate_job
17
19
  attr_accessor :api_version
18
20
 
21
+ attr_accessor :reauth_on_access_scope_changes
22
+
19
23
  # customise urls
20
24
  attr_accessor :root_url
21
25
  attr_writer :login_url
@@ -70,6 +74,16 @@ module ShopifyApp
70
74
  ShopifyApp::SessionRepository.shop_storage
71
75
  end
72
76
 
77
+ def shop_access_scopes_strategy
78
+ return ShopifyApp::AccessScopes::NoopStrategy unless reauth_on_access_scope_changes
79
+ ShopifyApp::AccessScopes::ShopStrategy
80
+ end
81
+
82
+ def user_access_scopes_strategy
83
+ return ShopifyApp::AccessScopes::NoopStrategy unless reauth_on_access_scope_changes
84
+ ShopifyApp::AccessScopes::UserStrategy
85
+ end
86
+
73
87
  def has_webhooks?
74
88
  webhooks.present?
75
89
  end
@@ -81,6 +95,14 @@ module ShopifyApp
81
95
  def enable_same_site_none
82
96
  !Rails.env.test? && (@enable_same_site_none.nil? ? embedded_app? : @enable_same_site_none)
83
97
  end
98
+
99
+ def shop_access_scopes
100
+ @shop_access_scopes || scope
101
+ end
102
+
103
+ def user_access_scopes
104
+ @user_access_scopes || scope
105
+ end
84
106
  end
85
107
 
86
108
  def self.configuration
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ShopifyApp
4
+ class OmniAuthConfiguration
5
+ attr_reader :strategy, :request
6
+ attr_writer :client_options_site, :scopes, :per_user_permissions
7
+
8
+ def initialize(strategy, request)
9
+ @strategy = strategy
10
+ @request = request
11
+ end
12
+
13
+ def build_options
14
+ strategy.options[:client_options][:site] = client_options_site
15
+ strategy.options[:scope] = scopes
16
+ strategy.options[:old_client_secret] = ShopifyApp.configuration.old_secret
17
+ strategy.options[:per_user_permissions] = request_online_tokens?
18
+ end
19
+
20
+ private
21
+
22
+ def request_online_tokens?
23
+ return @per_user_permissions unless @per_user_permissions.nil?
24
+ default_request_online_tokens?
25
+ end
26
+
27
+ def scopes
28
+ @scopes || default_scopes
29
+ end
30
+
31
+ def client_options_site
32
+ @client_options_site || default_client_options_site
33
+ end
34
+
35
+ def default_scopes
36
+ if request_online_tokens?
37
+ ShopifyApp.configuration.user_access_scopes
38
+ else
39
+ ShopifyApp.configuration.shop_access_scopes
40
+ end
41
+ end
42
+
43
+ def default_client_options_site
44
+ return '' unless shop_domain.present?
45
+ "https://#{shopify_auth_params[:shop]}"
46
+ end
47
+
48
+ def default_request_online_tokens?
49
+ strategy.session[:user_tokens] && !update_shop_scopes?
50
+ end
51
+
52
+ def update_shop_scopes?
53
+ ShopifyApp.configuration.shop_access_scopes_strategy.update_access_scopes?(shop_domain)
54
+ end
55
+
56
+ def shop_domain
57
+ request.params['shop'] || (shopify_auth_params && shopify_auth_params['shop'])
58
+ end
59
+
60
+ def shopify_auth_params
61
+ strategy.session['shopify.omniauth_params']&.with_indifferent_access
62
+ end
63
+ end
64
+ end