spree_api 5.6.0.rc4 → 5.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ca50a0fac8c0d346157c3d42a2969414d5f4e8a392aae87176cb02069c828e21
4
- data.tar.gz: dc56eff71e891ef304710a9a2f2bfc7c72be17a50d0a24ba1335b137ea817057
3
+ metadata.gz: 9f86ce456fb13e7e6f1fac5a39a1ad7f0a4befe5dc959a88c80b3bcaa3689b57
4
+ data.tar.gz: 4df7f39849201749a3b5a26337a009a36ab0b9fc4b4588d5d90accf859368ec7
5
5
  SHA512:
6
- metadata.gz: e1097d160a66eac8cee1e1b9169f06781c4105ddb01fce2bf560bcf0c94aeb7e0f947bd36675e17e7b6532af02c47999d242f23fb8359abaa866d3257775afd0
7
- data.tar.gz: a48b3317819fd03b05d71b016a7856b5c1a8da5d238080964dd9980b0eef328f2a712aed8a903efffde9a86ee1c902903495aee339d01e4a5e216fae0886ffb7
6
+ metadata.gz: 7fa240df22e6815d24910c37e32b873a8dae7a4e0ded6c00430e7d895e287ae3ae6e78cb11fcbb52d4930f876e48e22bf8be7353eb317722789772a17ceb6aa9
7
+ data.tar.gz: b4623924dc7d63bb2abae38790fa31c5140392e6838cce8c1c530fd6314110856fcf55c8c5c8a2c01a81fff58a1a7106787a3f3615d1b0f0dd16af4b911c027c
@@ -22,10 +22,13 @@ module Spree
22
22
  current_user.nil?
23
23
  end
24
24
 
25
- # Set Vary headers to ensure proper CDN caching by currency/locale
25
+ # Set Vary headers to ensure proper CDN caching by currency/locale/channel.
26
+ # X-Spree-Channel is included because the resolved channel changes the
27
+ # serialized body (price hiding + channel-scoped visibility), so a shared
28
+ # cache must not serve one channel's guest response to another channel.
26
29
  def set_vary_headers
27
30
  if guest_user?
28
- response.headers['Vary'] = 'Accept, x-spree-currency, x-spree-locale'
31
+ response.headers['Vary'] = 'Accept, x-spree-currency, x-spree-locale, x-spree-channel'
29
32
  else
30
33
  response.headers['Cache-Control'] = 'private, no-store'
31
34
  end
@@ -67,14 +70,21 @@ module Spree
67
70
 
68
71
  expires_in expires_in, public: true
69
72
 
70
- # Use Rails' stale? which handles ETag and Last-Modified
71
- stale?(resource, public: true)
73
+ # Use Rails' stale? which handles ETag and Last-Modified. The channel
74
+ # is folded into the ETag so a shared cache never serves one channel's
75
+ # guest response to another; Last-Modified mirrors the resource's own
76
+ # timestamp as before.
77
+ stale?(
78
+ etag: [resource, cache_channel_fragment],
79
+ last_modified: resource.try(:updated_at),
80
+ public: true
81
+ )
72
82
  end
73
83
 
74
84
  private
75
85
 
76
86
  # Build a cache key for a collection
77
- # Includes: latest updated_at, total count, query params, pagination, expand, currency, locale
87
+ # Includes: latest updated_at, total count, query params, pagination, expand, currency, locale, channel
78
88
  def collection_cache_key(collection)
79
89
  # For ActiveRecord collections use updated_at, for plain arrays use store's updated_at as proxy
80
90
  latest_updated_at = if collection.first&.respond_to?(:updated_at)
@@ -92,11 +102,22 @@ module Spree
92
102
  params[:page],
93
103
  params[:limit],
94
104
  current_currency,
95
- current_locale
105
+ current_locale,
106
+ cache_channel_fragment
96
107
  ]
97
108
 
98
109
  parts.compact.join('/')
99
110
  end
111
+
112
+ # Identifies the resolved channel for cache-key purposes. The channel
113
+ # changes the serialized body (price hiding + channel-scoped visibility),
114
+ # so it must be part of the guest cache identity. Uses the same
115
+ # +current_channel+ source of truth as serialization/gating.
116
+ def cache_channel_fragment
117
+ return nil unless respond_to?(:current_channel, true)
118
+
119
+ current_channel&.id
120
+ end
100
121
  end
