ruby_shopify_app 1.0.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.
- checksums.yaml +7 -0
- data/.babelrc +5 -0
- data/.github/CODEOWNERS +2 -0
- data/.github/ISSUE_TEMPLATE/bug-report.md +63 -0
- data/.github/ISSUE_TEMPLATE/config.yml +1 -0
- data/.github/ISSUE_TEMPLATE/feature-request.md +33 -0
- data/.github/PULL_REQUEST_TEMPLATE.md +22 -0
- data/.github/probots.yml +2 -0
- data/.github/workflows/build.yml +40 -0
- data/.github/workflows/release.yml +24 -0
- data/.github/workflows/rubocop.yml +22 -0
- data/.gitignore +14 -0
- data/.nvmrc +1 -0
- data/.rubocop.yml +18 -0
- data/.ruby-version +1 -0
- data/CHANGELOG-OLD.md +643 -0
- data/CHANGELOG.md +6 -0
- data/CONTRIBUTING.md +81 -0
- data/Gemfile +11 -0
- data/Gemfile.lock +280 -0
- data/LICENSE +19 -0
- data/README.md +132 -0
- data/Rakefile +7 -0
- data/SECURITY.md +59 -0
- data/app/assets/images/storage_access.svg +1 -0
- data/app/assets/javascripts/shopify_app/app_bridge_2.0.12.js +10 -0
- data/app/assets/javascripts/shopify_app/app_bridge_redirect.js +22 -0
- data/app/assets/javascripts/shopify_app/enable_cookies.js +3 -0
- data/app/assets/javascripts/shopify_app/itp_helper.js +40 -0
- data/app/assets/javascripts/shopify_app/partition_cookies.js +8 -0
- data/app/assets/javascripts/shopify_app/post_redirect.js +9 -0
- data/app/assets/javascripts/shopify_app/redirect.js +31 -0
- data/app/assets/javascripts/shopify_app/request_storage_access.js +3 -0
- data/app/assets/javascripts/shopify_app/storage_access.js +148 -0
- data/app/assets/javascripts/shopify_app/storage_access_redirect.js +17 -0
- data/app/assets/javascripts/shopify_app/top_level.js +2 -0
- data/app/assets/javascripts/shopify_app/top_level_interaction.js +11 -0
- data/app/controllers/concerns/shopify_app/authenticated.rb +16 -0
- data/app/controllers/concerns/shopify_app/ensure_authenticated_links.rb +39 -0
- data/app/controllers/concerns/shopify_app/require_known_shop.rb +40 -0
- data/app/controllers/concerns/shopify_app/shop_access_scopes_verification.rb +32 -0
- data/app/controllers/shopify_app/authenticated_controller.rb +8 -0
- data/app/controllers/shopify_app/callback_controller.rb +195 -0
- data/app/controllers/shopify_app/extension_verification_controller.rb +15 -0
- data/app/controllers/shopify_app/sessions_controller.rb +202 -0
- data/app/controllers/shopify_app/webhooks_controller.rb +36 -0
- data/app/views/shopify_app/partials/_button_styles.html.erb +109 -0
- data/app/views/shopify_app/partials/_card_styles.html.erb +33 -0
- data/app/views/shopify_app/partials/_empty_state_styles.html.erb +98 -0
- data/app/views/shopify_app/partials/_form_styles.html.erb +56 -0
- data/app/views/shopify_app/partials/_layout_styles.html.erb +182 -0
- data/app/views/shopify_app/partials/_typography_styles.html.erb +35 -0
- data/app/views/shopify_app/sessions/enable_cookies.html.erb +70 -0
- data/app/views/shopify_app/sessions/new.html.erb +51 -0
- data/app/views/shopify_app/sessions/request_storage_access.html.erb +68 -0
- data/app/views/shopify_app/sessions/top_level_interaction.html.erb +63 -0
- data/app/views/shopify_app/shared/post_redirect_to_auth_shopify.html.erb +13 -0
- data/app/views/shopify_app/shared/redirect.html.erb +23 -0
- data/config/locales/cs.yml +23 -0
- data/config/locales/da.yml +20 -0
- data/config/locales/de.yml +22 -0
- data/config/locales/en.yml +15 -0
- data/config/locales/es.yml +22 -0
- data/config/locales/fi.yml +20 -0
- data/config/locales/fr.yml +23 -0
- data/config/locales/it.yml +21 -0
- data/config/locales/ja.yml +17 -0
- data/config/locales/ko.yml +19 -0
- data/config/locales/nb.yml +21 -0
- data/config/locales/nl.yml +21 -0
- data/config/locales/pl.yml +21 -0
- data/config/locales/pt-BR.yml +21 -0
- data/config/locales/pt-PT.yml +22 -0
- data/config/locales/sv.yml +21 -0
- data/config/locales/th.yml +20 -0
- data/config/locales/tr.yml +22 -0
- data/config/locales/vi.yml +22 -0
- data/config/locales/zh-CN.yml +16 -0
- data/config/locales/zh-TW.yml +16 -0
- data/config/routes.rb +23 -0
- data/docs/Quickstart.md +31 -0
- data/docs/Releasing.md +21 -0
- data/docs/Troubleshooting.md +159 -0
- data/docs/Upgrading.md +132 -0
- data/docs/shopify_app/authentication.md +124 -0
- data/docs/shopify_app/engine.md +82 -0
- data/docs/shopify_app/generators.md +127 -0
- data/docs/shopify_app/handling-access-scopes-changes.md +24 -0
- data/docs/shopify_app/script-tags.md +28 -0
- data/docs/shopify_app/session-repository.md +88 -0
- data/docs/shopify_app/testing.md +38 -0
- data/docs/shopify_app/webhooks.md +72 -0
- data/images/app-proxy-screenshot.png +0 -0
- data/karma.conf.js +44 -0
- data/lib/generators/shopify_app/add_after_authenticate_job/add_after_authenticate_job_generator.rb +47 -0
- data/lib/generators/shopify_app/add_after_authenticate_job/templates/after_authenticate_job.rb +11 -0
- data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +40 -0
- data/lib/generators/shopify_app/add_marketing_activity_extension/templates/marketing_activities_controller.rb +62 -0
- data/lib/generators/shopify_app/add_webhook/add_webhook_generator.rb +69 -0
- data/lib/generators/shopify_app/add_webhook/templates/webhook_job.rb.tt +13 -0
- data/lib/generators/shopify_app/app_proxy_controller/app_proxy_controller_generator.rb +26 -0
- data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_controller.rb +8 -0
- data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_route.rb +11 -0
- data/lib/generators/shopify_app/app_proxy_controller/templates/index.html.erb +19 -0
- data/lib/generators/shopify_app/authenticated_controller/authenticated_controller_generator.rb +15 -0
- data/lib/generators/shopify_app/authenticated_controller/templates/authenticated_controller.rb +5 -0
- data/lib/generators/shopify_app/controllers/controllers_generator.rb +30 -0
- data/lib/generators/shopify_app/home_controller/home_controller_generator.rb +53 -0
- data/lib/generators/shopify_app/home_controller/templates/home_controller.rb +18 -0
- data/lib/generators/shopify_app/home_controller/templates/index.html.erb +75 -0
- data/lib/generators/shopify_app/home_controller/templates/unauthenticated_home_controller.rb +12 -0
- data/lib/generators/shopify_app/install/install_generator.rb +121 -0
- data/lib/generators/shopify_app/install/templates/_flash_messages.html.erb +3 -0
- data/lib/generators/shopify_app/install/templates/embedded_app.html.erb +44 -0
- data/lib/generators/shopify_app/install/templates/flash_messages.js +24 -0
- data/lib/generators/shopify_app/install/templates/omniauth.rb +4 -0
- data/lib/generators/shopify_app/install/templates/session_store.rb +4 -0
- data/lib/generators/shopify_app/install/templates/shopify_app.js +15 -0
- data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +25 -0
- data/lib/generators/shopify_app/install/templates/shopify_app_importmap.js +13 -0
- data/lib/generators/shopify_app/install/templates/shopify_app_index.js +2 -0
- data/lib/generators/shopify_app/install/templates/shopify_provider.rb.tt +8 -0
- data/lib/generators/shopify_app/install/templates/user_agent.rb +6 -0
- data/lib/generators/shopify_app/products_controller/products_controller_generator.rb +19 -0
- data/lib/generators/shopify_app/products_controller/templates/products_controller.rb +8 -0
- data/lib/generators/shopify_app/rotate_shopify_token_job/rotate_shopify_token_job_generator.rb +16 -0
- data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token.rake +17 -0
- data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token_job.rb +42 -0
- data/lib/generators/shopify_app/routes/routes_generator.rb +32 -0
- data/lib/generators/shopify_app/routes/templates/routes.rb +12 -0
- data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +70 -0
- data/lib/generators/shopify_app/shop_model/templates/db/migrate/add_shop_access_scopes_column.erb +5 -0
- data/lib/generators/shopify_app/shop_model/templates/db/migrate/create_shops.erb +15 -0
- data/lib/generators/shopify_app/shop_model/templates/shop.rb +8 -0
- data/lib/generators/shopify_app/shop_model/templates/shops.yml +3 -0
- data/lib/generators/shopify_app/shopify_app_generator.rb +18 -0
- data/lib/generators/shopify_app/user_model/templates/db/migrate/add_user_access_scopes_column.erb +5 -0
- data/lib/generators/shopify_app/user_model/templates/db/migrate/create_users.erb +16 -0
- data/lib/generators/shopify_app/user_model/templates/user.rb +8 -0
- data/lib/generators/shopify_app/user_model/templates/users.yml +4 -0
- data/lib/generators/shopify_app/user_model/user_model_generator.rb +70 -0
- data/lib/generators/shopify_app/views/views_generator.rb +30 -0
- data/lib/shopify_app/access_scopes/noop_strategy.rb +13 -0
- data/lib/shopify_app/access_scopes/shop_strategy.rb +24 -0
- data/lib/shopify_app/access_scopes/user_strategy.rb +41 -0
- data/lib/shopify_app/configuration.rb +119 -0
- data/lib/shopify_app/controller_concerns/app_proxy_verification.rb +38 -0
- data/lib/shopify_app/controller_concerns/csrf_protection.rb +15 -0
- data/lib/shopify_app/controller_concerns/embedded_app.rb +20 -0
- data/lib/shopify_app/controller_concerns/itp.rb +45 -0
- data/lib/shopify_app/controller_concerns/localization.rb +23 -0
- data/lib/shopify_app/controller_concerns/login_protection.rb +259 -0
- data/lib/shopify_app/controller_concerns/payload_verification.rb +24 -0
- data/lib/shopify_app/controller_concerns/webhook_verification.rb +23 -0
- data/lib/shopify_app/engine.rb +47 -0
- data/lib/shopify_app/jobs/scripttags_manager_job.rb +16 -0
- data/lib/shopify_app/jobs/webhooks_manager_job.rb +16 -0
- data/lib/shopify_app/managers/scripttags_manager.rb +78 -0
- data/lib/shopify_app/managers/webhooks_manager.rb +62 -0
- data/lib/shopify_app/middleware/jwt_middleware.rb +43 -0
- data/lib/shopify_app/middleware/same_site_cookie_middleware.rb +34 -0
- data/lib/shopify_app/omniauth/omniauth_configuration.rb +64 -0
- data/lib/shopify_app/session/in_memory_session_store.rb +31 -0
- data/lib/shopify_app/session/in_memory_shop_session_store.rb +16 -0
- data/lib/shopify_app/session/in_memory_user_session_store.rb +16 -0
- data/lib/shopify_app/session/jwt.rb +67 -0
- data/lib/shopify_app/session/null_user_session_store.rb +22 -0
- data/lib/shopify_app/session/session_repository.rb +56 -0
- data/lib/shopify_app/session/session_storage.rb +20 -0
- data/lib/shopify_app/session/shop_session_storage.rb +42 -0
- data/lib/shopify_app/session/shop_session_storage_with_scopes.rb +58 -0
- data/lib/shopify_app/session/user_session_storage.rb +42 -0
- data/lib/shopify_app/session/user_session_storage_with_scopes.rb +58 -0
- data/lib/shopify_app/test_helpers/all.rb +2 -0
- data/lib/shopify_app/test_helpers/webhook_verification_helper.rb +17 -0
- data/lib/shopify_app/utils.rb +37 -0
- data/lib/shopify_app/version.rb +4 -0
- data/lib/shopify_app.rb +80 -0
- data/package.json +27 -0
- data/service.yml +4 -0
- data/shipit.rubygems.yml +4 -0
- data/shopify_app.gemspec +39 -0
- data/translation.yml +7 -0
- data/webpack.config.js +24 -0
- data/yarn.lock +5230 -0
- metadata +465 -0
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShopifyApp
|
|
4
|
+
module Authenticated
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
include ShopifyApp::Localization
|
|
9
|
+
include ShopifyApp::LoginProtection
|
|
10
|
+
include ShopifyApp::CsrfProtection
|
|
11
|
+
include ShopifyApp::EmbeddedApp
|
|
12
|
+
before_action :login_again_if_different_user_or_shop
|
|
13
|
+
around_action :activate_shopify_session
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShopifyApp
|
|
4
|
+
module EnsureAuthenticatedLinks
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
before_action :redirect_to_splash_page, if: :missing_expected_jwt?
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
private
|
|
12
|
+
|
|
13
|
+
def splash_page
|
|
14
|
+
splash_page_with_params(
|
|
15
|
+
return_to: request.fullpath,
|
|
16
|
+
shop: current_shopify_domain,
|
|
17
|
+
host: params[:host]
|
|
18
|
+
)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def splash_page_with_params(params)
|
|
22
|
+
uri = URI(root_path)
|
|
23
|
+
uri.query = params.compact.to_query
|
|
24
|
+
uri.to_s
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def redirect_to_splash_page
|
|
28
|
+
redirect_to(splash_page)
|
|
29
|
+
rescue ShopifyApp::LoginProtection::ShopifyDomainNotFound => error
|
|
30
|
+
Rails.logger.warn("[ShopifyApp::EnsureAuthenticatedLinks] Redirecting to login: [#{error.class}] "\
|
|
31
|
+
"Could not determine current shop domain")
|
|
32
|
+
redirect_to(ShopifyApp.configuration.login_url)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def missing_expected_jwt?
|
|
36
|
+
jwt_shopify_domain.blank?
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShopifyApp
|
|
4
|
+
module RequireKnownShop
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
before_action :check_shop_domain
|
|
9
|
+
before_action :check_shop_known
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def current_shopify_domain
|
|
13
|
+
return if params[:shop].blank?
|
|
14
|
+
@shopify_domain ||= ShopifyApp::Utils.sanitize_shop_domain(params[:shop])
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def check_shop_domain
|
|
20
|
+
redirect_to(ShopifyApp.configuration.login_url) unless current_shopify_domain
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def check_shop_known
|
|
24
|
+
@shop = SessionRepository.retrieve_shop_session_by_shopify_domain(current_shopify_domain)
|
|
25
|
+
redirect_to(shop_login) unless @shop
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def shop_login
|
|
29
|
+
url = URI(ShopifyApp.configuration.login_url)
|
|
30
|
+
|
|
31
|
+
url.query = URI.encode_www_form(
|
|
32
|
+
shop: params[:shop],
|
|
33
|
+
host: params[:host],
|
|
34
|
+
return_to: request.fullpath,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
url.to_s
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShopifyApp
|
|
4
|
+
module ShopAccessScopesVerification
|
|
5
|
+
extend ActiveSupport::Concern
|
|
6
|
+
|
|
7
|
+
included do
|
|
8
|
+
before_action :login_on_scope_changes
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
protected
|
|
12
|
+
|
|
13
|
+
def login_on_scope_changes
|
|
14
|
+
redirect_to(shop_login) if scopes_mismatch?
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def scopes_mismatch?
|
|
20
|
+
ShopifyApp.configuration.shop_access_scopes_strategy.update_access_scopes?(current_shopify_domain)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def current_shopify_domain
|
|
24
|
+
return if params[:shop].blank?
|
|
25
|
+
ShopifyApp::Utils.sanitize_shop_domain(params[:shop])
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def shop_login
|
|
29
|
+
ShopifyApp::Utils.shop_login_url(shop: params[:shop], host: params[:host], return_to: request.fullpath)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShopifyApp
|
|
4
|
+
# Performs login after OAuth completes
|
|
5
|
+
class CallbackController < ActionController::Base
|
|
6
|
+
include ShopifyApp::LoginProtection
|
|
7
|
+
|
|
8
|
+
def callback
|
|
9
|
+
return respond_with_error if invalid_request?
|
|
10
|
+
|
|
11
|
+
store_access_token_and_build_session
|
|
12
|
+
|
|
13
|
+
if start_user_token_flow?
|
|
14
|
+
return respond_with_user_token_flow
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
perform_post_authenticate_jobs
|
|
18
|
+
|
|
19
|
+
respond_successfully
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def respond_successfully
|
|
25
|
+
if jwt_request?
|
|
26
|
+
head(:ok)
|
|
27
|
+
else
|
|
28
|
+
redirect_to(return_address)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def respond_with_user_token_flow
|
|
33
|
+
redirect_to(login_url_with_optional_shop)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def store_access_token_and_build_session
|
|
37
|
+
if native_browser_request?
|
|
38
|
+
reset_session_options
|
|
39
|
+
end
|
|
40
|
+
set_shopify_session
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def invalid_request?
|
|
44
|
+
return true unless auth_hash
|
|
45
|
+
|
|
46
|
+
jwt_request? && !valid_jwt_auth?
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def native_browser_request?
|
|
50
|
+
!jwt_request?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def perform_post_authenticate_jobs
|
|
54
|
+
install_webhooks
|
|
55
|
+
install_scripttags
|
|
56
|
+
perform_after_authenticate_job
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def respond_with_error
|
|
60
|
+
if jwt_request?
|
|
61
|
+
head(:unauthorized)
|
|
62
|
+
else
|
|
63
|
+
flash[:error] = I18n.t('could_not_log_in')
|
|
64
|
+
redirect_to(login_url_with_optional_shop)
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Override user_session_by_cookie from LoginProtection to bypass allow_cookie_authentication
|
|
69
|
+
# setting check because session cookies are justified at top level
|
|
70
|
+
def user_session_by_cookie
|
|
71
|
+
return unless session[:user_id].present?
|
|
72
|
+
ShopifyApp::SessionRepository.retrieve_user_session(session[:user_id])
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def start_user_token_flow?
|
|
76
|
+
if jwt_request?
|
|
77
|
+
false
|
|
78
|
+
else
|
|
79
|
+
return false unless ShopifyApp::SessionRepository.user_storage.present?
|
|
80
|
+
update_user_access_scopes?
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def update_user_access_scopes?
|
|
85
|
+
return true if user_session.blank?
|
|
86
|
+
user_access_scopes_strategy.update_access_scopes?(user_id: session[:user_id])
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def user_access_scopes_strategy
|
|
90
|
+
ShopifyApp.configuration.user_access_scopes_strategy
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def jwt_request?
|
|
94
|
+
jwt_shopify_domain || jwt_shopify_user_id
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
def valid_jwt_auth?
|
|
98
|
+
auth_hash && jwt_shopify_domain == shop_name && jwt_shopify_user_id == associated_user_id
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def auth_hash
|
|
102
|
+
request.env['omniauth.auth']
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def shop_name
|
|
106
|
+
auth_hash.uid
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def offline_access_token
|
|
110
|
+
ShopifyApp::SessionRepository.retrieve_shop_session_by_shopify_domain(shop_name)&.token
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def online_access_token
|
|
114
|
+
ShopifyApp::SessionRepository.retrieve_user_session_by_shopify_user_id(associated_user_id)&.token
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
def associated_user
|
|
118
|
+
return unless auth_hash.dig('extra', 'associated_user').present?
|
|
119
|
+
|
|
120
|
+
auth_hash['extra']['associated_user'].merge('scope' => auth_hash['extra']['associated_user_scope'])
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def associated_user_id
|
|
124
|
+
associated_user && associated_user['id']
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def token
|
|
128
|
+
auth_hash['credentials']['token']
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def access_scopes
|
|
132
|
+
auth_hash.dig('extra', 'scope')
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def reset_session_options
|
|
136
|
+
request.session_options[:renew] = true
|
|
137
|
+
session.delete(:_csrf_token)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def set_shopify_session
|
|
141
|
+
session_store = ShopifyAPI::Session.new(
|
|
142
|
+
domain: shop_name,
|
|
143
|
+
token: token,
|
|
144
|
+
api_version: ShopifyApp.configuration.api_version,
|
|
145
|
+
access_scopes: access_scopes
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
session[:shopify_user] = associated_user
|
|
149
|
+
if session[:shopify_user].present?
|
|
150
|
+
session[:shop_id] = nil if shop_session && shop_session.domain != shop_name
|
|
151
|
+
session[:user_id] = ShopifyApp::SessionRepository.store_user_session(session_store, associated_user)
|
|
152
|
+
else
|
|
153
|
+
session[:shop_id] = ShopifyApp::SessionRepository.store_shop_session(session_store)
|
|
154
|
+
session[:user_id] = nil if user_session && user_session.domain != shop_name
|
|
155
|
+
end
|
|
156
|
+
session[:shopify_domain] = shop_name
|
|
157
|
+
session[:user_session] = auth_hash&.extra&.session
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def install_webhooks
|
|
161
|
+
return unless ShopifyApp.configuration.has_webhooks?
|
|
162
|
+
|
|
163
|
+
WebhooksManager.queue(
|
|
164
|
+
shop_name,
|
|
165
|
+
offline_access_token || online_access_token,
|
|
166
|
+
ShopifyApp.configuration.webhooks
|
|
167
|
+
)
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def install_scripttags
|
|
171
|
+
return unless ShopifyApp.configuration.has_scripttags?
|
|
172
|
+
|
|
173
|
+
ScripttagsManager.queue(
|
|
174
|
+
shop_name,
|
|
175
|
+
offline_access_token || online_access_token,
|
|
176
|
+
ShopifyApp.configuration.scripttags
|
|
177
|
+
)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def perform_after_authenticate_job
|
|
181
|
+
config = ShopifyApp.configuration.after_authenticate_job
|
|
182
|
+
|
|
183
|
+
return unless config && config[:job].present?
|
|
184
|
+
|
|
185
|
+
job = config[:job]
|
|
186
|
+
job = job.constantize if job.is_a?(String)
|
|
187
|
+
|
|
188
|
+
if config[:inline] == true
|
|
189
|
+
job.perform_now(shop_domain: session[:shopify_domain])
|
|
190
|
+
else
|
|
191
|
+
job.perform_later(shop_domain: session[:shopify_domain])
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ShopifyApp
|
|
4
|
+
class ExtensionVerificationController < ActionController::Base
|
|
5
|
+
include ShopifyApp::PayloadVerification
|
|
6
|
+
protect_from_forgery with: :null_session
|
|
7
|
+
before_action :verify_request
|
|
8
|
+
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def verify_request
|
|
12
|
+
head(:unauthorized) unless hmac_valid?(request.body.read)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module ShopifyApp
|
|
3
|
+
class SessionsController < ActionController::Base
|
|
4
|
+
include ShopifyApp::LoginProtection
|
|
5
|
+
|
|
6
|
+
layout false, only: :new
|
|
7
|
+
|
|
8
|
+
after_action only: [:new, :create] do |controller|
|
|
9
|
+
controller.response.headers.except!('X-Frame-Options')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def new
|
|
13
|
+
authenticate if sanitized_shop_name.present?
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def create
|
|
17
|
+
authenticate
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def enable_cookies
|
|
21
|
+
return unless validate_shop_presence
|
|
22
|
+
|
|
23
|
+
render(:enable_cookies, layout: false, locals: {
|
|
24
|
+
does_not_have_storage_access_url: top_level_interaction_path(
|
|
25
|
+
shop: sanitized_shop_name,
|
|
26
|
+
host: host,
|
|
27
|
+
return_to: params[:return_to]
|
|
28
|
+
),
|
|
29
|
+
has_storage_access_url: login_url_with_optional_shop(top_level: true),
|
|
30
|
+
app_target_url: granted_storage_access_path(
|
|
31
|
+
shop: sanitized_shop_name,
|
|
32
|
+
host: host,
|
|
33
|
+
return_to: params[:return_to]
|
|
34
|
+
),
|
|
35
|
+
current_shopify_domain: current_shopify_domain,
|
|
36
|
+
})
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def top_level_interaction
|
|
40
|
+
@url = login_url_with_optional_shop(top_level: true)
|
|
41
|
+
validate_shop_presence
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def granted_storage_access
|
|
45
|
+
return unless validate_shop_presence
|
|
46
|
+
|
|
47
|
+
session['shopify.granted_storage_access'] = true
|
|
48
|
+
|
|
49
|
+
copy_return_to_param_to_session
|
|
50
|
+
|
|
51
|
+
redirect_to(return_address_with_params({ shop: @shop }))
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def destroy
|
|
55
|
+
reset_session
|
|
56
|
+
flash[:notice] = I18n.t('.logged_out')
|
|
57
|
+
redirect_to(login_url_with_optional_shop)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
private
|
|
61
|
+
|
|
62
|
+
def authenticate
|
|
63
|
+
return render_invalid_shop_error unless sanitized_shop_name.present?
|
|
64
|
+
session['shopify.omniauth_params'] = { shop: sanitized_shop_name }
|
|
65
|
+
|
|
66
|
+
copy_return_to_param_to_session
|
|
67
|
+
|
|
68
|
+
set_user_tokens_option
|
|
69
|
+
|
|
70
|
+
if user_agent_can_partition_cookies
|
|
71
|
+
authenticate_with_partitioning
|
|
72
|
+
else
|
|
73
|
+
authenticate_normally
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def authenticate_normally
|
|
78
|
+
if request_storage_access?
|
|
79
|
+
redirect_to_request_storage_access
|
|
80
|
+
elsif authenticate_in_context?
|
|
81
|
+
authenticate_in_context
|
|
82
|
+
else
|
|
83
|
+
authenticate_at_top_level
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def authenticate_with_partitioning
|
|
88
|
+
if session['shopify.cookies_persist']
|
|
89
|
+
clear_top_level_oauth_cookie
|
|
90
|
+
authenticate_in_context
|
|
91
|
+
else
|
|
92
|
+
set_top_level_oauth_cookie
|
|
93
|
+
enable_cookie_access
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# Override shop_session_by_cookie from LoginProtection to bypass allow_cookie_authentication
|
|
98
|
+
# setting check because session cookies are justified at top level
|
|
99
|
+
def shop_session_by_cookie
|
|
100
|
+
return unless session[:shop_id].present?
|
|
101
|
+
ShopifyApp::SessionRepository.retrieve_shop_session(session[:shop_id])
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# rubocop:disable Lint/SuppressedException
|
|
105
|
+
def set_user_tokens_option
|
|
106
|
+
current_shop_session = shop_session
|
|
107
|
+
|
|
108
|
+
if current_shop_session.blank?
|
|
109
|
+
session[:user_tokens] = false
|
|
110
|
+
return
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
session[:user_tokens] = ShopifyApp::SessionRepository.user_storage.present?
|
|
114
|
+
|
|
115
|
+
ShopifyAPI::Session.temp(
|
|
116
|
+
domain: current_shop_session.domain,
|
|
117
|
+
token: current_shop_session.token,
|
|
118
|
+
api_version: current_shop_session.api_version
|
|
119
|
+
) do
|
|
120
|
+
ShopifyAPI::Metafield.find(:token_validity_bogus_check)
|
|
121
|
+
end
|
|
122
|
+
rescue ActiveResource::UnauthorizedAccess
|
|
123
|
+
session[:user_tokens] = false
|
|
124
|
+
rescue StandardError
|
|
125
|
+
end
|
|
126
|
+
# rubocop:enable Lint/SuppressedException
|
|
127
|
+
|
|
128
|
+
def validate_shop_presence
|
|
129
|
+
@shop = sanitized_shop_name
|
|
130
|
+
unless @shop
|
|
131
|
+
render_invalid_shop_error
|
|
132
|
+
return false
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
true
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def copy_return_to_param_to_session
|
|
139
|
+
session[:return_to] = RedirectSafely.make_safe(params[:return_to], '/') if params[:return_to]
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def render_invalid_shop_error
|
|
143
|
+
flash[:error] = I18n.t('invalid_shop_url')
|
|
144
|
+
redirect_to(return_address)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def enable_cookie_access
|
|
148
|
+
fullpage_redirect_to(enable_cookies_path(
|
|
149
|
+
shop: sanitized_shop_name,
|
|
150
|
+
host: host,
|
|
151
|
+
return_to: session[:return_to]
|
|
152
|
+
))
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def authenticate_in_context
|
|
156
|
+
post_redirect_to_auth_shopify
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def post_redirect_to_auth_shopify
|
|
160
|
+
render('shopify_app/shared/post_redirect_to_auth_shopify', layout: false)
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def authenticate_at_top_level
|
|
164
|
+
fullpage_redirect_to(login_url_with_optional_shop(top_level: true))
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def authenticate_in_context?
|
|
168
|
+
return true unless ShopifyApp.configuration.embedded_app?
|
|
169
|
+
params[:top_level]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def request_storage_access?
|
|
173
|
+
return false unless ShopifyApp.configuration.embedded_app?
|
|
174
|
+
return false if params[:top_level]
|
|
175
|
+
return false if user_agent_is_mobile
|
|
176
|
+
return false if user_agent_is_pos
|
|
177
|
+
|
|
178
|
+
!session['shopify.granted_storage_access']
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def redirect_to_request_storage_access
|
|
182
|
+
render(
|
|
183
|
+
:request_storage_access,
|
|
184
|
+
layout: false,
|
|
185
|
+
locals: {
|
|
186
|
+
does_not_have_storage_access_url: top_level_interaction_path(
|
|
187
|
+
shop: sanitized_shop_name,
|
|
188
|
+
host: host,
|
|
189
|
+
return_to: session[:return_to]
|
|
190
|
+
),
|
|
191
|
+
has_storage_access_url: login_url_with_optional_shop(top_level: true),
|
|
192
|
+
app_target_url: granted_storage_access_path(
|
|
193
|
+
shop: sanitized_shop_name,
|
|
194
|
+
host: host,
|
|
195
|
+
return_to: session[:return_to]
|
|
196
|
+
),
|
|
197
|
+
current_shopify_domain: current_shopify_domain,
|
|
198
|
+
}
|
|
199
|
+
)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
module ShopifyApp
|
|
3
|
+
class MissingWebhookJobError < StandardError; end
|
|
4
|
+
|
|
5
|
+
class WebhooksController < ActionController::Base
|
|
6
|
+
include ShopifyApp::WebhookVerification
|
|
7
|
+
|
|
8
|
+
def receive
|
|
9
|
+
params.permit!
|
|
10
|
+
webhook_job_klass.perform_later(shop_domain: shop_domain, webhook: webhook_params.to_h)
|
|
11
|
+
head(:ok)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def webhook_params
|
|
17
|
+
params.except(:controller, :action, :type)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def webhook_job_klass
|
|
21
|
+
webhook_job_klass_name.safe_constantize || raise(ShopifyApp::MissingWebhookJobError)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def webhook_job_klass_name(type = webhook_type)
|
|
25
|
+
[webhook_namespace, "#{type}_job"].compact.join('/').classify
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def webhook_type
|
|
29
|
+
params[:type]
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def webhook_namespace
|
|
33
|
+
ShopifyApp.configuration.webhook_jobs_namespace
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
<style>
|
|
2
|
+
.Polaris-Button {
|
|
3
|
+
position:relative;
|
|
4
|
+
display:-webkit-inline-box;
|
|
5
|
+
display:-ms-inline-flexbox;
|
|
6
|
+
display:inline-flex;
|
|
7
|
+
-webkit-box-align:center;
|
|
8
|
+
-ms-flex-align:center;
|
|
9
|
+
align-items:center;
|
|
10
|
+
-webkit-box-pack:center;
|
|
11
|
+
-ms-flex-pack:center;
|
|
12
|
+
justify-content:center;
|
|
13
|
+
min-height:3.6rem;
|
|
14
|
+
min-width:3.6rem;
|
|
15
|
+
margin:0;
|
|
16
|
+
padding:0.7rem 1.6rem;
|
|
17
|
+
background-color:#ffffff;
|
|
18
|
+
border:1px solid #babfc3;
|
|
19
|
+
border-top-color: #c9cccf;
|
|
20
|
+
border-bottom-color: #babfc4;
|
|
21
|
+
box-shadow:0 1px 0 0 rgba(0, 0, 0, 0.05);
|
|
22
|
+
border-radius:4px;
|
|
23
|
+
line-height:1;
|
|
24
|
+
color:#202223;
|
|
25
|
+
text-align:center;
|
|
26
|
+
cursor:pointer;
|
|
27
|
+
-webkit-user-select:none;
|
|
28
|
+
-moz-user-select:none;
|
|
29
|
+
-ms-user-select:none;
|
|
30
|
+
user-select:none;
|
|
31
|
+
text-decoration:none;
|
|
32
|
+
transition-property:background, border, box-shadow;
|
|
33
|
+
transition-duration:100ms;
|
|
34
|
+
transition-timing-function:cubic-bezier(0.64, 0, 0.35, 1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.Polaris-Button:hover {
|
|
38
|
+
background-color:#f6f6f7;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.Polaris-Button:focus {
|
|
42
|
+
outline:0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.Polaris-Button:focus:after {
|
|
46
|
+
box-shadow:0 0 0 .2rem #448fff;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.Polaris-Button:after {
|
|
50
|
+
content:'';
|
|
51
|
+
position:absolute;
|
|
52
|
+
z-index:1;
|
|
53
|
+
top:-.2rem;
|
|
54
|
+
right:-.2rem;
|
|
55
|
+
bottom:-.2rem;
|
|
56
|
+
left:-.2rem;
|
|
57
|
+
display:block;
|
|
58
|
+
pointer-events:none;
|
|
59
|
+
box-shadow:0 0 0 -.2rem #448fff;
|
|
60
|
+
transition:box-shadow 100ms cubic-bezier(0.64, 0, 0.35, 1);
|
|
61
|
+
border-radius:5px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.Polaris-Button:active {
|
|
65
|
+
background-color:#f1f2f3);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.Polaris-Button__Content {
|
|
69
|
+
font-size:1.4rem;
|
|
70
|
+
font-weight:500;
|
|
71
|
+
line-height:1.6rem;
|
|
72
|
+
text-transform:initial;
|
|
73
|
+
letter-spacing:initial;
|
|
74
|
+
position:relative;
|
|
75
|
+
display:-webkit-box;
|
|
76
|
+
display:-ms-flexbox;
|
|
77
|
+
display:flex;
|
|
78
|
+
-webkit-box-pack:center;
|
|
79
|
+
-ms-flex-pack:center;
|
|
80
|
+
justify-content:center;
|
|
81
|
+
-webkit-box-align:center;
|
|
82
|
+
-ms-flex-align:center;
|
|
83
|
+
align-items:center;
|
|
84
|
+
min-width:1px;
|
|
85
|
+
min-height:1px;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.Polaris-Button--primary {
|
|
89
|
+
background-color:#008060;
|
|
90
|
+
border-color:transparent;
|
|
91
|
+
border-width:0;
|
|
92
|
+
color:white;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
.Polaris-Button--primary:hover {
|
|
96
|
+
background-color:#006e52;
|
|
97
|
+
border-color:transparent;
|
|
98
|
+
color:white;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.Polaris-Button--primary:active {
|
|
102
|
+
background-color:#005e46;
|
|
103
|
+
border-color:transparent;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
.Polaris-Button--sizeLarge {
|
|
107
|
+
padding:1.1rem 2.4rem;
|
|
108
|
+
}
|
|
109
|
+
</style>
|