spree_api 5.4.3 → 5.5.0.rc2

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 (149) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +36 -0
  3. data/app/controllers/concerns/spree/api/v3/admin/auth_cookies.rb +62 -0
  4. data/app/controllers/concerns/spree/api/v3/admin/role_grant_guard.rb +52 -0
  5. data/app/controllers/concerns/spree/api/v3/admin/subclassed_resource.rb +149 -0
  6. data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +54 -0
  7. data/app/controllers/concerns/spree/api/v3/bulk_operations.rb +103 -0
  8. data/app/controllers/concerns/spree/api/v3/channel_resolution.rb +60 -0
  9. data/app/controllers/concerns/spree/api/v3/error_handler.rb +4 -0
  10. data/app/controllers/concerns/spree/api/v3/params_normalizer.rb +84 -0
  11. data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +104 -0
  12. data/app/controllers/concerns/spree/api/v3/store/search_provider_support.rb +35 -1
  13. data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +109 -0
  14. data/app/controllers/spree/api/v3/admin/allowed_origins_controller.rb +25 -0
  15. data/app/controllers/spree/api/v3/admin/api_keys_controller.rb +84 -0
  16. data/app/controllers/spree/api/v3/admin/auth_controller.rb +134 -0
  17. data/app/controllers/spree/api/v3/admin/base_controller.rb +3 -17
  18. data/app/controllers/spree/api/v3/admin/categories_controller.rb +25 -0
  19. data/app/controllers/spree/api/v3/admin/channels_controller.rb +65 -0
  20. data/app/controllers/spree/api/v3/admin/countries_controller.rb +38 -0
  21. data/app/controllers/spree/api/v3/admin/coupon_codes_controller.rb +33 -0
  22. data/app/controllers/spree/api/v3/admin/custom_field_definitions_controller.rb +34 -0
  23. data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +129 -0
  24. data/app/controllers/spree/api/v3/admin/customer_groups_controller.rb +31 -0
  25. data/app/controllers/spree/api/v3/admin/customers/addresses_controller.rb +83 -0
  26. data/app/controllers/spree/api/v3/admin/customers/base_controller.rb +33 -0
  27. data/app/controllers/spree/api/v3/admin/customers/credit_cards_controller.rb +25 -0
  28. data/app/controllers/spree/api/v3/admin/customers/store_credits_controller.rb +92 -0
  29. data/app/controllers/spree/api/v3/admin/customers_controller.rb +119 -0
  30. data/app/controllers/spree/api/v3/admin/dashboard_controller.rb +44 -0
  31. data/app/controllers/spree/api/v3/admin/direct_uploads_controller.rb +40 -0
  32. data/app/controllers/spree/api/v3/admin/exports_controller.rb +136 -0
  33. data/app/controllers/spree/api/v3/admin/gift_card_batches_controller.rb +31 -0
  34. data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +33 -0
  35. data/app/controllers/spree/api/v3/admin/invitation_acceptances_controller.rb +138 -0
  36. data/app/controllers/spree/api/v3/admin/invitations_controller.rb +81 -0
  37. data/app/controllers/spree/api/v3/admin/markets_controller.rb +42 -0
  38. data/app/controllers/spree/api/v3/admin/me_controller.rb +69 -0
  39. data/app/controllers/spree/api/v3/admin/media_controller.rb +119 -0
  40. data/app/controllers/spree/api/v3/admin/option_types_controller.rb +34 -0
  41. data/app/controllers/spree/api/v3/admin/orders/adjustments_controller.rb +27 -0
  42. data/app/controllers/spree/api/v3/admin/orders/base_controller.rb +31 -0
  43. data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +104 -0
  44. data/app/controllers/spree/api/v3/admin/orders/gift_cards_controller.rb +79 -0
  45. data/app/controllers/spree/api/v3/admin/orders/items_controller.rb +92 -0
  46. data/app/controllers/spree/api/v3/admin/orders/payments_controller.rb +90 -0
  47. data/app/controllers/spree/api/v3/admin/orders/refunds_controller.rb +53 -0
  48. data/app/controllers/spree/api/v3/admin/orders/store_credits_controller.rb +59 -0
  49. data/app/controllers/spree/api/v3/admin/orders_controller.rb +190 -0
  50. data/app/controllers/spree/api/v3/admin/payment_methods_controller.rb +73 -0
  51. data/app/controllers/spree/api/v3/admin/price_lists_controller.rb +179 -0
  52. data/app/controllers/spree/api/v3/admin/prices_controller.rb +157 -0
  53. data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +48 -0
  54. data/app/controllers/spree/api/v3/admin/products_controller.rb +237 -0
  55. data/app/controllers/spree/api/v3/admin/promotion_actions_controller.rb +78 -0
  56. data/app/controllers/spree/api/v3/admin/promotion_rules_controller.rb +56 -0
  57. data/app/controllers/spree/api/v3/admin/promotions_controller.rb +78 -0
  58. data/app/controllers/spree/api/v3/admin/resource_controller.rb +29 -11
  59. data/app/controllers/spree/api/v3/admin/roles_controller.rb +29 -0
  60. data/app/controllers/spree/api/v3/admin/stock_items_controller.rb +35 -0
  61. data/app/controllers/spree/api/v3/admin/stock_locations_controller.rb +36 -0
  62. data/app/controllers/spree/api/v3/admin/stock_reservations_controller.rb +29 -0
  63. data/app/controllers/spree/api/v3/admin/stock_transfers_controller.rb +75 -0
  64. data/app/controllers/spree/api/v3/admin/store_controller.rb +53 -0
  65. data/app/controllers/spree/api/v3/admin/store_credit_categories_controller.rb +21 -0
  66. data/app/controllers/spree/api/v3/admin/tags_controller.rb +51 -0
  67. data/app/controllers/spree/api/v3/admin/tax_categories_controller.rb +21 -0
  68. data/app/controllers/spree/api/v3/admin/variants_controller.rb +33 -0
  69. data/app/controllers/spree/api/v3/admin/webhook_deliveries_controller.rb +49 -0
  70. data/app/controllers/spree/api/v3/admin/webhook_endpoints_controller.rb +75 -0
  71. data/app/controllers/spree/api/v3/resource_controller.rb +117 -8
  72. data/app/controllers/spree/api/v3/store/auth_controller.rb +8 -28
  73. data/app/controllers/spree/api/v3/store/base_controller.rb +6 -0
  74. data/app/controllers/spree/api/v3/store/carts_controller.rb +1 -0
  75. data/app/controllers/spree/api/v3/store/customers_controller.rb +6 -0
  76. data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +77 -0
  77. data/app/controllers/spree/api/v3/store/products/filters_controller.rb +2 -2
  78. data/app/controllers/spree/api/v3/store/products_controller.rb +4 -3
  79. data/app/controllers/spree/api/v3/store/resource_controller.rb +10 -2
  80. data/app/jobs/spree/webhook_delivery_job.rb +5 -0
  81. data/app/models/spree/api_key_ability.rb +16 -0
  82. data/app/serializers/spree/api/v3/admin/address_serializer.rb +2 -6
  83. data/app/serializers/spree/api/v3/admin/adjustment_serializer.rb +3 -15
  84. data/app/serializers/spree/api/v3/admin/admin_user_serializer.rb +19 -3
  85. data/app/serializers/spree/api/v3/admin/allowed_origin_serializer.rb +2 -6
  86. data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +42 -0
  87. data/app/serializers/spree/api/v3/admin/category_serializer.rb +4 -3
  88. data/app/serializers/spree/api/v3/admin/channel_serializer.rb +15 -0
  89. data/app/serializers/spree/api/v3/admin/country_serializer.rb +1 -1
  90. data/app/serializers/spree/api/v3/admin/coupon_code_serializer.rb +30 -0
  91. data/app/serializers/spree/api/v3/admin/credit_card_serializer.rb +4 -2
  92. data/app/serializers/spree/api/v3/admin/custom_field_definition_serializer.rb +21 -0
  93. data/app/serializers/spree/api/v3/admin/custom_field_serializer.rb +8 -3
  94. data/app/serializers/spree/api/v3/admin/customer_group_serializer.rb +27 -0
  95. data/app/serializers/spree/api/v3/admin/customer_serializer.rb +58 -2
  96. data/app/serializers/spree/api/v3/admin/dashboard_analytics_serializer.rb +143 -0
  97. data/app/serializers/spree/api/v3/admin/export_serializer.rb +40 -0
  98. data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +2 -6
  99. data/app/serializers/spree/api/v3/admin/{asset_serializer.rb → gift_card_batch_serializer.rb} +1 -1
  100. data/app/serializers/spree/api/v3/admin/gift_card_serializer.rb +39 -4
  101. data/app/serializers/spree/api/v3/admin/invitation_serializer.rb +64 -0
  102. data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +4 -16
  103. data/app/serializers/spree/api/v3/admin/media_serializer.rb +24 -2
  104. data/app/serializers/spree/api/v3/admin/option_type_serializer.rb +4 -1
  105. data/app/serializers/spree/api/v3/admin/option_value_serializer.rb +4 -1
  106. data/app/serializers/spree/api/v3/admin/order_serializer.rb +21 -6
  107. data/app/serializers/spree/api/v3/admin/payment_method_serializer.rb +11 -2
  108. data/app/serializers/spree/api/v3/admin/payment_serializer.rb +2 -6
  109. data/app/serializers/spree/api/v3/admin/payment_source_serializer.rb +4 -1
  110. data/app/serializers/spree/api/v3/admin/price_list_serializer.rb +51 -0
  111. data/app/serializers/spree/api/v3/admin/price_rule_serializer.rb +55 -0
  112. data/app/serializers/spree/api/v3/admin/price_serializer.rb +4 -0
  113. data/app/serializers/spree/api/v3/admin/product_publication_serializer.rb +11 -0
  114. data/app/serializers/spree/api/v3/admin/product_serializer.rb +34 -10
  115. data/app/serializers/spree/api/v3/admin/promotion_action_serializer.rb +71 -0
  116. data/app/serializers/spree/api/v3/admin/promotion_rule_serializer.rb +85 -0
  117. data/app/serializers/spree/api/v3/admin/promotion_serializer.rb +41 -0
  118. data/app/serializers/spree/api/v3/admin/refund_serializer.rb +4 -2
  119. data/app/serializers/spree/api/v3/admin/role_serializer.rb +17 -0
  120. data/app/serializers/spree/api/v3/admin/stock_item_serializer.rb +16 -1
  121. data/app/serializers/spree/api/v3/admin/stock_location_serializer.rb +11 -2
  122. data/app/serializers/spree/api/v3/admin/stock_reservation_serializer.rb +46 -0
  123. data/app/serializers/spree/api/v3/admin/stock_transfer_serializer.rb +37 -0
  124. data/app/serializers/spree/api/v3/admin/store_credit_category_serializer.rb +19 -0
  125. data/app/serializers/spree/api/v3/admin/store_credit_serializer.rb +11 -5
  126. data/app/serializers/spree/api/v3/admin/store_serializer.rb +55 -0
  127. data/app/serializers/spree/api/v3/admin/tax_category_serializer.rb +4 -2
  128. data/app/serializers/spree/api/v3/admin/variant_serializer.rb +37 -6
  129. data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +45 -0
  130. data/app/serializers/spree/api/v3/admin/webhook_endpoint_serializer.rb +69 -0
  131. data/app/serializers/spree/api/v3/channel_serializer.rb +14 -0
  132. data/app/serializers/spree/api/v3/custom_field_serializer.rb +9 -10
  133. data/app/serializers/spree/api/v3/customer_serializer.rb +5 -0
  134. data/app/serializers/spree/api/v3/market_serializer.rb +2 -1
  135. data/app/serializers/spree/api/v3/media_serializer.rb +8 -6
  136. data/app/serializers/spree/api/v3/order_serializer.rb +6 -1
  137. data/app/serializers/spree/api/v3/payment_method_serializer.rb +11 -2
  138. data/app/serializers/spree/api/v3/product_publication_serializer.rb +22 -0
  139. data/app/serializers/spree/api/v3/product_serializer.rb +6 -1
  140. data/app/serializers/spree/api/v3/stock_reservation_serializer.rb +10 -0
  141. data/config/locales/en.yml +2 -0
  142. data/config/routes.rb +235 -1
  143. data/lib/spree/api/configuration.rb +2 -2
  144. data/lib/spree/api/dependencies.rb +25 -1
  145. data/lib/spree/api/openapi/path_sorter.rb +126 -0
  146. data/lib/spree/api/openapi/schema_helper.rb +185 -6
  147. data/lib/spree/api/testing_support/v3/base.rb +28 -0
  148. metadata +98 -8
  149. 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