101
122
  end
102
123
  end
@@ -56,10 +56,34 @@ module Spree
56
56
  private
57
57
 
58
58
  def authorize_api_key_scope!
59
- return unless current_api_key
59
+ # Endpoints explicitly exempt from the scope mechanism (auth, me, public
60
+ # invitation acceptance, per-type-filtered exports, etc.) opt out first,
61
+ # before any key resolution or enforcement.
60
62
  return if self.class._scope_check_skipped
61
63
  return if self.class._scope_check_skipped_actions&.include?(action_name)
62
64
 
65
+ # Fail CLOSED, not open. A nil `current_api_key` legitimately means a
66
+ # non-secret-key request (JWT admin / publishable) authorized by other
67
+ # means, which correctly skips scope checks. But it ALSO happens when
68
+ # this guard runs before authentication resolved the key: a bare
69
+ # `return unless current_api_key` would then skip scope enforcement for
70
+ # an attacker-supplied secret key. So resolve the secret key in place
71
+ # here — never rely on before_action ordering. Only truly non-secret-key
72
+ # requests (nothing to resolve) are waved through.
73
+ resolve_current_api_key_for_scope_check!
74
+
75
+ unless current_api_key
76
+ return unless secret_key_request?
77
+
78
+ # A secret-key token was presented but did not resolve to a live key
79
+ # for this store — reject rather than silently skip the scope check.
80
+ return render_error(
81
+ code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:invalid_token],
82
+ message: 'Valid secret API key required',
83
+ status: :unauthorized
84
+ )
85
+ end
86
+
63
87
  resource = scoped_resource_name
64
88
  # Fail closed: a controller authenticated by API key MUST declare
65
89
  # either `scoped_resource :name`, override `scoped_resource_name`,
@@ -77,6 +101,29 @@ module Spree
77
101
  )
78
102
  end
79
103
 
104
+ # True when the request presents a secret API key token (`sk_` prefix),
105
+ # regardless of whether authentication has resolved it into
106
+ # `current_api_key` yet. Used to keep the scope guard fail-closed even if
107
+ # it runs before the API key is loaded: a secret-key request whose key is
108
+ # still nil must be denied, never waved through.
109
+ def secret_key_request?
110
+ extract_api_key.to_s.start_with?(Spree::ApiKey::PREFIXES['secret'])
111
+ end
112
+
113
+ # Resolves the request's secret API key into `@current_api_key` if a
114
+ # secret-key token is present and it hasn't been resolved yet, so the scope
115
+ # check does not depend on `authenticate_admin!` having already run. Mirrors
116
+ # the secret-key resolution + store binding in AdminAuthentication, and is
117
+ # idempotent: when the key is already loaded (guard runs after auth) it does
118
+ # nothing.
119
+ def resolve_current_api_key_for_scope_check!
120
+ return if @current_api_key
121
+ return unless secret_key_request?
122
+
123
+ key = Spree::ApiKey.find_by_secret_token(extract_api_key)
124
+ @current_api_key = key if key && key.store_id == current_store.id
125
+ end
126
+
80
127
  # The resource name used in scope strings (`read_<name>` / `write_<name>`).
81
128
  # Defaults to the class-level `scoped_resource :name` declaration.
82
129
  # Override in controllers that resolve scope at request time (e.g. the
@@ -86,10 +133,15 @@ module Spree
86
133
  self.class._scoped_resource
87
134
  end
88
135
 
89
- # Override in controllers with non-REST custom actions (e.g. dashboard
90
- # `analytics` should map to a read).
136
+ # Maps the action to the scope kind. Consults the controller's
137
+ # `read_actions` (ResourceController's overridable list) when defined,
138
+ # so declaring a custom read-only action once — e.g. `types` — fixes
139
+ # both the CanCanCan action mapping and the required scope kind.
140
+ # Controllers outside the ResourceController hierarchy can still
141
+ # override `action_kind` directly (e.g. dashboard `analytics`).
91
142
  def action_kind
