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.
- checksums.yaml +4 -4
- data/Rakefile +19 -0
- data/app/controllers/concerns/spree/api/v3/admin/auth_cookies.rb +62 -0
- data/app/controllers/concerns/spree/api/v3/admin/subclassed_resource.rb +149 -0
- data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +54 -0
- data/app/controllers/concerns/spree/api/v3/bulk_operations.rb +103 -0
- data/app/controllers/concerns/spree/api/v3/channel_resolution.rb +60 -0
- data/app/controllers/concerns/spree/api/v3/error_handler.rb +4 -0
- data/app/controllers/concerns/spree/api/v3/params_normalizer.rb +84 -0
- data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +88 -0
- data/app/controllers/concerns/spree/api/v3/store/search_provider_support.rb +35 -1
- data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +97 -0
- data/app/controllers/spree/api/v3/admin/allowed_origins_controller.rb +25 -0
- data/app/controllers/spree/api/v3/admin/api_keys_controller.rb +55 -0
- data/app/controllers/spree/api/v3/admin/auth_controller.rb +134 -0
- data/app/controllers/spree/api/v3/admin/base_controller.rb +3 -17
- data/app/controllers/spree/api/v3/admin/categories_controller.rb +25 -0
- data/app/controllers/spree/api/v3/admin/channels_controller.rb +65 -0
- data/app/controllers/spree/api/v3/admin/countries_controller.rb +38 -0
- data/app/controllers/spree/api/v3/admin/coupon_codes_controller.rb +33 -0
- data/app/controllers/spree/api/v3/admin/custom_field_definitions_controller.rb +34 -0
- data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +108 -0
- data/app/controllers/spree/api/v3/admin/customer_groups_controller.rb +31 -0
- data/app/controllers/spree/api/v3/admin/customers/addresses_controller.rb +88 -0
- data/app/controllers/spree/api/v3/admin/customers/credit_cards_controller.rb +31 -0
- data/app/controllers/spree/api/v3/admin/customers/store_credits_controller.rb +93 -0
- data/app/controllers/spree/api/v3/admin/customers_controller.rb +119 -0
- data/app/controllers/spree/api/v3/admin/dashboard_controller.rb +44 -0
- data/app/controllers/spree/api/v3/admin/direct_uploads_controller.rb +40 -0
- data/app/controllers/spree/api/v3/admin/exports_controller.rb +89 -0
- data/app/controllers/spree/api/v3/admin/gift_card_batches_controller.rb +31 -0
- data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +33 -0
- data/app/controllers/spree/api/v3/admin/invitation_acceptances_controller.rb +138 -0
- data/app/controllers/spree/api/v3/admin/invitations_controller.rb +70 -0
- data/app/controllers/spree/api/v3/admin/markets_controller.rb +42 -0
- data/app/controllers/spree/api/v3/admin/me_controller.rb +69 -0
- data/app/controllers/spree/api/v3/admin/media_controller.rb +119 -0
- data/app/controllers/spree/api/v3/admin/option_types_controller.rb +34 -0
- data/app/controllers/spree/api/v3/admin/orders/adjustments_controller.rb +27 -0
- data/app/controllers/spree/api/v3/admin/orders/base_controller.rb +26 -0
- data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +104 -0
- data/app/controllers/spree/api/v3/admin/orders/gift_cards_controller.rb +79 -0
- data/app/controllers/spree/api/v3/admin/orders/items_controller.rb +92 -0
- data/app/controllers/spree/api/v3/admin/orders/payments_controller.rb +90 -0
- data/app/controllers/spree/api/v3/admin/orders/refunds_controller.rb +53 -0
- data/app/controllers/spree/api/v3/admin/orders/store_credits_controller.rb +59 -0
- data/app/controllers/spree/api/v3/admin/orders_controller.rb +190 -0
- data/app/controllers/spree/api/v3/admin/payment_methods_controller.rb +73 -0
- data/app/controllers/spree/api/v3/admin/price_lists_controller.rb +156 -0
- data/app/controllers/spree/api/v3/admin/prices_controller.rb +129 -0
- data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +48 -0
- data/app/controllers/spree/api/v3/admin/products_controller.rb +237 -0
- data/app/controllers/spree/api/v3/admin/promotion_actions_controller.rb +78 -0
- data/app/controllers/spree/api/v3/admin/promotion_rules_controller.rb +56 -0
- data/app/controllers/spree/api/v3/admin/promotions_controller.rb +78 -0
- data/app/controllers/spree/api/v3/admin/resource_controller.rb +29 -11
- data/app/controllers/spree/api/v3/admin/roles_controller.rb +29 -0
- data/app/controllers/spree/api/v3/admin/stock_items_controller.rb +35 -0
- data/app/controllers/spree/api/v3/admin/stock_locations_controller.rb +36 -0
- data/app/controllers/spree/api/v3/admin/stock_reservations_controller.rb +29 -0
- data/app/controllers/spree/api/v3/admin/stock_transfers_controller.rb +75 -0
- data/app/controllers/spree/api/v3/admin/store_controller.rb +53 -0
- data/app/controllers/spree/api/v3/admin/store_credit_categories_controller.rb +21 -0
- data/app/controllers/spree/api/v3/admin/tags_controller.rb +51 -0
- data/app/controllers/spree/api/v3/admin/tax_categories_controller.rb +21 -0
- data/app/controllers/spree/api/v3/admin/variants_controller.rb +33 -0
- data/app/controllers/spree/api/v3/admin/webhook_deliveries_controller.rb +49 -0
- data/app/controllers/spree/api/v3/admin/webhook_endpoints_controller.rb +75 -0
- data/app/controllers/spree/api/v3/resource_controller.rb +90 -8
- data/app/controllers/spree/api/v3/store/auth_controller.rb +8 -28
- data/app/controllers/spree/api/v3/store/base_controller.rb +6 -0
- data/app/controllers/spree/api/v3/store/carts_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/customers_controller.rb +6 -0
- data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +77 -0
- data/app/controllers/spree/api/v3/store/products/filters_controller.rb +2 -2
- data/app/controllers/spree/api/v3/store/products_controller.rb +3 -3
- data/app/controllers/spree/api/v3/store/resource_controller.rb +10 -2
- data/app/jobs/spree/webhook_delivery_job.rb +5 -0
- data/app/models/spree/api_key_ability.rb +16 -0
- data/app/serializers/spree/api/v3/admin/address_serializer.rb +2 -6
- data/app/serializers/spree/api/v3/admin/adjustment_serializer.rb +3 -15
- data/app/serializers/spree/api/v3/admin/admin_user_serializer.rb +19 -3
- data/app/serializers/spree/api/v3/admin/allowed_origin_serializer.rb +2 -6
- data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +42 -0
- data/app/serializers/spree/api/v3/admin/category_serializer.rb +4 -3
- data/app/serializers/spree/api/v3/admin/channel_serializer.rb +15 -0
- data/app/serializers/spree/api/v3/admin/country_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/coupon_code_serializer.rb +30 -0
- data/app/serializers/spree/api/v3/admin/credit_card_serializer.rb +4 -2
- data/app/serializers/spree/api/v3/admin/custom_field_definition_serializer.rb +21 -0
- data/app/serializers/spree/api/v3/admin/custom_field_serializer.rb +8 -3
- data/app/serializers/spree/api/v3/admin/customer_group_serializer.rb +27 -0
- data/app/serializers/spree/api/v3/admin/customer_serializer.rb +58 -2
- data/app/serializers/spree/api/v3/admin/dashboard_analytics_serializer.rb +143 -0
- data/app/serializers/spree/api/v3/admin/export_serializer.rb +40 -0
- data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +2 -6
- data/app/serializers/spree/api/v3/admin/{asset_serializer.rb → gift_card_batch_serializer.rb} +1 -1
- data/app/serializers/spree/api/v3/admin/gift_card_serializer.rb +39 -4
- data/app/serializers/spree/api/v3/admin/invitation_serializer.rb +64 -0
- data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +4 -16
- data/app/serializers/spree/api/v3/admin/media_serializer.rb +24 -2
- data/app/serializers/spree/api/v3/admin/option_type_serializer.rb +4 -1
- data/app/serializers/spree/api/v3/admin/option_value_serializer.rb +4 -1
- data/app/serializers/spree/api/v3/admin/order_serializer.rb +21 -6
- data/app/serializers/spree/api/v3/admin/payment_method_serializer.rb +11 -2
- data/app/serializers/spree/api/v3/admin/payment_serializer.rb +2 -6
- data/app/serializers/spree/api/v3/admin/payment_source_serializer.rb +4 -1
- data/app/serializers/spree/api/v3/admin/price_list_serializer.rb +51 -0
- data/app/serializers/spree/api/v3/admin/price_rule_serializer.rb +55 -0
- data/app/serializers/spree/api/v3/admin/price_serializer.rb +4 -0
- data/app/serializers/spree/api/v3/admin/product_publication_serializer.rb +11 -0
- data/app/serializers/spree/api/v3/admin/product_serializer.rb +34 -10
- data/app/serializers/spree/api/v3/admin/promotion_action_serializer.rb +71 -0
- data/app/serializers/spree/api/v3/admin/promotion_rule_serializer.rb +85 -0
- data/app/serializers/spree/api/v3/admin/promotion_serializer.rb +41 -0
- data/app/serializers/spree/api/v3/admin/refund_serializer.rb +4 -2
- data/app/serializers/spree/api/v3/admin/role_serializer.rb +17 -0
- data/app/serializers/spree/api/v3/admin/stock_item_serializer.rb +16 -1
- data/app/serializers/spree/api/v3/admin/stock_location_serializer.rb +11 -2
- data/app/serializers/spree/api/v3/admin/stock_reservation_serializer.rb +46 -0
- data/app/serializers/spree/api/v3/admin/stock_transfer_serializer.rb +37 -0
- data/app/serializers/spree/api/v3/admin/store_credit_category_serializer.rb +19 -0
- data/app/serializers/spree/api/v3/admin/store_credit_serializer.rb +11 -5
- data/app/serializers/spree/api/v3/admin/store_serializer.rb +55 -0
- data/app/serializers/spree/api/v3/admin/tax_category_serializer.rb +4 -2
- data/app/serializers/spree/api/v3/admin/variant_serializer.rb +37 -6
- data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +45 -0
- data/app/serializers/spree/api/v3/admin/webhook_endpoint_serializer.rb +69 -0
- data/app/serializers/spree/api/v3/channel_serializer.rb +14 -0
- data/app/serializers/spree/api/v3/custom_field_serializer.rb +9 -10
- data/app/serializers/spree/api/v3/customer_serializer.rb +5 -0
- data/app/serializers/spree/api/v3/market_serializer.rb +2 -1
- data/app/serializers/spree/api/v3/media_serializer.rb +8 -6
- data/app/serializers/spree/api/v3/order_serializer.rb +6 -1
- data/app/serializers/spree/api/v3/payment_method_serializer.rb +11 -2
- data/app/serializers/spree/api/v3/product_publication_serializer.rb +22 -0
- data/app/serializers/spree/api/v3/product_serializer.rb +6 -1
- data/app/serializers/spree/api/v3/stock_reservation_serializer.rb +10 -0
- data/config/locales/en.yml +2 -0
- data/config/routes.rb +235 -1
- data/lib/spree/api/configuration.rb +2 -2
- data/lib/spree/api/dependencies.rb +25 -1
- data/lib/spree/api/openapi/path_sorter.rb +126 -0
- data/lib/spree/api/openapi/schema_helper.rb +185 -6
- metadata +96 -8
- 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.
|
|
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
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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, :
|
|
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 =
|
|
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
|
|
@@ -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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
7
|
-
|
|
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>
|
|
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
|
-
|
|
8
|
-
|
|
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, :
|
|
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,
|
|
7
|
-
|
|
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
|
|
8
|
+
typelize origin: :string
|
|
9
9
|
|
|
10
|
-
attributes :
|
|
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
|