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.
- checksums.yaml +4 -4
- data/Rakefile +36 -0
- data/app/controllers/concerns/spree/api/v3/admin/auth_cookies.rb +62 -0
- data/app/controllers/concerns/spree/api/v3/admin/role_grant_guard.rb +52 -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 +104 -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 +109 -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 +84 -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 +129 -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 +83 -0
- data/app/controllers/spree/api/v3/admin/customers/base_controller.rb +33 -0
- data/app/controllers/spree/api/v3/admin/customers/credit_cards_controller.rb +25 -0
- data/app/controllers/spree/api/v3/admin/customers/store_credits_controller.rb +92 -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 +136 -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 +81 -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 +31 -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 +179 -0
- data/app/controllers/spree/api/v3/admin/prices_controller.rb +157 -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 +117 -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 +4 -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
- data/lib/spree/api/testing_support/v3/base.rb +28 -0
- metadata +98 -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
|
+
# 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.
|
|
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,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,
|
|
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, :
|
|
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
|