92
- READ_ACTIONS.include?(action_name) ? 'read' : 'write'
143
+ read = respond_to?(:read_actions, true) ? read_actions : READ_ACTIONS
144
+ read.include?(action_name) ? 'read' : 'write'
93
145
  end
94
146
 
95
147
  # True when authorization derives from the API key's scopes rather
@@ -4,9 +4,13 @@ module Spree
4
4
  module Admin
5
5
  class BaseController < Spree::Api::V3::BaseController
6
6
  include Spree::Api::V3::AdminAuthentication
7
- include Spree::Api::V3::ScopedAuthorization
8
7
 
8
+ # Must be registered before ScopedAuthorization so authenticate_admin!
9
+ # resolves @current_api_key before authorize_api_key_scope! reads it —
10
+ # otherwise the scope guard short-circuits on a nil key and never runs.
9
11
  before_action :authenticate_admin!
12
+
13
+ include Spree::Api::V3::ScopedAuthorization
10
14
  end
11
15
  end
12
16
  end
@@ -0,0 +1,65 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # CRUD for `Spree::OrderRoutingRule` STI subclasses, nested under the
6
+ # owning channel. Same shape as PromotionRulesController — the registry
7
+ # is `Spree.order_routing.rules` and the parent is a Channel.
8
+ class OrderRoutingRulesController < ResourceController
9
+ include Spree::Api::V3::Admin::SubclassedResource
10
+
11
+ scoped_resource :settings
12
+
13
+ subclassed_via -> { Spree.order_routing.rules },
14
+ unknown_type_error: 'unknown_order_routing_rule_type'
15
+
16
+ def types
17
+ authorize! :read, model_class
18
+
19
+ render json: { data: model_class.subclasses_with_preference_schema }
20
+ end
21
+
22
+ protected
23
+
24
+ def model_class
25
+ Spree::OrderRoutingRule
26
+ end
27
+
28
+ def serializer_class
29
+ Spree.api.admin_order_routing_rule_serializer
30
+ end
31
+
32
+ def permitted_params
33
+ params.permit(:type, :active, :position, preferences: {})
34
+ end
35
+
36
+ # `types` is read-only discovery — maps to the read scope + :show ability.
37
+ def read_actions
38
+ super + %w[types]
39
+ end
40
+
41
+ def scope
42
+ super.ordered
43
+ end
44
+
45
+ def set_parent
46
+ return if action_name == 'types'
47
+
48
+ @parent = current_store.channels.accessible_by(current_ability, parent_ability_action)
49
+ .find_by_prefix_id!(params[:channel_id])
50
+ end
51
+
52
+ def parent_association
53
+ :order_routing_rules
54
+ end
55
+
56
+ private
57
+
58
+ def build_subclassed_resource(klass, attrs)
59
+ klass.new(attrs.merge(channel: @parent, store: @parent.store))
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -51,6 +51,11 @@ module Spree
51
51
  params.permit(:name, :description, :active, :storefront_visible, :auto_capture, :position, metadata: {}, preferences: {})
52
52
  end
53
53
 
54
+ # `types` is read-only discovery — maps to the read scope + :show ability.
55
+ def read_actions
56
+ super + %w[types]
57
+ end
58
+
54
59
  private
55
60
 
56
61
  # New payment methods are created through the current store.
@@ -55,6 +55,11 @@ module Spree
55
55
  params.permit(:type, preferences: {})
56
56
  end
57
57
 
58
+ # `types`/`calculators` are read-only discovery — read scope + :show ability.
59
+ def read_actions
60
+ super + %w[types calculators]
61
+ end
62
+
58
63
  def set_parent
59
64
  return if %w[types calculators].include?(action_name)
60
65
 
@@ -33,6 +33,11 @@ module Spree
33
33
  params.permit(:type, preferences: {})
34
34
  end
35
35
 
36
+ # `types` is read-only discovery — maps to the read scope + :show ability.
37
+ def read_actions
38
+ super + %w[types]
39
+ end
40
+
36
41
  def set_parent
37
42
  return if action_name == 'types'
38
43
 
@@ -42,6 +42,7 @@ module Spree
42
42
  :preferred_storefront_access,
