spree_api 5.4.3 → 5.5.0.rc1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +19 -0
  3. data/app/controllers/concerns/spree/api/v3/admin/auth_cookies.rb +62 -0
  4. data/app/controllers/concerns/spree/api/v3/admin/subclassed_resource.rb +149 -0
  5. data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +54 -0
  6. data/app/controllers/concerns/spree/api/v3/bulk_operations.rb +103 -0
  7. data/app/controllers/concerns/spree/api/v3/channel_resolution.rb +60 -0
  8. data/app/controllers/concerns/spree/api/v3/error_handler.rb +4 -0
  9. data/app/controllers/concerns/spree/api/v3/params_normalizer.rb +84 -0
  10. data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +88 -0
  11. data/app/controllers/concerns/spree/api/v3/store/search_provider_support.rb +35 -1
  12. data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +97 -0
  13. data/app/controllers/spree/api/v3/admin/allowed_origins_controller.rb +25 -0
  14. data/app/controllers/spree/api/v3/admin/api_keys_controller.rb +55 -0
  15. data/app/controllers/spree/api/v3/admin/auth_controller.rb +134 -0
  16. data/app/controllers/spree/api/v3/admin/base_controller.rb +3 -17
  17. data/app/controllers/spree/api/v3/admin/categories_controller.rb +25 -0
  18. data/app/controllers/spree/api/v3/admin/channels_controller.rb +65 -0
  19. data/app/controllers/spree/api/v3/admin/countries_controller.rb +38 -0
  20. data/app/controllers/spree/api/v3/admin/coupon_codes_controller.rb +33 -0
  21. data/app/controllers/spree/api/v3/admin/custom_field_definitions_controller.rb +34 -0
  22. data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +108 -0
  23. data/app/controllers/spree/api/v3/admin/customer_groups_controller.rb +31 -0
  24. data/app/controllers/spree/api/v3/admin/customers/addresses_controller.rb +88 -0
  25. data/app/controllers/spree/api/v3/admin/customers/credit_cards_controller.rb +31 -0
  26. data/app/controllers/spree/api/v3/admin/customers/store_credits_controller.rb +93 -0
  27. data/app/controllers/spree/api/v3/admin/customers_controller.rb +119 -0
  28. data/app/controllers/spree/api/v3/admin/dashboard_controller.rb +44 -0
  29. data/app/controllers/spree/api/v3/admin/direct_uploads_controller.rb +40 -0
  30. data/app/controllers/spree/api/v3/admin/exports_controller.rb +89 -0
  31. data/app/controllers/spree/api/v3/admin/gift_card_batches_controller.rb +31 -0
  32. data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +33 -0
  33. data/app/controllers/spree/api/v3/admin/invitation_acceptances_controller.rb +138 -0
  34. data/app/controllers/spree/api/v3/admin/invitations_controller.rb +70 -0
  35. data/app/controllers/spree/api/v3/admin/markets_controller.rb +42 -0
  36. data/app/controllers/spree/api/v3/admin/me_controller.rb +69 -0
  37. data/app/controllers/spree/api/v3/admin/media_controller.rb +119 -0
  38. data/app/controllers/spree/api/v3/admin/option_types_controller.rb +34 -0
  39. data/app/controllers/spree/api/v3/admin/orders/adjustments_controller.rb +27 -0
  40. data/app/controllers/spree/api/v3/admin/orders/base_controller.rb +26 -0
  41. data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +104 -0
  42. data/app/controllers/spree/api/v3/admin/orders/gift_cards_controller.rb +79 -0
  43. data/app/controllers/spree/api/v3/admin/orders/items_controller.rb +92 -0
  44. data/app/controllers/spree/api/v3/admin/orders/payments_controller.rb +90 -0
  45. data/app/controllers/spree/api/v3/admin/orders/refunds_controller.rb +53 -0
  46. data/app/controllers/spree/api/v3/admin/orders/store_credits_controller.rb +59 -0
  47. data/app/controllers/spree/api/v3/admin/orders_controller.rb +190 -0
  48. data/app/controllers/spree/api/v3/admin/payment_methods_controller.rb +73 -0
  49. data/app/controllers/spree/api/v3/admin/price_lists_controller.rb +156 -0
  50. data/app/controllers/spree/api/v3/admin/prices_controller.rb +129 -0
  51. data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +48 -0
  52. data/app/controllers/spree/api/v3/admin/products_controller.rb +237 -0
  53. data/app/controllers/spree/api/v3/admin/promotion_actions_controller.rb +78 -0
  54. data/app/controllers/spree/api/v3/admin/promotion_rules_controller.rb +56 -0
  55. data/app/controllers/spree/api/v3/admin/promotions_controller.rb +78 -0
  56. data/app/controllers/spree/api/v3/admin/resource_controller.rb +29 -11
  57. data/app/controllers/spree/api/v3/admin/roles_controller.rb +29 -0
  58. data/app/controllers/spree/api/v3/admin/stock_items_controller.rb +35 -0
  59. data/app/controllers/spree/api/v3/admin/stock_locations_controller.rb +36 -0
  60. data/app/controllers/spree/api/v3/admin/stock_reservations_controller.rb +29 -0
  61. data/app/controllers/spree/api/v3/admin/stock_transfers_controller.rb +75 -0
  62. data/app/controllers/spree/api/v3/admin/store_controller.rb +53 -0
  63. data/app/controllers/spree/api/v3/admin/store_credit_categories_controller.rb +21 -0
  64. data/app/controllers/spree/api/v3/admin/tags_controller.rb +51 -0
  65. data/app/controllers/spree/api/v3/admin/tax_categories_controller.rb +21 -0
  66. data/app/controllers/spree/api/v3/admin/variants_controller.rb +33 -0
  67. data/app/controllers/spree/api/v3/admin/webhook_deliveries_controller.rb +49 -0
  68. data/app/controllers/spree/api/v3/admin/webhook_endpoints_controller.rb +75 -0
  69. data/app/controllers/spree/api/v3/resource_controller.rb +90 -8
  70. data/app/controllers/spree/api/v3/store/auth_controller.rb +8 -28
  71. data/app/controllers/spree/api/v3/store/base_controller.rb +6 -0
  72. data/app/controllers/spree/api/v3/store/carts_controller.rb +1 -0
  73. data/app/controllers/spree/api/v3/store/customers_controller.rb +6 -0
  74. data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +77 -0
  75. data/app/controllers/spree/api/v3/store/products/filters_controller.rb +2 -2
  76. data/app/controllers/spree/api/v3/store/products_controller.rb +3 -3
  77. data/app/controllers/spree/api/v3/store/resource_controller.rb +10 -2
  78. data/app/jobs/spree/webhook_delivery_job.rb +5 -0
  79. data/app/models/spree/api_key_ability.rb +16 -0
  80. data/app/serializers/spree/api/v3/admin/address_serializer.rb +2 -6
  81. data/app/serializers/spree/api/v3/admin/adjustment_serializer.rb +3 -15
  82. data/app/serializers/spree/api/v3/admin/admin_user_serializer.rb +19 -3
  83. data/app/serializers/spree/api/v3/admin/allowed_origin_serializer.rb +2 -6
  84. data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +42 -0
  85. data/app/serializers/spree/api/v3/admin/category_serializer.rb +4 -3
  86. data/app/serializers/spree/api/v3/admin/channel_serializer.rb +15 -0
  87. data/app/serializers/spree/api/v3/admin/country_serializer.rb +1 -1
  88. data/app/serializers/spree/api/v3/admin/coupon_code_serializer.rb +30 -0
  89. data/app/serializers/spree/api/v3/admin/credit_card_serializer.rb +4 -2
  90. data/app/serializers/spree/api/v3/admin/custom_field_definition_serializer.rb +21 -0
  91. data/app/serializers/spree/api/v3/admin/custom_field_serializer.rb +8 -3
  92. data/app/serializers/spree/api/v3/admin/customer_group_serializer.rb +27 -0
  93. data/app/serializers/spree/api/v3/admin/customer_serializer.rb +58 -2
  94. data/app/serializers/spree/api/v3/admin/dashboard_analytics_serializer.rb +143 -0
  95. data/app/serializers/spree/api/v3/admin/export_serializer.rb +40 -0
  96. data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +2 -6
  97. data/app/serializers/spree/api/v3/admin/{asset_serializer.rb → gift_card_batch_serializer.rb} +1 -1
  98. data/app/serializers/spree/api/v3/admin/gift_card_serializer.rb +39 -4
  99. data/app/serializers/spree/api/v3/admin/invitation_serializer.rb +64 -0
  100. data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +4 -16
  101. data/app/serializers/spree/api/v3/admin/media_serializer.rb +24 -2
  102. data/app/serializers/spree/api/v3/admin/option_type_serializer.rb +4 -1
  103. data/app/serializers/spree/api/v3/admin/option_value_serializer.rb +4 -1
  104. data/app/serializers/spree/api/v3/admin/order_serializer.rb +21 -6
  105. data/app/serializers/spree/api/v3/admin/payment_method_serializer.rb +11 -2
  106. data/app/serializers/spree/api/v3/admin/payment_serializer.rb +2 -6
  107. data/app/serializers/spree/api/v3/admin/payment_source_serializer.rb +4 -1
  108. data/app/serializers/spree/api/v3/admin/price_list_serializer.rb +51 -0
  109. data/app/serializers/spree/api/v3/admin/price_rule_serializer.rb +55 -0
  110. data/app/serializers/spree/api/v3/admin/price_serializer.rb +4 -0
  111. data/app/serializers/spree/api/v3/admin/product_publication_serializer.rb +11 -0
  112. data/app/serializers/spree/api/v3/admin/product_serializer.rb +34 -10
  113. data/app/serializers/spree/api/v3/admin/promotion_action_serializer.rb +71 -0
  114. data/app/serializers/spree/api/v3/admin/promotion_rule_serializer.rb +85 -0
  115. data/app/serializers/spree/api/v3/admin/promotion_serializer.rb +41 -0
  116. data/app/serializers/spree/api/v3/admin/refund_serializer.rb +4 -2
  117. data/app/serializers/spree/api/v3/admin/role_serializer.rb +17 -0
  118. data/app/serializers/spree/api/v3/admin/stock_item_serializer.rb +16 -1
  119. data/app/serializers/spree/api/v3/admin/stock_location_serializer.rb +11 -2
  120. data/app/serializers/spree/api/v3/admin/stock_reservation_serializer.rb +46 -0
  121. data/app/serializers/spree/api/v3/admin/stock_transfer_serializer.rb +37 -0
  122. data/app/serializers/spree/api/v3/admin/store_credit_category_serializer.rb +19 -0
  123. data/app/serializers/spree/api/v3/admin/store_credit_serializer.rb +11 -5
  124. data/app/serializers/spree/api/v3/admin/store_serializer.rb +55 -0
  125. data/app/serializers/spree/api/v3/admin/tax_category_serializer.rb +4 -2
  126. data/app/serializers/spree/api/v3/admin/variant_serializer.rb +37 -6
  127. data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +45 -0
  128. data/app/serializers/spree/api/v3/admin/webhook_endpoint_serializer.rb +69 -0
  129. data/app/serializers/spree/api/v3/channel_serializer.rb +14 -0
  130. data/app/serializers/spree/api/v3/custom_field_serializer.rb +9 -10
  131. data/app/serializers/spree/api/v3/customer_serializer.rb +5 -0
  132. data/app/serializers/spree/api/v3/market_serializer.rb +2 -1
  133. data/app/serializers/spree/api/v3/media_serializer.rb +8 -6
  134. data/app/serializers/spree/api/v3/order_serializer.rb +6 -1
  135. data/app/serializers/spree/api/v3/payment_method_serializer.rb +11 -2
  136. data/app/serializers/spree/api/v3/product_publication_serializer.rb +22 -0
  137. data/app/serializers/spree/api/v3/product_serializer.rb +6 -1
  138. data/app/serializers/spree/api/v3/stock_reservation_serializer.rb +10 -0
  139. data/config/locales/en.yml +2 -0
  140. data/config/routes.rb +235 -1
  141. data/lib/spree/api/configuration.rb +2 -2
  142. data/lib/spree/api/dependencies.rb +25 -1
  143. data/lib/spree/api/openapi/path_sorter.rb +126 -0
  144. data/lib/spree/api/openapi/schema_helper.rb +185 -6
  145. metadata +96 -8
  146. data/app/serializers/spree/api/v3/admin/shipping_category_serializer.rb +0 -14
