spree_api 6.0.0.rc3 → 6.0.0.rc5
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 +4 -4
- data/app/controllers/concerns/spree/api/v3/admin/email_template_lookup.rb +6 -1
- data/app/controllers/concerns/spree/api/v3/admin/reporting_authorization.rb +17 -8
- data/app/controllers/concerns/spree/api/v3/admin/store_context.rb +7 -0
- data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +61 -0
- data/app/controllers/concerns/spree/api/v3/allowed_redirect_url.rb +27 -0
- data/app/controllers/concerns/spree/api/v3/cart_resolvable.rb +8 -2
- data/app/controllers/concerns/spree/api/v3/error_handler.rb +49 -0
- data/app/controllers/concerns/spree/api/v3/params_normalizer.rb +76 -7
- data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +66 -5
- data/app/controllers/spree/api/v3/admin/companies/addresses_controller.rb +7 -1
- data/app/controllers/spree/api/v3/admin/companies/tax_exemption_certificates_controller.rb +11 -5
- data/app/controllers/spree/api/v3/admin/concerns/external_references.rb +3 -0
- data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +30 -8
- data/app/controllers/spree/api/v3/admin/customers/addresses_controller.rb +4 -0
- data/app/controllers/spree/api/v3/admin/customers_controller.rb +1 -6
- data/app/controllers/spree/api/v3/admin/exports_controller.rb +15 -3
- data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +1 -8
- data/app/controllers/spree/api/v3/admin/imports_controller.rb +12 -3
- data/app/controllers/spree/api/v3/admin/markets_controller.rb +3 -1
- data/app/controllers/spree/api/v3/admin/me_controller.rb +21 -1
- data/app/controllers/spree/api/v3/admin/oauth/application_tokens_controller.rb +56 -0
- data/app/controllers/spree/api/v3/admin/oauth/applications_controller.rb +55 -0
- data/app/controllers/spree/api/v3/admin/oauth/authorizations_controller.rb +294 -0
- data/app/controllers/spree/api/v3/admin/order_groups_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +8 -0
- data/app/controllers/spree/api/v3/admin/orders/items_controller.rb +1 -0
- data/app/controllers/spree/api/v3/admin/orders/returns_controller.rb +7 -0
- data/app/controllers/spree/api/v3/admin/orders_controller.rb +20 -7
- data/app/controllers/spree/api/v3/admin/package_types_controller.rb +2 -3
- data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +3 -4
- data/app/controllers/spree/api/v3/admin/products_controller.rb +5 -3
- data/app/controllers/spree/api/v3/admin/purchase_orders_controller.rb +1 -0
- data/app/controllers/spree/api/v3/admin/reporting_controller.rb +20 -14
- data/app/controllers/spree/api/v3/admin/stock_levels_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/store_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/tags_controller.rb +8 -5
- data/app/controllers/spree/api/v3/admin/tax_exemption_reasons_controller.rb +58 -0
- data/app/controllers/spree/api/v3/admin/tax_rates_controller.rb +0 -1
- data/app/controllers/spree/api/v3/admin/translations/batches_controller.rb +17 -11
- data/app/controllers/spree/api/v3/admin/variants_controller.rb +96 -1
- data/app/controllers/spree/api/v3/oauth/authorizations_controller.rb +72 -0
- data/app/controllers/spree/api/v3/oauth/metadata_controller.rb +57 -0
- data/app/controllers/spree/api/v3/oauth/server_metadata_controller.rb +57 -0
- data/app/controllers/spree/api/v3/resource_controller.rb +36 -5
- data/app/controllers/spree/api/v3/seller/package_types_controller.rb +2 -2
- data/app/controllers/spree/api/v3/seller/products_controller.rb +7 -61
- data/app/controllers/spree/api/v3/seller/resource_controller.rb +2 -6
- data/app/controllers/spree/api/v3/store/carts/addresses_controller.rb +53 -0
- data/app/controllers/spree/api/v3/store/carts_controller.rb +7 -8
- data/app/controllers/spree/api/v3/store/companies/addresses_controller.rb +4 -0
- data/app/controllers/spree/api/v3/store/customer/addresses_controller.rb +4 -0
- data/app/controllers/spree/api/v3/store/customer/data_requests_controller.rb +19 -1
- data/app/controllers/spree/api/v3/store/customer/password_resets_controller.rb +3 -14
- data/app/controllers/spree/api/v3/store/data_request_confirmations_controller.rb +23 -0
- data/app/controllers/spree/api/v3/store/data_requests_controller.rb +56 -0
- data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +2 -11
- data/app/request_schemas/spree/api/v3/admin/email_template_preview_request_schema.rb +4 -0
- data/app/request_schemas/spree/api/v3/admin/variant_request_schema.rb +21 -0
- data/app/request_schemas/spree/api/v3/seller/price_request_schema.rb +20 -0
- data/app/request_schemas/spree/api/v3/seller/variant_request_schema.rb +19 -0
- data/app/request_schemas/spree/api/v3/variant_request_schema.rb +8 -13
- data/app/serializers/spree/api/v3/address_owner_serializer.rb +27 -0
- data/app/serializers/spree/api/v3/address_serializer.rb +5 -1
- data/app/serializers/spree/api/v3/admin/address_owner_serializer.rb +10 -0
- data/app/serializers/spree/api/v3/admin/address_serializer.rb +1 -10
- data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/catalog_assignment_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/catalog_price_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/collection_rule_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/collection_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/commission_line_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/commission_rate_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/consent_record_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/email_template_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +2 -0
- data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +2 -0
- data/app/serializers/spree/api/v3/admin/oauth_access_token_serializer.rb +62 -0
- data/app/serializers/spree/api/v3/admin/oauth_application_serializer.rb +56 -0
- data/app/serializers/spree/api/v3/admin/oauth_consent_request_serializer.rb +25 -0
- data/app/serializers/spree/api/v3/admin/oauth_protected_resource_serializer.rb +27 -0
- data/app/serializers/spree/api/v3/admin/oauth_redirect_serializer.rb +17 -0
- data/app/serializers/spree/api/v3/admin/order_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/package_type_serializer.rb +3 -3
- data/app/serializers/spree/api/v3/admin/payment_capture_event_serializer.rb +16 -0
- data/app/serializers/spree/api/v3/admin/payment_serializer.rb +12 -1
- data/app/serializers/spree/api/v3/admin/permission_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/price_list_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/purchase_order_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/return_serializer.rb +2 -0
- data/app/serializers/spree/api/v3/admin/seller_requirement_status_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/seller_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/seller_transfer_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/shipping_label_serializer.rb +3 -3
- data/app/serializers/spree/api/v3/admin/stock_location_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/stock_receipt_item_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/stock_receipt_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/stock_transfer_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/store_serializer.rb +1 -0
- data/app/serializers/spree/api/v3/admin/tax_exemption_certificate_serializer.rb +7 -3
- data/app/serializers/spree/api/v3/admin/tax_exemption_reason_serializer.rb +20 -0
- data/app/serializers/spree/api/v3/admin/tax_identifier_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/tax_line_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/tax_rate_serializer.rb +1 -2
- data/app/serializers/spree/api/v3/admin/variant_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +4 -1
- data/app/serializers/spree/api/v3/base_serializer.rb +41 -5
- data/app/serializers/spree/api/v3/claim_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/collection_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/company_invitation_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/company_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/data_request_confirmation_requested_event_serializer.rb +28 -0
- data/app/serializers/spree/api/v3/data_request_event_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/data_request_serializer.rb +8 -4
- data/app/serializers/spree/api/v3/delivery_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/delivery_zone_member_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/discount_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/file_upload_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/fulfillment_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/media_event_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/media_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/option_type_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/order_group_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/order_serializer.rb +4 -2
- data/app/serializers/spree/api/v3/payment_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/product_filter_availability_option_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/product_filter_option_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/seller/address_serializer.rb +3 -2
- data/app/serializers/spree/api/v3/seller/order_serializer.rb +3 -3
- data/app/serializers/spree/api/v3/seller/package_type_serializer.rb +3 -3
- data/app/serializers/spree/api/v3/seller/profile_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/seller/requirement_status_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/seller/tax_identifier_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/seller/variant_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/shipping_label_event_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/stock_movement_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/stock_receipt_event_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/variant_serializer.rb +2 -2
- data/app/services/spree/api/v3/filter_table.rb +22 -3
- data/app/services/spree/api/v3/request_schema.rb +64 -17
- data/config/locales/en.yml +1 -0
- data/config/routes.rb +80 -12
- data/lib/spree/api/agent_membership_map.rb +73 -0
- data/lib/spree/api/agent_resource_map.rb +350 -0
- data/lib/spree/api/agent_workflow_endpoints.rb +113 -0
- data/lib/spree/api/agent_write_schemas.rb +131 -0
- data/lib/spree/api/dependencies.rb +5 -4
- data/lib/spree/api/engine.rb +40 -0
- data/lib/spree/api/enum_types.rb +118 -0
- data/lib/spree/api/extension_fields.rb +167 -0
- data/lib/spree/api/oauth.rb +243 -0
- data/lib/spree/api/openapi/filter_documentation.rb +8 -1
- data/lib/spree/api/typelizer_generation.rb +5 -0
- data/lib/spree/api.rb +8 -0
- data/lib/typelizer/open_enums.rb +48 -10
- metadata +37 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: cf869734311256969147c858239c279f8aec3faac383f6bf030937eedc9fc54e
|
|
4
|
+
data.tar.gz: e8a7fa2d529fd09e06f6d9b3ad7007e5122c2b56ad0a0e3b32ce8fccb0d4d073
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ef0d1a79bbdcd73ce34176fbe9054090214f326458c2840ac0937582334f639a4907df1cfa7bae30b566976bec1770fa32f455369eded75a8fc456b01efcbb08
|
|
7
|
+
data.tar.gz: 439f5b98f55aab51ce2d07f037336b175dce46c5e3bc9383685beebe35c34a7d519c004cf2697f68821b52ce538023a3de0dd68ee3b2522062a119db8906b59c
|
|
@@ -60,12 +60,17 @@ module Spree
|
|
|
60
60
|
|
|
61
61
|
# An unsaved subject and body are previewed only when sent, so an
|
|
62
62
|
# empty body still previews as empty. `branding` previews unsaved
|
|
63
|
-
# colors and font
|
|
63
|
+
# colors and font, `mailer_logo_signed_id` an unsaved logo (null
|
|
64
|
+
# removes it, as it does when saving the store).
|
|
64
65
|
def preview_arguments
|
|
65
66
|
arguments = params.permit(:record_id, :subject, :body).to_h.symbolize_keys
|
|
66
67
|
arguments[:locale] = language
|
|
67
68
|
arguments[:email_key] = params[:email_key].to_s.tr('.', '/').presence
|
|
68
69
|
arguments[:branding] = params.fetch(:branding, {}).permit(*Spree::Emails::Branding.attribute_names).to_h
|
|
70
|
+
if params.key?(:mailer_logo_signed_id)
|
|
71
|
+
signed_id = params[:mailer_logo_signed_id].presence&.to_s
|
|
72
|
+
arguments.merge!(mailer_logo_signed_id: signed_id, mailer_logo_cleared: signed_id.nil?)
|
|
73
|
+
end
|
|
69
74
|
arguments
|
|
70
75
|
end
|
|
71
76
|
|
|
@@ -3,22 +3,31 @@ module Spree
|
|
|
3
3
|
module V3
|
|
4
4
|
module Admin
|
|
5
5
|
# Per-member authorization for the reporting vocabulary. Metrics,
|
|
6
|
-
# dimensions and counters all declare a `
|
|
7
|
-
#
|
|
8
|
-
# secret keys), so one predicate answers for every member and every
|
|
6
|
+
# dimensions and counters all declare a `key_scope` paired with a
|
|
7
|
+
# `subject`, so one predicate answers for every member and every
|
|
9
8
|
# surface that lists them — the schema, a query, the home screen's
|
|
10
9
|
# counters — filters by the same rule.
|
|
10
|
+
#
|
|
11
|
+
# Asked of the credential rather than of its owner. Branching on
|
|
12
|
+
# `current_api_key` measured an OAuth grant against the approving
|
|
13
|
+
# staff member's whole role, because a token authenticates as that
|
|
14
|
+
# person and carries no key: a grant for `read_reports` alone could
|
|
15
|
+
# group a report by customer and read names, emails and spend.
|
|
16
|
+
# `holds_permission?` is the narrowed answer for all three
|
|
17
|
+
# principals — a key's scopes, a grant's, or the role when someone is
|
|
18
|
+
# acting directly at the dashboard.
|
|
11
19
|
module ReportingAuthorization
|
|
12
20
|
extend ActiveSupport::Concern
|
|
13
21
|
|
|
14
22
|
private
|
|
15
23
|
|
|
16
24
|
def member_allowed?(member)
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
25
|
+
return false unless member.key_scope.blank? || holds_permission?(member.key_scope)
|
|
26
|
+
|
|
27
|
+
# The permission says whether this kind of data is in scope at
|
|
28
|
+
# all; a host app's record-level rule can still narrow which of
|
|
29
|
+
# it this person sees.
|
|
30
|
+
member.subject.nil? || can?(:read, member.subject.call)
|
|
22
31
|
end
|
|
23
32
|
end
|
|
24
33
|
end
|
|
@@ -60,6 +60,13 @@ module Spree
|
|
|
60
60
|
def resolve_admin_store
|
|
61
61
|
return secret_api_key.store if secret_api_key
|
|
62
62
|
|
|
63
|
+
# An OAuth client is registered for one store and consented to on
|
|
64
|
+
# that store, so its token names the tenancy as surely as a secret
|
|
65
|
+
# key does. Reading the header instead would let a client name
|
|
66
|
+
# another merchant's store on a grant that never covered it.
|
|
67
|
+
oauth_store = respond_to?(:admin_oauth_token, true) && admin_oauth_token&.application&.store
|
|
68
|
+
return oauth_store if oauth_store
|
|
69
|
+
|
|
63
70
|
if store_id_header
|
|
64
71
|
Spree::Store.find_by_prefix_id(store_id_header)
|
|
65
72
|
else
|
|
@@ -50,6 +50,11 @@ module Spree
|
|
|
50
50
|
return true
|
|
51
51
|
end
|
|
52
52
|
|
|
53
|
+
# Then an OAuth token, which carries both the admin who granted it
|
|
54
|
+
# and the store its client was registered for, so neither a JWT's
|
|
55
|
+
# store-membership check nor a key's store selection applies.
|
|
56
|
+
return true if authenticate_admin_oauth_token
|
|
57
|
+
|
|
53
58
|
# Fall back to JWT authentication, then bind the admin to the store
|
|
54
59
|
# they hold a role on (the token itself is store-agnostic).
|
|
55
60
|
return false unless require_authentication!
|
|
@@ -59,6 +64,62 @@ module Spree
|
|
|
59
64
|
|
|
60
65
|
private
|
|
61
66
|
|
|
67
|
+
# An OAuth bearer token, accepted only when it was issued for this
|
|
68
|
+
# API. A token names the resources it may be used against (RFC 8707),
|
|
69
|
+
# and a client asking for one endpoint must not receive a credential
|
|
70
|
+
# good across the rest — so a token naming only the MCP endpoint is
|
|
71
|
+
# refused here, and the MCP controller refuses one naming only this.
|
|
72
|
+
#
|
|
73
|
+
# @return [Boolean] true when the request is now authenticated
|
|
74
|
+
def authenticate_admin_oauth_token
|
|
75
|
+
token = admin_oauth_token
|
|
76
|
+
return false if token.nil?
|
|
77
|
+
|
|
78
|
+
owner = token.resource_owner
|
|
79
|
+
return false unless owner.is_a?(Spree.admin_user_class)
|
|
80
|
+
|
|
81
|
+
@current_api_key = nil
|
|
82
|
+
@current_user = owner
|
|
83
|
+
true
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# @return [Spree::OauthAccessToken, nil]
|
|
87
|
+
def admin_oauth_token
|
|
88
|
+
return @admin_oauth_token if defined?(@admin_oauth_token)
|
|
89
|
+
|
|
90
|
+
@admin_oauth_token = begin
|
|
91
|
+
token = Spree::OauthAccessToken.by_token(oauth_bearer_value)
|
|
92
|
+
token if token&.accessible? && admin_audience_matches?(token)
|
|
93
|
+
end
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Split rather than matched: a regex with `\s+(.+)` backtracks on a
|
|
97
|
+
# header of many spaces, and this one is attacker-supplied on an
|
|
98
|
+
# unauthenticated request.
|
|
99
|
+
#
|
|
100
|
+
# @return [String, nil]
|
|
101
|
+
def oauth_bearer_value
|
|
102
|
+
scheme, value = request.headers['Authorization'].to_s.split(' ', 2)
|
|
103
|
+
return unless scheme&.casecmp?('Bearer')
|
|
104
|
+
|
|
105
|
+
value.to_s.strip.presence
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Compared by path, because the origin a client was given varies with
|
|
109
|
+
# how the installation is reached — a tunnel, a proxy, a custom
|
|
110
|
+
# domain — while the path it names does not. An unbound token is
|
|
111
|
+
# refused rather than trusted.
|
|
112
|
+
def admin_audience_matches?(token)
|
|
113
|
+
expected = Spree::Api::Oauth.resources[:admin]
|
|
114
|
+
return false if expected.blank?
|
|
115
|
+
|
|
116
|
+
token.resource.to_s.split.any? do |indicator|
|
|
117
|
+
URI.parse(indicator).path == expected
|
|
118
|
+
rescue URI::InvalidURIError
|
|
119
|
+
false
|
|
120
|
+
end
|
|
121
|
+
end
|
|
122
|
+
|
|
62
123
|
# Rejects an authenticated JWT admin who has no role on +current_store+.
|
|
63
124
|
# API-key principals are already store-bound and skip this check.
|
|
64
125
|
def require_store_membership!
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
# Reads the `redirect_url` a storefront passes for an emailed link.
|
|
5
|
+
#
|
|
6
|
+
# The link carries a live token, so a page outside the store's allowed
|
|
7
|
+
# origins would hand that token to whoever named the page. An address
|
|
8
|
+
# that does not match is dropped rather than refused, so callers cannot
|
|
9
|
+
# probe the allow-list through error responses.
|
|
10
|
+
module AllowedRedirectUrl
|
|
11
|
+
extend ActiveSupport::Concern
|
|
12
|
+
|
|
13
|
+
private
|
|
14
|
+
|
|
15
|
+
# @return [String, nil] the redirect URL when it matches one of the
|
|
16
|
+
# store's allowed origins
|
|
17
|
+
def validated_redirect_url
|
|
18
|
+
redirect_url = params[:redirect_url]
|
|
19
|
+
return if redirect_url.blank?
|
|
20
|
+
return unless current_store.allowed_origin?(redirect_url)
|
|
21
|
+
|
|
22
|
+
redirect_url
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -11,10 +11,16 @@ module Spree
|
|
|
11
11
|
# Find cart by prefixed ID and authorize read access (owner or guest
|
|
12
12
|
# token). Completed carts are not carts anymore — they 404 unless the
|
|
13
13
|
# caller opts in (idempotent completion, payment-session confirm races).
|
|
14
|
+
#
|
|
15
|
+
# Pass +token: nil+ to demand ownership. A guest token proves the
|
|
16
|
+
# caller holds this cart, which is the right bar for the cart's own
|
|
17
|
+
# contents and no higher — an endpoint reaching past the cart into
|
|
18
|
+
# something the account holds asks for the account instead.
|
|
19
|
+
#
|
|
14
20
|
# @return [Spree::Cart]
|
|
15
|
-
def find_cart(include_completed: false)
|
|
21
|
+
def find_cart(include_completed: false, token: cart_token)
|
|
16
22
|
@cart = resolve_cart(include_completed: include_completed)
|
|
17
|
-
authorize_storefront_read!(@cart, token:
|
|
23
|
+
authorize_storefront_read!(@cart, token: token)
|
|
18
24
|
@cart
|
|
19
25
|
end
|
|
20
26
|
|
|
@@ -90,6 +90,11 @@ module Spree
|
|
|
90
90
|
# Request errors
|
|
91
91
|
request_too_large: 'request_too_large',
|
|
92
92
|
|
|
93
|
+
# Tax engine errors — the same two codes the completion workflow
|
|
94
|
+
# reports, so a client sees one vocabulary wherever tax failed.
|
|
95
|
+
tax_provider_unavailable: 'tax_provider_unavailable',
|
|
96
|
+
tax_calculation_refused: 'tax_calculation_refused',
|
|
97
|
+
|
|
93
98
|
# General errors
|
|
94
99
|
processing_error: 'processing_error',
|
|
95
100
|
invalid_request: 'invalid_request'
|
|
@@ -101,6 +106,12 @@ module Spree
|
|
|
101
106
|
rescue_from CanCan::AccessDenied, with: :handle_access_denied
|
|
102
107
|
rescue_from Spree::Storefront::AccessDenied, with: :handle_access_denied
|
|
103
108
|
rescue_from Spree::Core::GatewayError, with: :handle_gateway_error
|
|
109
|
+
# The backstop for every path that prices tax. Cart updates and
|
|
110
|
+
# completion answer for themselves, with a message shaped to what the
|
|
111
|
+
# customer was doing; anything else — adding an item, changing a
|
|
112
|
+
# quantity, a recalculation — reaches here rather than escaping as a
|
|
113
|
+
# 500 with an HTML body that a storefront cannot tell from a bug.
|
|
114
|
+
rescue_from Spree::Tax::ProviderError, with: :handle_tax_provider_error
|
|
104
115
|
rescue_from ActionController::ParameterMissing, with: :handle_parameter_missing
|
|
105
116
|
rescue_from ActiveRecord::RecordInvalid, with: :handle_record_invalid
|
|
106
117
|
rescue_from Spree::Preferences::InvalidPreferences, with: :handle_invalid_preferences
|
|
@@ -171,6 +182,25 @@ module Spree
|
|
|
171
182
|
end
|
|
172
183
|
end
|
|
173
184
|
|
|
185
|
+
# Renders a failed workflow or service result. A tax engine failure the
|
|
186
|
+
# workflow caught itself (so it could roll the cart back) answers as
|
|
187
|
+
# handle_tax_provider_error would have: 503 for an outage, the one
|
|
188
|
+
# status the SDK retries, and 422 for a refusal. Anything else is
|
|
189
|
+
# rendered with +code+.
|
|
190
|
+
#
|
|
191
|
+
# @param result [Spree::ServiceModule::Result]
|
|
192
|
+
# @param code [String] the error code for any other failure
|
|
193
|
+
def render_result_failure(result, code:)
|
|
194
|
+
case result.error.code
|
|
195
|
+
when ERROR_CODES[:tax_provider_unavailable]
|
|
196
|
+
render_error(code: ERROR_CODES[:tax_provider_unavailable], message: result.error.to_s, status: :service_unavailable)
|
|
197
|
+
when ERROR_CODES[:tax_calculation_refused]
|
|
198
|
+
render_error(code: ERROR_CODES[:tax_calculation_refused], message: result.error.to_s, status: :unprocessable_content)
|
|
199
|
+
else
|
|
200
|
+
render_service_error(result.error, code: code)
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
174
204
|
# Legacy support - redirect to new error handling
|
|
175
205
|
def render_errors(errors, status = :unprocessable_content)
|
|
176
206
|
code = infer_error_code(errors, status)
|
|
@@ -235,6 +265,25 @@ module Spree
|
|
|
235
265
|
)
|
|
236
266
|
end
|
|
237
267
|
|
|
268
|
+
# Refused and unavailable are different instructions to the customer, so
|
|
269
|
+
# they keep different codes: one names something to fix, the other is
|
|
270
|
+
# worth retrying. Avalara's own wording is passed through for a refusal
|
|
271
|
+
# because it names the problem better than we can; an outage says only
|
|
272
|
+
# that, since its detail is about our endpoint and belongs in the error
|
|
273
|
+
# tracker.
|
|
274
|
+
def handle_tax_provider_error(exception)
|
|
275
|
+
Rails.error.report(exception, context: error_context, source: 'spree.api.v3')
|
|
276
|
+
|
|
277
|
+
refused = exception.is_a?(Spree::Tax::CalculationRefused)
|
|
278
|
+
render_error(
|
|
279
|
+
code: refused ? ERROR_CODES[:tax_calculation_refused] : ERROR_CODES[:tax_provider_unavailable],
|
|
280
|
+
message: refused ? exception.message.to_s : I18n.t('spree.cart_line_item.tax_unavailable'),
|
|
281
|
+
# A refusal is the request's fault, an outage is not. 503 is also
|
|
282
|
+
# the one the SDK retries — it retries on status, not on our code.
|
|
283
|
+
status: refused ? :unprocessable_content : :service_unavailable
|
|
284
|
+
)
|
|
285
|
+
end
|
|
286
|
+
|
|
238
287
|
def handle_parameter_missing(exception)
|
|
239
288
|
Rails.error.report(exception, context: error_context, source: 'spree.api.v3')
|
|
240
289
|
render_error(
|
|
@@ -27,7 +27,9 @@ module Spree
|
|
|
27
27
|
ActionController::Parameters.new(hash).permit!
|
|
28
28
|
end
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
# @param owner [Class, nil] the model whose associations name these
|
|
31
|
+
# keys, so `variant_id` is checked against `Spree::Variant`
|
|
32
|
+
def resolve_prefixed_ids(hash, owner = (model_class if respond_to?(:model_class, true)))
|
|
31
33
|
hash.each_with_object({}.with_indifferent_access) do |(key, value), result|
|
|
32
34
|
result[key] = case
|
|
33
35
|
# Preferences declare which of their values are ids
|
|
@@ -36,19 +38,49 @@ module Spree
|
|
|
36
38
|
when key.to_s == 'preferences'
|
|
37
39
|
value
|
|
38
40
|
when key.to_s.end_with?('_id') && prefixed_id?(value)
|
|
39
|
-
decode_prefixed_id(value)
|
|
41
|
+
decode_prefixed_id(value, key, owner)
|
|
40
42
|
when key.to_s.end_with?('_ids') && value.is_a?(Array)
|
|
41
|
-
value.map { |v| prefixed_id?(v) ? decode_prefixed_id(v) : v }
|
|
43
|
+
value.map { |v| prefixed_id?(v) ? decode_prefixed_id(v, key, owner) : v }
|
|
42
44
|
when value.is_a?(Hash)
|
|
43
|
-
resolve_prefixed_ids(value)
|
|
45
|
+
resolve_prefixed_ids(value, nested_owner(key, owner))
|
|
44
46
|
when value.is_a?(Array)
|
|
45
|
-
|
|
47
|
+
nested = nested_owner(key, owner)
|
|
48
|
+
value.map { |v| v.is_a?(Hash) ? resolve_prefixed_ids(v, nested) : v }
|
|
46
49
|
else
|
|
47
50
|
value
|
|
48
51
|
end
|
|
49
52
|
end
|
|
50
53
|
end
|
|
51
54
|
|
|
55
|
+
# The model a nested hash or array of hashes describes, so the keys
|
|
56
|
+
# inside it are checked against the right associations: an order's
|
|
57
|
+
# `items` are line items, whose `variant_id` is a variant.
|
|
58
|
+
#
|
|
59
|
+
# A payload name is a public contract and does not always match the
|
|
60
|
+
# association — `items` is `line_items` — so a controller whose
|
|
61
|
+
# names differ declares the mapping in {nested_param_models}.
|
|
62
|
+
def nested_owner(key, owner)
|
|
63
|
+
return if owner.nil?
|
|
64
|
+
|
|
65
|
+
declared = nested_param_models[key.to_s]
|
|
66
|
+
return declared if declared
|
|
67
|
+
|
|
68
|
+
name = key.to_s
|
|
69
|
+
reflection = owner.try(:reflect_on_association, name) ||
|
|
70
|
+
owner.try(:reflect_on_association, name.singularize)
|
|
71
|
+
reflection&.klass
|
|
72
|
+
rescue StandardError
|
|
73
|
+
nil
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Payload key => the model its rows describe, for the keys that do not
|
|
77
|
+
# name an association of their own.
|
|
78
|
+
#
|
|
79
|
+
# @return [Hash{String => Class}]
|
|
80
|
+
def nested_param_models
|
|
81
|
+
{}
|
|
82
|
+
end
|
|
83
|
+
|
|
52
84
|
def normalize_nested_attributes(hash, klass = model_class)
|
|
53
85
|
return hash unless klass.respond_to?(:nested_attributes_options)
|
|
54
86
|
|
|
@@ -83,8 +115,45 @@ module Spree
|
|
|
83
115
|
Spree::PrefixedId.prefixed_id?(value)
|
|
84
116
|
end
|
|
85
117
|
|
|
86
|
-
|
|
87
|
-
|
|
118
|
+
# A prefixed id is decoded only when its prefix is the one the column
|
|
119
|
+
# expects. The sqid alphabet is shared, so `prod_` and `variant_`
|
|
120
|
+
# decode to the same integers: without this an order asked for a
|
|
121
|
+
# product answered 201 and held a line item for whatever variant
|
|
122
|
+
# happened to carry that number.
|
|
123
|
+
#
|
|
124
|
+
# An id of the wrong kind is left as it was. The association then
|
|
125
|
+
# fails to find it, which is a 404 or a validation error naming the
|
|
126
|
+
# field — the same answer any other unknown id gets, rather than a
|
|
127
|
+
# quiet write of the wrong record.
|
|
128
|
+
def decode_prefixed_id(prefixed_id_string, key = nil, owner = nil)
|
|
129
|
+
target = association_class(key, owner)
|
|
130
|
+
|
|
131
|
+
decoded = if target.respond_to?(:decode_own_prefixed_id)
|
|
132
|
+
target.decode_own_prefixed_id(prefixed_id_string)
|
|
133
|
+
else
|
|
134
|
+
Spree::PrefixedId.decode_prefixed_id(prefixed_id_string)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
decoded || prefixed_id_string
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# What `variant_id` points at, read from the owner's own association
|
|
141
|
+
# rather than guessed from the name — `reason_id` is a different class
|
|
142
|
+
# on a cancellation than on a refund.
|
|
143
|
+
def association_class(key, owner)
|
|
144
|
+
return if key.nil? || owner.nil?
|
|
145
|
+
|
|
146
|
+
name = key.to_s.sub(/_ids?\z/, '')
|
|
147
|
+
# `category_ids` names `has_many :categories`, so the singular form
|
|
148
|
+
# finds nothing and the id went through unchecked — the same hole
|
|
149
|
+
# the singular `_id` case had.
|
|
150
|
+
reflection = owner.try(:reflect_on_association, name) ||
|
|
151
|
+
owner.try(:reflect_on_association, name.pluralize)
|
|
152
|
+
return if reflection.nil? || reflection.polymorphic?
|
|
153
|
+
|
|
154
|
+
reflection.klass
|
|
155
|
+
rescue StandardError
|
|
156
|
+
nil
|
|
88
157
|
end
|
|
89
158
|
end
|
|
90
159
|
end
|
|
@@ -98,7 +98,17 @@ module Spree
|
|
|
98
98
|
# A `jwt_only` skip exempts the JWT/publishable principal we now know
|
|
99
99
|
# this is (no secret key resolved) — shell data the dashboard reads
|
|
100
100
|
# without a specific scope.
|
|
101
|
-
|
|
101
|
+
#
|
|
102
|
+
# Not a delegated credential, though. The exemption exists because
|
|
103
|
+
# a signed-in staff member is already bounded by their role, and an
|
|
104
|
+
# OAuth grant is bounded by something narrower that this would skip
|
|
105
|
+
# — it carries no API key, so it was taking the human's exemption
|
|
106
|
+
# and reading settings its scopes never covered.
|
|
107
|
+
if scope_check_skipped_for_action? && self.class._scope_check_skip_jwt_only
|
|
108
|
+
return if current_oauth_token.blank?
|
|
109
|
+
|
|
110
|
+
return authorize_staff_permission!
|
|
111
|
+
end
|
|
102
112
|
|
|
103
113
|
# JWT staff pass through the same key gate as secret keys: their
|
|
104
114
|
# roles' catalog keys play the role scopes play for keys, so both
|
|
@@ -142,18 +152,22 @@ module Spree
|
|
|
142
152
|
# fallback, host-registered types) defer to CanCanCan's `authorize!`
|
|
143
153
|
# — the caller stays gated per subject, just not by key.
|
|
144
154
|
return unless Spree.permissions.key?(required)
|
|
145
|
-
return if
|
|
155
|
+
return if staff_permission_keys.include?(required)
|
|
146
156
|
|
|
147
157
|
Rails.logger.info do
|
|
148
158
|
"[Spree] Access denied: user=#{current_user.id} required=#{required} " \
|
|
149
159
|
"held=#{ability.permission_keys.join(',')}"
|
|
150
160
|
end
|
|
151
161
|
|
|
162
|
+
render_missing_permission(required)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def render_missing_permission(permission)
|
|
152
166
|
render_error(
|
|
153
167
|
code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:access_denied],
|
|
154
|
-
message: "Missing permission: #{
|
|
168
|
+
message: "Missing permission: #{permission}",
|
|
155
169
|
status: :forbidden,
|
|
156
|
-
details: { required_permission:
|
|
170
|
+
details: { required_permission: permission }
|
|
157
171
|
)
|
|
158
172
|
end
|
|
159
173
|
|
|
@@ -216,7 +230,54 @@ module Spree
|
|
|
216
230
|
ability = current_ability
|
|
217
231
|
return true unless ability.respond_to?(:permission_keys)
|
|
218
232
|
|
|
219
|
-
|
|
233
|
+
staff_permission_keys.include?(key.to_s)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# What a staff principal may do here, as catalog keys.
|
|
237
|
+
#
|
|
238
|
+
# Usually their roles' keys. When the request arrived on a delegated
|
|
239
|
+
# credential — an OAuth token a staff member approved for an agent or
|
|
240
|
+
# an app — the grant's scopes narrow that set: the person consented to
|
|
241
|
+
# part of their authority, not all of it. The grant can only ever
|
|
242
|
+
# subtract, so approving a client never widens what its owner holds.
|
|
243
|
+
#
|
|
244
|
+
# @return [Array<String>]
|
|
245
|
+
def staff_permission_keys
|
|
246
|
+
@staff_permission_keys ||= begin
|
|
247
|
+
ability = current_ability
|
|
248
|
+
held = ability.respond_to?(:permission_keys) ? ability.permission_keys.map(&:to_s) : []
|
|
249
|
+
granted = delegated_permission_keys
|
|
250
|
+
granted.nil? ? held : held & granted
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
# The scopes of the delegated credential this request arrived on, or
|
|
255
|
+
# nil when the staff member is acting directly (a dashboard session).
|
|
256
|
+
#
|
|
257
|
+
# Controllers that accept an OAuth token override
|
|
258
|
+
# `current_oauth_token`; everything else inherits nil and is unchanged.
|
|
259
|
+
#
|
|
260
|
+
# @return [Array<String>, nil]
|
|
261
|
+
def delegated_permission_keys
|
|
262
|
+
token = current_oauth_token
|
|
263
|
+
return nil if token.blank?
|
|
264
|
+
|
|
265
|
+
Spree.permissions.expand_keys(token.scopes.to_a)
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# The delegated credential this request arrived on.
|
|
269
|
+
#
|
|
270
|
+
# Resolved here rather than overridden per controller: this module is
|
|
271
|
+
# included after the authentication concerns, so an override in one of
|
|
272
|
+
# them loses to this definition and the narrowing silently stops
|
|
273
|
+
# happening — which is how a token scoped to `read_products` once
|
|
274
|
+
# created one.
|
|
275
|
+
#
|
|
276
|
+
# @return [Spree::OauthAccessToken, nil]
|
|
277
|
+
def current_oauth_token
|
|
278
|
+
return nil unless respond_to?(:admin_oauth_token, true)
|
|
279
|
+
|
|
280
|
+
admin_oauth_token
|
|
220
281
|
end
|
|
221
282
|
end
|
|
222
283
|
end
|
|
@@ -61,8 +61,14 @@ module Spree
|
|
|
61
61
|
Spree.api.admin_address_serializer
|
|
62
62
|
end
|
|
63
63
|
|
|
64
|
+
def collection_includes
|
|
65
|
+
[:owner]
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# The listing includes what the node inherits: a division's orders may ship to its headquarters' sites,
|
|
69
|
+
# so staff placing one must be able to see them — while writes stay with the node that owns the row.
|
|
64
70
|
def scope
|
|
65
|
-
@parent.addresses
|
|
71
|
+
action_name == 'index' ? @parent.address_book : @parent.addresses
|
|
66
72
|
end
|
|
67
73
|
end
|
|
68
74
|
end
|
|
@@ -12,9 +12,8 @@ module Spree
|
|
|
12
12
|
request_params :tax_exemption_certificate do |body|
|
|
13
13
|
body.attribute :certificate_number, :string, required: true, on: :create,
|
|
14
14
|
example: 'DE-RESALE-7'
|
|
15
|
-
body.attribute :
|
|
16
|
-
|
|
17
|
-
description: "Becomes the tax provider's entity use code."
|
|
15
|
+
body.attribute :tax_exemption_reason_id, :id, required: true, on: :create,
|
|
16
|
+
description: "One of the store's active tax exemption reasons. The tax provider receives its `code`."
|
|
18
17
|
body.attribute :issuing_authority, :string, nullable: true, example: 'Finanzamt Berlin'
|
|
19
18
|
body.attribute :issued_at, :datetime, nullable: true
|
|
20
19
|
body.attribute :expires_at, :datetime, nullable: true
|
|
@@ -100,11 +99,18 @@ module Spree
|
|
|
100
99
|
end
|
|
101
100
|
|
|
102
101
|
def collection_includes
|
|
103
|
-
[{ document_attachment: :blob }]
|
|
102
|
+
[:tax_exemption_reason, { document_attachment: :blob }]
|
|
104
103
|
end
|
|
105
104
|
|
|
105
|
+
# Read through the store, so another store's reason is a 404
|
|
106
|
+
# rather than a certificate pointing across stores.
|
|
106
107
|
def permitted_params
|
|
107
|
-
params.permit(*declared_permitted_attributes)
|
|
108
|
+
attributes = params.permit(*declared_permitted_attributes)
|
|
109
|
+
return attributes if attributes[:tax_exemption_reason_id].blank?
|
|
110
|
+
|
|
111
|
+
attributes.except(:tax_exemption_reason_id).merge(
|
|
112
|
+
tax_exemption_reason: current_store.tax_exemption_reasons.find_by_prefix_id!(attributes[:tax_exemption_reason_id])
|
|
113
|
+
)
|
|
108
114
|
end
|
|
109
115
|
|
|
110
116
|
def build_resource
|
|
@@ -75,6 +75,9 @@ module Spree
|
|
|
75
75
|
|
|
76
76
|
@resource = existing
|
|
77
77
|
authorize_resource!(@resource, :update)
|
|
78
|
+
# This runs before a nested controller's own parent check, and
|
|
79
|
+
# rendering here ends the request before it would run.
|
|
80
|
+
authorize_parent!(@parent) if @parent.present?
|
|
78
81
|
normalize_upsert_params!
|
|
79
82
|
update
|
|
80
83
|
end
|
|
@@ -64,16 +64,37 @@ module Spree
|
|
|
64
64
|
# store can't read or write custom fields on another store's records.
|
|
65
65
|
# Users are intentionally global in Spree (`User.for_store` is a
|
|
66
66
|
# no-op), so the store boundary for customer parents is enforced by
|
|
67
|
-
# the `authorize!(:
|
|
67
|
+
# the `authorize!(:update, @parent)` ability check above.
|
|
68
|
+
#
|
|
69
|
+
# Fails closed: `Spree::Base.for_store` answers the bare class when the
|
|
70
|
+
# store has no association for the model, which would search every
|
|
71
|
+
# store's records.
|
|
68
72
|
def parent_relation
|
|
69
73
|
klass = parent_lookup.klass
|
|
70
|
-
klass.
|
|
74
|
+
return klass.where(id: current_store.id) if klass <= Spree::Store
|
|
75
|
+
return klass if klass <= Spree.customer_class
|
|
76
|
+
|
|
77
|
+
relation = klass.for_store(current_store)
|
|
78
|
+
raise ActiveRecord::RecordNotFound, 'Parent resource not found' if relation.equal?(klass)
|
|
79
|
+
|
|
80
|
+
relation
|
|
71
81
|
end
|
|
72
82
|
|
|
73
83
|
# Product-family parents have no scope pair of their own — their
|
|
74
84
|
# custom fields are catalog data, gated by `products` like the rest
|
|
75
85
|
# of the variant/option-type surface.
|
|
76
|
-
|
|
86
|
+
# Parents whose own controller is gated by a scope other than their
|
|
87
|
+
# plural name share that controller's scope.
|
|
88
|
+
SCOPE_OVERRIDES = {
|
|
89
|
+
'option_type' => :products,
|
|
90
|
+
'variant' => :products,
|
|
91
|
+
'company' => :customers,
|
|
92
|
+
'stock_transfer' => :stock,
|
|
93
|
+
'purchase_order' => :purchasing,
|
|
94
|
+
'payment_method' => :settings,
|
|
95
|
+
'tax_rate' => :settings,
|
|
96
|
+
'store' => :settings
|
|
97
|
+
}.freeze
|
|
77
98
|
|
|
78
99
|
# Per-parent scope check: a key holding `write_products` may write a
|
|
79
100
|
# product's custom fields, `write_orders` may write an order's, etc.
|
|
@@ -104,12 +125,13 @@ module Spree
|
|
|
104
125
|
# code reloads — `enabled_resources` is captured at boot and its
|
|
105
126
|
# class references go stale. Aliases `'customer'` because the route
|
|
106
127
|
# uses `customer_id` while user_class.model_name.element is `'user'`,
|
|
107
|
-
#
|
|
108
|
-
# (5.5 rename) while the model's element is still `'taxon'
|
|
128
|
+
# `'category'` because the routes expose taxons as categories
|
|
129
|
+
# (5.5 rename) while the model's element is still `'taxon'`, and
|
|
130
|
+
# `'medium'` because Rails singularizes the `media` route to `medium_id`.
|
|
109
131
|
def parent_route_map
|
|
110
132
|
@parent_route_map ||= Spree.custom_fields.enabled_resources.each_with_object({}) do |klass, m|
|
|
111
133
|
m[klass.model_name.element.to_s] = klass.name
|
|
112
|
-
end.merge('customer' => Spree.customer_class.name, 'category' => 'Spree::Category')
|
|
134
|
+
end.merge('customer' => Spree.customer_class.name, 'category' => 'Spree::Category', 'medium' => 'Spree::Media')
|
|
113
135
|
end
|
|
114
136
|
|
|
115
137
|
# Returns the first segment whose `<segment>_id` is in the route,
|
|
@@ -117,8 +139,8 @@ module Spree
|
|
|
117
139
|
# by both `set_parent` and `scoped_resource_name`.
|
|
118
140
|
#
|
|
119
141
|
# Read from the path only: a `store_id` or `refund_id` added to the
|
|
120
|
-
# query string or body must not switch the parent
|
|
121
|
-
#
|
|
142
|
+
# query string or body must not switch the parent away from the record
|
|
143
|
+
# the route names.
|
|
122
144
|
def parent_lookup
|
|
123
145
|
return @parent_lookup if defined?(@parent_lookup)
|
|
124
146
|
|
|
@@ -248,12 +248,7 @@ module Spree
|
|
|
248
248
|
missing = EXPORT_PERMISSIONS.reject { |permission| holds_permission?(permission) }
|
|
249
249
|
return true if missing.empty?
|
|
250
250
|
|
|
251
|
-
|
|
252
|
-
code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:access_denied],
|
|
253
|
-
message: "Missing permission: #{missing.first}",
|
|
254
|
-
status: :forbidden,
|
|
255
|
-
details: { required_permission: missing.first }
|
|
256
|
-
)
|
|
251
|
+
render_missing_permission(missing.first)
|
|
257
252
|
false
|
|
258
253
|
end
|
|
259
254
|
|