spree_api 5.4.3 → 5.5.0.rc1

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.
Files changed (146) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +19 -0
  3. data/app/controllers/concerns/spree/api/v3/admin/auth_cookies.rb +62 -0
  4. data/app/controllers/concerns/spree/api/v3/admin/subclassed_resource.rb +149 -0
  5. data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +54 -0
  6. data/app/controllers/concerns/spree/api/v3/bulk_operations.rb +103 -0
  7. data/app/controllers/concerns/spree/api/v3/channel_resolution.rb +60 -0
  8. data/app/controllers/concerns/spree/api/v3/error_handler.rb +4 -0
  9. data/app/controllers/concerns/spree/api/v3/params_normalizer.rb +84 -0
  10. data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +88 -0
  11. data/app/controllers/concerns/spree/api/v3/store/search_provider_support.rb +35 -1
  12. data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +97 -0
  13. data/app/controllers/spree/api/v3/admin/allowed_origins_controller.rb +25 -0
  14. data/app/controllers/spree/api/v3/admin/api_keys_controller.rb +55 -0
  15. data/app/controllers/spree/api/v3/admin/auth_controller.rb +134 -0
  16. data/app/controllers/spree/api/v3/admin/base_controller.rb +3 -17
  17. data/app/controllers/spree/api/v3/admin/categories_controller.rb +25 -0
  18. data/app/controllers/spree/api/v3/admin/channels_controller.rb +65 -0
  19. data/app/controllers/spree/api/v3/admin/countries_controller.rb +38 -0
  20. data/app/controllers/spree/api/v3/admin/coupon_codes_controller.rb +33 -0
  21. data/app/controllers/spree/api/v3/admin/custom_field_definitions_controller.rb +34 -0
  22. data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +108 -0
  23. data/app/controllers/spree/api/v3/admin/customer_groups_controller.rb +31 -0
  24. data/app/controllers/spree/api/v3/admin/customers/addresses_controller.rb +88 -0
  25. data/app/controllers/spree/api/v3/admin/customers/credit_cards_controller.rb +31 -0
  26. data/app/controllers/spree/api/v3/admin/customers/store_credits_controller.rb +93 -0
  27. data/app/controllers/spree/api/v3/admin/customers_controller.rb +119 -0
  28. data/app/controllers/spree/api/v3/admin/dashboard_controller.rb +44 -0
  29. data/app/controllers/spree/api/v3/admin/direct_uploads_controller.rb +40 -0
  30. data/app/controllers/spree/api/v3/admin/exports_controller.rb +89 -0
  31. data/app/controllers/spree/api/v3/admin/gift_card_batches_controller.rb +31 -0
  32. data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +33 -0
  33. data/app/controllers/spree/api/v3/admin/invitation_acceptances_controller.rb +138 -0
  34. data/app/controllers/spree/api/v3/admin/invitations_controller.rb +70 -0
  35. data/app/controllers/spree/api/v3/admin/markets_controller.rb +42 -0
  36. data/app/controllers/spree/api/v3/admin/me_controller.rb +69 -0
  37. data/app/controllers/spree/api/v3/admin/media_controller.rb +119 -0
  38. data/app/controllers/spree/api/v3/admin/option_types_controller.rb +34 -0
  39. data/app/controllers/spree/api/v3/admin/orders/adjustments_controller.rb +27 -0
  40. data/app/controllers/spree/api/v3/admin/orders/base_controller.rb +26 -0
  41. data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +104 -0
  42. data/app/controllers/spree/api/v3/admin/orders/gift_cards_controller.rb +79 -0
  43. data/app/controllers/spree/api/v3/admin/orders/items_controller.rb +92 -0
  44. data/app/controllers/spree/api/v3/admin/orders/payments_controller.rb +90 -0
  45. data/app/controllers/spree/api/v3/admin/orders/refunds_controller.rb +53 -0
  46. data/app/controllers/spree/api/v3/admin/orders/store_credits_controller.rb +59 -0
  47. data/app/controllers/spree/api/v3/admin/orders_controller.rb +190 -0
  48. data/app/controllers/spree/api/v3/admin/payment_methods_controller.rb +73 -0
  49. data/app/controllers/spree/api/v3/admin/price_lists_controller.rb +156 -0
  50. data/app/controllers/spree/api/v3/admin/prices_controller.rb +129 -0
  51. data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +48 -0
  52. data/app/controllers/spree/api/v3/admin/products_controller.rb +237 -0
  53. data/app/controllers/spree/api/v3/admin/promotion_actions_controller.rb +78 -0
  54. data/app/controllers/spree/api/v3/admin/promotion_rules_controller.rb +56 -0
  55. data/app/controllers/spree/api/v3/admin/promotions_controller.rb +78 -0
  56. data/app/controllers/spree/api/v3/admin/resource_controller.rb +29 -11
  57. data/app/controllers/spree/api/v3/admin/roles_controller.rb +29 -0
  58. data/app/controllers/spree/api/v3/admin/stock_items_controller.rb +35 -0
  59. data/app/controllers/spree/api/v3/admin/stock_locations_controller.rb +36 -0
  60. data/app/controllers/spree/api/v3/admin/stock_reservations_controller.rb +29 -0
  61. data/app/controllers/spree/api/v3/admin/stock_transfers_controller.rb +75 -0
  62. data/app/controllers/spree/api/v3/admin/store_controller.rb +53 -0
  63. data/app/controllers/spree/api/v3/admin/store_credit_categories_controller.rb +21 -0
  64. data/app/controllers/spree/api/v3/admin/tags_controller.rb +51 -0
  65. data/app/controllers/spree/api/v3/admin/tax_categories_controller.rb +21 -0
  66. data/app/controllers/spree/api/v3/admin/variants_controller.rb +33 -0
  67. data/app/controllers/spree/api/v3/admin/webhook_deliveries_controller.rb +49 -0
  68. data/app/controllers/spree/api/v3/admin/webhook_endpoints_controller.rb +75 -0
  69. data/app/controllers/spree/api/v3/resource_controller.rb +90 -8
  70. data/app/controllers/spree/api/v3/store/auth_controller.rb +8 -28
  71. data/app/controllers/spree/api/v3/store/base_controller.rb +6 -0
  72. data/app/controllers/spree/api/v3/store/carts_controller.rb +1 -0
  73. data/app/controllers/spree/api/v3/store/customers_controller.rb +6 -0
  74. data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +77 -0
  75. data/app/controllers/spree/api/v3/store/products/filters_controller.rb +2 -2
  76. data/app/controllers/spree/api/v3/store/products_controller.rb +3 -3
  77. data/app/controllers/spree/api/v3/store/resource_controller.rb +10 -2
  78. data/app/jobs/spree/webhook_delivery_job.rb +5 -0
  79. data/app/models/spree/api_key_ability.rb +16 -0
  80. data/app/serializers/spree/api/v3/admin/address_serializer.rb +2 -6
  81. data/app/serializers/spree/api/v3/admin/adjustment_serializer.rb +3 -15
  82. data/app/serializers/spree/api/v3/admin/admin_user_serializer.rb +19 -3
  83. data/app/serializers/spree/api/v3/admin/allowed_origin_serializer.rb +2 -6
  84. data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +42 -0
  85. data/app/serializers/spree/api/v3/admin/category_serializer.rb +4 -3
  86. data/app/serializers/spree/api/v3/admin/channel_serializer.rb +15 -0
  87. data/app/serializers/spree/api/v3/admin/country_serializer.rb +1 -1
  88. data/app/serializers/spree/api/v3/admin/coupon_code_serializer.rb +30 -0
  89. data/app/serializers/spree/api/v3/admin/credit_card_serializer.rb +4 -2
  90. data/app/serializers/spree/api/v3/admin/custom_field_definition_serializer.rb +21 -0
  91. data/app/serializers/spree/api/v3/admin/custom_field_serializer.rb +8 -3
  92. data/app/serializers/spree/api/v3/admin/customer_group_serializer.rb +27 -0
  93. data/app/serializers/spree/api/v3/admin/customer_serializer.rb +58 -2
  94. data/app/serializers/spree/api/v3/admin/dashboard_analytics_serializer.rb +143 -0
  95. data/app/serializers/spree/api/v3/admin/export_serializer.rb +40 -0
  96. data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +2 -6
  97. data/app/serializers/spree/api/v3/admin/{asset_serializer.rb → gift_card_batch_serializer.rb} +1 -1
  98. data/app/serializers/spree/api/v3/admin/gift_card_serializer.rb +39 -4
  99. data/app/serializers/spree/api/v3/admin/invitation_serializer.rb +64 -0
  100. data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +4 -16
  101. data/app/serializers/spree/api/v3/admin/media_serializer.rb +24 -2
  102. data/app/serializers/spree/api/v3/admin/option_type_serializer.rb +4 -1
  103. data/app/serializers/spree/api/v3/admin/option_value_serializer.rb +4 -1
  104. data/app/serializers/spree/api/v3/admin/order_serializer.rb +21 -6
  105. data/app/serializers/spree/api/v3/admin/payment_method_serializer.rb +11 -2
  106. data/app/serializers/spree/api/v3/admin/payment_serializer.rb +2 -6
  107. data/app/serializers/spree/api/v3/admin/payment_source_serializer.rb +4 -1
  108. data/app/serializers/spree/api/v3/admin/price_list_serializer.rb +51 -0
  109. data/app/serializers/spree/api/v3/admin/price_rule_serializer.rb +55 -0
  110. data/app/serializers/spree/api/v3/admin/price_serializer.rb +4 -0
  111. data/app/serializers/spree/api/v3/admin/product_publication_serializer.rb +11 -0
  112. data/app/serializers/spree/api/v3/admin/product_serializer.rb +34 -10
  113. data/app/serializers/spree/api/v3/admin/promotion_action_serializer.rb +71 -0
  114. data/app/serializers/spree/api/v3/admin/promotion_rule_serializer.rb +85 -0
  115. data/app/serializers/spree/api/v3/admin/promotion_serializer.rb +41 -0
  116. data/app/serializers/spree/api/v3/admin/refund_serializer.rb +4 -2
  117. data/app/serializers/spree/api/v3/admin/role_serializer.rb +17 -0
  118. data/app/serializers/spree/api/v3/admin/stock_item_serializer.rb +16 -1
  119. data/app/serializers/spree/api/v3/admin/stock_location_serializer.rb +11 -2
  120. data/app/serializers/spree/api/v3/admin/stock_reservation_serializer.rb +46 -0
  121. data/app/serializers/spree/api/v3/admin/stock_transfer_serializer.rb +37 -0
  122. data/app/serializers/spree/api/v3/admin/store_credit_category_serializer.rb +19 -0
  123. data/app/serializers/spree/api/v3/admin/store_credit_serializer.rb +11 -5
  124. data/app/serializers/spree/api/v3/admin/store_serializer.rb +55 -0
  125. data/app/serializers/spree/api/v3/admin/tax_category_serializer.rb +4 -2
  126. data/app/serializers/spree/api/v3/admin/variant_serializer.rb +37 -6
  127. data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +45 -0
  128. data/app/serializers/spree/api/v3/admin/webhook_endpoint_serializer.rb +69 -0
  129. data/app/serializers/spree/api/v3/channel_serializer.rb +14 -0
  130. data/app/serializers/spree/api/v3/custom_field_serializer.rb +9 -10
  131. data/app/serializers/spree/api/v3/customer_serializer.rb +5 -0
  132. data/app/serializers/spree/api/v3/market_serializer.rb +2 -1
  133. data/app/serializers/spree/api/v3/media_serializer.rb +8 -6
  134. data/app/serializers/spree/api/v3/order_serializer.rb +6 -1
  135. data/app/serializers/spree/api/v3/payment_method_serializer.rb +11 -2
  136. data/app/serializers/spree/api/v3/product_publication_serializer.rb +22 -0
  137. data/app/serializers/spree/api/v3/product_serializer.rb +6 -1
  138. data/app/serializers/spree/api/v3/stock_reservation_serializer.rb +10 -0
  139. data/config/locales/en.yml +2 -0
  140. data/config/routes.rb +235 -1
  141. data/lib/spree/api/configuration.rb +2 -2
  142. data/lib/spree/api/dependencies.rb +25 -1
  143. data/lib/spree/api/openapi/path_sorter.rb +126 -0
  144. data/lib/spree/api/openapi/schema_helper.rb +185 -6
  145. metadata +96 -8
  146. data/app/serializers/spree/api/v3/admin/shipping_category_serializer.rb +0 -14