@@ -5,6 +5,16 @@ module Spree
5
5
  module SearchProviderSupport
6
6
  extend ActiveSupport::Concern
7
7
 
8
+ # Matches `*_id_in`/`id_eq`/etc. and the bare `id_in`/`id_eq` on
9
+ # the resource's primary key. Mirrors the regex on
10
+ # `ResourceController` — duplicated here because `FiltersController`
11
+ # extends `Store::BaseController`, not `ResourceController`, and
12
+ # would otherwise NoMethodError on `decode_prefixed_id_predicates`.
13
+ # Requires a Ransack-predicate suffix so we don't match scope
14
+ # names like `with_option_value_ids` (which handle their own
15
+ # prefix decoding).
16
+ RANSACK_ID_PREDICATE_RE = /(?:\A|_)id(?:s)?_(?:eq|not_eq|in|not_in|lt|lteq|gt|gteq)\z/.freeze
17
+
8
18
  private
9
19
 
10
20
  def search_query
@@ -14,12 +24,36 @@ module Spree
14
24
  def search_filters
15
25
  q = params[:q]&.to_unsafe_h || params[:q] || {}
16
26
  q = q.to_h if q.respond_to?(:to_h) && !q.is_a?(Hash)
17
- q.except('search').presence
27
+ # Decode Stripe-style prefixed IDs in `*_id_in`/`id_eq`/etc. so
28
+ # SPA + storefront filters can pass `prod_…` keys; the search
29
+ # provider hands the filter hash straight to Ransack on the
30
+ # underlying scope, which expects raw integer IDs.
31
+ decode_prefixed_id_predicates(q.except('search')).presence
18
32
  end
