spree_api 6.0.0.beta2 → 6.0.0.beta3
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/embed_permissions.rb +55 -0
- data/app/controllers/concerns/spree/api/v3/admin/role_grant_guard.rb +34 -5
- data/app/controllers/concerns/spree/api/v3/current_password_confirmation.rb +8 -0
- data/app/controllers/concerns/spree/api/v3/orders/fulfillment_actions.rb +15 -2
- data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +14 -0
- data/app/controllers/concerns/spree/api/v3/store/storefront_products.rb +35 -0
- data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +9 -0
- data/app/controllers/spree/api/v3/admin/base_controller.rb +1 -0
- data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +8 -3
- data/app/controllers/spree/api/v3/admin/customers_controller.rb +0 -14
- data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +29 -2
- data/app/controllers/spree/api/v3/admin/invitation_acceptances_controller.rb +4 -1
- data/app/controllers/spree/api/v3/admin/invitations/acceptance_links_controller.rb +41 -0
- data/app/controllers/spree/api/v3/admin/me_controller.rb +11 -0
- data/app/controllers/spree/api/v3/admin/orders/claims_controller.rb +14 -1
- data/app/controllers/spree/api/v3/admin/orders/exchanges_controller.rb +22 -1
- data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/orders/returns_controller.rb +22 -1
- data/app/controllers/spree/api/v3/admin/orders_controller.rb +27 -1
- data/app/controllers/spree/api/v3/admin/password_resets_controller.rb +31 -11
- data/app/controllers/spree/api/v3/admin/price_lists_controller.rb +1 -19
- data/app/controllers/spree/api/v3/admin/prices_controller.rb +33 -2
- data/app/controllers/spree/api/v3/admin/products_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/resource_controller.rb +1 -0
- data/app/controllers/spree/api/v3/admin/sellers/invitations/acceptance_links_controller.rb +37 -0
- data/app/controllers/spree/api/v3/admin/setup_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/store_controller.rb +4 -0
- data/app/controllers/spree/api/v3/admin/translations/batches_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/translations_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/webhook_deliveries_controller.rb +12 -0
- data/app/controllers/spree/api/v3/admin/webhook_endpoints_controller.rb +27 -0
- data/app/controllers/spree/api/v3/base_controller.rb +7 -3
- data/app/controllers/spree/api/v3/resource_controller.rb +11 -1
- data/app/controllers/spree/api/v3/seller/auth_controller.rb +1 -1
- data/app/controllers/spree/api/v3/seller/invitation_acceptances_controller.rb +5 -2
- data/app/controllers/spree/api/v3/seller/invitations/acceptance_links_controller.rb +34 -0
- data/app/controllers/spree/api/v3/seller/me_controller.rb +25 -2
- data/app/controllers/spree/api/v3/seller/orders_controller.rb +19 -24
- data/app/controllers/spree/api/v3/seller/password_resets_controller.rb +1 -1
- data/app/controllers/spree/api/v3/seller/resource_controller.rb +4 -0
- data/app/controllers/spree/api/v3/store/carts/items_controller.rb +7 -4
- data/app/controllers/spree/api/v3/store/carts/payment_sessions_controller.rb +10 -1
- data/app/controllers/spree/api/v3/store/carts/payments_controller.rb +5 -1
- data/app/controllers/spree/api/v3/store/companies/members_controller.rb +10 -11
- data/app/controllers/spree/api/v3/store/companies/orders_controller.rb +6 -0
- data/app/controllers/spree/api/v3/store/products/filters_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/products_controller.rb +6 -12
- data/app/controllers/spree/api/v3/store/resource_controller.rb +4 -0
- data/app/controllers/spree/api/v3/store/wishlist_items_controller.rb +11 -0
- data/app/controllers/spree/api/v3/webhooks/fulfillments_controller.rb +10 -6
- data/app/controllers/spree/api/v3/webhooks/payments_controller.rb +6 -2
- data/app/controllers/spree/api/v3/webhooks/payouts_controller.rb +6 -2
- data/app/jobs/spree/webhook_delivery_job.rb +18 -4
- data/app/serializers/spree/api/v3/admin/gift_card_serializer.rb +6 -0
- data/app/serializers/spree/api/v3/admin/invitation_acceptance_link_serializer.rb +15 -0
- data/app/serializers/spree/api/v3/admin/invitation_serializer.rb +0 -11
- data/app/serializers/spree/api/v3/admin/order_group_serializer.rb +7 -0
- data/app/serializers/spree/api/v3/admin/store_serializer.rb +8 -0
- data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +6 -2
- data/app/serializers/spree/api/v3/gift_card_serializer.rb +3 -1
- data/app/serializers/spree/api/v3/line_item_serializer.rb +2 -1
- data/app/serializers/spree/api/v3/market_serializer.rb +5 -0
- data/app/serializers/spree/api/v3/product_filters_serializer.rb +3 -0
- data/app/serializers/spree/api/v3/seller/account_serializer.rb +19 -0
- data/app/serializers/spree/api/v3/seller/invitation_acceptance_link_serializer.rb +15 -0
- data/app/serializers/spree/api/v3/seller/invitation_serializer.rb +1 -10
- data/app/services/spree/api/v3/filters_aggregator.rb +5 -2
- data/config/routes.rb +7 -0
- data/lib/spree/api/dependencies.rb +3 -0
- data/lib/spree/api/openapi/schema_helper.rb +10 -4
- metadata +14 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: d30acba1d9395e7575f92eefe7c86b5cd8c73eae44cceb1e178a853963e856c4
|
|
4
|
+
data.tar.gz: 562e93949b2a3e5b124c06896821d26554329f0e332faa8f9de2d35b6d40dc71
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 401bbdc3ddb5eb121c8ad9eac1d5341aa130c0f1f61135e54b14c8eabe577c1845442f28e20e4bf608e35d5dc5f757b25b4b41697dd1010c78ae9f2ede161362
|
|
7
|
+
data.tar.gz: e27e19331d0e63339aa5bd21586f1f4ed74b095689a3c31252faee126f9d896f3579cef931a6fa66afa5b6e0c001f7759f19838f3a2914de105090f598b4c2ae
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# Keeps records another permission guards out of a response.
|
|
6
|
+
#
|
|
7
|
+
# An endpoint is gated on its own resource's key, but expanding an
|
|
8
|
+
# association pulls in whole records of another kind — a customer's
|
|
9
|
+
# orders, an order's payments, a gift card's customer. Without this a
|
|
10
|
+
# role cleared only for customers could read every order through
|
|
11
|
+
# `?expand=orders`. Each segment of an expand path is checked, so
|
|
12
|
+
# `orders.payments` needs both keys.
|
|
13
|
+
#
|
|
14
|
+
# Gift card codes are bearer credentials that ride along on every
|
|
15
|
+
# order, so serializers show them in full only to a caller who may read
|
|
16
|
+
# gift cards and masked to everyone else.
|
|
17
|
+
module EmbedPermissions
|
|
18
|
+
extend ActiveSupport::Concern
|
|
19
|
+
|
|
20
|
+
# Association name in an expand path => the catalog key it needs.
|
|
21
|
+
EXPANSION_PERMISSIONS = {
|
|
22
|
+
'order' => 'read_orders',
|
|
23
|
+
'orders' => 'read_orders',
|
|
24
|
+
'payment' => 'read_payments',
|
|
25
|
+
'payments' => 'read_payments',
|
|
26
|
+
'payment_splits' => 'read_payments',
|
|
27
|
+
'customer' => 'read_customers',
|
|
28
|
+
'customers' => 'read_customers',
|
|
29
|
+
'gift_card' => 'read_gift_cards',
|
|
30
|
+
'gift_cards' => 'read_gift_cards',
|
|
31
|
+
'store_credit' => 'read_store_credits',
|
|
32
|
+
'store_credits' => 'read_store_credits'
|
|
33
|
+
}.freeze
|
|
34
|
+
|
|
35
|
+
protected
|
|
36
|
+
|
|
37
|
+
def expand_list
|
|
38
|
+
super.select { |path| path.split('.').all? { |segment| expansion_permitted?(segment) } }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def serializer_params
|
|
42
|
+
super.merge(gift_card_codes: holds_permission?('read_gift_cards'))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def expansion_permitted?(segment)
|
|
48
|
+
key = EXPANSION_PERMISSIONS[segment]
|
|
49
|
+
key.nil? || holds_permission?(key)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -23,13 +23,13 @@ module Spree
|
|
|
23
23
|
# flow leaves it off (its gate is `manage Invitation`).
|
|
24
24
|
# @return [Boolean] true if the request was rejected (caller should return)
|
|
25
25
|
def reject_unauthorized_role_grant!(role_ids, require_role_management: false)
|
|
26
|
-
return false if role_ids.blank?
|
|
27
|
-
|
|
28
26
|
# The management gate runs whenever a role mutation is attempted —
|
|
29
|
-
# before resolving ids
|
|
30
|
-
#
|
|
31
|
-
# past a caller
|
|
27
|
+
# before resolving ids, and even for an empty list — so passing
|
|
28
|
+
# unknown ids, or none at all, can't slip the reconciliation (which
|
|
29
|
+
# would still remove the user's current roles) past a caller
|
|
30
|
+
# without role-management authority.
|
|
32
31
|
return true if require_role_management && reject_without_role_management!
|
|
32
|
+
return false if role_ids.blank?
|
|
33
33
|
|
|
34
34
|
roles = current_store.roles.where(id: role_ids.map(&:to_s)).to_a
|
|
35
35
|
return false if roles.empty?
|
|
@@ -40,6 +40,30 @@ module Spree
|
|
|
40
40
|
false
|
|
41
41
|
end
|
|
42
42
|
|
|
43
|
+
# Removing a role is bounded like granting it: a caller may only take
|
|
44
|
+
# away authority they could hand out, so a staff member who can
|
|
45
|
+
# manage staff cannot strip the admin role from the store's owners.
|
|
46
|
+
# The store also keeps at least one admin, whoever asks.
|
|
47
|
+
#
|
|
48
|
+
# @param user [Object] whose roles are being removed
|
|
49
|
+
# @param roles [Array<Spree::Role>] the store roles being removed
|
|
50
|
+
# @return [Boolean] true if the request was rejected (caller should return)
|
|
51
|
+
def reject_unauthorized_role_removal!(user, roles)
|
|
52
|
+
return false if roles.empty?
|
|
53
|
+
|
|
54
|
+
if roles.any?(&:admin?)
|
|
55
|
+
return deny_role_grant!('You cannot remove the admin role.') unless caller_holds_admin_role?
|
|
56
|
+
return deny_role_grant!('The store must keep at least one admin.') if last_admin?(user)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
return false if caller_holds_admin_role?
|
|
60
|
+
|
|
61
|
+
beyond_own = roles.reject { |role| excess_permission_keys(role.permissions).empty? }
|
|
62
|
+
return false if beyond_own.empty?
|
|
63
|
+
|
|
64
|
+
deny_role_grant!("You cannot remove roles beyond your own privileges: #{beyond_own.map(&:name).join(', ')}")
|
|
65
|
+
end
|
|
66
|
+
|
|
43
67
|
# Rejects granting permission keys beyond the caller's own — used by
|
|
44
68
|
# roles#create/#update on the requested `permissions` array.
|
|
45
69
|
#
|
|
@@ -125,6 +149,11 @@ module Spree
|
|
|
125
149
|
end
|
|
126
150
|
end
|
|
127
151
|
|
|
152
|
+
def last_admin?(user)
|
|
153
|
+
admin_roles = current_store.roles.select(&:admin?)
|
|
154
|
+
Spree::RoleUser.where(role: admin_roles).where.not(user: user).none?
|
|
155
|
+
end
|
|
156
|
+
|
|
128
157
|
def caller_holds_admin_role?
|
|
129
158
|
return current_api_key.has_scope?('write_all') if scope_limited_principal?
|
|
130
159
|
|
|
@@ -13,12 +13,20 @@ module Spree
|
|
|
13
13
|
|
|
14
14
|
private
|
|
15
15
|
|
|
16
|
+
# Runs under the login lockout, so a stolen session cannot use this
|
|
17
|
+
# check to guess the password: misses count towards the lockout, and a
|
|
18
|
+
# locked account is refused even with the right password.
|
|
19
|
+
#
|
|
16
20
|
# @return [Boolean] whether `current_password` matches the signed-in
|
|
17
21
|
# account. False when the parameter is missing — an absent password
|
|
18
22
|
# is not a correct one.
|
|
19
23
|
def valid_current_password?
|
|
20
24
|
return false if params[:current_password].blank?
|
|
21
25
|
|
|
26
|
+
Spree::Authentication::Lockout.check(current_user) { current_password_matches? } == :valid
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def current_password_matches?
|
|
22
30
|
if current_user.respond_to?(:valid_password?)
|
|
23
31
|
current_user.valid_password?(params[:current_password])
|
|
24
32
|
elsif current_user.respond_to?(:authenticate)
|
|
@@ -135,9 +135,22 @@ module Spree
|
|
|
135
135
|
end
|
|
136
136
|
|
|
137
137
|
# The attributes handed to the update workflow. The workflow assigns
|
|
138
|
-
# ids raw, so
|
|
138
|
+
# ids raw, so the stock location is resolved first, through the same
|
|
139
|
+
# lookup create and split use.
|
|
139
140
|
def update_attributes
|
|
140
|
-
permitted_params.to_h
|
|
141
|
+
attributes = permitted_params.to_h
|
|
142
|
+
|
|
143
|
+
if attributes['stock_location_id'].present?
|
|
144
|
+
attributes['stock_location_id'] = find_stock_location!(attributes['stock_location_id']).id
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
attributes
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
# A stock location of the current store. A branch that must narrow it
|
|
151
|
+
# further (by ability, by seller) overrides this.
|
|
152
|
+
def find_stock_location!(id)
|
|
153
|
+
current_store.stock_locations.find_by_prefix_id!(id)
|
|
141
154
|
end
|
|
142
155
|
|
|
143
156
|
# What may be split out of this parcel, and where the split half
|
|
@@ -204,6 +204,20 @@ module Spree
|
|
|
204
204
|
def scope_limited_principal?
|
|
205
205
|
current_api_key.present? && current_user.blank?
|
|
206
206
|
end
|
|
207
|
+
|
|
208
|
+
# Whether the caller holds a catalog key beyond the one the action
|
|
209
|
+
# needs — for data another permission guards riding along in a
|
|
210
|
+
# response (an expanded association, an embedded record). Mirrors the
|
|
211
|
+
# key gate's two principals: a secret key carries scopes, a signed-in
|
|
212
|
+
# staffer carries their roles' catalog keys.
|
|
213
|
+
def holds_permission?(key)
|
|
214
|
+
return current_api_key.has_scope?(key) if scope_limited_principal?
|
|
215
|
+
|
|
216
|
+
ability = current_ability
|
|
217
|
+
return true unless ability.respond_to?(:permission_keys)
|
|
218
|
+
|
|
219
|
+
ability.permission_keys.include?(key.to_s)
|
|
220
|
+
end
|
|
207
221
|
end
|
|
208
222
|
end
|
|
209
223
|
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Store
|
|
5
|
+
# The products a storefront buyer may see: available now (or on
|
|
6
|
+
# pre-order) in the current currency, narrowed to the catalogs their
|
|
7
|
+
# company, customer group or channel resolve to. The product listing
|
|
8
|
+
# reads through it, and so does anything else that accepts a product
|
|
9
|
+
# or variant from the buyer, so an id cannot reach what the listing
|
|
10
|
+
# would never show.
|
|
11
|
+
module StorefrontProducts
|
|
12
|
+
extend ActiveSupport::Concern
|
|
13
|
+
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
# @param base [ActiveRecord::Relation] products to narrow; the store's by default
|
|
17
|
+
# @return [ActiveRecord::Relation<Spree::Product>]
|
|
18
|
+
def storefront_products(base = current_store.products)
|
|
19
|
+
Spree.products_for_context_service.call(
|
|
20
|
+
store: current_store,
|
|
21
|
+
channel: current_channel,
|
|
22
|
+
customer: current_user,
|
|
23
|
+
base: base.available(Time.current, Spree::Current.currency, include_preorderable: true)
|
|
24
|
+
).value
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# @return [ActiveRecord::Relation<Spree::Variant>]
|
|
28
|
+
def storefront_variants
|
|
29
|
+
Spree::Variant.where(product_id: storefront_products.reorder(nil).select(Spree::Product.arel_table[:id]))
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -24,6 +24,8 @@ module Spree
|
|
|
24
24
|
# the account globally. The user keeps access to any other stores.
|
|
25
25
|
def destroy
|
|
26
26
|
authorize!(:destroy, @resource)
|
|
27
|
+
return if reject_unauthorized_role_removal!(@resource, @resource.spree_roles.for_resource(current_store).to_a)
|
|
28
|
+
|
|
27
29
|
@resource.role_users.where(role: current_store.roles).destroy_all
|
|
28
30
|
head :no_content
|
|
29
31
|
end
|
|
@@ -42,6 +44,7 @@ module Spree
|
|
|
42
44
|
# RoleManagement permission set, not by profile-edit rights.
|
|
43
45
|
role_ids = role_ids_param if params.key?(:role_ids)
|
|
44
46
|
return if role_ids && reject_unauthorized_role_grant!(role_ids, require_role_management: true)
|
|
47
|
+
return if role_ids && reject_unauthorized_role_removal!(@resource, removed_roles(role_ids))
|
|
45
48
|
|
|
46
49
|
if @resource.update(identity_params)
|
|
47
50
|
apply_role_ids(role_ids) if role_ids
|
|
@@ -89,6 +92,12 @@ module Spree
|
|
|
89
92
|
ids.map { |id| Spree::PrefixedId.prefixed_id?(id) ? Spree::PrefixedId.decode_prefixed_id(id) : id }.compact
|
|
90
93
|
end
|
|
91
94
|
|
|
95
|
+
# The store roles the user holds that `desired_role_ids` leaves out.
|
|
96
|
+
def removed_roles(desired_role_ids)
|
|
97
|
+
target = desired_role_ids.map(&:to_s)
|
|
98
|
+
@resource.spree_roles.for_resource(current_store).reject { |role| target.include?(role.id.to_s) }
|
|
99
|
+
end
|
|
100
|
+
|
|
92
101
|
# Reconcile the user's roles on this store to match `desired_role_ids`.
|
|
93
102
|
# Adds missing assignments and removes extras — no-op for unchanged.
|
|
94
103
|
def apply_role_ids(desired_role_ids)
|
|
@@ -109,17 +109,22 @@ module Spree
|
|
|
109
109
|
end.merge('customer' => Spree.customer_class.name, 'category' => 'Spree::Category')
|
|
110
110
|
end
|
|
111
111
|
|
|
112
|
-
# Returns the first segment whose `<segment>_id` is
|
|
112
|
+
# Returns the first segment whose `<segment>_id` is in the route,
|
|
113
113
|
# paired with its class and the raw id value, or nil. Memoized — read
|
|
114
114
|
# by both `set_parent` and `scoped_resource_name`.
|
|
115
|
+
#
|
|
116
|
+
# Read from the path only: a `store_id` or `refund_id` added to the
|
|
117
|
+
# query string or body must not switch the parent to a type that is
|
|
118
|
+
# never routed here and has no store scope to narrow it.
|
|
115
119
|
def parent_lookup
|
|
116
120
|
return @parent_lookup if defined?(@parent_lookup)
|
|
117
121
|
|
|
118
|
-
|
|
122
|
+
route_params = request.path_parameters
|
|
123
|
+
match = parent_route_map.find { |segment, _| route_params[:"#{segment}_id"].present? }
|
|
119
124
|
@parent_lookup =
|
|
120
125
|
if match
|
|
121
126
|
segment, klass_name = match
|
|
122
|
-
ParentLookup.new(klass_name.constantize,
|
|
127
|
+
ParentLookup.new(klass_name.constantize, route_params[:"#{segment}_id"], segment)
|
|
123
128
|
end
|
|
124
129
|
end
|
|
125
130
|
end
|
|
@@ -223,20 +223,6 @@ module Spree
|
|
|
223
223
|
false
|
|
224
224
|
end
|
|
225
225
|
|
|
226
|
-
# Mirrors the key gate's two principals: a secret key carries scopes,
|
|
227
|
-
# a signed-in staffer carries their roles' catalog keys.
|
|
228
|
-
#
|
|
229
|
-
# @param key [String]
|
|
230
|
-
# @return [Boolean]
|
|
231
|
-
def holds_permission?(key)
|
|
232
|
-
return current_api_key.has_scope?(key) if scope_limited_principal?
|
|
233
|
-
|
|
234
|
-
ability = current_ability
|
|
235
|
-
return true unless ability.respond_to?(:permission_keys)
|
|
236
|
-
|
|
237
|
-
ability.permission_keys.include?(key)
|
|
238
|
-
end
|
|
239
|
-
|
|
240
226
|
# `export` maps to `:show`; `anonymize` is a destructive write, so it
|
|
241
227
|
# maps to `:destroy` rather than `:update` — a role that may correct
|
|
242
228
|
# a customer's name should not thereby be able to erase their history.
|
|
@@ -13,6 +13,8 @@ module Spree
|
|
|
13
13
|
class GiftCardsController < ResourceController
|
|
14
14
|
scoped_resource :gift_cards
|
|
15
15
|
|
|
16
|
+
before_action :resolve_customer, only: [:create, :update]
|
|
17
|
+
|
|
16
18
|
protected
|
|
17
19
|
|
|
18
20
|
def model_class
|
|
@@ -24,8 +26,33 @@ module Spree
|
|
|
24
26
|
end
|
|
25
27
|
|
|
26
28
|
def permitted_params
|
|
27
|
-
params.permit(*model_additional_permitted_attributes, :code, :amount, :expires_at,
|
|
28
|
-
|
|
29
|
+
params.permit(*model_additional_permitted_attributes, :code, :amount, :expires_at, :currency).tap do |attributes|
|
|
30
|
+
attributes[:customer] = @customer if params.key?(:customer_id)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
# Assigning a customer embeds their record in the response, so it is
|
|
37
|
+
# a read of customers: the caller must hold that permission, and the
|
|
38
|
+
# customer is resolved through their ability — never a raw id the
|
|
39
|
+
# model would look up across every customer.
|
|
40
|
+
def resolve_customer
|
|
41
|
+
return unless params.key?(:customer_id)
|
|
42
|
+
return @customer = nil if params[:customer_id].blank?
|
|
43
|
+
|
|
44
|
+
unless holds_permission?('read_customers')
|
|
45
|
+
return render_error(
|
|
46
|
+
code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:access_denied],
|
|
47
|
+
message: 'Missing permission: read_customers',
|
|
48
|
+
status: :forbidden,
|
|
49
|
+
details: { required_permission: 'read_customers' }
|
|
50
|
+
)
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
@customer = Spree.customer_class.
|
|
54
|
+
accessible_by(current_ability, :show).
|
|
55
|
+
find_by_prefix_id!(params[:customer_id])
|
|
29
56
|
end
|
|
30
57
|
end
|
|
31
58
|
end
|
|
@@ -72,7 +72,10 @@ module Spree
|
|
|
72
72
|
end
|
|
73
73
|
|
|
74
74
|
def authenticate_existing(user)
|
|
75
|
-
|
|
75
|
+
# Under the login lockout: otherwise this is an unthrottled way to
|
|
76
|
+
# guess an existing account's password with an invitation token.
|
|
77
|
+
verdict = Spree::Authentication::Lockout.check(user) { user.valid_password?(params[:password].to_s) }
|
|
78
|
+
return user if verdict == :valid
|
|
76
79
|
|
|
77
80
|
render_error(
|
|
78
81
|
code: ERROR_CODES[:authentication_failed],
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
module Invitations
|
|
6
|
+
# The acceptance link of a pending staff invitation.
|
|
7
|
+
#
|
|
8
|
+
# The link carries the token that creates the account and grants the
|
|
9
|
+
# role, so it is authorized like sending the invitation: the caller
|
|
10
|
+
# needs write access to staff and must be allowed to grant the
|
|
11
|
+
# invitation's role. Reading staff is not enough.
|
|
12
|
+
class AcceptanceLinksController < Admin::BaseController
|
|
13
|
+
include Spree::Api::V3::Admin::RoleGrantGuard
|
|
14
|
+
|
|
15
|
+
scoped_resource :staff
|
|
16
|
+
|
|
17
|
+
# GET /api/v3/admin/invitations/:invitation_id/acceptance_link
|
|
18
|
+
def show
|
|
19
|
+
invitation = Spree::Invitation.
|
|
20
|
+
where(resource: current_store).
|
|
21
|
+
pending.
|
|
22
|
+
find_by_prefix_id!(params[:invitation_id])
|
|
23
|
+
authorize!(:update, invitation)
|
|
24
|
+
return if reject_unauthorized_role_grant!([invitation.role_id])
|
|
25
|
+
|
|
26
|
+
render json: Spree.api.admin_invitation_acceptance_link_serializer.new(
|
|
27
|
+
invitation, params: { store: current_store }
|
|
28
|
+
).to_h
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
protected
|
|
32
|
+
|
|
33
|
+
def read_actions
|
|
34
|
+
[]
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -58,6 +58,17 @@ module Spree
|
|
|
58
58
|
)
|
|
59
59
|
end
|
|
60
60
|
|
|
61
|
+
# /me describes the signed-in user rather than a store, and the
|
|
62
|
+
# dashboard reads it before any store is selected. A user who holds
|
|
63
|
+
# no role on any store (not yet given one, or removed from all of
|
|
64
|
+
# them) must still reach it, or they cannot finish signing in. A
|
|
65
|
+
# request that names a store keeps the membership check.
|
|
66
|
+
def require_store_membership!
|
|
67
|
+
return true if store_id_header.nil?
|
|
68
|
+
|
|
69
|
+
super
|
|
70
|
+
end
|
|
71
|
+
|
|
61
72
|
def permitted_params
|
|
62
73
|
params.permit(:selected_locale, :first_name, :last_name, :avatar)
|
|
63
74
|
end
|
|
@@ -18,7 +18,7 @@ module Spree
|
|
|
18
18
|
#
|
|
19
19
|
# Editable fields only — status moves through the member actions.
|
|
20
20
|
def update
|
|
21
|
-
if @resource.update(
|
|
21
|
+
if @resource.update(update_attributes)
|
|
22
22
|
render json: serialize_resource(@resource.reload)
|
|
23
23
|
else
|
|
24
24
|
render_validation_error(@resource.errors)
|
|
@@ -41,6 +41,19 @@ module Spree
|
|
|
41
41
|
|
|
42
42
|
private
|
|
43
43
|
|
|
44
|
+
# The reason is resolved through the store rather than assigned
|
|
45
|
+
# raw, so another store's reads as missing.
|
|
46
|
+
def update_attributes
|
|
47
|
+
attributes = permitted_params.to_h.except('reason_id')
|
|
48
|
+
|
|
49
|
+
if params.key?(:reason_id)
|
|
50
|
+
attributes['reason'] =
|
|
51
|
+
(current_store.claim_reasons.find_by_prefix_id!(params[:reason_id]) if params[:reason_id].present?)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
attributes
|
|
55
|
+
end
|
|
56
|
+
|
|
44
57
|
# The whole store's catalogue: an operator may promise anything
|
|
45
58
|
# the store sells as a replacement.
|
|
46
59
|
def replacement_variant_for(variant_id)
|
|
@@ -19,7 +19,7 @@ module Spree
|
|
|
19
19
|
#
|
|
20
20
|
# Editable fields only — status moves through the member actions.
|
|
21
21
|
def update
|
|
22
|
-
if @resource.update(
|
|
22
|
+
if @resource.update(update_attributes)
|
|
23
23
|
render json: serialize_resource(@resource.reload)
|
|
24
24
|
else
|
|
25
25
|
render_validation_error(@resource.errors)
|
|
@@ -42,6 +42,27 @@ module Spree
|
|
|
42
42
|
|
|
43
43
|
private
|
|
44
44
|
|
|
45
|
+
# The ids are resolved through the store rather than assigned raw,
|
|
46
|
+
# so another store's reason or warehouse reads as missing.
|
|
47
|
+
def update_attributes
|
|
48
|
+
attributes = permitted_params.to_h.except('reason_id', 'stock_location_id')
|
|
49
|
+
|
|
50
|
+
if params.key?(:reason_id)
|
|
51
|
+
attributes['reason'] =
|
|
52
|
+
(current_store.return_reasons.find_by_prefix_id!(params[:reason_id]) if params[:reason_id].present?)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
if params.key?(:stock_location_id)
|
|
56
|
+
attributes['stock_location'] =
|
|
57
|
+
if params[:stock_location_id].present?
|
|
58
|
+
current_store.stock_locations.accessible_by(current_ability, :show).
|
|
59
|
+
find_by_prefix_id!(params[:stock_location_id])
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
attributes
|
|
64
|
+
end
|
|
65
|
+
|
|
45
66
|
# The whole store's catalogue, and any warehouse the caller may
|
|
46
67
|
# see: an operator exchanges into anything the store sells.
|
|
47
68
|
def replacement_variant_for(variant_id)
|
|
@@ -113,7 +113,7 @@ module Spree
|
|
|
113
113
|
end
|
|
114
114
|
|
|
115
115
|
def find_stock_location!(id)
|
|
116
|
-
|
|
116
|
+
current_store.stock_locations.accessible_by(current_ability, :show).find_by_prefix_id!(id)
|
|
117
117
|
end
|
|
118
118
|
|
|
119
119
|
def stock_location_for_create
|
|
@@ -25,7 +25,7 @@ module Spree
|
|
|
25
25
|
#
|
|
26
26
|
# Editable fields only — status moves through the member actions.
|
|
27
27
|
def update
|
|
28
|
-
if @resource.update(
|
|
28
|
+
if @resource.update(update_attributes)
|
|
29
29
|
render json: serialize_resource(@resource.reload)
|
|
30
30
|
else
|
|
31
31
|
render_validation_error(@resource.errors)
|
|
@@ -55,6 +55,27 @@ module Spree
|
|
|
55
55
|
|
|
56
56
|
private
|
|
57
57
|
|
|
58
|
+
# The ids are resolved through the store rather than assigned raw,
|
|
59
|
+
# so another store's reason or warehouse reads as missing.
|
|
60
|
+
def update_attributes
|
|
61
|
+
attributes = permitted_params.to_h.except('reason_id', 'stock_location_id')
|
|
62
|
+
|
|
63
|
+
if params.key?(:reason_id)
|
|
64
|
+
attributes['reason'] =
|
|
65
|
+
(current_store.return_reasons.find_by_prefix_id!(params[:reason_id]) if params[:reason_id].present?)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
if params.key?(:stock_location_id)
|
|
69
|
+
attributes['stock_location'] =
|
|
70
|
+
if params[:stock_location_id].present?
|
|
71
|
+
current_store.stock_locations.accessible_by(current_ability, :show).
|
|
72
|
+
find_by_prefix_id!(params[:stock_location_id])
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
attributes
|
|
77
|
+
end
|
|
78
|
+
|
|
58
79
|
# Any of the store's warehouses the caller may see.
|
|
59
80
|
def stock_location_for_create
|
|
60
81
|
return nil if create_params[:stock_location_id].blank?
|
|
@@ -52,6 +52,10 @@ module Spree
|
|
|
52
52
|
end
|
|
53
53
|
|
|
54
54
|
# PATCH /api/v3/admin/orders/:id/complete
|
|
55
|
+
#
|
|
56
|
+
# An order holding several sellers' goods divides into one order per
|
|
57
|
+
# seller, so the group is what comes back — the orders it produced
|
|
58
|
+
# are nested inside it.
|
|
55
59
|
def complete
|
|
56
60
|
with_order_lock do
|
|
57
61
|
result = Spree.order_complete_service.call(
|
|
@@ -61,7 +65,7 @@ module Spree
|
|
|
61
65
|
)
|
|
62
66
|
|
|
63
67
|
if result.success?
|
|
64
|
-
render json:
|
|
68
|
+
render json: serialize_completion(result.value)
|
|
65
69
|
else
|
|
66
70
|
render_service_error(@resource.errors.presence || result.error, code: ERROR_CODES[:order_cannot_complete])
|
|
67
71
|
end
|
|
@@ -149,6 +153,28 @@ module Spree
|
|
|
149
153
|
Spree.api.admin_order_serializer
|
|
150
154
|
end
|
|
151
155
|
|
|
156
|
+
# Completion answers with whatever it produced: one order, or the
|
|
157
|
+
# group a multi-seller order divided into.
|
|
158
|
+
#
|
|
159
|
+
# The group is rendered whole rather than through `filter_fields`: a
|
|
160
|
+
# `fields` list names an order's attributes, and narrowing a group by
|
|
161
|
+
# it can drop the very keys that say it is one.
|
|
162
|
+
def serialize_completion(record)
|
|
163
|
+
return serialize_resource(record.reload) unless record.is_a?(Spree::OrderGroup)
|
|
164
|
+
|
|
165
|
+
Spree.api.admin_order_group_serializer.new(completed_group(record), params: serializer_params).to_h
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# Every child is rendered in full, so its associations are loaded once
|
|
169
|
+
# here rather than per order.
|
|
170
|
+
def completed_group(group)
|
|
171
|
+
Spree::OrderGroup.includes(
|
|
172
|
+
:customer, :ship_address, :bill_address,
|
|
173
|
+
{ payments: %i[payment_method source] },
|
|
174
|
+
{ orders: [:seller, :market, :gift_card, :customer, { line_items: { variant: :prices } }] }
|
|
175
|
+
).find(group.id)
|
|
176
|
+
end
|
|
177
|
+
|
|
152
178
|
# Override scope — Order uses SingleStoreResource (for_store).
|
|
153
179
|
# Variant prices and each line's price list are named here rather
|
|
154
180
|
# than via scope_includes, which this override bypasses; the
|
|
@@ -17,20 +17,10 @@ module Spree
|
|
|
17
17
|
|
|
18
18
|
# POST /api/v3/admin/password_resets
|
|
19
19
|
def create
|
|
20
|
-
redirect_url = params[:redirect_url]
|
|
21
|
-
|
|
22
|
-
# Validate redirect_url against allowed origins (secure by default).
|
|
23
|
-
# If no allowed origins are configured, redirect_url is silently ignored
|
|
24
|
-
# to prevent open redirect / token exfiltration attacks.
|
|
25
|
-
if redirect_url.present?
|
|
26
|
-
unless current_store.allowed_origins.exists? && current_store.allowed_origin?(redirect_url)
|
|
27
|
-
redirect_url = nil
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
20
|
user = Spree.admin_user_class.find_by(email: params[:email])
|
|
32
21
|
|
|
33
22
|
if user
|
|
23
|
+
redirect_url = trusted_redirect_url(user)
|
|
34
24
|
token = user.generate_token_for(:password_reset)
|
|
35
25
|
event_payload = { reset_token: token, email: user.email, store_id: current_store.prefixed_id }
|
|
36
26
|
event_payload[:redirect_url] = redirect_url if redirect_url.present?
|
|
@@ -71,6 +61,36 @@ module Spree
|
|
|
71
61
|
|
|
72
62
|
private
|
|
73
63
|
|
|
64
|
+
# The emailed link carries the reset token, so it may only point at
|
|
65
|
+
# the dashboard — never at a storefront origin, which anyone who can
|
|
66
|
+
# edit a store's allowed origins could add — and only when the
|
|
67
|
+
# account belongs to the store the request names. Anything else is
|
|
68
|
+
# ignored and the email links to the dashboard's own reset page.
|
|
69
|
+
#
|
|
70
|
+
# @return [String, nil]
|
|
71
|
+
def trusted_redirect_url(user)
|
|
72
|
+
redirect_url = params[:redirect_url].presence
|
|
73
|
+
return if redirect_url.blank?
|
|
74
|
+
return unless same_origin?(redirect_url, Spree::Stores::DashboardUrl.call(store: current_store))
|
|
75
|
+
return unless staff_of_current_store?(user)
|
|
76
|
+
|
|
77
|
+
redirect_url
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def same_origin?(url, other)
|
|
81
|
+
first = URI.parse(url.to_s)
|
|
82
|
+
second = URI.parse(other.to_s)
|
|
83
|
+
first.host.present? && [first.scheme, first.host, first.port] == [second.scheme, second.host, second.port]
|
|
84
|
+
rescue URI::InvalidURIError
|
|
85
|
+
false
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def staff_of_current_store?(user)
|
|
89
|
+
return false unless user.respond_to?(:spree_roles)
|
|
90
|
+
|
|
91
|
+
user.spree_roles.for_resource(current_store).exists?
|
|
92
|
+
end
|
|
93
|
+
|
|
74
94
|
def auth_response(user)
|
|
75
95
|
{
|
|
76
96
|
token: generate_jwt(user, audience: JWT_AUDIENCE_ADMIN),
|