@@ -0,0 +1,75 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Admin API for outbound webhook endpoints — CRUD plus the three
6
+ # endpoint-scoped actions the legacy admin had (send_test, enable,
7
+ # disable).
8
+ class WebhookEndpointsController < ResourceController
9
+ scoped_resource :settings
10
+
11
+ # POST /api/v3/admin/webhook_endpoints/:id/send_test
12
+ #
13
+ # Fires a synthetic `webhook.test` delivery so admins can verify the
14
+ # endpoint is reachable + their signature-verification code works.
15
+ #
16
+ # @return [Hash] the serialized {Spree::WebhookDelivery}, HTTP 201.
17
+ def send_test
18
+ @resource = find_resource
19
+ authorize!(:update, @resource)
20
+
21
+ delivery = @resource.send_test!
22
+ render json: Spree.api.admin_webhook_delivery_serializer.new(delivery).to_h, status: :created
23
+ end
24
+
25
+ # PATCH /api/v3/admin/webhook_endpoints/:id/enable
26
+ #
27
+ # Re-enables an endpoint that was auto-disabled after repeated failures.
28
+ #
29
+ # @return [Hash] the serialized {Spree::WebhookEndpoint}.
30
+ def enable
31
+ @resource = find_resource
32
+ authorize!(:update, @resource)
33
+
34
+ @resource.enable!
35
+ render json: serialize_resource(@resource)
36
+ end
37
+
38
+ # PATCH /api/v3/admin/webhook_endpoints/:id/disable
39
+ #
40
+ # Manual disable — separate from the auto-disable threshold so the
41
+ # caller can pause an endpoint without waiting for failures.
42
+ #
43
+ # @param reason [String] optional human-readable reason; defaults to
44
+ # `"Manually disabled"` when blank.
45
+ # @return [Hash] the serialized {Spree::WebhookEndpoint}.
46
+ def disable
47
+ @resource = find_resource
48
+ authorize!(:update, @resource)
49
+
50
+ @resource.disable!(reason: params[:reason].presence || 'Manually disabled', notify: false)
51
+ render json: serialize_resource(@resource)
52
+ end
53
+
54
+ protected
55
+
56
+ def model_class
57
+ Spree::WebhookEndpoint
58
+ end
59
+
60
+ def serializer_class
61
+ Spree.api.admin_webhook_endpoint_serializer
62
+ end
63
+
64
+ def scope
65
+ current_store.webhook_endpoints.accessible_by(current_ability, :show)
66
+ end
67
+
68
+ def permitted_params
69
+ params.permit(:name, :url, :active, subscriptions: [])
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -2,6 +2,11 @@ module Spree
2
2
  module Api