+ # Inventory movement between stock locations, or vendor → location
6
+ # for receives. Pass `source_location_id` for transfers; omit it to
7
+ # record an external receive.
8
+ class StockTransfersController < ResourceController
9
+ scoped_resource :stock
10
+
11
+ def create
12
+ authorize!(:create, model_class)
13
+
14
+ destination = Spree::StockLocation.find_by_prefix_id!(params[:destination_location_id])
15
+ source = params[:source_location_id].present? ?
16
+ Spree::StockLocation.find_by_prefix_id!(params[:source_location_id]) : nil
17
+
18
+ variants_map = build_variants_map
19
+ if variants_map.empty?
20
+ return render_error(
21
+ code: 'invalid_variants',
22
+ message: Spree.t('stock_transfer.errors.must_have_variant'),
23
+ status: :unprocessable_content
24
+ )
25
+ end
26
+
27
+ @resource = source ?
28
+ Spree::StockTransfer.new(reference: params[:reference]).tap { |t| t.transfer(source, destination, variants_map) } :
29
+ Spree::StockTransfer.new(reference: params[:reference]).tap { |t| t.receive(destination, variants_map) }
30
+
31
+ if @resource.persisted?
32
+ render json: serialize_resource(@resource), status: :created
33
+ else
34
+ render_validation_error(@resource.errors)
35
+ end
36
+ end
37
+
38
+ protected
39
+
40
+ def model_class
41
+ Spree::StockTransfer
42
+ end
43
+
44
+ def serializer_class
45
+ Spree.api.admin_stock_transfer_serializer
46
+ end
47
+
48
+ def collection_includes
49
+ [:source_location, :destination_location]
50
+ end
51
+
52
+ private
53
+
54
+ # Variants the merchant doesn't have access to are dropped silently;
55
+ # if the resulting map is empty the action surfaces a 422
56
+ # `invalid_variants` so callers can distinguish "nothing supplied"
57
+ # from "all variants were rejected." A single SELECT covers any
58
+ # number of variants instead of N round-trips.
59
+ def build_variants_map
60
+ entries = params.permit(variants: [:variant_id, :quantity]).fetch(:variants, [])
61
+ quantities_by_id = entries.each_with_object({}) do |entry, hash|
62
+ decoded = Spree::PrefixedId.decode_prefixed_id(entry[:variant_id])
63
+ hash[decoded.to_i] = entry[:quantity].to_i if decoded
64
+ end
65
+
66
+ Spree::Variant.where(id: quantities_by_id.keys).each_with_object({}) do |variant, acc|
67
+ quantity = quantities_by_id[variant.id]
68
+ acc[variant] = quantity if quantity&.positive?
69
+ end
70
+ end
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,53 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ class StoreController < Admin::BaseController
6
+ scoped_resource :settings
7
+
8
+ # GET /api/v3/admin/store
9
+ def show
10
+ authorize! :show, current_store
11
+ render json: serialize_store
12
+ end
13
+
14
+ # PATCH /api/v3/admin/store
15
+ def update
16
+ authorize! :update, current_store
17
+
18
+ if current_store.update(permitted_params)
19
+ render json: serialize_store
20
+ else
21
+ render_validation_error(current_store.errors)
22
+ end
23
+ end
24
+
25
+ private
26
+
27
+ def serialize_store
28
+ serializer_class.new(current_store, params: serializer_params).to_h
29
+ end
30
+
31
+ def serializer_class
32
+ Spree.api.admin_store_serializer
33
+ end
34
+
35
+ def permitted_params
36
+ params.permit(
37
+ :name,
38
+ :preferred_admin_locale,
39
+ :preferred_timezone,
40
+ :preferred_weight_unit,
41
+ :preferred_unit_system,
42
+ :mail_from_address,
43
+ :customer_support_email,
44
+ :new_order_notifications_email,
45
+ :preferred_send_consumer_transactional_emails,
46
+ :mailer_logo
47
+ )
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,21 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ class StoreCreditCategoriesController < ResourceController
6
+ scoped_resource :settings
7
+
8
+ protected
9
+
10
+ def model_class
11
+ Spree::StoreCreditCategory
12
+ end
13
+
14
+ def serializer_class
15
+ Spree.api.admin_store_credit_category_serializer
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,51 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ class TagsController < BaseController
6
+ skip_scope_check!
7
+
8
+ MAX_RESULTS = 50
9
+
10
+ def index
11
+ taggable_type = params[:taggable_type].to_s
12
+ unless allowed_taggable_types.include?(taggable_type)
13
+ render_error(
14
+ code: 'invalid_taggable_type',
15
+ message: "taggable_type must be one of #{allowed_taggable_types.join(', ')}",
16
+ status: :unprocessable_content
17
+ )
18
+ return
19
+ end
20
+
21
+ scope = ActsAsTaggableOn::Tag.
22
+ joins(:taggings).
23
+ where(ActsAsTaggableOn.taggings_table => { taggable_type: taggable_type, context: 'tags' }).
24
+ distinct.
25
+ order(:name).
26
+ limit(MAX_RESULTS)
27
+
28
+ if params[:q].present?
29
+ # Escape LIKE wildcards in user input so a query like "foo_" matches
30
+ # only the literal underscore, not any single character.
31
+ escaped = params[:q].to_s.downcase.gsub(/[\\%_]/) { |c| "\\#{c}" }
32
+ scope = scope.where('LOWER(name) LIKE ? ESCAPE ?', "%#{escaped}%", '\\')
33
+ end
34
+
35
+ render json: { data: scope.pluck(:name).map { |name| { name: name } } }
36
+ end
37
+
38
+ private
39
+
40
+ # Sourced from `Spree.taggable_types` (registered in
41
+ # `Spree::Core::Engine`'s after_initialize block). Apps extend the
42
+ # list in an initializer without overriding this controller:
43
+ # Spree.taggable_types << 'MyApp::Vendor'
44
+ def allowed_taggable_types
45
+ Spree.taggable_types
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,21 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ class TaxCategoriesController < ResourceController
6
+ scoped_resource :settings
7
+
8
+ protected
9
+
10
+ def model_class
11
+ Spree::TaxCategory
12
+ end
13
+
14
+ def serializer_class
15
+ Spree.api.admin_tax_category_serializer
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,33 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ class VariantsController < ResourceController
6
+ scoped_resource :products
7
+
8
+ protected
9
+
10
+ def model_class
11
+ Spree::Variant
12
+ end
13
+
14
+ def serializer_class
15
+ Spree.api.admin_variant_serializer
16
+ end
17
+
18
+ def scope
19
+ current_store.variants.eligible.accessible_by(current_ability, ability_action_for_request)
20
+ end
21
+
22
+ def scope_includes
23
+ [
24
+ :prices, stock_items: :stock_location,
25
+ option_values: :option_type,
26
+ primary_media: [attachment_attachment: :blob]
27
+ ]
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,49 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Nested under WebhookEndpoint — deliveries are always read in the
6
+ # context of their endpoint (the delivery log on the endpoint detail
7
+ # page) and never accessed by ID at the top level.
8
+ class WebhookDeliveriesController < ResourceController
9
+ scoped_resource :webhooks
10
+
11
+ # POST /api/v3/admin/webhook_endpoints/:webhook_endpoint_id/deliveries/:id/redeliver
12
+ #
13
+ # Creates a new delivery row with the same payload + event_name and
14
+ # queues it. The original row is preserved for audit history.
15
+ #
16
+ # @return [Hash] the serialized newly-queued {Spree::WebhookDelivery},
17
+ # HTTP 201.
18
+ def redeliver
19
+ @resource = find_resource
20
+ authorize!(:update, webhook_endpoint)
21
+
22
+ new_delivery = @resource.redeliver!
23
+ render json: serialize_resource(new_delivery), status: :created
24
+ end
25
+
26
+ protected
27
+
28
+ def model_class
29
+ Spree::WebhookDelivery
30
+ end
31
+
32
+ def serializer_class
33
+ Spree.api.admin_webhook_delivery_serializer
34
+ end
35
+
36
+ def scope
37
+ webhook_endpoint.webhook_deliveries.recent
38
+ end
39
+
40
+ def webhook_endpoint
41
+ @webhook_endpoint ||= current_store.webhook_endpoints.find_by_prefix_id!(
42
+ params[:webhook_endpoint_id]
43
+ )
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -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 :webhooks
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,12 +261,54 @@ 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
+ base_scope = base_scope.accessible_by(current_ability, ability_action_for_request) unless @parent.present?
198
265
  base_scope = base_scope.includes(scope_includes) if scope_includes.any?