43
43
  :preferred_storefront_url,
44
44
  :preferred_guest_checkout,
45
+ :preferred_order_routing_strategy,
45
46
  :mail_from_address,
46
47
  :customer_support_email,
47
48
  :new_order_notifications_email,
@@ -60,15 +60,24 @@ module Spree
60
60
  false
61
61
  end
62
62
 
63
- # Tagging filter. `Spree::Order` carries a `tenant` (store_id) column
64
- # via `acts_as_taggable_tenant`, so its tag vocabulary is bounded to
65
- # the current store; other taggables fall back to the type filter.
63
+ # Tagging filter. Store-owned taggables (`Spree::Product`,
64
+ # `Spree::Order`) tenant their taggings by `store_id` via
65
+ # `acts_as_taggable_tenant`, so their vocabulary is bounded to the
66
+ # current store with a single indexed `tenant` filter. Global
67
+ # taggables like the customer/user type carry no store, so their tags
68
+ # are not store-scoped here.
66
69
  def taggings_conditions
67
70
  conditions = { taggable_type: taggable_type, context: 'tags' }
68
- conditions[:tenant] = current_store.id.to_s if taggable_type == 'Spree::Order'
71
+ conditions[:tenant] = current_store.id.to_s if store_tenanted_taggable?
69
72
  conditions
70
73
  end
71
74
 
75
+ # True for taggables whose taggings carry a store `tenant`
76
+ # (`acts_as_taggable_tenant :store_id`), i.e. store-owned records.
77
+ def store_tenanted_taggable?
78
+ %w[Spree::Product Spree::Order].include?(taggable_type)
79
+ end
80
+
72
81
  # Per-type scope check for API-key principals: a key listing product
73
82
  # tags needs `read_products`, order tags `read_orders`, etc. JWT
74
83
  # admins are gated by store membership + CanCanCan, not scopes.
@@ -0,0 +1,40 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Spree
4
+ module Api
5
+ module V3
6
+ module Admin
7
+ # Serializes Spree::OrderRoutingRule (and its STI subclasses) for the
8
+ # per-channel routing-rules editor. Same shape as PromotionRule so the
9
+ # frontend renders both with the same schema-driven preference form.
10
+ class OrderRoutingRuleSerializer < BaseSerializer
11
+ typelize type: :string,
12
+ channel_id: :string,
13
+ position: :number,
14
+ active: :boolean,
15
+ preferences: 'Record<string, unknown>',
16
+ preference_schema: "Array<{ key: string; type: string; default: unknown }>",
17
+ label: :string,
18
+ description: :string
19
+
20
+ attributes :position, :active
21
+ attributes created_at: :iso8601, updated_at: :iso8601
22
+
23
+ attribute :type do |rule|
24
+ rule.class.api_type
25
+ end
26
+
27
+ attribute :channel_id do |rule|
28
+ rule.channel&.prefixed_id
29
+ end
30
+
31
+ attribute :preferences, &:serialized_preferences
32
+ attribute :preference_schema, &:serialized_preference_schema
33
+
34
+ attribute :label, &:human_name
35
+ attribute :description, &:human_description
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
@@ -21,6 +21,7 @@ module Spree
21
21
  preferred_unit_system: :string,
22
22
  preferred_storefront_access: :string,
23
23
  preferred_guest_checkout: :boolean,
24
+ preferred_order_routing_strategy: :string,
24
25
  metadata: 'Record<string, unknown>'
25
26
 
26
27
  attributes :metadata,
@@ -39,6 +40,7 @@ module Spree
39
40
  :preferred_unit_system,
40
41
  :preferred_storefront_access,
41
42
  :preferred_guest_checkout,
43
+ :preferred_order_routing_strategy,
42
44
  created_at: :iso8601, updated_at: :iso8601
43
45
 
44
46
  attribute :url, &:storefront_url
data/config/routes.rb CHANGED
@@ -345,8 +345,15 @@ Spree::Core::Engine.add_routes do
345
345
  post :add_products
346
346
  post :remove_products
347
347
  end
348
+
349
+ resources :order_routing_rules, only: [:index, :show, :create, :update, :destroy]
348
350
  end
349
351
 