19
33
 
20
34
  def search_provider
21
35
  @search_provider ||= Spree.search_provider.constantize.new(current_store)
22
36
  end
37
+
38
+ def decode_prefixed_id_predicates(hash)
39
+ return hash unless hash.is_a?(Hash)
40
+
41
+ hash.each_with_object({}) do |(key, value), result|
42
+ result[key] = if ransack_id_predicate?(key)
43
+ Array(value).map { |v| Spree::PrefixedId.prefixed_id?(v) ? Spree::PrefixedId.decode_prefixed_id(v) || v : v }.then { |arr|
44
+ value.is_a?(Array) ? arr : arr.first
45
+ }
46
+ elsif value.is_a?(Hash)
47
+ decode_prefixed_id_predicates(value)
48
+ else
49
+ value
50
+ end
51
+ end
52
+ end
53
+
54
+ def ransack_id_predicate?(key)
55
+ RANSACK_ID_PREDICATE_RE.match?(key.to_s)
56
+ end
23
57
  end
24
58
  end
25
59
  end
@@ -0,0 +1,97 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Manages staff for the current store. "Staff" = admin users with at
6
+ # least one `Spree::RoleUser` whose `resource` is the current store.
7
+ # The legacy controller hard-deletes the global account on destroy;
8
+ # this v3 endpoint instead removes the per-store `RoleUser` rows so
9
+ # the user keeps their account (and access to other stores).
10
+ class AdminUsersController < ResourceController
11
+ scoped_resource :settings
12
+
13
+ # POST is not exposed — staff are created via invitations.
14
+ def create
15
+ head :method_not_allowed
16
+ end
17
+
18
+ # DELETE /api/v3/admin/admin_users/:id
19
+ # Removes role assignments for the current store rather than deleting
20
+ # the account globally. The user keeps access to any other stores.
21
+ def destroy
22
+ authorize!(:destroy, @resource)
23
+ @resource.role_users.where(resource: current_store).destroy_all
24
+ head :no_content
25
+ end
26
+
27
+ # PATCH allows updating identity fields and replacing the user's
28
+ # roles for this store. `role_ids` accepts prefixed IDs and is
29
+ # applied via `add_role`/`remove_role` so the change is scoped to
30
+ # `current_store` and never touches other-store assignments.
31
+ def update
32
+ authorize!(:update, @resource)
33
+
34
+ attrs = identity_params
35
+ if @resource.update(attrs)
36
+ apply_role_ids(role_ids_param) if params.key?(:role_ids)
37
+ render json: serialize_resource(@resource)
38
+ else
39
+ render_validation_error(@resource.errors)
40
+ end
41
+ end
42
+
43
+ protected
44
+
45
+ def model_class
46
+ Spree.admin_user_class
47
+ end
48
+
49
+ def serializer_class
50
+ Spree.api.admin_admin_user_serializer
51
+ end
52
+
53
+ def collection_includes
54
+ [{ role_users: :role }]
55
+ end
56
+
57
+ # Restrict to users with a role assignment on the current store.
58
+ # `accessible_by` enforces CanCanCan on top.
59
+ def scope
60
+ model_class.
61
+ joins(:role_users).
62
+ where(spree_role_users: { resource: current_store }).
63
+ distinct.
64
+ accessible_by(current_ability, :show)
65
+ end
66
+
67
+ private
68
+
69
+ def identity_params
70
+ params.permit(:first_name, :last_name)
71
+ end
72
+
73
+ def role_ids_param
74
+ ids = Array(params[:role_ids])
75
+ ids.map { |id| Spree::PrefixedId.prefixed_id?(id) ? Spree::PrefixedId.decode_prefixed_id(id) : id }.compact
76
+ end
77
+
78
+ # Reconcile the user's roles on this store to match `desired_role_ids`.
79
+ # Adds missing assignments and removes extras — no-op for unchanged.
80
+ def apply_role_ids(desired_role_ids)
81
+ current = @resource.role_users.where(resource: current_store).pluck(:role_id).map(&:to_s)
82
+ target = desired_role_ids.map(&:to_s)
83
+
84
+ (target - current).each do |role_id|
85
+ role = Spree::Role.find_by(id: role_id)
86
+ @resource.role_users.find_or_create_by!(role: role, resource: current_store) if role
87
+ end
88
+
89
+ (current - target).each do |role_id|
90
+ @resource.role_users.where(role_id: role_id, resource: current_store).destroy_all
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
96
+ end
97
+ end
@@ -0,0 +1,25 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ class AllowedOriginsController < ResourceController
6
+ scoped_resource :settings
7
+
8
+ protected
9
+
10
+ def model_class
11
+ Spree::AllowedOrigin
12
+ end
13
+
14
+ def serializer_class
15
+ Spree.api.admin_allowed_origin_serializer
16
+ end
17
+
18
+ def permitted_params
19
+ params.permit(:origin)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,55 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ class ApiKeysController < ResourceController
6
+ scoped_resource :settings
7
+
8
+ # PATCH /api/v3/admin/api_keys/:id/revoke
9
+ # Marks the key revoked rather than deleting it — the row stays so
10
+ # audit logs and `created_by`/`revoked_by` remain queryable. Hard
11
+ # deletion is available via `destroy` for cleanup.
12
+ def revoke
13
+ @resource = find_resource
14
+ authorize!(:update, @resource)
15
+
16
+ @resource.revoke!(try_spree_current_user)
17
+ render json: serialize_resource(@resource)
18
+ end
19
+
20
+ protected
21
+
22
+ def model_class
23
+ Spree::ApiKey
24
+ end
25
+
26
+ def serializer_class
27
+ Spree.api.admin_api_key_serializer
28
+ end
29
+
30
+ def scope
31
+ current_store.api_keys.accessible_by(current_ability, :show)
32
+ end
33
+
34
+ # Stamp the creating user; key generation (token, prefix, digest)
35
+ # happens in `before_validation :generate_token` on the model.
36
+ def build_resource
37
+ scope.new(permitted_params).tap do |key|
38
+ key.created_by = try_spree_current_user
39
+ end
40
+ end
41
+
42
+ # `key_type` is set on create only — flipping a publishable key to a
43
+ # secret one (or vice versa) would invalidate every consumer of the
44
+ # plaintext token. We strip it on update so that's impossible to do
45
+ # by accident through the API.
46
+ def permitted_params
47
+ attrs = params.permit(:name, :key_type, scopes: [])
48
+ attrs.delete(:key_type) if action_name == 'update'
49
+ attrs
50
+ end
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,134 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ class AuthController < Admin::BaseController
6
+ include Spree::Api::V3::Admin::AuthCookies
7
+
8
+ skip_scope_check!
9
+
10
+ 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
11
+ 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
12
+
13
+ skip_before_action :authenticate_admin!, only: [:create, :refresh, :logout]
14
+
15
+ # POST /api/v3/admin/auth/login
16
+ def create
17
+ strategy = authentication_strategy
18
+ return unless strategy
19
+
20
+ result = strategy.authenticate
21
+
22
+ if result.success?
23
+ user = result.value
24
+ refresh_token = Spree::RefreshToken.create_for(user, request_env: request_env_for_token)
25
+ set_refresh_cookie(refresh_token)
26
+ render json: auth_response(user)
27
+ else
28
+ render_error(
29
+ code: ERROR_CODES[:authentication_failed],
30
+ message: result.error,
31
+ status: :unauthorized
32
+ )
33
+ end
34
+ end
35
+
36
+ # POST /api/v3/admin/auth/refresh
37
+ def refresh
38
+ refresh_token_value = refresh_token_from_cookie
39
+
40
+ if refresh_token_value.blank?
41
+ return render_error(
42
+ code: ERROR_CODES[:invalid_refresh_token],
43
+ message: 'Refresh token cookie missing',
44
+ status: :unauthorized
45
+ )
46
+ end
47
+
48
+ refresh_token = Spree::RefreshToken.active.find_by(token: refresh_token_value)
49
+
50
+ if refresh_token.nil?
51
+ clear_refresh_cookie
52
+ return render_error(
53
+ code: ERROR_CODES[:invalid_refresh_token],
54
+ message: 'Invalid or expired refresh token',
55
+ status: :unauthorized
56
+ )
57
+ end
58
+
59
+ user = refresh_token.user
60
+ new_refresh_token = refresh_token.rotate!(request_env: request_env_for_token)
61
+ set_refresh_cookie(new_refresh_token)
62
+
63
+ render json: auth_response(user)
64
+ end
65
+
66
+ # POST /api/v3/admin/auth/logout
67
+ def logout
68
+ refresh_token_value = refresh_token_from_cookie
69
+ Spree::RefreshToken.active.find_by(token: refresh_token_value)&.destroy if refresh_token_value.present?
70
+
71
+ clear_refresh_cookie
72
+ head :no_content
73
+ end
74
+
75
+ private
76
+
77
+ def authentication_strategy
78
+ provider = params[:provider].presence || 'email'
79
+ strategy_class = Spree.admin_authentication_strategies[provider]
80
+
81
+ unless strategy_class
82
+ render_error(
83
+ code: ERROR_CODES[:invalid_provider],
84
+ message: "Unsupported authentication provider: #{provider}",
85
+ status: :bad_request
86
+ )
87
+ return nil
88
+ end
89
+
90
+ strategy_class.new(
91
+ params: params,
92
+ request_env: request.headers.env,
93
+ user_class: Spree.admin_user_class
94
+ )
95
+ end
96
+
97
+ def serializer_params
98
+ {
99
+ store: current_store,
100
+ locale: current_locale,
101
+ currency: current_currency,
102
+ user: current_user,
103
+ includes: []
104
+ }
105
+ end
106
+
107
+ def auth_response(user)
108
+ {
109
+ token: generate_jwt(user, audience: JWT_AUDIENCE_ADMIN),
110
+ user: admin_user_serializer.new(user, params: serializer_params).to_h
111
+ }
112
+ end
113
+
114
+ def request_env_for_token
115
+ {
116
+ ip_address: request.remote_ip,
117
+ user_agent: request.user_agent&.truncate(255)
118
+ }
119
+ end
120
+
121
+ def admin_user_serializer
122
+ Spree.api.admin_admin_user_serializer
123
+ end
124
+
125
+ # Admin tokens have higher blast radius than customer tokens, so they get a
126
+ # shorter TTL (5 min by default) — overrides the storefront default (1h).
127
+ def jwt_expiration
128
+ Spree::Api::Config[:admin_jwt_expiration]
129
+ end
130
+ end
131
+ end
132
+ end
133
+ end
134
+ end
@@ -3,24 +3,10 @@ module Spree
3
3
  module V3