3
3
  module V3
4
4
  class ResourceController < BaseController
5
+ include Spree::Api::V3::ParamsNormalizer
6
+
7
+ # Must run before +set_resource+: +scope+'s +accessible_by+ depends on
8
+ # the post-authentication +current_ability+.
9
+ before_action :authenticate_request!
5
10
  before_action :set_parent
6
11
  before_action :set_resource, only: [:show, :update, :destroy]
7
12
 
@@ -48,13 +53,36 @@ module Spree
48
53
  end
49
54
 
50
55
  # DELETE /api/v3/resource/:id
56
+ # Domain rules like "redeemed gift cards cannot be deleted" live on
57
+ # the model via `can_be_deleted?` and apply to all callers (JWT and
58
+ # API key). When `can_be_deleted?` returns false we render 422
59
+ # (resource state forbids the request) rather than 403, since the
60
+ # caller is authorized — it's the resource's state that's blocking
61
+ # the operation. Models that prefer CanCan-gated destroy can opt in
62
+ # via their ability (e.g. `can :destroy, Spree::Order, &:can_be_deleted?`),
63
+ # which raises before the controller hook fires and yields 403.
51
64
  def destroy
52
- @resource.destroy
65
+ if @resource.respond_to?(:can_be_deleted?) && !@resource.can_be_deleted?
66
+ message = Spree.t(:cannot_delete, scope: 'api', model: @resource.class.model_name.human)
67
+ return render_error(
68
+ code: ERROR_CODES[:validation_error],
69
+ message: message,
70
+ status: :unprocessable_content
71
+ )
72
+ end
73
+
74
+ @resource.destroy!
53
75
  head :no_content
