spree_api 6.0.0.rc4 → 6.0.0.rc6
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/auth_cookies.rb +23 -0
- data/app/controllers/concerns/spree/api/v3/admin/refund_authorization.rb +26 -0
- data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +1 -0
- data/app/controllers/concerns/spree/api/v3/allowed_redirect_url.rb +27 -0
- data/app/controllers/concerns/spree/api/v3/api_key_authentication.rb +14 -2
- data/app/controllers/concerns/spree/api/v3/cart_resolvable.rb +23 -4
- data/app/controllers/concerns/spree/api/v3/digital_assets_resource.rb +68 -0
- data/app/controllers/concerns/spree/api/v3/error_handler.rb +3 -0
- data/app/controllers/concerns/spree/api/v3/jwt_authentication.rb +27 -1
- data/app/controllers/concerns/spree/api/v3/orders/fulfillment_actions.rb +6 -1
- data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +6 -2
- data/app/controllers/spree/api/v3/admin/allowed_origins_controller.rb +4 -1
- data/app/controllers/spree/api/v3/admin/auth_controller.rb +32 -5
- data/app/controllers/spree/api/v3/admin/commission_lines_controller.rb +2 -2
- data/app/controllers/spree/api/v3/admin/companies/addresses_controller.rb +7 -1
- data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +27 -8
- data/app/controllers/spree/api/v3/admin/customer_groups_controller.rb +4 -0
- data/app/controllers/spree/api/v3/admin/customers/addresses_controller.rb +4 -0
- data/app/controllers/spree/api/v3/admin/customers/store_credits_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/customers_controller.rb +14 -14
- data/app/controllers/spree/api/v3/admin/delivery_profiles/origin_groups_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/delivery_profiles_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +1 -8
- data/app/controllers/spree/api/v3/admin/invitation_acceptances_controller.rb +2 -0
- data/app/controllers/spree/api/v3/admin/me_controller.rb +2 -0
- data/app/controllers/spree/api/v3/admin/option_types_controller.rb +13 -0
- data/app/controllers/spree/api/v3/admin/orders/claims_controller.rb +2 -0
- data/app/controllers/spree/api/v3/admin/orders/exchanges_controller.rb +9 -0
- data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/orders/payments_controller.rb +7 -4
- data/app/controllers/spree/api/v3/admin/orders/returns_controller.rb +2 -0
- data/app/controllers/spree/api/v3/admin/orders_controller.rb +21 -7
- data/app/controllers/spree/api/v3/admin/package_types_controller.rb +2 -3
- data/app/controllers/spree/api/v3/admin/password_resets_controller.rb +7 -1
- data/app/controllers/spree/api/v3/admin/price_lists_controller.rb +18 -0
- data/app/controllers/spree/api/v3/admin/product_types_controller.rb +12 -1
- data/app/controllers/spree/api/v3/admin/products/digital_assets_controller.rb +5 -109
- data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +7 -5
- data/app/controllers/spree/api/v3/admin/products_controller.rb +33 -6
- data/app/controllers/spree/api/v3/admin/sellers/payouts_controller.rb +4 -3
- data/app/controllers/spree/api/v3/admin/stock_transfers_controller.rb +3 -2
- data/app/controllers/spree/api/v3/admin/store_controller.rb +8 -1
- data/app/controllers/spree/api/v3/admin/tags_controller.rb +11 -9
- data/app/controllers/spree/api/v3/admin/variants_controller.rb +96 -1
- data/app/controllers/spree/api/v3/admin/webhook_endpoints_controller.rb +5 -2
- data/app/controllers/spree/api/v3/oauth/authorizations_controller.rb +17 -6
- data/app/controllers/spree/api/v3/resource_controller.rb +50 -7
- data/app/controllers/spree/api/v3/seller/auth_controller.rb +10 -0
- data/app/controllers/spree/api/v3/seller/invitation_acceptances_controller.rb +2 -0
- data/app/controllers/spree/api/v3/seller/me_controller.rb +4 -0
- data/app/controllers/spree/api/v3/seller/orders_controller.rb +11 -3
- data/app/controllers/spree/api/v3/seller/package_types_controller.rb +2 -2
- data/app/controllers/spree/api/v3/seller/password_resets_controller.rb +5 -1
- data/app/controllers/spree/api/v3/seller/products/digital_assets_controller.rb +67 -0
- data/app/controllers/spree/api/v3/seller/products_controller.rb +11 -63
- data/app/controllers/spree/api/v3/seller/resource_controller.rb +2 -6
- data/app/controllers/spree/api/v3/seller/team_controller.rb +1 -1
- data/app/controllers/spree/api/v3/store/carts/addresses_controller.rb +53 -0
- data/app/controllers/spree/api/v3/store/carts/store_credits_controller.rb +9 -1
- data/app/controllers/spree/api/v3/store/carts_controller.rb +17 -3
- 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/customers_controller.rb +13 -1
- 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/delivery_methods_controller.rb +6 -1
- data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +2 -11
- data/app/controllers/spree/api/v3/store/products/filters_controller.rb +19 -4
- data/app/request_schemas/spree/api/v3/admin/variant_request_schema.rb +21 -0
- data/app/request_schemas/spree/api/v3/digital_asset_request_schema.rb +25 -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/concerns/spree/api/v3/digital_asset_details.rb +70 -0
- 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/delivery_origin_group_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/delivery_profile_serializer.rb +3 -5
- data/app/serializers/spree/api/v3/admin/digital_asset_serializer.rb +3 -47
- data/app/serializers/spree/api/v3/admin/email_template_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/newsletter_subscriber_serializer.rb +1 -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/permission_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/price_history_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/price_list_serializer.rb +9 -4
- data/app/serializers/spree/api/v3/admin/price_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/purchase_order_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/seller_payout_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/seller_requirement_status_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/seller_serializer.rb +3 -5
- 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_credit_serializer.rb +3 -5
- data/app/serializers/spree/api/v3/admin/store_serializer.rb +1 -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/variant_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/base_serializer.rb +33 -6
- 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/newsletter_subscriber_serializer.rb +5 -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/digital_asset_serializer.rb +16 -0
- 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/payout_serializer.rb +2 -2
- 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/store_serializer.rb +18 -0
- 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/filters_aggregator.rb +63 -18
- data/app/services/spree/api/v3/request_schema.rb +64 -17
- data/config/locales/en.yml +2 -0
- data/config/routes.rb +22 -11
- data/lib/spree/api/agent_write_schemas.rb +15 -3
- data/lib/spree/api/dependencies.rb +4 -4
- data/lib/spree/api/engine.rb +6 -0
- data/lib/spree/api/enum_types.rb +118 -0
- data/lib/spree/api/middleware/no_index.rb +33 -0
- data/lib/spree/api/openapi/filter_documentation.rb +8 -1
- data/lib/spree/api/openapi/schema_helper.rb +2 -1
- data/lib/spree/api/testing_support/v3/base.rb +18 -0
- data/lib/spree/api/typelizer_generation.rb +5 -0
- data/lib/typelizer/open_enums.rb +40 -16
- metadata +25 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 9a4438212680676faa48449ff17a89b8ee1039a707cc68521a72b54aaa08be90
|
|
4
|
+
data.tar.gz: 0454d9e97dcebc99b1e00bef4a8793d760953f2bfffce98de7d0fb6e4064ae6e
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: d5695765e1b4f9dea758cba076d7f6fbdd0c1f9bb95e164bf9af94c5a310b989424516ed29057bb9bc6140d8acf1e327f7cd2024fba16bf6727b8089d98e2618
|
|
7
|
+
data.tar.gz: 3182e8e1f5b03612236658108788cb633aea6297487adb7d56aa88157301362839c423926969aa3a1205393b5630f9a923b1d8f546fe85daf7532362976446b9
|
|
@@ -27,6 +27,7 @@ module Spree
|
|
|
27
27
|
end
|
|
28
28
|
|
|
29
29
|
REFRESH_COOKIE_NAME = :spree_admin_refresh_token
|
|
30
|
+
OAUTH_NONCE_COOKIE_NAME = :spree_admin_oauth_nonce
|
|
30
31
|
COOKIE_PATH = '/api/v3/admin/auth'.freeze
|
|
31
32
|
|
|
32
33
|
private
|
|
@@ -48,6 +49,28 @@ module Spree
|
|
|
48
49
|
cookies.signed[REFRESH_COOKIE_NAME].presence
|
|
49
50
|
end
|
|
50
51
|
|
|
52
|
+
# Binds an SSO attempt to the browser that started it. The nonce
|
|
53
|
+
# signed into the state is kept here too, so a callback carrying a
|
|
54
|
+
# state minted in another browser — a login an attacker completed
|
|
55
|
+
# with their own account — is refused rather than signing the
|
|
56
|
+
# victim into it.
|
|
57
|
+
def set_oauth_nonce_cookie(nonce, expires_in:)
|
|
58
|
+
cookies.signed[OAUTH_NONCE_COOKIE_NAME] = base_cookie_attributes.merge(
|
|
59
|
+
value: nonce,
|
|
60
|
+
expires: expires_in.from_now,
|
|
61
|
+
path: COOKIE_PATH,
|
|
62
|
+
httponly: true
|
|
63
|
+
)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def oauth_nonce_from_cookie
|
|
67
|
+
cookies.signed[OAUTH_NONCE_COOKIE_NAME].presence
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def clear_oauth_nonce_cookie
|
|
71
|
+
cookies.delete(OAUTH_NONCE_COOKIE_NAME, path: COOKIE_PATH)
|
|
72
|
+
end
|
|
73
|
+
|
|
51
74
|
# Keyed off the request scheme, not the Rails env: Rails silently refuses to
|
|
52
75
|
# write a Secure cookie on a non-SSL request, so a production app served over
|
|
53
76
|
# plain http (local Docker image, http-only intranet) would never get the
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# Handing money back is the `refunds` key's own authority, as it is on
|
|
6
|
+
# the nested refunds endpoint: a role that settles returns, claims and
|
|
7
|
+
# exchanges, or cancels orders, under `orders` does not thereby decide
|
|
8
|
+
# to refund them. The store back office's gate only — a seller refunds
|
|
9
|
+
# their own orders under the seller panel's `orders` key.
|
|
10
|
+
module RefundAuthorization
|
|
11
|
+
extend ActiveSupport::Concern
|
|
12
|
+
|
|
13
|
+
REFUND_PERMISSION = 'write_refunds'.freeze
|
|
14
|
+
|
|
15
|
+
private
|
|
16
|
+
|
|
17
|
+
def require_refund_permission!
|
|
18
|
+
return if holds_permission?(REFUND_PERMISSION)
|
|
19
|
+
|
|
20
|
+
render_missing_permission(REFUND_PERMISSION)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -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
|
|
@@ -67,7 +67,8 @@ module Spree
|
|
|
67
67
|
return @publishable_api_key if defined?(@publishable_api_key)
|
|
68
68
|
|
|
69
69
|
@publishable_api_key = Spree::StoreScopeGuard.skip do
|
|
70
|
-
extract_api_key ? Spree::ApiKey.active.publishable.find_by(token: extract_api_key) : nil
|
|
70
|
+
api_key = extract_api_key ? Spree::ApiKey.active.publishable.find_by(token: extract_api_key) : nil
|
|
71
|
+
api_key if store_present?(api_key)
|
|
71
72
|
end
|
|
72
73
|
end
|
|
73
74
|
|
|
@@ -78,10 +79,21 @@ module Spree
|
|
|
78
79
|
return @secret_api_key if defined?(@secret_api_key)
|
|
79
80
|
|
|
80
81
|
@secret_api_key = Spree::StoreScopeGuard.skip do
|
|
81
|
-
Spree::ApiKey.find_by_secret_token(extract_api_key)
|
|
82
|
+
api_key = Spree::ApiKey.find_by_secret_token(extract_api_key)
|
|
83
|
+
api_key if store_present?(api_key)
|
|
82
84
|
end
|
|
83
85
|
end
|
|
84
86
|
|
|
87
|
+
# A key selects its store, so a key whose store has been deleted no
|
|
88
|
+
# longer authenticates. Without this the request would run with no
|
|
89
|
+
# store at all, and fail deep in the first store-scoped query.
|
|
90
|
+
#
|
|
91
|
+
# @param api_key [Spree::ApiKey, nil]
|
|
92
|
+
# @return [Boolean]
|
|
93
|
+
def store_present?(api_key)
|
|
94
|
+
api_key&.store.present?
|
|
95
|
+
end
|
|
96
|
+
|
|
85
97
|
# Marks the API key as used at most once per hour
|
|
86
98
|
# to avoid unnecessary DB writes and job queue pressure on every request.
|
|
87
99
|
# This follows the same approach as GitHub's personal access tokens.
|
|
@@ -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
|
|
|
@@ -44,8 +50,15 @@ module Spree
|
|
|
44
50
|
# Render the cart as JSON using the cart serializer. Rendering never
|
|
45
51
|
# mutates: an action that wants unbuyable lines swept calls
|
|
46
52
|
# +sweep_unbuyable_lines!+ itself first.
|
|
53
|
+
#
|
|
54
|
+
# Serialized from a fresh, lazily preloading read, since lazy
|
|
55
|
+
# preloading does not reach lines and fulfillments a write built. The
|
|
56
|
+
# warnings exist only in memory, so they are carried over.
|
|
47
57
|
def render_cart(status: :ok)
|
|
48
|
-
|
|
58
|
+
cart = current_store.carts.where(id: @cart.id).preload_associations_lazily.take!
|
|
59
|
+
cart.warnings = @cart.warnings
|
|
60
|
+
|
|
61
|
+
render json: Spree.api.cart_serializer.new(cart, params: serializer_params).to_h, status: status
|
|
49
62
|
end
|
|
50
63
|
|
|
51
64
|
# Drops lines the customer can no longer buy — a discontinued product,
|
|
@@ -74,7 +87,13 @@ module Spree
|
|
|
74
87
|
# storefront that knows nothing about groups still finds every order it
|
|
75
88
|
# needs there.
|
|
76
89
|
def render_order(status: :ok)
|
|
77
|
-
|
|
90
|
+
# Read back through a lazily preloading relation rather than
|
|
91
|
+
# reloaded in place: a group's orders, and everything each of them
|
|
92
|
+
# renders, then load once for all of them instead of once per order.
|
|
93
|
+
purchases = @cart.is_a?(Spree::OrderGroup) ? current_store.order_groups : current_store.orders
|
|
94
|
+
# Chained through `where` first: an association collection drops the
|
|
95
|
+
# lazy flag when `find` rebuilds the query from the association.
|
|
96
|
+
record = purchases.where(id: @cart.id).preload_associations_lazily.take!
|
|
78
97
|
|
|
79
98
|
render json: purchase_serializer_for(record).new(record, params: serializer_params).to_h, status: status
|
|
80
99
|
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
# The downloadable files on one product, shared by the operator's and a
|
|
5
|
+
# seller's branch. Writes run through the digital asset workflows; each
|
|
6
|
+
# includer finds and authorizes the product in `set_parent`, names its
|
|
7
|
+
# serializer, and may add to the workflow arguments.
|
|
8
|
+
module DigitalAssetsResource
|
|
9
|
+
extend ActiveSupport::Concern
|
|
10
|
+
|
|
11
|
+
protected
|
|
12
|
+
|
|
13
|
+
def model_class
|
|
14
|
+
Spree::DigitalAsset
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Assets reach the product through `variants`, whose ORDER BY
|
|
18
|
+
# PostgreSQL rejects alongside the collection's DISTINCT.
|
|
19
|
+
def scope
|
|
20
|
+
super.reorder(created_at: :asc)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def scope_includes
|
|
24
|
+
[:variant, { attachment_attachment: :blob }]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Not normalized: `provider_settings` may hold values shaped like
|
|
28
|
+
# prefixed ids.
|
|
29
|
+
def permitted_params
|
|
30
|
+
params.permit(*declared_permitted_attributes, *model_additional_permitted_attributes)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def create_workflow
|
|
34
|
+
Spree.digital_asset_create_workflow
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def update_workflow
|
|
38
|
+
Spree.digital_asset_update_workflow
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# An asset belongs to a variant, not directly to a store; the workflow
|
|
42
|
+
# resolves the variant through the product.
|
|
43
|
+
def create_workflow_arguments
|
|
44
|
+
{ product: @parent, attributes: workflow_attributes }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def update_workflow_arguments
|
|
48
|
+
{ digital_asset: @resource, attributes: workflow_attributes }
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
# `signed_id` and `variant_id` are declared `permit: false` because
|
|
54
|
+
# neither is a column: the workflow attaches the one and resolves the
|
|
55
|
+
# other through the product, so they travel beside the attributes.
|
|
56
|
+
def workflow_attributes
|
|
57
|
+
permitted_params.to_h.merge(params.permit(:signed_id, :variant_id).to_h)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Only authorized, never saved: the workflow builds the record it
|
|
61
|
+
# writes. The variant is what an ability's conditions read.
|
|
62
|
+
def build_resource
|
|
63
|
+
model_class.new(variant: @parent.default_variant)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -16,6 +16,9 @@ module Spree
|
|
|
16
16
|
# Distinct from authentication_failed: the person proved who they are to
|
|
17
17
|
# the identity provider, but no account here is authorized for them.
|
|
18
18
|
account_not_provisioned: 'account_not_provisioned',
|
|
19
|
+
# The person proved who they are, but the account has been switched
|
|
20
|
+
# off (Spree::AccountDeactivation).
|
|
21
|
+
account_deactivated: 'account_deactivated',
|
|
19
22
|
invalid_oauth_state: 'invalid_oauth_state',
|
|
20
23
|
current_password_invalid: 'current_password_invalid',
|
|
21
24
|
password_reset_token_invalid: 'password_reset_token_invalid',
|
|
@@ -111,7 +111,7 @@ module Spree
|
|
|
111
111
|
|
|
112
112
|
case user_type
|
|
113
113
|
when USER_TYPE_ADMIN
|
|
114
|
-
|
|
114
|
+
admin_user_from(user_id)
|
|
115
115
|
else
|
|
116
116
|
customer_from(user_id)
|
|
117
117
|
end
|
|
@@ -128,6 +128,32 @@ module Spree
|
|
|
128
128
|
customer.respond_to?(:anonymized?) && customer.anonymized? ? nil : customer
|
|
129
129
|
end
|
|
130
130
|
|
|
131
|
+
# A deactivated staff account stops answering to its old tokens on the
|
|
132
|
+
# next request, rather than when they expire.
|
|
133
|
+
def admin_user_from(user_id)
|
|
134
|
+
admin_user = Spree.admin_user_class.find(user_id)
|
|
135
|
+
|
|
136
|
+
account_deactivated?(admin_user) ? nil : admin_user
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Duck-typed, like the anonymized check above: a custom admin user
|
|
140
|
+
# class without Spree::AccountDeactivation is never refused.
|
|
141
|
+
def account_deactivated?(user)
|
|
142
|
+
user.respond_to?(:deactivated?) && user.deactivated?
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# For a person who proved who they are: a password that matched, a
|
|
146
|
+
# provider that vouched for them, a reset or invitation link they hold.
|
|
147
|
+
# Anyone else gets the same answer as for any failed attempt, so this
|
|
148
|
+
# never tells a stranger which accounts are switched off.
|
|
149
|
+
def render_account_deactivated
|
|
150
|
+
render_error(
|
|
151
|
+
code: ErrorHandler::ERROR_CODES[:account_deactivated],
|
|
152
|
+
message: I18n.t('spree.api.account_deactivated'),
|
|
153
|
+
status: :forbidden
|
|
154
|
+
)
|
|
155
|
+
end
|
|
156
|
+
|
|
131
157
|
def determine_user_type(user)
|
|
132
158
|
if user.is_a?(Spree.admin_user_class)
|
|
133
159
|
USER_TYPE_ADMIN
|
|
@@ -72,8 +72,13 @@ module Spree
|
|
|
72
72
|
)
|
|
73
73
|
|
|
74
74
|
if changer.run!
|
|
75
|
+
# Read back through a lazily preloading query: what each parcel
|
|
76
|
+
# renders, its number included, then loads once for all of them.
|
|
77
|
+
order = current_store.orders.where(id: @order.id).preload_associations_lazily.take!
|
|
78
|
+
fulfillments = order.fulfillments.to_a
|
|
79
|
+
|
|
75
80
|
render json: {
|
|
76
|
-
data:
|
|
81
|
+
data: fulfillments.map { |fulfillment| serialize_resource(fulfillment) }
|
|
77
82
|
}
|
|
78
83
|
else
|
|
79
84
|
render_validation_error(changer.errors)
|
|
@@ -159,11 +159,15 @@ module Spree
|
|
|
159
159
|
"held=#{ability.permission_keys.join(',')}"
|
|
160
160
|
end
|
|
161
161
|
|
|
162
|
+
render_missing_permission(required)
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def render_missing_permission(permission)
|
|
162
166
|
render_error(
|
|
163
167
|
code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:access_denied],
|
|
164
|
-
message: "Missing permission: #{
|
|
168
|
+
message: "Missing permission: #{permission}",
|
|
165
169
|
status: :forbidden,
|
|
166
|
-
details: { required_permission:
|
|
170
|
+
details: { required_permission: permission }
|
|
167
171
|
)
|
|
168
172
|
end
|
|
169
173
|
|
|
@@ -3,7 +3,10 @@ module Spree
|
|
|
3
3
|
module V3
|
|
4
4
|
module Admin
|
|
5
5
|
class AllowedOriginsController < ResourceController
|
|
6
|
-
|
|
6
|
+
# A credential, not a setting: an allowed origin can read every
|
|
7
|
+
# admin's session through the cookie-authenticated refresh endpoint,
|
|
8
|
+
# so the list is managed under the same scope as API keys.
|
|
9
|
+
scoped_resource :api_keys
|
|
7
10
|
|
|
8
11
|
request_params :allowed_origin do |body|
|
|
9
12
|
body.attribute :origin, :string,
|
|
@@ -36,6 +36,8 @@ module Spree
|
|
|
36
36
|
|
|
37
37
|
if result.success?
|
|
38
38
|
user = result.value
|
|
39
|
+
return render_account_deactivated if account_deactivated?(user)
|
|
40
|
+
|
|
39
41
|
refresh_token = Spree::RefreshToken.create_for(user, audience: JWT_AUDIENCE_ADMIN, request_env: request_env_for_token)
|
|
40
42
|
set_refresh_cookie(refresh_token)
|
|
41
43
|
render json: auth_response(user)
|
|
@@ -56,7 +58,16 @@ module Spree
|
|
|
56
58
|
# anyone can log in — so it exposes only provider keys, kinds, labels
|
|
57
59
|
# and authorization URLs.
|
|
58
60
|
def providers
|
|
59
|
-
|
|
61
|
+
# The login page may load again while a sign-in is in flight, so a
|
|
62
|
+
# nonce the browser already holds is kept rather than replaced —
|
|
63
|
+
# replacing it would fail the callback of the state minted first.
|
|
64
|
+
nonce = oauth_nonce_from_cookie || SecureRandom.hex(16)
|
|
65
|
+
minted = false
|
|
66
|
+
described = Spree.admin_authentication_strategies.describe do |key|
|
|
67
|
+
minted = true
|
|
68
|
+
issue_oauth_state(key, nonce)
|
|
69
|
+
end
|
|
70
|
+
set_oauth_nonce_cookie(nonce, expires_in: OAUTH_STATE_EXPIRY) if minted
|
|
60
71
|
|
|
61
72
|
render json: { providers: described }
|
|
62
73
|
end
|
|
@@ -73,6 +84,9 @@ module Spree
|
|
|
73
84
|
def callback
|
|
74
85
|
return redirect_to_dashboard(error: ERROR_CODES[:invalid_oauth_state]) unless valid_oauth_state?
|
|
75
86
|
|
|
87
|
+
# Used once: a state that signed in cannot sign in again.
|
|
88
|
+
clear_oauth_nonce_cookie
|
|
89
|
+
|
|
76
90
|
strategy = Spree.admin_authentication_strategies.build(
|
|
77
91
|
params[:provider],
|
|
78
92
|
params: params,
|
|
@@ -87,6 +101,8 @@ module Spree
|
|
|
87
101
|
return redirect_to_dashboard(error: callback_error_code(strategy))
|
|
88
102
|
end
|
|
89
103
|
|
|
104
|
+
return redirect_to_dashboard(error: ERROR_CODES[:account_deactivated]) if account_deactivated?(result.value)
|
|
105
|
+
|
|
90
106
|
set_refresh_cookie(
|
|
91
107
|
Spree::RefreshToken.create_for(result.value, audience: JWT_AUDIENCE_ADMIN, request_env: request_env_for_token)
|
|
92
108
|
)
|
|
@@ -119,6 +135,14 @@ module Spree
|
|
|
119
135
|
end
|
|
120
136
|
|
|
121
137
|
user = refresh_token.user
|
|
138
|
+
# Deactivating deletes the account's refresh tokens; this catches a
|
|
139
|
+
# refresh already under way when that happened.
|
|
140
|
+
if account_deactivated?(user)
|
|
141
|
+
refresh_token.destroy
|
|
142
|
+
clear_refresh_cookie
|
|
143
|
+
return render_account_deactivated
|
|
144
|
+
end
|
|
145
|
+
|
|
122
146
|
new_refresh_token = refresh_token.rotate!(request_env: request_env_for_token)
|
|
123
147
|
set_refresh_cookie(new_refresh_token)
|
|
124
148
|
|
|
@@ -158,10 +182,11 @@ module Spree
|
|
|
158
182
|
# expired value fails verification.
|
|
159
183
|
#
|
|
160
184
|
# The provider key is signed into the payload so a state minted for one
|
|
161
|
-
# provider cannot be replayed against another's callback
|
|
162
|
-
|
|
185
|
+
# provider cannot be replayed against another's callback, and the
|
|
186
|
+
# nonce ties it to the browser holding the matching cookie.
|
|
187
|
+
def issue_oauth_state(provider, nonce)
|
|
163
188
|
Rails.application.message_verifier(OAUTH_STATE_PURPOSE).generate(
|
|
164
|
-
{ provider: provider.to_s, nonce:
|
|
189
|
+
{ provider: provider.to_s, nonce: nonce },
|
|
165
190
|
expires_in: OAUTH_STATE_EXPIRY
|
|
166
191
|
)
|
|
167
192
|
end
|
|
@@ -173,8 +198,10 @@ module Spree
|
|
|
173
198
|
# The verifier round-trips through JSON, so payload keys come back as strings.
|
|
174
199
|
payload = Rails.application.message_verifier(OAUTH_STATE_PURPOSE).verified(state)
|
|
175
200
|
return false unless payload.is_a?(Hash)
|
|
201
|
+
return false unless payload['provider'].to_s == params[:provider].to_s
|
|
176
202
|
|
|
177
|
-
|
|
203
|
+
nonce = oauth_nonce_from_cookie
|
|
204
|
+
nonce.present? && ActiveSupport::SecurityUtils.secure_compare(payload['nonce'].to_s, nonce)
|
|
178
205
|
end
|
|
179
206
|
|
|
180
207
|
# Sends the browser back to the dashboard. On failure the login page
|
|
@@ -23,8 +23,8 @@ module Spree
|
|
|
23
23
|
|
|
24
24
|
# Reached through the store's own orders: the lines carry no store of
|
|
25
25
|
# their own, and the order is what ties them to one.
|
|
26
|
-
def
|
|
27
|
-
|
|
26
|
+
def store_scope
|
|
27
|
+
model_class.where(order_id: current_store.orders.select(:id))
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
# Every association the serializer reads, so a page of lines costs a
|
|
@@ -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
|
|
@@ -64,16 +64,34 @@ 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
|
+
# `Spree::Base.for_store` answers nothing for a model the store has
|
|
70
|
+
# no association for, so an unexpected parent type 404s rather than
|
|
71
|
+
# searching every 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
|
+
klass.for_store(current_store)
|
|
71
78
|
end
|
|
72
79
|
|
|
73
80
|
# Product-family parents have no scope pair of their own — their
|
|
74
81
|
# custom fields are catalog data, gated by `products` like the rest
|
|
75
82
|
# of the variant/option-type surface.
|
|
76
|
-
|
|
83
|
+
# Parents whose own controller is gated by a scope other than their
|
|
84
|
+
# plural name share that controller's scope.
|
|
85
|
+
SCOPE_OVERRIDES = {
|
|
86
|
+
'option_type' => :products,
|
|
87
|
+
'variant' => :products,
|
|
88
|
+
'company' => :customers,
|
|
89
|
+
'stock_transfer' => :stock,
|
|
90
|
+
'purchase_order' => :purchasing,
|
|
91
|
+
'payment_method' => :settings,
|
|
92
|
+
'tax_rate' => :settings,
|
|
93
|
+
'store' => :settings
|
|
94
|
+
}.freeze
|
|
77
95
|
|
|
78
96
|
# Per-parent scope check: a key holding `write_products` may write a
|
|
79
97
|
# product's custom fields, `write_orders` may write an order's, etc.
|
|
@@ -104,12 +122,13 @@ module Spree
|
|
|
104
122
|
# code reloads — `enabled_resources` is captured at boot and its
|
|
105
123
|
# class references go stale. Aliases `'customer'` because the route
|
|
106
124
|
# uses `customer_id` while user_class.model_name.element is `'user'`,
|
|
107
|
-
#
|
|
108
|
-
# (5.5 rename) while the model's element is still `'taxon'
|
|
125
|
+
# `'category'` because the routes expose taxons as categories
|
|
126
|
+
# (5.5 rename) while the model's element is still `'taxon'`, and
|
|
127
|
+
# `'medium'` because Rails singularizes the `media` route to `medium_id`.
|
|
109
128
|
def parent_route_map
|
|
110
129
|
@parent_route_map ||= Spree.custom_fields.enabled_resources.each_with_object({}) do |klass, m|
|
|
111
130
|
m[klass.model_name.element.to_s] = klass.name
|
|
112
|
-
end.merge('customer' => Spree.customer_class.name, 'category' => 'Spree::Category')
|
|
131
|
+
end.merge('customer' => Spree.customer_class.name, 'category' => 'Spree::Category', 'medium' => 'Spree::Media')
|
|
113
132
|
end
|
|
114
133
|
|
|
115
134
|
# Returns the first segment whose `<segment>_id` is in the route,
|
|
@@ -117,8 +136,8 @@ module Spree
|
|
|
117
136
|
# by both `set_parent` and `scoped_resource_name`.
|
|
118
137
|
#
|
|
119
138
|
# 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
|
-
#
|
|
139
|
+
# query string or body must not switch the parent away from the record
|
|
140
|
+
# the route names.
|
|
122
141
|
def parent_lookup
|
|
123
142
|
return @parent_lookup if defined?(@parent_lookup)
|
|
124
143
|
|
|
@@ -18,7 +18,7 @@ module Spree
|
|
|
18
18
|
# POST /api/v3/admin/customers/:customer_id/store_credits
|
|
19
19
|
def create
|
|
20
20
|
@resource = @parent.store_credits.new(store_credit_attributes)
|
|
21
|
-
@resource.created_by =
|
|
21
|
+
@resource.created_by = current_actor
|
|
22
22
|
@resource.store ||= current_store
|
|
23
23
|
authorize_resource!(@resource, :create)
|
|
24
24
|
|
|
@@ -12,9 +12,13 @@ module Spree
|
|
|
12
12
|
body.attribute :first_name, :string
|
|
13
13
|
body.attribute :last_name, :string
|
|
14
14
|
body.attribute :phone, :string
|
|
15
|
-
|
|
15
|
+
# On create only: once the account exists its password is the
|
|
16
|
+
# customer's, changed by them or through their own reset email —
|
|
17
|
+
# never set by staff, who would otherwise be able to sign in as
|
|
18
|
+
# any customer.
|
|
19
|
+
body.attribute :password, :string, only: :create,
|
|
16
20
|
description: 'Optional: a customer created without one claims the account through a password reset.'
|
|
17
|
-
body.attribute :password_confirmation, :string,
|
|
21
|
+
body.attribute :password_confirmation, :string, only: :create,
|
|
18
22
|
description: 'Optional: a customer created without one claims the account through a password reset.'
|
|
19
23
|
body.attribute :selected_locale, :string
|
|
20
24
|
body.attribute :avatar_signed_id, :string, description: 'A `signed_id` from `files.upload()`.'
|
|
@@ -175,12 +179,12 @@ module Spree
|
|
|
175
179
|
# Bulk add the given customers to the given groups. Idempotent —
|
|
176
180
|
# customers already in a group are skipped at the model layer.
|
|
177
181
|
def bulk_add_to_groups
|
|
178
|
-
apply_groups(:
|
|
182
|
+
apply_groups(:add_customers_to)
|
|
179
183
|
end
|
|
180
184
|
|
|
181
185
|
# Bulk remove the given customers from the given groups.
|
|
182
186
|
def bulk_remove_from_groups
|
|
183
|
-
apply_groups(:
|
|
187
|
+
apply_groups(:remove_customers_from)
|
|
184
188
|
end
|
|
185
189
|
|
|
186
190
|
protected
|
|
@@ -248,12 +252,7 @@ module Spree
|
|
|
248
252
|
missing = EXPORT_PERMISSIONS.reject { |permission| holds_permission?(permission) }
|
|
249
253
|
return true if missing.empty?
|
|
250
254
|
|
|
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
|
-
)
|
|
255
|
+
render_missing_permission(missing.first)
|
|
257
256
|
false
|
|
258
257
|
end
|
|
259
258
|
|
|
@@ -296,8 +295,9 @@ module Spree
|
|
|
296
295
|
end
|
|
297
296
|
|
|
298
297
|
# Authorises bulk group mutation, decodes prefixed IDs, then dispatches
|
|
299
|
-
# to `
|
|
300
|
-
# counts of records actually affected so the UI can
|
|
298
|
+
# to `add_customers_to` / `remove_customers_from` for all groups at
|
|
299
|
+
# once. Returns the counts of records actually affected so the UI can
|
|
300
|
+
# show a toast.
|
|
301
301
|
def apply_groups(method)
|
|
302
302
|
authorize! :update, model_class
|
|
303
303
|
|
|
@@ -305,9 +305,9 @@ module Spree
|
|
|
305
305
|
group_ids = decode_ids(params[:customer_group_ids])
|
|
306
306
|
|
|
307
307
|
scoped_user_ids = scope.where(id: user_ids).pluck(:id)
|
|
308
|
-
scoped_groups = Spree::CustomerGroup.for_store(current_store).where(id: group_ids)
|
|
308
|
+
scoped_groups = Spree::CustomerGroup.for_store(current_store).where(id: group_ids).to_a
|
|
309
309
|
|
|
310
|
-
|
|
310
|
+
Spree::CustomerGroup.public_send(method, scoped_groups, scoped_user_ids)
|
|
311
311
|
|
|
312
312
|
render json: { customer_count: scoped_user_ids.size, customer_group_count: scoped_groups.size }
|
|
313
313
|
end
|