4
4
  module Admin
5
5
  class BaseController < Spree::Api::V3::BaseController
6
- # Require secret API key for all Admin API requests
7
- before_action :authenticate_secret_key!
6
+ include Spree::Api::V3::AdminAuthentication
7
+ include Spree::Api::V3::ScopedAuthorization
8
8
 
9
- # Admin API responses must never be cached
10
- after_action :set_no_store_cache
11
-
12
- protected
13
-
14
- # Override JWT audience to require admin tokens
15
- def expected_audience
16
- JWT_AUDIENCE_ADMIN
17
- end
18
-
19
- private
20
-
21
- def set_no_store_cache
22
- response.headers['Cache-Control'] = 'private, no-store'
23
- end
9
+ before_action :authenticate_admin!
24
10
  end
25
11
  end
26
12
  end
@@ -0,0 +1,25 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ class CategoriesController < ResourceController
6
+ scoped_resource :categories
7
+
8
+ protected
9
+
10
+ def model_class
11
+ Spree::Category
12
+ end
13
+
14
+ def serializer_class
15
+ Spree.api.admin_category_serializer
16
+ end
17
+
18
+ def scope
19
+ super.where(taxonomy: current_store.taxonomies)
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,65 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ class ChannelsController < ResourceController
6
+ scoped_resource :settings
7
+
8
+ # POST /api/v3/admin/channels/:id/add_products
9
+ # Body: { product_ids: [...], published_at: nil, unpublished_at: nil }
10
+ def add_products
11
+ channel = find_resource
12
+ authorize! :update, channel
13
+
14
+ count = channel.add_products(
15
+ scoped_product_ids,
16
+ published_at: params[:published_at].presence,
17
+ unpublished_at: params[:unpublished_at].presence
18
+ )
19
+ render json: { product_count: count }
20
+ end
21
+
22
+ # POST /api/v3/admin/channels/:id/remove_products
23
+ # Body: { product_ids: [...] }
24
+ def remove_products
25
+ channel = find_resource
26
+ authorize! :update, channel
27
+
28
+ removed = channel.remove_products(scoped_product_ids)
29
+ render json: { product_count: removed }
30
+ end
31
+
32
+ protected
33
+
34
+ def model_class
35
+ Spree::Channel
36
+ end
37
+
38
+ def serializer_class
39
+ Spree.api.admin_channel_serializer
40
+ end
41
+
42
+ def scope
43
+ super.for_store(current_store)
44
+ end
45
+
46
+ def permitted_params
47
+ params.permit(:name, :code, :active, :default, :preferred_order_routing_strategy)
48
+ end
49
+
50
+ private
51
+
52
+ # Deliberately not scoped through `for_store` — this endpoint is the
53
+ # path by which a product *enters* the store, so requiring an
54
+ # existing publication would block first-time onboarding. The
55
+ # cross-store guard runs one level up: `find_resource` already
56
+ # restricted the channel to `current_store`.
57
+ def scoped_product_ids
58
+ ids = decode_prefixed_ids(params[:product_ids])
59
+ Spree::Product.accessible_by(current_ability, :update).where(id: ids).ids
60
+ end
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,38 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ class CountriesController < ResourceController
6
+ scoped_resource :settings
7
+
8
+ # Override base index to skip pagination — there are ~250 countries
9
+ # and address-form dropdowns need them all at once. Pagy's global
10
+ # max_limit (100) prevents using the paginated path for this.
11
+ def index
12
+ authorize!(:read, model_class)
13
+ @collection = scope
14
+ render json: { data: serialize_collection(@collection), meta: { count: @collection.size } }
15
+ end
16
+
17
+ protected
18
+
19
+ def model_class
20
+ Spree::Country
21
+ end
22
+
23
+ def serializer_class
24
+ Spree.api.admin_country_serializer
25
+ end
26
+
27
+ def scope
28
+ Spree::Country.all.order(:name)
29
+ end
30
+
31
+ def find_resource
32
+ scope.find_by!(iso: params[:id].upcase)
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
38
+ end
@@ -0,0 +1,33 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Read-only listing of coupon codes. Codes are generated server-side
6
+ # based on the parent promotion's `code_prefix` and `number_of_codes`,
7
+ # so this controller intentionally only exposes index/show.
8
+ class CouponCodesController < ResourceController
9
+ scoped_resource :promotions
10
+
11
+ protected
12
+
13
+ def model_class
14
+ Spree::CouponCode
15
+ end
16
+
17
+ def serializer_class
18
+ Spree.api.admin_coupon_code_serializer
19
+ end
20
+
21
+ def set_parent
22
+ @parent = Spree::Promotion.accessible_by(current_ability, :show)
23
+ .find_by_prefix_id!(params[:promotion_id])
24
+ end
25
+
26
+ def parent_association
27
+ :coupon_codes
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,34 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Schema-side metadata for custom fields. Definitions are per resource
6
+ # *type* (every Spree::Product shares the same definitions), so this is
7
+ # a flat top-level endpoint. Filter by `?resource_type=Spree::Product`
8
+ # (or any other registered custom-field-bearing resource) to scope the
9
+ # list to one parent type.
10
+ class CustomFieldDefinitionsController < ResourceController
11
+ scoped_resource :custom_field_definitions
12
+
13
+ protected
14
+
15
+ def model_class
16
+ Spree::CustomFieldDefinition
17
+ end
18
+
19
+ def serializer_class
20
+ Spree.api.admin_custom_field_definition_serializer
21
+ end
22
+
23
+ # `label`, `field_type`, `storefront_visible` are model-side aliases
24
+ # (alias_attribute / accessors) on Spree::CustomFieldDefinition. The
25
+ # API → DB column rename lands in 6.0 and this controller stays flat.
26
+ def permitted_params
27
+ params.permit(:namespace, :key, :label, :field_type,
28
+ :resource_type, :storefront_visible)
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end