352
+ # Subclass discovery for the routing-rules editor, mirroring
353
+ # `/promotion_rules/types`. Top-level so the SPA can build the
354
+ # "Add rule" picker without a parent channel.
355
+ get 'order_routing_rules/types', to: 'order_routing_rules#types'
356
+
350
357
  # Variants (top-level, for search/autocomplete across all products)
351
358
  resources :variants, only: [:index, :show], concerns: :custom_fieldable
352
359
 
@@ -202,6 +202,7 @@ module Spree
202
202
  admin_admin_user_serializer: 'Spree::Api::V3::Admin::AdminUserSerializer',
203
203
  admin_address_serializer: 'Spree::Api::V3::Admin::AddressSerializer',
204
204
  admin_channel_serializer: 'Spree::Api::V3::Admin::ChannelSerializer',
205
+ admin_order_routing_rule_serializer: 'Spree::Api::V3::Admin::OrderRoutingRuleSerializer',
205
206
  admin_product_publication_serializer: 'Spree::Api::V3::Admin::ProductPublicationSerializer',
206
207
  admin_market_serializer: 'Spree::Api::V3::Admin::MarketSerializer',
207
208
  admin_shipping_method_serializer: 'Spree::Api::V3::Admin::DeliveryMethodSerializer',
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: spree_api
3
3
  version: !ruby/object:Gem::Version
4
- version: 5.6.0.rc4
4
+ version: 5.6.0.rc6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vendo Connect Inc.
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2026-07-20 00:00:00.000000000 Z
11
+ date: 2026-07-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rswag-specs
@@ -72,14 +72,14 @@ dependencies:
72
72
  requirements:
73
73
  - - '='
74
74
  - !ruby/object:Gem::Version
75
- version: 5.6.0.rc4
75
+ version: 5.6.0.rc6
76
76
  type: :runtime
77
77
  prerelease: false
78
78
  version_requirements: !ruby/object:Gem::Requirement
79
79
  requirements:
80
80
  - - '='
81
81
  - !ruby/object:Gem::Version
82
- version: 5.6.0.rc4
82
+ version: 5.6.0.rc6
83
83
  description: Spree's API
84
84
  email:
85
85
  - hello@spreecommerce.org
@@ -142,6 +142,7 @@ files:
142
142
  - app/controllers/spree/api/v3/admin/me_controller.rb
143
143
  - app/controllers/spree/api/v3/admin/media_controller.rb
144
144
  - app/controllers/spree/api/v3/admin/option_types_controller.rb
145
+ - app/controllers/spree/api/v3/admin/order_routing_rules_controller.rb
145
146
  - app/controllers/spree/api/v3/admin/orders/adjustments_controller.rb
146
147
  - app/controllers/spree/api/v3/admin/orders/base_controller.rb
147
148
  - app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb
@@ -252,6 +253,7 @@ files:
252
253
  - app/serializers/spree/api/v3/admin/newsletter_subscriber_serializer.rb
253
254
  - app/serializers/spree/api/v3/admin/option_type_serializer.rb
254
255
  - app/serializers/spree/api/v3/admin/option_value_serializer.rb
256
+ - app/serializers/spree/api/v3/admin/order_routing_rule_serializer.rb
255
257
  - app/serializers/spree/api/v3/admin/order_serializer.rb
256
258
  - app/serializers/spree/api/v3/admin/payment_method_serializer.rb
257
259
  - app/serializers/spree/api/v3/admin/payment_serializer.rb
@@ -382,9 +384,9 @@ licenses:
382
384
  - BSD-3-Clause
383
385
  metadata:
384
386
  bug_tracker_uri: https://github.com/spree/spree/issues
385
- changelog_uri: https://github.com/spree/spree/releases/tag/v5.6.0.rc4
387
+ changelog_uri: https://github.com/spree/spree/releases/tag/v5.6.0.rc6
386
388
  documentation_uri: https://docs.spreecommerce.org/
387
- source_code_uri: https://github.com/spree/spree/tree/v5.6.0.rc4
389
+ source_code_uri: https://github.com/spree/spree/tree/v5.6.0.rc6
388
390
  post_install_message:
389
391
  rdoc_options: []
390
392
  require_paths: