shopify_app 13.2.0 → 20.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/CODEOWNERS +1 -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/workflows/build.yml +40 -0
- data/.github/workflows/cla.yml +22 -0
- data/.github/workflows/close-waiting-for-response-issues.yml +20 -0
- data/.github/workflows/release.yml +24 -0
- data/.github/workflows/remove-labels-on-activity.yml +16 -0
- data/.github/workflows/rubocop.yml +22 -0
- data/.github/workflows/stale.yml +31 -0
- data/.gitignore +1 -2
- data/.nvmrc +1 -1
- data/.rubocop.yml +2 -0
- data/.ruby-version +1 -1
- data/CHANGELOG.md +221 -0
- data/CONTRIBUTING.md +81 -0
- data/Gemfile +5 -2
- data/Gemfile.lock +248 -0
- data/README.md +74 -563
- data/Rakefile +4 -3
- data/SECURITY.md +59 -0
- data/app/assets/images/storage_access.svg +1 -2
- data/app/assets/javascripts/shopify_app/app_bridge_3.1.1.js +10 -0
- data/app/assets/javascripts/shopify_app/app_bridge_redirect.js +22 -0
- data/app/assets/javascripts/shopify_app/app_bridge_utils_3.1.1.js +1 -0
- data/app/assets/javascripts/shopify_app/post_redirect.js +9 -0
- data/app/assets/javascripts/shopify_app/redirect.js +10 -14
- data/app/assets/javascripts/shopify_app/storage_access.js +5 -10
- data/app/assets/javascripts/shopify_app/top_level_interaction.js +1 -1
- data/app/controllers/concerns/shopify_app/authenticated.rb +4 -0
- data/app/controllers/concerns/shopify_app/ensure_authenticated_links.rb +39 -0
- data/app/controllers/concerns/shopify_app/require_known_shop.rb +48 -0
- data/app/controllers/concerns/shopify_app/shop_access_scopes_verification.rb +40 -0
- data/app/controllers/shopify_app/authenticated_controller.rb +1 -0
- data/app/controllers/shopify_app/callback_controller.rb +56 -77
- data/app/controllers/shopify_app/extension_verification_controller.rb +2 -7
- data/app/controllers/shopify_app/sessions_controller.rb +33 -117
- data/app/controllers/shopify_app/webhooks_controller.rb +5 -26
- data/app/views/shopify_app/partials/_button_styles.html.erb +41 -36
- data/app/views/shopify_app/partials/_card_styles.html.erb +3 -3
- data/app/views/shopify_app/partials/_empty_state_styles.html.erb +28 -59
- data/app/views/shopify_app/partials/_form_styles.html.erb +56 -0
- data/app/views/shopify_app/partials/_layout_styles.html.erb +16 -1
- data/app/views/shopify_app/partials/_typography_styles.html.erb +6 -6
- data/app/views/shopify_app/sessions/enable_cookies.html.erb +2 -7
- data/app/views/shopify_app/sessions/new.html.erb +38 -110
- data/app/views/shopify_app/sessions/request_storage_access.html.erb +12 -12
- data/app/views/shopify_app/sessions/top_level_interaction.html.erb +21 -22
- data/app/views/shopify_app/shared/post_redirect_to_auth_shopify.html.erb +13 -0
- data/app/views/shopify_app/shared/redirect.html.erb +2 -2
- data/config/locales/de.yml +11 -11
- data/config/locales/ja.yml +4 -4
- data/config/locales/nl.yml +2 -2
- data/config/locales/th.yml +4 -4
- data/config/locales/vi.yml +22 -0
- data/config/locales/zh-CN.yml +2 -2
- data/config/routes.rb +20 -12
- data/docs/Quickstart.md +19 -83
- data/docs/Releasing.md +18 -15
- data/docs/Troubleshooting.md +140 -5
- data/docs/Upgrading.md +247 -0
- data/docs/shopify_app/authentication.md +128 -0
- data/docs/shopify_app/content-security-policy.md +10 -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/karma.conf.js +1 -1
- data/lib/generators/shopify_app/add_after_authenticate_job/add_after_authenticate_job_generator.rb +10 -9
- data/lib/generators/shopify_app/add_after_authenticate_job/templates/after_authenticate_job.rb +1 -0
- data/lib/generators/shopify_app/add_marketing_activity_extension/add_marketing_activity_extension_generator.rb +4 -3
- data/lib/generators/shopify_app/add_webhook/add_webhook_generator.rb +15 -14
- data/lib/generators/shopify_app/add_webhook/templates/webhook_job.rb.tt +9 -1
- data/lib/generators/shopify_app/app_proxy_controller/app_proxy_controller_generator.rb +7 -6
- data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_controller.rb +2 -1
- data/lib/generators/shopify_app/app_proxy_controller/templates/app_proxy_route.rb +1 -1
- data/lib/generators/shopify_app/authenticated_controller/authenticated_controller_generator.rb +4 -4
- data/lib/generators/shopify_app/controllers/controllers_generator.rb +5 -4
- data/lib/generators/shopify_app/home_controller/home_controller_generator.rb +27 -4
- data/lib/generators/shopify_app/home_controller/templates/home_controller.rb +12 -2
- data/lib/generators/shopify_app/home_controller/templates/index.html.erb +74 -16
- data/lib/generators/shopify_app/home_controller/templates/unauthenticated_home_controller.rb +16 -0
- data/lib/generators/shopify_app/install/install_generator.rb +52 -40
- data/lib/generators/shopify_app/install/templates/embedded_app.html.erb +5 -2
- data/lib/generators/shopify_app/install/templates/flash_messages.js +0 -2
- data/lib/generators/shopify_app/install/templates/session_store.rb +2 -1
- data/lib/generators/shopify_app/install/templates/shopify_app.js +1 -1
- data/lib/generators/shopify_app/install/templates/shopify_app.rb.tt +43 -5
- data/lib/generators/shopify_app/install/templates/shopify_app_importmap.js +13 -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 +4 -4
- data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token.rake +1 -0
- data/lib/generators/shopify_app/rotate_shopify_token_job/templates/rotate_shopify_token_job.rb +1 -1
- data/lib/generators/shopify_app/routes/routes_generator.rb +6 -5
- data/lib/generators/shopify_app/routes/templates/routes.rb +5 -5
- data/lib/generators/shopify_app/shop_model/shop_model_generator.rb +35 -7
- 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/shop.rb +2 -1
- data/lib/generators/shopify_app/shopify_app_generator.rb +4 -3
- 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/user.rb +2 -1
- data/lib/generators/shopify_app/user_model/user_model_generator.rb +35 -7
- data/lib/generators/shopify_app/views/views_generator.rb +5 -4
- 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 +58 -11
- data/lib/shopify_app/controller_concerns/app_proxy_verification.rb +4 -4
- data/lib/shopify_app/controller_concerns/csrf_protection.rb +16 -0
- data/lib/shopify_app/controller_concerns/embedded_app.rb +6 -3
- data/lib/shopify_app/controller_concerns/ensure_billing.rb +243 -0
- data/lib/shopify_app/controller_concerns/frame_ancestors.rb +16 -0
- data/lib/shopify_app/controller_concerns/itp.rb +3 -3
- data/lib/shopify_app/controller_concerns/localization.rb +1 -0
- data/lib/shopify_app/controller_concerns/login_protection.rb +105 -90
- data/lib/shopify_app/controller_concerns/payload_verification.rb +25 -0
- data/lib/shopify_app/controller_concerns/redirect_for_embedded.rb +36 -0
- data/lib/shopify_app/controller_concerns/sanitized_params.rb +36 -0
- data/lib/shopify_app/controller_concerns/webhook_verification.rb +3 -18
- data/lib/shopify_app/engine.rb +26 -11
- data/lib/shopify_app/errors.rb +34 -0
- data/lib/shopify_app/jobs/scripttags_manager_job.rb +2 -2
- data/lib/shopify_app/jobs/webhooks_manager_job.rb +4 -5
- data/lib/shopify_app/managers/scripttags_manager.rb +12 -6
- data/lib/shopify_app/managers/webhooks_manager.rb +62 -42
- data/lib/shopify_app/middleware/jwt_middleware.rb +6 -3
- data/lib/shopify_app/session/in_memory_session_store.rb +2 -3
- data/lib/shopify_app/session/in_memory_shop_session_store.rb +10 -7
- data/lib/shopify_app/session/in_memory_user_session_store.rb +10 -7
- data/lib/shopify_app/session/jwt.rb +19 -16
- data/lib/shopify_app/session/null_user_session_store.rb +2 -1
- data/lib/shopify_app/session/session_repository.rb +40 -2
- data/lib/shopify_app/session/session_storage.rb +4 -6
- data/lib/shopify_app/session/shop_session_storage.rb +6 -6
- data/lib/shopify_app/session/shop_session_storage_with_scopes.rb +57 -0
- data/lib/shopify_app/session/user_session_storage.rb +20 -7
- data/lib/shopify_app/session/user_session_storage_with_scopes.rb +71 -0
- data/lib/shopify_app/test_helpers/all.rb +2 -1
- data/lib/shopify_app/test_helpers/webhook_verification_helper.rb +4 -3
- data/lib/shopify_app/utils.rb +14 -7
- data/lib/shopify_app/version.rb +2 -1
- data/lib/shopify_app.rb +52 -29
- data/package.json +7 -8
- data/service.yml +1 -5
- data/shopify_app.gemspec +22 -20
- data/translation.yml +1 -1
- data/yarn.lock +2173 -2206
- metadata +110 -56
- data/.github/ISSUE_TEMPLATE.md +0 -14
- data/.github/probots.yml +0 -2
- data/.travis.yml +0 -28
- data/config/locales/hi.yml +0 -23
- data/config/locales/ms.yml +0 -22
- data/docs/install-on-dev-shop.png +0 -0
- data/docs/test-your-app.png +0 -0
- data/lib/generators/shopify_app/install/templates/omniauth.rb +0 -3
- data/lib/generators/shopify_app/install/templates/shopify_provider.rb +0 -20
- data/lib/generators/shopify_app/install/templates/user_agent.rb +0 -6
- data/lib/shopify_app/middleware/same_site_cookie_middleware.rb +0 -34
- data/package-lock.json +0 -7245
@@ -1,62 +1,82 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "uri"
|
4
|
+
|
2
5
|
module ShopifyApp
|
3
6
|
class WebhooksManager
|
4
|
-
class
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
)
|
12
|
-
end
|
7
|
+
class << self
|
8
|
+
def queue(shop_domain, shop_token)
|
9
|
+
ShopifyApp::WebhooksManagerJob.perform_later(
|
10
|
+
shop_domain: shop_domain,
|
11
|
+
shop_token: shop_token
|
12
|
+
)
|
13
|
+
end
|
13
14
|
|
14
|
-
|
15
|
+
def create_webhooks(session:)
|
16
|
+
return unless ShopifyApp.configuration.has_webhooks?
|
15
17
|
|
16
|
-
|
17
|
-
|
18
|
-
end
|
18
|
+
ShopifyAPI::Webhooks::Registry.register_all(session: session)
|
19
|
+
end
|
19
20
|
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
end
|
21
|
+
def recreate_webhooks!(session:)
|
22
|
+
destroy_webhooks
|
23
|
+
return unless ShopifyApp.configuration.has_webhooks?
|
24
24
|
|
25
|
-
|
26
|
-
|
25
|
+
add_registrations
|
26
|
+
ShopifyAPI::Webhooks::Registry.register_all(session: session)
|
27
|
+
end
|
27
28
|
|
28
|
-
|
29
|
-
|
29
|
+
def destroy_webhooks
|
30
|
+
return unless ShopifyApp.configuration.has_webhooks?
|
31
|
+
|
32
|
+
ShopifyApp.configuration.webhooks.each do |attributes|
|
33
|
+
ShopifyAPI::Webhooks::Registry.unregister(topic: attributes[:topic])
|
34
|
+
end
|
30
35
|
end
|
31
|
-
end
|
32
36
|
|
33
|
-
|
34
|
-
|
35
|
-
|
37
|
+
def add_registrations
|
38
|
+
return unless ShopifyApp.configuration.has_webhooks?
|
39
|
+
|
40
|
+
ShopifyApp.configuration.webhooks.each do |attributes|
|
41
|
+
webhook_path = path(attributes)
|
42
|
+
|
43
|
+
ShopifyAPI::Webhooks::Registry.add_registration(
|
44
|
+
topic: attributes[:topic],
|
45
|
+
delivery_method: attributes[:delivery_method] || :http,
|
46
|
+
path: webhook_path,
|
47
|
+
handler: webhook_job_klass(webhook_path),
|
48
|
+
fields: attributes[:fields]
|
49
|
+
)
|
50
|
+
end
|
36
51
|
end
|
37
52
|
|
38
|
-
|
39
|
-
end
|
53
|
+
private
|
40
54
|
|
41
|
-
|
55
|
+
def path(webhook_attributes)
|
56
|
+
path = webhook_attributes[:path]
|
57
|
+
address = webhook_attributes[:address]
|
58
|
+
uri = URI(address) if address
|
42
59
|
|
43
|
-
|
44
|
-
|
45
|
-
|
60
|
+
if path.present?
|
61
|
+
path
|
62
|
+
elsif uri&.path&.present?
|
63
|
+
uri.path
|
64
|
+
else
|
65
|
+
raise ::ShopifyApp::MissingWebhookJobError,
|
66
|
+
"The :path attribute is required for webhook registration."
|
67
|
+
end
|
68
|
+
end
|
46
69
|
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
raise CreationFailed, webhook.errors.full_messages.to_sentence unless webhook.persisted?
|
51
|
-
webhook
|
52
|
-
end
|
70
|
+
def webhook_job_klass(path)
|
71
|
+
webhook_job_klass_name(path).safe_constantize || raise(::ShopifyApp::MissingWebhookJobError)
|
72
|
+
end
|
53
73
|
|
54
|
-
|
55
|
-
|
56
|
-
end
|
74
|
+
def webhook_job_klass_name(path)
|
75
|
+
job_file_name = Pathname(path.to_s).basename
|
57
76
|
|
58
|
-
|
59
|
-
|
77
|
+
[ShopifyApp.configuration.webhook_jobs_namespace,
|
78
|
+
"#{job_file_name}_job",].compact.join("/").classify
|
79
|
+
end
|
60
80
|
end
|
61
81
|
end
|
62
82
|
end
|
@@ -1,3 +1,5 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
1
3
|
module ShopifyApp
|
2
4
|
class JWTMiddleware
|
3
5
|
TOKEN_REGEX = /^Bearer\s+(.*?)$/
|
@@ -23,7 +25,7 @@ module ShopifyApp
|
|
23
25
|
end
|
24
26
|
|
25
27
|
def authorization_header(env)
|
26
|
-
env[
|
28
|
+
env["HTTP_AUTHORIZATION"]
|
27
29
|
end
|
28
30
|
|
29
31
|
def extract_token(env)
|
@@ -34,8 +36,9 @@ module ShopifyApp
|
|
34
36
|
def set_env_variables(token, env)
|
35
37
|
jwt = ShopifyApp::JWT.new(token)
|
36
38
|
|
37
|
-
env[
|
38
|
-
env[
|
39
|
+
env["jwt.shopify_domain"] = jwt.shopify_domain
|
40
|
+
env["jwt.shopify_user_id"] = jwt.shopify_user_id
|
41
|
+
env["jwt.expire_at"] = jwt.expire_at
|
39
42
|
end
|
40
43
|
end
|
41
44
|
end
|
@@ -1,10 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module ShopifyApp
|
3
4
|
# rubocop:disable Style/ClassVars
|
4
5
|
# Class var repo is needed here in order to share data between the 2 child classes.
|
5
6
|
class InMemorySessionStore
|
6
|
-
class EnvironmentError < StandardError; end
|
7
|
-
|
8
7
|
def self.retrieve(id)
|
9
8
|
repo[id]
|
10
9
|
end
|
@@ -21,7 +20,7 @@ module ShopifyApp
|
|
21
20
|
|
22
21
|
def self.repo
|
23
22
|
if Rails.env.production?
|
24
|
-
raise EnvironmentError, "Cannot use InMemorySessionStore in a Production environment. \
|
23
|
+
raise ::ShopifyApp::EnvironmentError, "Cannot use InMemorySessionStore in a Production environment. \
|
25
24
|
Please initialize ShopifyApp with a model that can store and retrieve sessions"
|
26
25
|
end
|
27
26
|
@@repo ||= {}
|
@@ -1,14 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module ShopifyApp
|
3
4
|
class InMemoryShopSessionStore < InMemorySessionStore
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
class << self
|
6
|
+
def store(session, *args)
|
7
|
+
id = super
|
8
|
+
repo[session.shop] = session
|
9
|
+
id
|
10
|
+
end
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
+
def retrieve_by_shopify_domain(shopify_domain)
|
13
|
+
repo[shopify_domain]
|
14
|
+
end
|
12
15
|
end
|
13
16
|
end
|
14
17
|
end
|
@@ -1,14 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module ShopifyApp
|
3
4
|
class InMemoryUserSessionStore < InMemorySessionStore
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
class << self
|
6
|
+
def store(session, user)
|
7
|
+
id = super
|
8
|
+
repo[user.shopify_user_id] = session
|
9
|
+
id
|
10
|
+
end
|
9
11
|
|
10
|
-
|
11
|
-
|
12
|
+
def retrieve_by_shopify_user_id(user_id)
|
13
|
+
repo[user_id]
|
14
|
+
end
|
12
15
|
end
|
13
16
|
end
|
14
17
|
end
|
@@ -1,18 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module ShopifyApp
|
3
4
|
class JWT
|
4
|
-
class InvalidDestinationError < StandardError; end
|
5
|
-
class MismatchedHostsError < StandardError; end
|
6
|
-
class InvalidAudienceError < StandardError; end
|
7
|
-
|
8
5
|
WARN_EXCEPTIONS = [
|
9
6
|
::JWT::DecodeError,
|
10
7
|
::JWT::ExpiredSignature,
|
11
8
|
::JWT::ImmatureSignature,
|
12
9
|
::JWT::VerificationError,
|
13
|
-
InvalidAudienceError,
|
14
|
-
InvalidDestinationError,
|
15
|
-
MismatchedHostsError,
|
10
|
+
::ShopifyApp::InvalidAudienceError,
|
11
|
+
::ShopifyApp::InvalidDestinationError,
|
12
|
+
::ShopifyApp::MismatchedHostsError,
|
16
13
|
]
|
17
14
|
|
18
15
|
def initialize(token)
|
@@ -21,11 +18,15 @@ module ShopifyApp
|
|
21
18
|
end
|
22
19
|
|
23
20
|
def shopify_domain
|
24
|
-
@payload && ShopifyApp::Utils.sanitize_shop_domain(@payload[
|
21
|
+
@payload && ShopifyApp::Utils.sanitize_shop_domain(@payload["dest"])
|
25
22
|
end
|
26
23
|
|
27
24
|
def shopify_user_id
|
28
|
-
@payload && @payload[
|
25
|
+
@payload["sub"].to_i if @payload && @payload["sub"]
|
26
|
+
end
|
27
|
+
|
28
|
+
def expire_at
|
29
|
+
@payload["exp"].to_i if @payload && @payload["exp"]
|
29
30
|
end
|
30
31
|
|
31
32
|
private
|
@@ -39,21 +40,23 @@ module ShopifyApp
|
|
39
40
|
end
|
40
41
|
|
41
42
|
def parse_token_data(secret, old_secret)
|
42
|
-
::JWT.decode(@token, secret, true, { algorithm:
|
43
|
+
::JWT.decode(@token, secret, true, { algorithm: "HS256" })
|
43
44
|
rescue ::JWT::VerificationError
|
44
45
|
raise unless old_secret
|
45
46
|
|
46
|
-
::JWT.decode(@token, old_secret, true, { algorithm:
|
47
|
+
::JWT.decode(@token, old_secret, true, { algorithm: "HS256" })
|
47
48
|
end
|
48
49
|
|
49
50
|
def validate_payload(payload)
|
50
|
-
dest_host = ShopifyApp::Utils.sanitize_shop_domain(payload[
|
51
|
-
iss_host = ShopifyApp::Utils.sanitize_shop_domain(payload[
|
51
|
+
dest_host = ShopifyApp::Utils.sanitize_shop_domain(payload["dest"])
|
52
|
+
iss_host = ShopifyApp::Utils.sanitize_shop_domain(payload["iss"])
|
52
53
|
api_key = ShopifyApp.configuration.api_key
|
53
54
|
|
54
|
-
raise InvalidAudienceError,
|
55
|
-
|
56
|
-
raise
|
55
|
+
raise ::ShopifyApp::InvalidAudienceError,
|
56
|
+
"'aud' claim does not match api_key" unless payload["aud"] == api_key
|
57
|
+
raise ::ShopifyApp::InvalidDestinationError, "'dest' claim host not a valid shopify host" unless dest_host
|
58
|
+
raise ::ShopifyApp::MismatchedHostsError,
|
59
|
+
"'dest' claim host does not match 'iss' claim host" unless dest_host == iss_host
|
57
60
|
|
58
61
|
payload
|
59
62
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module ShopifyApp
|
3
4
|
class NullUserSessionStore
|
4
5
|
class << self
|
@@ -7,7 +8,7 @@ module ShopifyApp
|
|
7
8
|
end
|
8
9
|
|
9
10
|
def store(_, _)
|
10
|
-
raise
|
11
|
+
raise ::ShopifyApp::ConfigurationError, "user_storage is not configured"
|
11
12
|
end
|
12
13
|
|
13
14
|
def retrieve_by_shopify_user_id(_)
|
@@ -1,7 +1,8 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module ShopifyApp
|
3
4
|
class SessionRepository
|
4
|
-
|
5
|
+
extend ShopifyAPI::Auth::SessionStorage
|
5
6
|
|
6
7
|
class << self
|
7
8
|
attr_writer :shop_storage
|
@@ -33,22 +34,59 @@ module ShopifyApp
|
|
33
34
|
end
|
34
35
|
|
35
36
|
def shop_storage
|
36
|
-
load_shop_storage || raise(ConfigurationError,
|
37
|
+
load_shop_storage || raise(::ShopifyApp::ConfigurationError,
|
38
|
+
"ShopifySessionRepository.shop_storage is not configured!")
|
37
39
|
end
|
38
40
|
|
39
41
|
def user_storage
|
40
42
|
load_user_storage
|
41
43
|
end
|
42
44
|
|
45
|
+
# ShopifyAPI::Auth::SessionStorage override
|
46
|
+
def store_session(session)
|
47
|
+
if session.online?
|
48
|
+
user_storage.store(session, session.associated_user)
|
49
|
+
else
|
50
|
+
shop_storage.store(session)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
# ShopifyAPI::Auth::SessionStorage override
|
55
|
+
def load_session(id)
|
56
|
+
match = id.match(/^offline_(.*)/)
|
57
|
+
if match
|
58
|
+
retrieve_shop_session_by_shopify_domain(match[1])
|
59
|
+
else
|
60
|
+
retrieve_user_session_by_shopify_user_id(id.split("_").last)
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# ShopifyAPI::Auth::SessionStorage override
|
65
|
+
def delete_session(id)
|
66
|
+
match = id.match(/^offline_(.*)/)
|
67
|
+
|
68
|
+
record = if match
|
69
|
+
Shop.find_by(shopify_domain: match[1])
|
70
|
+
else
|
71
|
+
User.find_by(shopify_user_id: id.split("_").last)
|
72
|
+
end
|
73
|
+
|
74
|
+
record.destroy
|
75
|
+
|
76
|
+
true
|
77
|
+
end
|
78
|
+
|
43
79
|
private
|
44
80
|
|
45
81
|
def load_shop_storage
|
46
82
|
return unless @shop_storage
|
83
|
+
|
47
84
|
@shop_storage.respond_to?(:safe_constantize) ? @shop_storage.safe_constantize : @shop_storage
|
48
85
|
end
|
49
86
|
|
50
87
|
def load_user_storage
|
51
88
|
return NullUserSessionStore unless @user_storage
|
89
|
+
|
52
90
|
@user_storage.respond_to?(:safe_constantize) ? @user_storage.safe_constantize : @user_storage
|
53
91
|
end
|
54
92
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module ShopifyApp
|
3
4
|
module SessionStorage
|
4
5
|
extend ActiveSupport::Concern
|
@@ -9,12 +10,9 @@ module ShopifyApp
|
|
9
10
|
end
|
10
11
|
|
11
12
|
def with_shopify_session(&block)
|
12
|
-
ShopifyAPI::Session.temp(
|
13
|
-
|
14
|
-
|
15
|
-
api_version: api_version,
|
16
|
-
&block
|
17
|
-
)
|
13
|
+
ShopifyAPI::Auth::Session.temp(shop: shopify_domain, access_token: shopify_token) do
|
14
|
+
yield block
|
15
|
+
end
|
18
16
|
end
|
19
17
|
end
|
20
18
|
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module ShopifyApp
|
3
4
|
module ShopSessionStorage
|
4
5
|
extend ActiveSupport::Concern
|
@@ -10,8 +11,8 @@ module ShopifyApp
|
|
10
11
|
|
11
12
|
class_methods do
|
12
13
|
def store(auth_session, *_args)
|
13
|
-
shop = find_or_initialize_by(shopify_domain: auth_session.
|
14
|
-
shop.shopify_token = auth_session.
|
14
|
+
shop = find_or_initialize_by(shopify_domain: auth_session.shop)
|
15
|
+
shop.shopify_token = auth_session.access_token
|
15
16
|
shop.save!
|
16
17
|
shop.id
|
17
18
|
end
|
@@ -31,10 +32,9 @@ module ShopifyApp
|
|
31
32
|
def construct_session(shop)
|
32
33
|
return unless shop
|
33
34
|
|
34
|
-
ShopifyAPI::Session.new(
|
35
|
-
|
36
|
-
|
37
|
-
api_version: shop.api_version,
|
35
|
+
ShopifyAPI::Auth::Session.new(
|
36
|
+
shop: shop.shopify_domain,
|
37
|
+
access_token: shop.shopify_token
|
38
38
|
)
|
39
39
|
end
|
40
40
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ShopifyApp
|
4
|
+
module ShopSessionStorageWithScopes
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
include ::ShopifyApp::SessionStorage
|
7
|
+
|
8
|
+
included do
|
9
|
+
validates :shopify_domain, presence: true, uniqueness: { case_sensitive: false }
|
10
|
+
end
|
11
|
+
|
12
|
+
class_methods do
|
13
|
+
def store(auth_session, *_args)
|
14
|
+
shop = find_or_initialize_by(shopify_domain: auth_session.shop)
|
15
|
+
shop.shopify_token = auth_session.access_token
|
16
|
+
shop.access_scopes = auth_session.scope.to_s
|
17
|
+
|
18
|
+
shop.save!
|
19
|
+
shop.id
|
20
|
+
end
|
21
|
+
|
22
|
+
def retrieve(id)
|
23
|
+
shop = find_by(id: id)
|
24
|
+
construct_session(shop)
|
25
|
+
end
|
26
|
+
|
27
|
+
def retrieve_by_shopify_domain(domain)
|
28
|
+
shop = find_by(shopify_domain: domain)
|
29
|
+
construct_session(shop)
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def construct_session(shop)
|
35
|
+
return unless shop
|
36
|
+
|
37
|
+
ShopifyAPI::Auth::Session.new(
|
38
|
+
shop: shop.shopify_domain,
|
39
|
+
access_token: shop.shopify_token,
|
40
|
+
scope: shop.access_scopes
|
41
|
+
)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def access_scopes=(scopes)
|
46
|
+
super(scopes)
|
47
|
+
rescue NotImplementedError, NoMethodError
|
48
|
+
raise NotImplementedError, "#access_scopes= must be defined to handle storing access scopes: #{scopes}"
|
49
|
+
end
|
50
|
+
|
51
|
+
def access_scopes
|
52
|
+
super
|
53
|
+
rescue NotImplementedError, NoMethodError
|
54
|
+
raise NotImplementedError, "#access_scopes= must be defined to hook into stored access scopes"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -1,4 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module ShopifyApp
|
3
4
|
module UserSessionStorage
|
4
5
|
extend ActiveSupport::Concern
|
@@ -10,9 +11,9 @@ module ShopifyApp
|
|
10
11
|
|
11
12
|
class_methods do
|
12
13
|
def store(auth_session, user)
|
13
|
-
user = find_or_initialize_by(shopify_user_id: user
|
14
|
-
user.shopify_token = auth_session.
|
15
|
-
user.shopify_domain = auth_session.
|
14
|
+
user = find_or_initialize_by(shopify_user_id: user.id)
|
15
|
+
user.shopify_token = auth_session.access_token
|
16
|
+
user.shopify_domain = auth_session.shop
|
16
17
|
user.save!
|
17
18
|
user.id
|
18
19
|
end
|
@@ -31,10 +32,22 @@ module ShopifyApp
|
|
31
32
|
|
32
33
|
def construct_session(user)
|
33
34
|
return unless user
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
35
|
+
|
36
|
+
associated_user = ShopifyAPI::Auth::AssociatedUser.new(
|
37
|
+
id: user.shopify_user_id,
|
38
|
+
first_name: "",
|
39
|
+
last_name: "",
|
40
|
+
email: "",
|
41
|
+
email_verified: false,
|
42
|
+
account_owner: false,
|
43
|
+
locale: "",
|
44
|
+
collaborator: false
|
45
|
+
)
|
46
|
+
|
47
|
+
ShopifyAPI::Auth::Session.new(
|
48
|
+
shop: user.shopify_domain,
|
49
|
+
access_token: user.shopify_token,
|
50
|
+
associated_user: associated_user
|
38
51
|
)
|
39
52
|
end
|
40
53
|
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ShopifyApp
|
4
|
+
module UserSessionStorageWithScopes
|
5
|
+
extend ActiveSupport::Concern
|
6
|
+
include ::ShopifyApp::SessionStorage
|
7
|
+
|
8
|
+
included do
|
9
|
+
validates :shopify_domain, presence: true
|
10
|
+
end
|
11
|
+
|
12
|
+
class_methods do
|
13
|
+
def store(auth_session, user)
|
14
|
+
user = find_or_initialize_by(shopify_user_id: user.id)
|
15
|
+
user.shopify_token = auth_session.access_token
|
16
|
+
user.shopify_domain = auth_session.shop
|
17
|
+
user.access_scopes = auth_session.scope.to_s
|
18
|
+
|
19
|
+
user.save!
|
20
|
+
user.id
|
21
|
+
end
|
22
|
+
|
23
|
+
def retrieve(id)
|
24
|
+
user = find_by(id: id)
|
25
|
+
construct_session(user)
|
26
|
+
end
|
27
|
+
|
28
|
+
def retrieve_by_shopify_user_id(user_id)
|
29
|
+
user = find_by(shopify_user_id: user_id)
|
30
|
+
construct_session(user)
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
|
35
|
+
def construct_session(user)
|
36
|
+
return unless user
|
37
|
+
|
38
|
+
associated_user = ShopifyAPI::Auth::AssociatedUser.new(
|
39
|
+
id: user.shopify_user_id,
|
40
|
+
first_name: "",
|
41
|
+
last_name: "",
|
42
|
+
email: "",
|
43
|
+
email_verified: false,
|
44
|
+
account_owner: false,
|
45
|
+
locale: "",
|
46
|
+
collaborator: false
|
47
|
+
)
|
48
|
+
|
49
|
+
ShopifyAPI::Auth::Session.new(
|
50
|
+
shop: user.shopify_domain,
|
51
|
+
access_token: user.shopify_token,
|
52
|
+
scope: user.access_scopes,
|
53
|
+
associated_user_scope: user.access_scopes,
|
54
|
+
associated_user: associated_user
|
55
|
+
)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def access_scopes=(scopes)
|
60
|
+
super(scopes)
|
61
|
+
rescue NotImplementedError, NoMethodError
|
62
|
+
raise NotImplementedError, "#access_scopes= must be defined to handle storing access scopes: #{scopes}"
|
63
|
+
end
|
64
|
+
|
65
|
+
def access_scopes
|
66
|
+
super
|
67
|
+
rescue NotImplementedError, NoMethodError
|
68
|
+
raise NotImplementedError, "#access_scopes= must be defined to hook into stored access scopes"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
@@ -1,16 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module ShopifyApp
|
3
4
|
module TestHelpers
|
4
5
|
module WebhookVerificationHelper
|
5
6
|
def authorized_webhook_verification_headers!(params = {})
|
6
|
-
digest = OpenSSL::Digest.new(
|
7
|
+
digest = OpenSSL::Digest.new("sha256")
|
7
8
|
secret = ShopifyApp.configuration.secret
|
8
9
|
valid_hmac = Base64.encode64(OpenSSL::HMAC.digest(digest, secret, params.to_query)).strip
|
9
|
-
@request.headers[
|
10
|
+
@request.headers["HTTP_X_SHOPIFY_HMAC_SHA256"] = valid_hmac
|
10
11
|
end
|
11
12
|
|
12
13
|
def unauthorized_webhook_verification_headers!
|
13
|
-
@request.headers[
|
14
|
+
@request.headers["HTTP_X_SHOPIFY_HMAC_SHA256"] = "invalid_hmac"
|
14
15
|
end
|
15
16
|
end
|
16
17
|
end
|
data/lib/shopify_app/utils.rb
CHANGED
@@ -1,11 +1,12 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
+
|
2
3
|
module ShopifyApp
|
3
4
|
module Utils
|
4
5
|
def self.sanitize_shop_domain(shop_domain)
|
5
6
|
myshopify_domain = ShopifyApp.configuration.myshopify_domain
|
6
7
|
name = shop_domain.to_s.downcase.strip
|
7
8
|
name += ".#{myshopify_domain}" if !name.include?(myshopify_domain.to_s) && !name.include?(".")
|
8
|
-
name.sub!(%r|https?://|,
|
9
|
+
name.sub!(%r|https?://|, "")
|
9
10
|
|
10
11
|
u = URI("http://#{name}")
|
11
12
|
u.host if u.host&.match(/^[a-z0-9][a-z0-9\-]*[a-z0-9]\.#{Regexp.escape(myshopify_domain)}$/)
|
@@ -13,12 +14,18 @@ module ShopifyApp
|
|
13
14
|
nil
|
14
15
|
end
|
15
16
|
|
16
|
-
def self.
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
17
|
+
def self.shop_login_url(shop:, host:, return_to:)
|
18
|
+
return ShopifyApp.configuration.login_url unless shop
|
19
|
+
|
20
|
+
url = URI(ShopifyApp.configuration.login_url)
|
21
|
+
|
22
|
+
url.query = URI.encode_www_form(
|
23
|
+
shop: shop,
|
24
|
+
host: host,
|
25
|
+
return_to: return_to,
|
26
|
+
)
|
27
|
+
|
28
|
+
url.to_s
|
22
29
|
end
|
23
30
|
end
|
24
31
|
end
|