76
+ rescue ActiveRecord::RecordNotDestroyed => e
77
+ render_validation_error(e.record.errors.presence || e.message)
54
78
  end
55
79
 
56
80
  protected
57
81
 
82
+ def authenticate_request!
83
+ raise NotImplementedError, "#{self.class} must implement authenticate_request!"
84
+ end
85
+
58
86
  # No-op HTTP caching methods. Include Spree::Api::V3::HttpCaching
59
87
  # in specific controllers to enable HTTP caching for their actions.
60
88
  def cache_collection(_collection, **_options)
@@ -80,11 +108,16 @@ module Spree
80
108
 
81
109
  # Builds a new resource, using parent association when @parent is set
82
110
  def build_resource
83
- if @parent.present?
84
- @parent.send(parent_association).build(permitted_params)
85
- else
86
- model_class.new(permitted_params)
87
- end
111
+ resource = if @parent.present?
112
+ @parent.send(parent_association).build(permitted_params)
113
+ else
114
+ model_class.new(permitted_params)
115
+ end
116
+ resource.store = current_store if resource.respond_to?(:store_id) && resource.store_id.blank?
117
+ # very ugly code we need to still support for promotion/payment_method until we migrate them into single store in spree 6.0
118
+ resource.store_ids = [current_store.id] if resource.respond_to?(:store_ids) && resource.store_ids.blank? && !resource.respond_to?(:store_id)
119
+ resource.created_by = try_spree_current_user if resource.respond_to?(:created_by_id)
120
+ resource
88
121
  end
89
122
 
90
123
  # Finds a single resource within scope using prefixed ID
@@ -133,8 +166,11 @@ module Spree
133
166
  # Ransack query parameters with sort translation.
134
167
  # Translates `-field` notation (JSON:API standard) to Ransack `s` format.
135
168
  # e.g., sort=-price,name → s=price desc,name asc
169
+ # Also decodes Stripe-style prefixed IDs found in keys like `*_id_eq`,
170
+ # `*_id_in`, `*_id_not_eq`, etc. so SPA filters can pass prefixed IDs.
136
171
  def ransack_params
137
172
  rp = params[:q]&.to_unsafe_h || params[:q] || {}
173
+ rp = decode_prefixed_id_predicates(rp)
138
174
  sort_value = sort_param
139
175
 
140
176
  if sort_value.present?
@@ -151,6 +187,37 @@ module Spree
151
187
  rp
152
188
  end
153
189
 