199
266
  base_scope = base_scope.preload_associations_lazily
200
267
  model_class.include?(Spree::TranslatableResource) ? base_scope.i18n : base_scope
201
268
  end
202
269
 
270
+ # Action names treated as reads. Override in subclasses with custom
271
+ # read-only member/collection actions (e.g. add `analytics`, `types`)
272
+ # so they map to the `:show` ability instead of a write.
273
+ def read_actions
274
+ %w[index show]
275
+ end
276
+
277
+ # Maps the current request to the CanCanCan action used to scope the
278
+ # collection. Read actions (see +read_actions+) map to `:show`; every
279
+ # other request maps by HTTP method. Exposed so controllers that
280
+ # override +scope+ can keep the same `accessible_by` action as the
281
+ # base implementation.
282
+ def ability_action_for_request
283
+ return :show if read_actions.include?(action_name)
284
+
285
+ case request.method
286
+ when 'GET', 'HEAD' then :show
287
+ when 'POST' then :create
288
+ when 'PATCH', 'PUT' then :update
289
+ when 'DELETE' then :destroy
290
+ else
291
+ raise ActionController::MethodNotAllowed, request.method
292
+ end
293
+ end
294
+
295
+ # The ability action a nested resource needs on its PARENT: read
296
+ # actions (see +read_actions+) need only `:show`; every write needs
297
+ # `:update`, since mutating a nested collection is an update to the
298
+ # parent (not a create/destroy of it). Distinct from
299
+ # +ability_action_for_request+, which maps POST/DELETE to
300
+ # `:create`/`:destroy` for the resource itself.
301
+ def parent_ability_action
302
+ read_actions.include?(action_name) ? :show : :update
303
+ end
304
+
305
+ # Authorizes the parent resource for nested controllers: a role that
306
+ # can view a parent can't mutate its nested collection. Call from
307
+ # +set_parent+ after loading the parent.
308
+ def authorize_parent!(parent)
309
+ authorize!(parent_ability_action, parent)
310
+ end
311
+
203
312
  # Override to specify the association name on @parent
204
313
  # Defaults to controller_name (e.g., 'wished_items' for WishlistItemsController)
205
314
  def parent_association
@@ -228,7 +337,7 @@ module Spree
228
337
  #
229
338
  # Override in subclass for custom parameter handling
230
339
  def permitted_params
231
- params.permit(permitted_attributes)
340
+ normalize_params(params.permit(permitted_attributes))
232
341
  end
233
342
 
234
343
  # 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