spree_api 5.5.0.rc2 → 5.5.0.rc3
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/app/controllers/concerns/spree/api/v3/admin/role_grant_guard.rb +83 -25
- data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +24 -2
- data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +4 -1
- data/app/controllers/spree/api/v3/admin/api_keys_controller.rb +32 -7
- data/app/controllers/spree/api/v3/admin/channels_controller.rb +4 -6
- data/app/controllers/spree/api/v3/admin/coupon_codes_controller.rb +2 -2
- data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/customers/credit_cards_controller.rb +7 -0
- data/app/controllers/spree/api/v3/admin/customers/store_credits_controller.rb +6 -0
- data/app/controllers/spree/api/v3/admin/me_controller.rb +17 -0
- data/app/controllers/spree/api/v3/admin/media_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/orders/adjustments_controller.rb +0 -2
- data/app/controllers/spree/api/v3/admin/orders/base_controller.rb +2 -0
- data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +5 -3
- data/app/controllers/spree/api/v3/admin/orders/gift_cards_controller.rb +0 -2
- data/app/controllers/spree/api/v3/admin/orders/items_controller.rb +0 -2
- data/app/controllers/spree/api/v3/admin/orders/payments_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/orders/refunds_controller.rb +3 -3
- data/app/controllers/spree/api/v3/admin/orders/store_credits_controller.rb +0 -2
- data/app/controllers/spree/api/v3/admin/payment_methods_controller.rb +2 -4
- data/app/controllers/spree/api/v3/admin/prices_controller.rb +22 -22
- data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/promotion_actions_controller.rb +2 -2
- data/app/controllers/spree/api/v3/admin/promotion_rules_controller.rb +2 -2
- data/app/controllers/spree/api/v3/admin/stock_items_controller.rb +7 -0
- data/app/controllers/spree/api/v3/admin/stock_locations_controller.rb +6 -0
- data/app/controllers/spree/api/v3/admin/stock_transfers_controller.rb +4 -3
- data/app/controllers/spree/api/v3/admin/tags_controller.rb +60 -10
- data/config/routes.rb +4 -1
- metadata +6 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 782979132fc5d443901d5df5881143bec024d695856ae2a321574004ae83ac1a
|
|
4
|
+
data.tar.gz: c8b368f528c51d9bb5772bdde7d743043c7bc5b85046e560219549254c42f6ca
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a846e352c895c3df11ed61ee931b31ecea07f5d8183021bdd5aa5711907a7a199de928402601cebdab7ac3bb2ed241b1d4a8a2c692d759ffcac15b749df7ed3b
|
|
7
|
+
data.tar.gz: 626baa65dd040b25cd6cb8cf7308e601668012933a3a7e7f0d4e5e8cb0888e7424db6924e003f80f39278dfb762ae79435e5f078f8823a506db53c3692878a19
|
|
@@ -2,49 +2,107 @@ module Spree
|
|
|
2
2
|
module Api
|
|
3
3
|
module V3
|
|
4
4
|
module Admin
|
|
5
|
-
# Shared guard
|
|
6
|
-
#
|
|
7
|
-
# let a caller hand out the `admin` super-role unless they already hold
|
|
8
|
-
# it on the current store. API-key principals have no human identity to
|
|
9
|
-
# bound the grant, so they can never grant the admin role.
|
|
5
|
+
# Shared guard for staff role grants (admin_users#update and
|
|
6
|
+
# invitations#create). A grant is rejected when, in order:
|
|
10
7
|
#
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
14
|
-
#
|
|
8
|
+
# 1. (opt-in) the caller can't `:create` a Spree::RoleUser — i.e. lacks
|
|
9
|
+
# the RoleManagement permission set;
|
|
10
|
+
# 2. it includes the literal `admin` role and the caller does not hold
|
|
11
|
+
# it on the current store;
|
|
12
|
+
# 3. it includes any role whose permission sets exceed the caller's own
|
|
13
|
+
# (catches SuperUser-equivalent custom roles the name check misses).
|
|
14
|
+
#
|
|
15
|
+
# API-key principals hold no roles, so they can grant only roles that
|
|
16
|
+
# activate no permission sets.
|
|
15
17
|
module RoleGrantGuard
|
|
16
18
|
extend ActiveSupport::Concern
|
|
17
19
|
|
|
18
20
|
private
|
|
19
21
|
|
|
20
22
|
# @param role_ids [Array<Integer,String>] resolved (decoded) role ids
|
|
23
|
+
# @param require_role_management [Boolean] also require CanCanCan
|
|
24
|
+
# authority to create a RoleUser. Set by `admin_users#update`, whose
|
|
25
|
+
# own authorization is only `:update` on the user; the invitations
|
|
26
|
+
# flow leaves it off (its gate is `manage Invitation`).
|
|
21
27
|
# @return [Boolean] true if the request was rejected (caller should return)
|
|
22
|
-
def reject_unauthorized_role_grant!(role_ids)
|
|
28
|
+
def reject_unauthorized_role_grant!(role_ids, require_role_management: false)
|
|
23
29
|
return false if role_ids.blank?
|
|
24
30
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
31
|
+
# The management gate runs whenever a role mutation is attempted —
|
|
32
|
+
# before resolving ids — so passing unknown ids can't slip the
|
|
33
|
+
# reconciliation (which would still remove the user's current roles)
|
|
34
|
+
# past a caller without role-management authority.
|
|
35
|
+
return true if require_role_management && reject_without_role_management!
|
|
36
|
+
|
|
37
|
+
roles = Spree::Role.where(id: role_ids.map(&:to_s)).to_a
|
|
38
|
+
return false if roles.empty?
|
|
39
|
+
|
|
40
|
+
return true if reject_admin_role_grant!(roles)
|
|
41
|
+
return true if reject_privilege_escalating_grant!(roles)
|
|
42
|
+
|
|
43
|
+
false
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# For API-key principals CanCanCan is permissive (ScopedAuthorization is
|
|
47
|
+
# their gate), so this only constrains JWT admins.
|
|
48
|
+
def reject_without_role_management!
|
|
49
|
+
return false if scope_limited_principal?
|
|
50
|
+
return false if can?(:create, Spree::RoleUser)
|
|
51
|
+
|
|
52
|
+
deny_role_grant!('You are not authorized to assign roles.')
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def reject_admin_role_grant!(roles)
|
|
56
|
+
return false unless roles.any? { |role| role.name == Spree::Role::ADMIN_ROLE }
|
|
28
57
|
return false if caller_holds_admin_role?
|
|
29
58
|
|
|
59
|
+
deny_role_grant!('You cannot grant the admin role.')
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# A caller holding the admin role bounds nothing; the literal admin role
|
|
63
|
+
# is already gated by reject_admin_role_grant!.
|
|
64
|
+
def reject_privilege_escalating_grant!(roles)
|
|
65
|
+
return false if caller_holds_admin_role?
|
|
66
|
+
|
|
67
|
+
caller_sets = Spree.permissions.permission_sets_for_roles(caller_role_names)
|
|
68
|
+
escalating = roles.reject { |role| grantable_within?(role, caller_sets) }
|
|
69
|
+
return false if escalating.empty?
|
|
70
|
+
|
|
71
|
+
deny_role_grant!("You cannot grant roles beyond your own privileges: #{escalating.map(&:name).join(', ')}")
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
# A role is grantable when every permission set it activates is one the
|
|
75
|
+
# caller already holds.
|
|
76
|
+
def grantable_within?(role, caller_sets)
|
|
77
|
+
(Spree.permissions.permission_sets_for(role.name) - caller_sets).empty?
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# The caller's store-scoped role names, fetched once per request.
|
|
81
|
+
def caller_role_names
|
|
82
|
+
return @caller_role_names if defined?(@caller_role_names)
|
|
83
|
+
|
|
84
|
+
user = try_spree_current_user
|
|
85
|
+
@caller_role_names =
|
|
86
|
+
if user.respond_to?(:role_users)
|
|
87
|
+
user.role_users.where(resource: current_store).joins(:role).
|
|
88
|
+
pluck("#{Spree::Role.table_name}.name")
|
|
89
|
+
else
|
|
90
|
+
[]
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def caller_holds_admin_role?
|
|
95
|
+
caller_role_names.include?(Spree::Role::ADMIN_ROLE)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def deny_role_grant!(message)
|
|
30
99
|
render_error(
|
|
31
100
|
code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:access_denied],
|
|
32
|
-
message:
|
|
101
|
+
message: message,
|
|
33
102
|
status: :forbidden
|
|
34
103
|
)
|
|
35
104
|
true
|
|
36
105
|
end
|
|
37
|
-
|
|
38
|
-
# Mirrors how Spree::Ability activates the super-user permission set:
|
|
39
|
-
# admin-ness is decided by `spree_roles` membership (resource-agnostic),
|
|
40
|
-
# not by a per-store RoleUser. API-key principals have no user and so
|
|
41
|
-
# never count as holding the admin role.
|
|
42
|
-
def caller_holds_admin_role?
|
|
43
|
-
user = try_spree_current_user
|
|
44
|
-
return false unless user.respond_to?(:spree_roles)
|
|
45
|
-
|
|
46
|
-
user.spree_roles.exists?(name: Spree::Role::ADMIN_ROLE)
|
|
47
|
-
end
|
|
48
106
|
end
|
|
49
107
|
end
|
|
50
108
|
end
|
|
@@ -39,12 +39,34 @@ module Spree
|
|
|
39
39
|
return true
|
|
40
40
|
end
|
|
41
41
|
|
|
42
|
-
# Fall back to JWT authentication
|
|
43
|
-
|
|
42
|
+
# Fall back to JWT authentication, then bind the admin to the store
|
|
43
|
+
# they hold a role on (the token itself is store-agnostic).
|
|
44
|
+
return false unless require_authentication!
|
|
45
|
+
|
|
46
|
+
require_store_membership!
|
|
44
47
|
end
|
|
45
48
|
|
|
46
49
|
private
|
|
47
50
|
|
|
51
|
+
# Rejects an authenticated JWT admin who has no role on +current_store+.
|
|
52
|
+
# API-key principals are already store-bound and skip this check.
|
|
53
|
+
def require_store_membership!
|
|
54
|
+
return true if current_user_member_of_store?
|
|
55
|
+
|
|
56
|
+
render_error(
|
|
57
|
+
code: ErrorHandler::ERROR_CODES[:access_denied],
|
|
58
|
+
message: 'You do not have access to this store.',
|
|
59
|
+
status: :forbidden
|
|
60
|
+
)
|
|
61
|
+
false
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def current_user_member_of_store?
|
|
65
|
+
return false unless current_user.respond_to?(:role_users)
|
|
66
|
+
|
|
67
|
+
current_user.role_users.exists?(resource: current_store)
|
|
68
|
+
end
|
|
69
|
+
|
|
48
70
|
def set_no_store_cache
|
|
49
71
|
response.headers['Cache-Control'] = 'private, no-store'
|
|
50
72
|
end
|
|
@@ -35,8 +35,11 @@ module Spree
|
|
|
35
35
|
|
|
36
36
|
# `nil` when the key is absent (leave roles untouched); an array
|
|
37
37
|
# (possibly empty, to clear) when the client sends `role_ids`.
|
|
38
|
+
# `require_role_management:` — this action only authorizes `:update`
|
|
39
|
+
# on the user, so role assignment must be separately gated by the
|
|
40
|
+
# RoleManagement permission set, not by profile-edit rights.
|
|
38
41
|
role_ids = role_ids_param if params.key?(:role_ids)
|
|
39
|
-
return if role_ids && reject_unauthorized_role_grant!(role_ids)
|
|
42
|
+
return if role_ids && reject_unauthorized_role_grant!(role_ids, require_role_management: true)
|
|
40
43
|
|
|
41
44
|
if @resource.update(identity_params)
|
|
42
45
|
apply_role_ids(role_ids) if role_ids
|
|
@@ -8,6 +8,10 @@ module Spree
|
|
|
8
8
|
# revoke or destroy higher-privileged keys.
|
|
9
9
|
scoped_resource :api_keys
|
|
10
10
|
|
|
11
|
+
# Introspecting the credential you authenticated with never requires
|
|
12
|
+
# `read_api_keys` — any key can describe itself.
|
|
13
|
+
skip_scope_check! only: :current
|
|
14
|
+
|
|
11
15
|
# POST /api/v3/admin/api_keys
|
|
12
16
|
# Prevents scope amplification: a key minted via a secret API key can
|
|
13
17
|
# only carry scopes that key already holds. A JWT admin is governed by
|
|
@@ -40,6 +44,27 @@ module Spree
|
|
|
40
44
|
render json: serialize_resource(@resource)
|
|
41
45
|
end
|
|
42
46
|
|
|
47
|
+
# GET /api/v3/admin/api_keys/current
|
|
48
|
+
# Describes the key that authenticated this request, including its
|
|
49
|
+
# live scopes — so a client (e.g. the `spree api` CLI) can show the
|
|
50
|
+
# real, current authority instead of a stale local snapshot. Only
|
|
51
|
+
# secret-key principals have a single key; a JWT admin does not.
|
|
52
|
+
#
|
|
53
|
+
# This is the secret-key half of "describe the current credential";
|
|
54
|
+
# the JWT-admin half is GET /api/v3/admin/me (see MeController), which
|
|
55
|
+
# returns the user + their CanCanCan permissions.
|
|
56
|
+
def current
|
|
57
|
+
unless current_api_key
|
|
58
|
+
return render_error(
|
|
59
|
+
code: ERROR_CODES[:record_not_found],
|
|
60
|
+
message: Spree.t(:api_key_no_current_key),
|
|
61
|
+
status: :not_found
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
render json: serialize_resource(current_api_key)
|
|
66
|
+
end
|
|
67
|
+
|
|
43
68
|
protected
|
|
44
69
|
|
|
45
70
|
def model_class
|
|
@@ -62,14 +87,14 @@ module Spree
|
|
|
62
87
|
end
|
|
63
88
|
end
|
|
64
89
|
|
|
65
|
-
# `key_type`
|
|
66
|
-
#
|
|
67
|
-
#
|
|
68
|
-
#
|
|
90
|
+
# `key_type` and `scopes` are create-only (scope immutability lives on
|
|
91
|
+
# Spree::ApiKey); update is limited to the human-facing `name`. Stripping
|
|
92
|
+
# them here keeps them out of mass assignment so a rename returns 200
|
|
93
|
+
# rather than 422.
|
|
69
94
|
def permitted_params
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
95
|
+
return params.permit(:name) if action_name == 'update'
|
|
96
|
+
|
|
97
|
+
params.permit(:name, :key_type, scopes: [])
|
|
73
98
|
end
|
|
74
99
|
|
|
75
100
|
private
|
|
@@ -49,14 +49,12 @@ module Spree
|
|
|
49
49
|
|
|
50
50
|
private
|
|
51
51
|
|
|
52
|
-
#
|
|
53
|
-
#
|
|
54
|
-
#
|
|
55
|
-
# cross-store guard runs one level up: `find_resource` already
|
|
56
|
-
# restricted the channel to `current_store`.
|
|
52
|
+
# Scoped to the current store: a product can only be published to a
|
|
53
|
+
# channel of the store that owns it (products are single-owner via
|
|
54
|
+
# `belongs_to :store`). Foreign product IDs are silently dropped.
|
|
57
55
|
def scoped_product_ids
|
|
58
56
|
ids = decode_prefixed_ids(params[:product_ids])
|
|
59
|
-
|
|
57
|
+
current_store.products.accessible_by(current_ability, :update).where(id: ids).ids
|
|
60
58
|
end
|
|
61
59
|
end
|
|
62
60
|
end
|
|
@@ -19,8 +19,8 @@ module Spree
|
|
|
19
19
|
end
|
|
20
20
|
|
|
21
21
|
def set_parent
|
|
22
|
-
@parent =
|
|
23
|
-
|
|
22
|
+
@parent = current_store.promotions.accessible_by(current_ability, :show)
|
|
23
|
+
.find_by_prefix_id!(params[:promotion_id])
|
|
24
24
|
end
|
|
25
25
|
|
|
26
26
|
def parent_association
|
|
@@ -54,7 +54,7 @@ module Spree
|
|
|
54
54
|
raise ActiveRecord::RecordNotFound, 'Parent resource not found' unless parent_lookup
|
|
55
55
|
|
|
56
56
|
@parent = parent_relation.find_by_prefix_id!(parent_lookup.value)
|
|
57
|
-
authorize!(:
|
|
57
|
+
authorize!(:update, @parent)
|
|
58
58
|
end
|
|
59
59
|
|
|
60
60
|
# Resolves the parent within the current store so a token for one
|
|
@@ -10,6 +10,13 @@ module Spree
|
|
|
10
10
|
:credit_cards
|
|
11
11
|
end
|
|
12
12
|
|
|
13
|
+
# Customers are global; a saved card belongs to the store-scoped
|
|
14
|
+
# payment method it was created against, so the nested collection is
|
|
15
|
+
# bound to the current store's payment methods.
|
|
16
|
+
def scope
|
|
17
|
+
@parent.credit_cards.where(payment_method_id: current_store.payment_methods.select(:id))
|
|
18
|
+
end
|
|
19
|
+
|
|
13
20
|
def model_class
|
|
14
21
|
Spree::CreditCard
|
|
15
22
|
end
|
|
@@ -67,6 +67,12 @@ module Spree
|
|
|
67
67
|
:store_credits
|
|
68
68
|
end
|
|
69
69
|
|
|
70
|
+
# Customers are global; store credits belong to a store, so the
|
|
71
|
+
# nested collection is bound to the current store.
|
|
72
|
+
def scope
|
|
73
|
+
@parent.store_credits.for_store(current_store)
|
|
74
|
+
end
|
|
75
|
+
|
|
70
76
|
def model_class
|
|
71
77
|
Spree::StoreCredit
|
|
72
78
|
end
|
|
@@ -11,7 +11,24 @@ module Spree
|
|
|
11
11
|
# the permissions list to decide which UI elements to show or hide.
|
|
12
12
|
# The actual authorization check is still enforced server-side by
|
|
13
13
|
# CanCanCan — the SPA list is purely for UX.
|
|
14
|
+
#
|
|
15
|
+
# This is the JWT-admin half of "describe the current credential"; the
|
|
16
|
+
# secret-key half is GET /api/v3/admin/api_keys/current (see
|
|
17
|
+
# ApiKeysController#current), which returns the key + its scopes.
|
|
18
|
+
#
|
|
19
|
+
# A request authenticated by a secret API key has no Spree user to
|
|
20
|
+
# describe, so it gets a 404 pointing at the key endpoint rather than
|
|
21
|
+
# a 500 from serializing a nil user — mirroring how #current 404s for
|
|
22
|
+
# a JWT principal that has no single key.
|
|
14
23
|
def show
|
|
24
|
+
unless current_user
|
|
25
|
+
return render_error(
|
|
26
|
+
code: ERROR_CODES[:record_not_found],
|
|
27
|
+
message: Spree.t(:me_no_current_user),
|
|
28
|
+
status: :not_found
|
|
29
|
+
)
|
|
30
|
+
end
|
|
31
|
+
|
|
15
32
|
render json: {
|
|
16
33
|
user: admin_user_serializer.new(current_user, params: serializer_params).to_h,
|
|
17
34
|
permissions: serialize_permissions(current_ability)
|
|
@@ -34,7 +34,7 @@ module Spree
|
|
|
34
34
|
|
|
35
35
|
def set_parent
|
|
36
36
|
@product = current_store.products.find_by_prefix_id!(params[:product_id])
|
|
37
|
-
authorize!(:
|
|
37
|
+
authorize!(:update, @product)
|
|
38
38
|
|
|
39
39
|
@parent = if params[:variant_id].present?
|
|
40
40
|
@product.variants_including_master.find_by_prefix_id!(params[:variant_id])
|
|
@@ -57,11 +57,11 @@ module Spree
|
|
|
57
57
|
# PATCH /api/v3/admin/orders/:order_id/fulfillments/:id/split
|
|
58
58
|
def split
|
|
59
59
|
with_order_lock do
|
|
60
|
-
variant =
|
|
60
|
+
variant = current_store.variants.find_by_prefix_id!(params[:variant_id])
|
|
61
61
|
quantity = params[:quantity].to_i
|
|
62
62
|
|
|
63
63
|
stock_location = if params[:stock_location_id].present?
|
|
64
|
-
Spree::StockLocation.find_by_prefix_id!(params[:stock_location_id])
|
|
64
|
+
Spree::StockLocation.accessible_by(current_ability, :show).find_by_prefix_id!(params[:stock_location_id])
|
|
65
65
|
else
|
|
66
66
|
@resource.stock_location
|
|
67
67
|
end
|
|
@@ -93,8 +93,10 @@ module Spree
|
|
|
93
93
|
:shipments
|
|
94
94
|
end
|
|
95
95
|
|
|
96
|
+
# State changes go through the dedicated `fulfill`/`cancel`/`resume`
|
|
97
|
+
# member actions, not mass assignment.
|
|
96
98
|
def permitted_params
|
|
97
|
-
params.permit(:tracking, :selected_shipping_rate_id, :stock_location_id
|
|
99
|
+
params.permit(:tracking, :selected_shipping_rate_id, :stock_location_id)
|
|
98
100
|
end
|
|
99
101
|
end
|
|
100
102
|
end
|
|
@@ -15,7 +15,7 @@ module Spree
|
|
|
15
15
|
# The source must belong to the customer assigned to the order.
|
|
16
16
|
def create
|
|
17
17
|
with_order_lock do
|
|
18
|
-
payment_method =
|
|
18
|
+
payment_method = current_store.payment_methods.accessible_by(current_ability, :show).find_by_prefix_id!(params[:payment_method_id])
|
|
19
19
|
@resource = @parent.payments.build(
|
|
20
20
|
amount: params[:amount] || @parent.order_total_after_store_credit,
|
|
21
21
|
payment_method: payment_method
|
|
@@ -9,9 +9,9 @@ module Spree
|
|
|
9
9
|
# POST /api/v3/admin/orders/:order_id/refunds
|
|
10
10
|
def create
|
|
11
11
|
with_order_lock do
|
|
12
|
-
payment = @parent.payments.find_by_prefix_id!(params[:payment_id])
|
|
13
|
-
reason = Spree::RefundReason.find_by_prefix_id!(params[:refund_reason_id]) if params[:refund_reason_id].present?
|
|
14
|
-
reason ||= Spree::RefundReason.first
|
|
12
|
+
payment = @parent.payments.accessible_by(current_ability, :update).find_by_prefix_id!(params[:payment_id])
|
|
13
|
+
reason = Spree::RefundReason.accessible_by(current_ability, :show).find_by_prefix_id!(params[:refund_reason_id]) if params[:refund_reason_id].present?
|
|
14
|
+
reason ||= Spree::RefundReason.accessible_by(current_ability, :show).first
|
|
15
15
|
|
|
16
16
|
@resource = payment.refunds.build(
|
|
17
17
|
amount: params[:amount],
|
|
@@ -60,11 +60,9 @@ module Spree
|
|
|
60
60
|
|
|
61
61
|
private
|
|
62
62
|
|
|
63
|
-
# New payment methods
|
|
63
|
+
# New payment methods are created through the current store.
|
|
64
64
|
def build_subclassed_resource(klass, attrs)
|
|
65
|
-
|
|
66
|
-
resource.stores = [current_store] if resource.stores.empty?
|
|
67
|
-
resource
|
|
65
|
+
current_store.payment_methods.build(attrs.merge(type: klass.sti_name))
|
|
68
66
|
end
|
|
69
67
|
end
|
|
70
68
|
end
|
|
@@ -35,7 +35,13 @@ module Spree
|
|
|
35
35
|
)
|
|
36
36
|
end
|
|
37
37
|
|
|
38
|
-
|
|
38
|
+
store_variant_ids = store_variants.where(id: rows.map { |r| r[:variant_id] }).ids.map(&:to_s).to_set
|
|
39
|
+
store_price_list_ids = store_price_lists.where(id: rows.filter_map { |r| r[:price_list_id] }).ids.map(&:to_s).to_set
|
|
40
|
+
foreign = rows.each_with_index.filter_map do |row, idx|
|
|
41
|
+
variant_ok = store_variant_ids.include?(row[:variant_id].to_s)
|
|
42
|
+
price_list_ok = row[:price_list_id].blank? || store_price_list_ids.include?(row[:price_list_id].to_s)
|
|
43
|
+
{ index: idx } unless variant_ok && price_list_ok
|
|
44
|
+
end
|
|
39
45
|
if foreign.any?
|
|
40
46
|
return render_error(
|
|
41
47
|
code: 'invalid_prices',
|
|
@@ -91,14 +97,26 @@ module Spree
|
|
|
91
97
|
false
|
|
92
98
|
end
|
|
93
99
|
|
|
100
|
+
# Resolves variant_id / price_list_id through the current store's
|
|
101
|
+
# scopes so a foreign or unknown id 404s instead of binding the
|
|
102
|
+
# price to another store's record.
|
|
94
103
|
def permitted_params
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
)
|
|
104
|
+
permitted = params.permit(:variant_id, :currency, :amount, :compare_at_amount, :price_list_id)
|
|
105
|
+
permitted[:variant_id] = store_variants.find_by_prefix_id!(permitted[:variant_id]).id if permitted[:variant_id].present?
|
|
106
|
+
permitted[:price_list_id] = store_price_lists.find_by_prefix_id!(permitted[:price_list_id]).id if permitted[:price_list_id].present?
|
|
107
|
+
permitted
|
|
98
108
|
end
|
|
99
109
|
|
|
100
110
|
private
|
|
101
111
|
|
|
112
|
+
def store_variants
|
|
113
|
+
current_store.variants.accessible_by(current_ability, :update)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def store_price_lists
|
|
117
|
+
current_store.price_lists.accessible_by(current_ability, :update)
|
|
118
|
+
end
|
|
119
|
+
|
|
102
120
|
def bulk_record_count_key
|
|
103
121
|
:price_count
|
|
104
122
|
end
|
|
@@ -132,24 +150,6 @@ module Spree
|
|
|
132
150
|
|
|
133
151
|
Spree::PrefixedId.prefixed_id?(value) ? Spree::PrefixedId.decode_prefixed_id(value) : value
|
|
134
152
|
end
|
|
135
|
-
|
|
136
|
-
# Rejects rows whose variant or price list belongs to another store.
|
|
137
|
-
# `Spree::Prices::BulkUpsert` writes rows keyed on the raw variant_id
|
|
138
|
-
# with no ownership check, so the store boundary is enforced here.
|
|
139
|
-
def foreign_rows(rows)
|
|
140
|
-
variant_ids = rows.map { |r| r[:variant_id] }.compact.uniq
|
|
141
|
-
price_list_ids = rows.map { |r| r[:price_list_id] }.compact.uniq
|
|
142
|
-
|
|
143
|
-
store_variant_ids = current_store.variants.where(id: variant_ids).pluck(:id).map(&:to_s).to_set
|
|
144
|
-
store_price_list_ids = Spree::PriceList.for_store(current_store).where(id: price_list_ids).pluck(:id).map(&:to_s).to_set
|
|
145
|
-
|
|
146
|
-
rows.each_with_index.filter_map do |row, idx|
|
|
147
|
-
variant_ok = store_variant_ids.include?(row[:variant_id].to_s)
|
|
148
|
-
price_list_ok = row[:price_list_id].blank? || store_price_list_ids.include?(row[:price_list_id].to_s)
|
|
149
|
-
|
|
150
|
-
{ index: idx } unless variant_ok && price_list_ok
|
|
151
|
-
end
|
|
152
|
-
end
|
|
153
153
|
end
|
|
154
154
|
end
|
|
155
155
|
end
|
|
@@ -31,7 +31,7 @@ module Spree
|
|
|
31
31
|
|
|
32
32
|
def permitted_params
|
|
33
33
|
params.permit(
|
|
34
|
-
:sku, :barcode,
|
|
34
|
+
:sku, :barcode,
|
|
35
35
|
:cost_price, :cost_currency,
|
|
36
36
|
:weight, :height, :width, :depth, :weight_unit, :dimensions_unit,
|
|
37
37
|
:track_inventory, :tax_category_id, :position,
|
|
@@ -58,8 +58,8 @@ module Spree
|
|
|
58
58
|
def set_parent
|
|
59
59
|
return if %w[types calculators].include?(action_name)
|
|
60
60
|
|
|
61
|
-
@parent =
|
|
62
|
-
|
|
61
|
+
@parent = current_store.promotions.accessible_by(current_ability, :update)
|
|
62
|
+
.find_by_prefix_id!(params[:promotion_id])
|
|
63
63
|
end
|
|
64
64
|
|
|
65
65
|
def parent_association
|
|
@@ -36,8 +36,8 @@ module Spree
|
|
|
36
36
|
def set_parent
|
|
37
37
|
return if action_name == 'types'
|
|
38
38
|
|
|
39
|
-
@parent =
|
|
40
|
-
|
|
39
|
+
@parent = current_store.promotions.accessible_by(current_ability, :update)
|
|
40
|
+
.find_by_prefix_id!(params[:promotion_id])
|
|
41
41
|
end
|
|
42
42
|
|
|
43
43
|
def parent_association
|
|
@@ -28,6 +28,13 @@ module Spree
|
|
|
28
28
|
def apply_collection_sort(collection)
|
|
29
29
|
collection.order(Spree::StockItem.arel_table[:id].asc)
|
|
30
30
|
end
|
|
31
|
+
|
|
32
|
+
# Stock items are auto-created against a (variant, stock_location)
|
|
33
|
+
# pair and never re-pointed, so update only touches the count and
|
|
34
|
+
# backorder flag — not the variant or location FKs.
|
|
35
|
+
def permitted_params
|
|
36
|
+
params.permit(:count_on_hand, :backorderable, metadata: {})
|
|
37
|
+
end
|
|
31
38
|
end
|
|
32
39
|
end
|
|
33
40
|
end
|
|
@@ -15,6 +15,12 @@ module Spree
|
|
|
15
15
|
Spree.api.admin_stock_location_serializer
|
|
16
16
|
end
|
|
17
17
|
|
|
18
|
+
# Stock locations are shared across stores, so writes are store-wide
|
|
19
|
+
# administration: reads need `read_stock`, writes need `write_settings`.
|
|
20
|
+
def scoped_resource_name
|
|
21
|
+
read_actions.include?(action_name) ? :stock : :settings
|
|
22
|
+
end
|
|
23
|
+
|
|
18
24
|
def scope
|
|
19
25
|
super.order_default
|
|
20
26
|
end
|
|
@@ -11,9 +11,10 @@ module Spree
|
|
|
11
11
|
def create
|
|
12
12
|
authorize!(:create, model_class)
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
stock_locations = Spree::StockLocation.accessible_by(current_ability, :show)
|
|
15
|
+
destination = stock_locations.find_by_prefix_id!(params[:destination_location_id])
|
|
15
16
|
source = params[:source_location_id].present? ?
|
|
16
|
-
|
|
17
|
+
stock_locations.find_by_prefix_id!(params[:source_location_id]) : nil
|
|
17
18
|
|
|
18
19
|
variants_map = build_variants_map
|
|
19
20
|
if variants_map.empty?
|
|
@@ -63,7 +64,7 @@ module Spree
|
|
|
63
64
|
hash[decoded.to_i] = entry[:quantity].to_i if decoded
|
|
64
65
|
end
|
|
65
66
|
|
|
66
|
-
|
|
67
|
+
current_store.variants.accessible_by(current_ability, :update).where(id: quantities_by_id.keys).each_with_object({}) do |variant, acc|
|
|
67
68
|
quantity = quantities_by_id[variant.id]
|
|
68
69
|
acc[variant] = quantity if quantity&.positive?
|
|
69
70
|
end
|
|
@@ -3,24 +3,32 @@ module Spree
|
|
|
3
3
|
module V3
|
|
4
4
|
module Admin
|
|
5
5
|
class TagsController < BaseController
|
|
6
|
+
# The required scope depends on the requested `taggable_type`, so it's
|
|
7
|
+
# resolved per request rather than via a static `scoped_resource`.
|
|
8
|
+
# `authorize_taggable_scope!` enforces it for API-key principals.
|
|
6
9
|
skip_scope_check!
|
|
7
10
|
|
|
11
|
+
before_action :authorize_taggable_scope!, only: :index
|
|
12
|
+
|
|
8
13
|
MAX_RESULTS = 50
|
|
9
14
|
|
|
15
|
+
# Maps a taggable type to the read scope an API key must hold to
|
|
16
|
+
# enumerate its tag vocabulary. Types absent from this map require
|
|
17
|
+
# `read_all`.
|
|
18
|
+
def self.scope_for_taggable_type
|
|
19
|
+
{
|
|
20
|
+
'Spree::Product' => 'read_products',
|
|
21
|
+
'Spree::Order' => 'read_orders',
|
|
22
|
+
Spree.user_class.to_s => 'read_customers'
|
|
23
|
+
}
|
|
24
|
+
end
|
|
25
|
+
|
|
10
26
|
def index
|
|
11
|
-
|
|
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
|
|
27
|
+
return unless valid_taggable_type?
|
|
20
28
|
|
|
21
29
|
scope = ActsAsTaggableOn::Tag.
|
|
22
30
|
joins(:taggings).
|
|
23
|
-
where(ActsAsTaggableOn.taggings_table =>
|
|
31
|
+
where(ActsAsTaggableOn.taggings_table => taggings_conditions).
|
|
24
32
|
distinct.
|
|
25
33
|
order(:name).
|
|
26
34
|
limit(MAX_RESULTS)
|
|
@@ -37,6 +45,48 @@ module Spree
|
|
|
37
45
|
|
|
38
46
|
private
|
|
39
47
|
|
|
48
|
+
def taggable_type
|
|
49
|
+
params[:taggable_type].to_s
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def valid_taggable_type?
|
|
53
|
+
return true if allowed_taggable_types.include?(taggable_type)
|
|
54
|
+
|
|
55
|
+
render_error(
|
|
56
|
+
code: 'invalid_taggable_type',
|
|
57
|
+
message: "taggable_type must be one of #{allowed_taggable_types.join(', ')}",
|
|
58
|
+
status: :unprocessable_content
|
|
59
|
+
)
|
|
60
|
+
false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Tagging filter. `Spree::Order` carries a `tenant` (store_id) column
|
|
64
|
+
# via `acts_as_taggable_tenant`, so its tag vocabulary is bounded to
|
|
65
|
+
# the current store; other taggables fall back to the type filter.
|
|
66
|
+
def taggings_conditions
|
|
67
|
+
conditions = { taggable_type: taggable_type, context: 'tags' }
|
|
68
|
+
conditions[:tenant] = current_store.id.to_s if taggable_type == 'Spree::Order'
|
|
69
|
+
conditions
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Per-type scope check for API-key principals: a key listing product
|
|
73
|
+
# tags needs `read_products`, order tags `read_orders`, etc. JWT
|
|
74
|
+
# admins are gated by store membership + CanCanCan, not scopes.
|
|
75
|
+
def authorize_taggable_scope!
|
|
76
|
+
return unless current_api_key
|
|
77
|
+
return unless allowed_taggable_types.include?(taggable_type)
|
|
78
|
+
|
|
79
|
+
required = self.class.scope_for_taggable_type.fetch(taggable_type, 'read_all')
|
|
80
|
+
return if current_api_key.has_scope?(required)
|
|
81
|
+
|
|
82
|
+
render_error(
|
|
83
|
+
code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:access_denied],
|
|
84
|
+
message: "API key lacks scope: #{required}",
|
|
85
|
+
status: :forbidden,
|
|
86
|
+
details: { required_scope: required }
|
|
87
|
+
)
|
|
88
|
+
end
|
|
89
|
+
|
|
40
90
|
# Sourced from `Spree.taggable_types` (registered in
|
|
41
91
|
# `Spree::Core::Engine`'s after_initialize block). Apps extend the
|
|
42
92
|
# list in an initializer without overriding this controller:
|
data/config/routes.rb
CHANGED
|
@@ -135,7 +135,10 @@ Spree::Core::Engine.add_routes do
|
|
|
135
135
|
patch :resend
|
|
136
136
|
end
|
|
137
137
|
end
|
|
138
|
-
resources :api_keys, only: [:index, :show, :create, :destroy] do
|
|
138
|
+
resources :api_keys, only: [:index, :show, :create, :update, :destroy] do
|
|
139
|
+
collection do
|
|
140
|
+
get :current
|
|
141
|
+
end
|
|
139
142
|
member do
|
|
140
143
|
patch :revoke
|
|
141
144
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: spree_api
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 5.5.0.
|
|
4
|
+
version: 5.5.0.rc3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Vendo Connect Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-06-
|
|
11
|
+
date: 2026-06-18 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: rswag-specs
|
|
@@ -72,14 +72,14 @@ dependencies:
|
|
|
72
72
|
requirements:
|
|
73
73
|
- - '='
|
|
74
74
|
- !ruby/object:Gem::Version
|
|
75
|
-
version: 5.5.0.
|
|
75
|
+
version: 5.5.0.rc3
|
|
76
76
|
type: :runtime
|
|
77
77
|
prerelease: false
|
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
|
79
79
|
requirements:
|
|
80
80
|
- - '='
|
|
81
81
|
- !ruby/object:Gem::Version
|
|
82
|
-
version: 5.5.0.
|
|
82
|
+
version: 5.5.0.rc3
|
|
83
83
|
description: Spree's API
|
|
84
84
|
email:
|
|
85
85
|
- hello@spreecommerce.org
|
|
@@ -353,9 +353,9 @@ licenses:
|
|
|
353
353
|
- BSD-3-Clause
|
|
354
354
|
metadata:
|
|
355
355
|
bug_tracker_uri: https://github.com/spree/spree/issues
|
|
356
|
-
changelog_uri: https://github.com/spree/spree/releases/tag/v5.5.0.
|
|
356
|
+
changelog_uri: https://github.com/spree/spree/releases/tag/v5.5.0.rc3
|
|
357
357
|
documentation_uri: https://docs.spreecommerce.org/
|
|
358
|
-
source_code_uri: https://github.com/spree/spree/tree/v5.5.0.
|
|
358
|
+
source_code_uri: https://github.com/spree/spree/tree/v5.5.0.rc3
|
|
359
359
|
post_install_message:
|
|
360
360
|
rdoc_options: []
|
|
361
361
|
require_paths:
|