190
+ def decode_prefixed_id_predicates(hash)
191
+ return hash unless hash.is_a?(Hash)
192
+
193
+ hash.each_with_object({}) do |(key, value), result|
194
+ result[key] = if ransack_id_predicate?(key)
195
+ Array(value).map { |v| Spree::PrefixedId.prefixed_id?(v) ? Spree::PrefixedId.decode_prefixed_id(v) || v : v }.then { |arr|
196
+ value.is_a?(Array) ? arr : arr.first
197
+ }
198
+ elsif value.is_a?(Hash)
199
+ decode_prefixed_id_predicates(value)
200
+ else
201
+ value
202
+ end
203
+ end
204
+ end
205
+
206
+ # Matches both prefixed-FK predicates (`product_id_in`, `tax_category_id_eq`)
207
+ # and the bare-`id` predicates (`id_in`, `id_eq`) on the resource's
208
+ # primary key. Without the bare-id branch, `q[id_in][]=prod_x` would
209
+ # be passed to Ransack verbatim and never match any row.
210
+ #
211
+ # Requires a Ransack-predicate suffix (`_eq`, `_in`, ...) — bare
212
+ # `_id`/`_ids` keys without a suffix are scope names, not predicates
213
+ # (e.g. `with_option_value_ids` is a custom scope that handles its
214
+ # own decoding). Decoding those would double-strip prefixes and
215
+ # break downstream filter code.
216
+ RANSACK_ID_PREDICATE_RE = /(?:\A|_)id(?:s)?_(?:eq|not_eq|in|not_in|lt|lteq|gt|gteq)\z/.freeze
217
+ def ransack_id_predicate?(key)
218
+ RANSACK_ID_PREDICATE_RE.match?(key.to_s)
219
+ end
220
+
154
221
  # Sort parameter from the request
155
222
  def sort_param
156
223
  params[:sort]
@@ -194,7 +261,22 @@ module Spree
194
261
  else
195
262
  model_class.for_store(current_store)
196
263
  end
197
- base_scope = base_scope.accessible_by(current_ability, :show) unless @parent.present?
264
+ unless @parent.present?
265
+ action_name = case request.method
266
+ when 'GET', 'HEAD'
267
+ :show
268
+ when 'POST'
269
+ :create
270
+ when 'PATCH', 'PUT'
271
+ :update
272
+ when 'DELETE'
273
+ :destroy
274
+ else
275
+ raise ActionController::MethodNotAllowed, request.method
276
+ end
277
+
278
+ base_scope = base_scope.accessible_by(current_ability, action_name)
279
+ end
198
280
  base_scope = base_scope.includes(scope_includes) if scope_includes.any?
199
281
  base_scope = base_scope.preload_associations_lazily
200
282
  model_class.include?(Spree::TranslatableResource) ? base_scope.i18n : base_scope
@@ -228,7 +310,7 @@ module Spree
228
310
  #
229
311
  # Override in subclass for custom parameter handling
230
312
  def permitted_params
231
- params.permit(permitted_attributes)
313
+ normalize_params(params.permit(permitted_attributes))
232
314
  end
233
315
 
234
316
  # Returns the permitted attributes list for the model
@@ -6,10 +6,9 @@ module Spree
6
6
  # Tighter rate limits for auth endpoints (per IP to prevent brute force)
7
7
  rate_limit to: Spree::Api::Config[:rate_limit_login], within: Spree::Api::Config[:rate_limit_window].seconds, store: Rails.cache, only: :create, with: RATE_LIMIT_RESPONSE
8
8
  rate_limit to: Spree::Api::Config[:rate_limit_refresh], within: Spree::Api::Config[:rate_limit_window].seconds, store: Rails.cache, only: :refresh, with: RATE_LIMIT_RESPONSE
9
- rate_limit to: Spree::Api::Config[:rate_limit_oauth], within: Spree::Api::Config[:rate_limit_window].seconds, store: Rails.cache, only: :oauth_callback, with: RATE_LIMIT_RESPONSE
10
9
  rate_limit to: Spree::Api::Config[:rate_limit_refresh], within: Spree::Api::Config[:rate_limit_window].seconds, store: Rails.cache, only: :logout, with: RATE_LIMIT_RESPONSE
11
10
 
12
- skip_before_action :authenticate_user, only: [:create, :refresh, :oauth_callback]
11
+ skip_before_action :authenticate_user, only: [:create, :refresh, :logout]
13
12
 
14
13
  # POST /api/v3/store/auth/login
15
14
  # Supports multiple authentication providers via :provider param
@@ -69,37 +68,17 @@ module Spree
69
68
 
70
69
  # POST /api/v3/store/auth/logout
71
70
  # Accepts: { "refresh_token": "rt_xxx" }
72
- # Revokes the refresh token
71
+ # Revokes the submitted refresh token. The token itself is the
72
+ # credential — no access JWT is required, so clients with an expired
73
+ # access token can still log out.
73
74
  def logout
74
75
  refresh_token_value = params[:refresh_token]
75
76
 
76
- if refresh_token_value.present?
77
- Spree::RefreshToken.find_by(token: refresh_token_value)&.destroy
78
- end
77
+ Spree::RefreshToken.find_by(token: refresh_token_value)&.destroy if refresh_token_value.present?
79
78
 
80
79
  head :no_content
81
80
  end
82
81
 
83
- # POST /api/v3/store/auth/oauth/callback
84
- # OAuth callback endpoint for server-side OAuth flows
85
- def oauth_callback
86
- strategy = authentication_strategy
87
- return unless strategy # Error already rendered by determine_strategy
88
-
89
- result = strategy.authenticate
90
-
91
- if result.success?
92
- user = result.value
93
- render json: auth_response(user)
94
- else
95
- render_error(
96
- code: ERROR_CODES[:authentication_failed],
97
- message: result.error,
98
- status: :unauthorized
99
- )
100
- end
101
- end
102
-
103
82
  protected
104
83
 
105
84
  def serializer_params
@@ -133,6 +112,8 @@ module Spree
133
112
 
134
113
  def authentication_strategy
135
114
  strategy_class = determine_strategy
115
+ return nil unless strategy_class
116
+
136
117
  strategy_class.new(
137
118
  params: params,
138
119
  request_env: request.headers.env,
@@ -142,10 +123,9 @@ module Spree
142
123
 
143
124
  def determine_strategy
144
125
  provider = params[:provider].presence || 'email'
145
- provider_key = provider.to_sym
146
126
 
147
127
  # Retrieve pre-loaded strategy class from configuration
148
- strategy_class = Rails.application.config.spree.store_authentication_strategies[provider_key]
128
+ strategy_class = Spree.store_authentication_strategies[provider]
149
129
 
150
130
  unless strategy_class
151
131
  render_error(
@@ -3,6 +3,12 @@ module Spree
3
3
  module V3
4
4
  module Store
5
5
  class BaseController < Spree::Api::V3::BaseController
6
+ # Channel resolution is a Store API concern — admin endpoints return
7
+ # data across all channels and filter via Ransack instead. Including
8
+ # this here keeps the +X-Spree-Channel+ header from accidentally
9
+ # narrowing admin queries.
10
+ include Spree::Api::V3::ChannelResolution
11
+
6
12
  # Require publishable API key for all Store API requests
7
13
  before_action :authenticate_api_key!
8
14
  end
@@ -34,6 +34,7 @@ module Spree
34
34
  params: permitted_params.merge(
35
35
  user: current_user,
36
36
  store: current_store,
37
+ channel: current_channel,
37
38
  currency: current_currency,
38
39
  locale: current_locale
39
40
  )
@@ -13,6 +13,7 @@ module Spree
13
13
  user = Spree.user_class.new(permitted_params.except(:current_password))
14
14
 
15
15
  if user.save
16
+ link_matching_newsletter_subscriber!(user)
16
17
  refresh_token = Spree::RefreshToken.create_for(user, request_env: {
17
18
  ip_address: request.remote_ip,
18
19
  user_agent: request.user_agent&.truncate(255)
@@ -94,6 +95,11 @@ module Spree
94
95
  def user_serializer
95
96
  Spree.api.customer_serializer
96
97
  end
98
+
99
+ def link_matching_newsletter_subscriber!(user)
100
+ subscriber = Spree::NewsletterSubscriber.find_by(email: user.email, store: current_store)
101
+ Spree::Newsletter::LinkUser.new(subscriber: subscriber, user: user).call
102
+ end
97
103
  end
98
104
  end
99
105
  end
@@ -0,0 +1,77 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Store
5
+ class NewsletterSubscribersController < Store::BaseController
6
+ rate_limit to: Spree::Api::Config[:rate_limit_register],
7
+ within: Spree::Api::Config[:rate_limit_window].seconds,
8
+ store: Rails.cache,
9
+ only: [:create, :verify],
10
+ with: RATE_LIMIT_RESPONSE
11
+
12
+ # POST /api/v3/store/newsletter_subscribers
13
+ def create
14
+ subscriber = Spree::NewsletterSubscriber.subscribe(
15
+ email: params[:email],
16
+ user: current_user,
17
+ store: current_store,
18
+ redirect_url: validated_redirect_url
19
+ )
20
+
21
+ if subscriber.errors.any?
22
+ render_errors(subscriber.errors)
23
+ else
24
+ render json: serialize_resource(subscriber), status: :created
25
+ end
26
+ end
27
+
28
+ # POST /api/v3/store/newsletter_subscribers/verify
29
+ def verify
30
+ token = params[:token]
31
+
32
+ if token.blank?
33
+ return render_error(
34
+ code: ERROR_CODES[:parameter_missing],
35
+ message: 'token is required',
36
+ status: :unprocessable_content
37
+ )
38
+ end
39
+
40
+ subscriber = Spree::NewsletterSubscriber.for_store(current_store).unverified.find_by(verification_token: token)
41
+
42
+ unless subscriber
43
+ return render_error(
44
+ code: ERROR_CODES[:invalid_token],
45
+ message: Spree.t(:newsletter_verification_token_invalid, scope: :api),
46
+ status: :unprocessable_content
47
+ )
48
+ end
49
+
50
+ Spree::Newsletter::Verify.new(subscriber: subscriber).call
51
+
52
+ render json: serialize_resource(subscriber)
53
+ end
54
+
55
+ protected
56
+
57
+ def serializer_class
58
+ Spree::Api::V3::NewsletterSubscriberSerializer
59
+ end
60
+
61
+ private
62
+
63
+ # Drop redirect_url when it isn't in the store's allow-list — secure-by-default,
64
+ # mirrors password_resets. Returning nil omits it from the webhook payload rather
65
+ # than rejecting the request, so callers can't probe the allow-list via 4xx errors.
66
+ def validated_redirect_url
67
+ redirect_url = params[:redirect_url]
68
+ return nil if redirect_url.blank?
69
+ return nil unless current_store.allowed_origin?(redirect_url)
70
+
71
+ redirect_url
72
+ end
73
+ end
74
+ end
75
+ end
76
+ end
77
+ end
@@ -29,7 +29,7 @@ module Spree
29
29
 
30
30
  def filters_cache_key
31
31
  products_table = Spree::Product.table_name
32
- stats = current_store.products.active(current_currency)
32
+ stats = current_store.products.available(Time.current, current_currency)
33
33
  .pick(Arel.sql("MAX(#{products_table}.updated_at)"), Arel.sql("COUNT(DISTINCT #{products_table}.id)"))
34
34
  max_updated = stats&.first&.to_i
35
35
  product_count = stats&.last || 0
@@ -50,7 +50,7 @@ module Spree
50
50
  end
51
51
 
52
52
  def filters_scope
53
- scope = current_store.products.active(current_currency)
53
+ scope = current_store.products.available(Time.current, current_currency)
54
54
  scope = scope.in_category(category) if category.present?
55
55
  scope.accessible_by(current_ability, :show)
56
56
  end
@@ -29,15 +29,15 @@ module Spree
29
29
  end
30
30
 
31
31
  def scope
32
- super.active(Spree::Current.currency)
32
+ super.available(Time.current, Spree::Current.currency)
33
33
  end
34
34
 
35
35
  # these scopes are not automatically picked by ar_lazy_preload gem and we need to explicitly include them
36
36
  def scope_includes
37
37
  [
38
38
  primary_media: [attachment_attachment: :blob],
39
- master: [:prices, stock_items: :stock_location],
40
- variants: [:prices, stock_items: :stock_location]
39
+ master: [:prices, stock_items: [:stock_location, :active_stock_reservations]],
40
+ variants: [:prices, stock_items: [:stock_location, :active_stock_reservations]]
41
41
  ]
42
42
  end
43
43
 
@@ -2,9 +2,17 @@ module Spree
2
2
  module Api
3
3
  module V3
4
4
  module Store
5
+ # Mirrors Store::BaseController's concerns. Both classes anchor parallel
6
+ # inheritance branches (V3::BaseController vs V3::ResourceController);
7
+ # any concern added here MUST also be added to Store::BaseController.
5
8
  class ResourceController < Spree::Api::V3::ResourceController
6
- # Require publishable API key for all Store API requests
7
- before_action :authenticate_api_key!
9
+ include Spree::Api::V3::ChannelResolution
10
+
11
+ protected
12
+
13
+ def authenticate_request!
14
+ authenticate_api_key!
15
+ end
8
16
  end
9
17
  end
10
18
  end
@@ -4,7 +4,12 @@ module Spree
4
4
  class WebhookDeliveryJob < Spree::BaseJob
5
5
  queue_as Spree.queues.webhooks
6
6
 
7
+ # Webhook delivery hits external endpoints; broad retry covers network timeouts,
8
+ # 5xx, DNS failures, etc.
7
9
  retry_on StandardError, wait: :polynomially_longer, attempts: 5
10
+ # Must come after `retry_on StandardError` so DeserializationError lands in discard
11
+ # (ActiveJob handler lookup is reverse-declaration-order).
12
+ discard_on ActiveJob::DeserializationError
8
13
 
9
14
  # Accept optional second argument for backward compatibility with jobs
10
15
  # enqueued before this change was deployed.
@@ -0,0 +1,16 @@
1
+ module Spree
2
+ # CanCanCan ability used for API-key-authenticated admin requests.
3
+ # Grants full access — authorization happens at the scope-check layer
4
+ # (Spree::Api::V3::ScopedAuthorization), not at the per-record CanCanCan
5
+ # layer. This exists so that `accessible_by(current_ability, :show)` in
6
+ # admin controllers returns the unrestricted scope (it would otherwise
7
+ # require a real Spree::Ability with role lookups, which doesn't apply
8
+ # to API key principals).
9
+ class ApiKeyAbility
10
+ include CanCan::Ability
11
+
12
+ def initialize(_options = {})
13
+ can :manage, :all
14
+ end
15
+ end
16
+ end
@@ -5,18 +5,14 @@ module Spree
5
5
  class AddressSerializer < V3::AddressSerializer
6
6
  typelize label: [:string, nullable: true],
7
7
  customer_id: [:string, nullable: true],
8
- metadata: 'Record<string, unknown> | null'
8
+ metadata: 'Record<string, unknown>'
9
9
 
10
- attributes :label,
10
+ attributes :label, :metadata,
11
11
  created_at: :iso8601, updated_at: :iso8601
12
12
 
13
13
  attribute :customer_id do |address|
14
14
  address.user&.prefixed_id
15
15
  end
16
-
17
- attribute :metadata do |address|
18
- address.metadata.presence
19
- end
20
16
  end
21
17
  end
22
18
  end
@@ -4,31 +4,19 @@ module Spree
4
4
  module Admin
5
5
  class AdjustmentSerializer < V3::BaseSerializer
6
6
  typelize label: :string, amount: :string, display_amount: :string,
7
- state: :string, eligible: :boolean, mandatory: :boolean, included: :boolean,
8
- source_type: [:string, nullable: true],
9
- adjustable_type: :string, adjustable_id: :string,
10
- order_id: [:string, nullable: true],
11
- source_id: [:string, nullable: true]
7
+ included: :boolean,
8
+ order_id: [:string, nullable: true]
12
9
 
13
- attributes :label, :display_amount, :state, :eligible, :mandatory, :included,
14
- :source_type, :adjustable_type,
10
+ attributes :label, :display_amount, :included,
15
11
  created_at: :iso8601, updated_at: :iso8601
16
12
 
17
13
  attribute :amount do |adjustment|
18
14
  adjustment.amount.to_s
19
15
  end
20
16
 
21
- attribute :adjustable_id do |adjustment|
22
- adjustment.adjustable&.prefixed_id
23
- end
24
-
25
17
  attribute :order_id do |adjustment|
26
18
  adjustment.order&.prefixed_id
27
19
  end
28
-
29
- attribute :source_id do |adjustment|
30
- adjustment.source&.prefixed_id
31
- end
32
20
  end
33
21
  end
34
22
  end
@@ -3,11 +3,27 @@ module Spree
3
3
  module V3
4
4
  module Admin
5
5
  class AdminUserSerializer < V3::BaseSerializer
6
- typelize email: :string, first_name: [:string, nullable: true],
7
- last_name: [:string, nullable: true]
6
+ typelize email: :string,
7
+ first_name: [:string, nullable: true],
8
+ last_name: [:string, nullable: true],
9
+ full_name: [:string, nullable: true],
10
+ roles: 'Array<{ id: string; name: string }>'
8
11
 
9
- attributes :email, :first_name, :last_name,
12
+ attributes :email, :first_name, :last_name, :full_name,
10
13
  created_at: :iso8601, updated_at: :iso8601
14
+
15
+ # Roles assigned to this user *for the current store*. Each store
16
+ # gets its own role set via `Spree::RoleUser`, so this attribute is
17
+ # scoped against `current_store` rather than returning every role
18
+ # the user might have on other stores. Block receives `params`
19
+ # only when Alba passes it through the `serializer_params` hash —
20
+ # we fall back to `Spree::Current.store` if not.
21
+ attribute :roles do |user, params|
22
+ store = params&.dig(:store) || Spree::Current.store
23
+ scope = user.role_users
24
+ scope = scope.where(resource: store) if store
25
+ scope.includes(:role).map { |ru| { id: ru.role.prefixed_id, name: ru.role.name } }
26
+ end
11
27
  end
12
28
  end
13
29
  end
@@ -5,13 +5,9 @@ module Spree
5
5
  module V3
6
6
  module Admin
7
7
  class AllowedOriginSerializer < V3::BaseSerializer
8
- typelize store_id: :string
8
+ typelize origin: :string
9
9
 
10
- attributes :id, :origin, created_at: :iso8601, updated_at: :iso8601
11
-
12
- attribute :store_id do |allowed_origin|
13
- allowed_origin.store&.prefixed_id
14
- end
10
+ attributes :origin, created_at: :iso8601, updated_at: :iso8601
15
11
  end
16
12
  end
17
13
  end
@@ -0,0 +1,42 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Admin API serializer for {Spree::ApiKey}.
6
+ #
7
+ # Never exposes `token` or `token_digest` — only the 12-char
8
+ # `token_prefix` (e.g. `sk_abc123def`) so existing keys can be
9
+ # identified in the UI without leaking material that would let an
10
+ # attacker make requests. The full plaintext token is delivered
11
+ # exactly once, as the response body of `POST /api/v3/admin/api_keys`,
12
+ # via {#plaintext_token} below — it is `nil` everywhere else.
13
+ class ApiKeySerializer < V3::BaseSerializer
14
+ typelize name: :string,
15
+ key_type: :string,
16
+ token_prefix: [:string, nullable: true],
17
+ plaintext_token: [:string, nullable: true],
18
+ scopes: [:string, multi: true],
19
+ revoked_at: [:string, nullable: true],
20
+ last_used_at: [:string, nullable: true],
21
+ created_by_email: [:string, nullable: true]
22
+
23
+ attributes :name, :key_type, :token_prefix, :scopes,
24
+ created_at: :iso8601, updated_at: :iso8601,
25
+ revoked_at: :iso8601, last_used_at: :iso8601
26
+
27
+ # Returned only on the create response — `plaintext_token` is held in
28
+ # memory on the model after `generate_token` and is never persisted
29
+ # for secret keys, so we serialize it whenever it's available rather
30
+ # than gating on the action.
31
+ attribute :plaintext_token do |key|
32
+ key.plaintext_token
33
+ end
34
+
35
+ attribute :created_by_email do |key|
36
+ key.created_by&.email
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end