spree_api 6.0.0.beta1 → 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.
Files changed (93) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/spree/api/v3/admin/embed_permissions.rb +55 -0
  3. data/app/controllers/concerns/spree/api/v3/admin/role_grant_guard.rb +34 -5
  4. data/app/controllers/concerns/spree/api/v3/admin/stock_receipt_actions.rb +1 -1
  5. data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +11 -0
  6. data/app/controllers/concerns/spree/api/v3/current_password_confirmation.rb +8 -0
  7. data/app/controllers/concerns/spree/api/v3/orders/claim_actions.rb +2 -2
  8. data/app/controllers/concerns/spree/api/v3/orders/exchange_actions.rb +3 -3
  9. data/app/controllers/concerns/spree/api/v3/orders/fulfillment_actions.rb +15 -2
  10. data/app/controllers/concerns/spree/api/v3/orders/post_sale_actions.rb +1 -1
  11. data/app/controllers/concerns/spree/api/v3/orders/return_actions.rb +3 -3
  12. data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +14 -0
  13. data/app/controllers/concerns/spree/api/v3/store/storefront_products.rb +35 -0
  14. data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +9 -0
  15. data/app/controllers/spree/api/v3/admin/api_keys_controller.rb +2 -2
  16. data/app/controllers/spree/api/v3/admin/base_controller.rb +1 -0
  17. data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +8 -3
  18. data/app/controllers/spree/api/v3/admin/customers_controller.rb +0 -14
  19. data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +29 -2
  20. data/app/controllers/spree/api/v3/admin/invitation_acceptances_controller.rb +4 -1
  21. data/app/controllers/spree/api/v3/admin/invitations/acceptance_links_controller.rb +41 -0
  22. data/app/controllers/spree/api/v3/admin/me_controller.rb +11 -0
  23. data/app/controllers/spree/api/v3/admin/orders/claims_controller.rb +14 -1
  24. data/app/controllers/spree/api/v3/admin/orders/exchanges_controller.rb +22 -1
  25. data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +1 -1
  26. data/app/controllers/spree/api/v3/admin/orders/refunds_controller.rb +1 -1
  27. data/app/controllers/spree/api/v3/admin/orders/returns_controller.rb +22 -1
  28. data/app/controllers/spree/api/v3/admin/orders_controller.rb +41 -9
  29. data/app/controllers/spree/api/v3/admin/password_resets_controller.rb +31 -11
  30. data/app/controllers/spree/api/v3/admin/price_lists_controller.rb +1 -19
  31. data/app/controllers/spree/api/v3/admin/prices_controller.rb +33 -2
  32. data/app/controllers/spree/api/v3/admin/products_controller.rb +1 -1
  33. data/app/controllers/spree/api/v3/admin/resource_controller.rb +1 -0
  34. data/app/controllers/spree/api/v3/admin/sellers/invitations/acceptance_links_controller.rb +37 -0
  35. data/app/controllers/spree/api/v3/admin/setup_controller.rb +9 -1
  36. data/app/controllers/spree/api/v3/admin/store_controller.rb +5 -0
  37. data/app/controllers/spree/api/v3/admin/translations/batches_controller.rb +1 -1
  38. data/app/controllers/spree/api/v3/admin/translations_controller.rb +1 -1
  39. data/app/controllers/spree/api/v3/admin/webhook_deliveries_controller.rb +12 -0
  40. data/app/controllers/spree/api/v3/admin/webhook_endpoints_controller.rb +27 -0
  41. data/app/controllers/spree/api/v3/base_controller.rb +22 -3
  42. data/app/controllers/spree/api/v3/resource_controller.rb +27 -2
  43. data/app/controllers/spree/api/v3/seller/auth_controller.rb +1 -1
  44. data/app/controllers/spree/api/v3/seller/invitation_acceptances_controller.rb +5 -2
  45. data/app/controllers/spree/api/v3/seller/invitations/acceptance_links_controller.rb +34 -0
  46. data/app/controllers/spree/api/v3/seller/me_controller.rb +25 -2
  47. data/app/controllers/spree/api/v3/seller/orders_controller.rb +20 -25
  48. data/app/controllers/spree/api/v3/seller/password_resets_controller.rb +1 -1
  49. data/app/controllers/spree/api/v3/seller/resource_controller.rb +4 -0
  50. data/app/controllers/spree/api/v3/store/carts/items_controller.rb +7 -4
  51. data/app/controllers/spree/api/v3/store/carts/payment_sessions_controller.rb +10 -1
  52. data/app/controllers/spree/api/v3/store/carts/payments_controller.rb +5 -1
  53. data/app/controllers/spree/api/v3/store/companies/members_controller.rb +10 -11
  54. data/app/controllers/spree/api/v3/store/companies/orders_controller.rb +6 -0
  55. data/app/controllers/spree/api/v3/store/products/filters_controller.rb +1 -0
  56. data/app/controllers/spree/api/v3/store/products_controller.rb +6 -12
  57. data/app/controllers/spree/api/v3/store/resource_controller.rb +4 -0
  58. data/app/controllers/spree/api/v3/store/wishlist_items_controller.rb +11 -0
  59. data/app/controllers/spree/api/v3/webhooks/fulfillments_controller.rb +10 -6
  60. data/app/controllers/spree/api/v3/webhooks/payments_controller.rb +6 -2
  61. data/app/controllers/spree/api/v3/webhooks/payouts_controller.rb +6 -2
  62. data/app/jobs/spree/webhook_delivery_job.rb +18 -4
  63. data/app/serializers/spree/api/v3/admin/actor_serializer.rb +28 -0
  64. data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +14 -1
  65. data/app/serializers/spree/api/v3/admin/claim_serializer.rb +3 -4
  66. data/app/serializers/spree/api/v3/admin/exchange_serializer.rb +3 -4
  67. data/app/serializers/spree/api/v3/admin/gift_card_serializer.rb +6 -0
  68. data/app/serializers/spree/api/v3/admin/invitation_acceptance_link_serializer.rb +15 -0
  69. data/app/serializers/spree/api/v3/admin/invitation_serializer.rb +0 -11
  70. data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +28 -1
  71. data/app/serializers/spree/api/v3/admin/order_group_serializer.rb +7 -0
  72. data/app/serializers/spree/api/v3/admin/order_serializer.rb +7 -24
  73. data/app/serializers/spree/api/v3/admin/refund_serializer.rb +6 -0
  74. data/app/serializers/spree/api/v3/admin/return_serializer.rb +2 -3
  75. data/app/serializers/spree/api/v3/admin/saved_report_serializer.rb +3 -4
  76. data/app/serializers/spree/api/v3/admin/stock_receipt_serializer.rb +2 -3
  77. data/app/serializers/spree/api/v3/admin/store_serializer.rb +10 -0
  78. data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +6 -2
  79. data/app/serializers/spree/api/v3/base_serializer.rb +22 -0
  80. data/app/serializers/spree/api/v3/gift_card_serializer.rb +3 -1
  81. data/app/serializers/spree/api/v3/line_item_serializer.rb +2 -1
  82. data/app/serializers/spree/api/v3/market_serializer.rb +5 -0
  83. data/app/serializers/spree/api/v3/product_filters_serializer.rb +3 -0
  84. data/app/serializers/spree/api/v3/seller/account_serializer.rb +19 -0
  85. data/app/serializers/spree/api/v3/seller/invitation_acceptance_link_serializer.rb +15 -0
  86. data/app/serializers/spree/api/v3/seller/invitation_serializer.rb +1 -10
  87. data/app/services/spree/api/v3/filters_aggregator.rb +5 -2
  88. data/config/routes.rb +7 -0
  89. data/lib/spree/api/configuration.rb +19 -14
  90. data/lib/spree/api/dependencies.rb +4 -0
  91. data/lib/spree/api/engine.rb +6 -0
  92. data/lib/spree/api/openapi/schema_helper.rb +10 -4
  93. metadata +15 -6
@@ -26,7 +26,7 @@ module Spree
26
26
  payment: payment,
27
27
  amount: params[:amount],
28
28
  reason: reason,
29
- refunder: try_spree_current_user,
29
+ refunder: current_actor,
30
30
  # Names which order is being refunded. Only matters when the
31
31
  # payment is shared by a split checkout, where it covers several
32
32
  # and the payment cannot say which one this is for.
@@ -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(permitted_params)
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?
@@ -23,7 +23,7 @@ module Spree
23
23
  result = Spree.order_create_service.call(
24
24
  store: current_store,
25
25
  customer: resolve_customer,
26
- created_by: try_spree_current_user,
26
+ created_by: current_actor,
27
27
  params: order_create_params
28
28
  )
29
29
 
@@ -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: serialize_resource(@resource.reload)
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
@@ -73,7 +77,7 @@ module Spree
73
77
  with_order_lock do
74
78
  result = Spree.order_cancel_workflow.call(
75
79
  order: @resource,
76
- canceler: try_spree_current_user,
80
+ canceler: current_actor,
77
81
  reason: cancel_reason,
78
82
  note: params[:cancel_note].presence,
79
83
  refund_payments: params[:refund_payments].to_b,
@@ -92,7 +96,7 @@ module Spree
92
96
  # PATCH /api/v3/admin/orders/:id/approve
93
97
  def approve
94
98
  with_order_lock do
95
- @resource.approved_by(try_spree_current_user)
99
+ @resource.approved_by(current_actor)
96
100
  render json: serialize_resource(@resource.reload)
97
101
  end
98
102
  end
@@ -149,12 +153,36 @@ 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
- # Variant prices are preloaded here rather than via scope_includes,
154
- # which this override bypasses; the serializer reads them per row.
179
+ # Variant prices and each line's price list are named here rather
180
+ # than via scope_includes, which this override bypasses; the
181
+ # serializer reads both per row.
155
182
  def scope
156
183
  base = current_store.orders.accessible_by(current_ability, :show).
157
- includes(line_items: { variant: :prices }).preload_associations_lazily
184
+ includes(line_items: [{ variant: :prices }, { price_list: :catalog }]).
185
+ preload_associations_lazily
158
186
 
159
187
  # Transient completion drafts (status draft + cart_id set) belong
160
188
  # to in-flight checkouts, never to the admin. Admin drafts are the
@@ -190,7 +218,10 @@ module Spree
190
218
  # only its id is reported and that comes off the order's own column.
191
219
  # Variant prices ride along because the admin line-item serializer
192
220
  # reads the base catalog price for every row (the negotiated-price
193
- # comparison); without it each line costs its own price query.
221
+ # comparison); without it each line costs its own price query. Each
222
+ # line's price list and its catalog are named for the same reason —
223
+ # stated rather than left to ar_lazy_preload, which happens to cover
224
+ # them today.
194
225
  def collection_includes
195
226
  # `market` is the withdrawal deadline's other input. Fulfillments
196
227
  # are loaded with their selected rate because the freight summary
@@ -198,7 +229,8 @@ module Spree
198
229
  # fulfillment for a field that is nil on every parcel order.
199
230
  [:customer, :channel, :seller, :external_references, :cancel_reason,
200
231
  :market, { fulfillments: :selected_delivery_rate },
201
- { line_items: { variant: :prices } }, { po_document_attachment: :blob }]
232
+ { line_items: [{ variant: :prices }, { price_list: :catalog }] },
233
+ { po_document_attachment: :blob }]
202
234
  end
203
235
 
204
236
  # Read through the store's own vocabulary, so a reason belonging to
@@ -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),
@@ -122,7 +122,7 @@ module Spree
122
122
  end
123
123
 
124
124
  def permitted_params
125
- attrs = normalize_params(
125
+ normalize_params(
126
126
  params.permit(
127
127
  :name, :description, :position,
128
128
  :starts_at, :ends_at, :match_policy,
@@ -132,24 +132,6 @@ module Spree
132
132
  prices: [:id, :variant_id, :currency, :min_quantity, :amount, :compare_at_amount]
133
133
  )
134
134
  )
135
- reject_foreign_variants(attrs)
136
- end
137
-
138
- # The price rows resolve variant ids with no store scoping, so a
139
- # list in this store could otherwise be populated with another
140
- # store's variants. Drop any id that isn't in the current store
141
- # before assignment.
142
- def reject_foreign_variants(attrs)
143
- if attrs[:prices].present?
144
- incoming = Array(attrs[:prices])
145
- store_variant_ids = current_store.variants.where(id: incoming.map { |r| r[:variant_id] }.compact).
146
- pluck(:id).map(&:to_s).to_set
147
- attrs[:prices] = incoming.select do |row|
148
- row[:variant_id].blank? || store_variant_ids.include?(row[:variant_id].to_s)
149
- end
150
- end
151
-
152
- attrs
153
135
  end
154
136
 
155
137
  private
@@ -58,6 +58,10 @@ module Spree
58
58
  # validations (docs/plans/6.0-volume-pricing.md). Each branch is
59
59
  # guarded on the key its own failure carries, so a failure mode the
60
60
  # service grows later is not reported under one of these names.
61
+ #
62
+ # `I18n.t` rather than `Spree.t` for these: the models raise them
63
+ # under `activerecord.errors`, and `Spree.t` would scope the lookup
64
+ # to `spree.` and miss the key.
61
65
  if (invalid = result.error&.value.try(:[], :invalid_quantities))
62
66
  return render_error(
63
67
  code: 'invalid_min_quantity',
@@ -70,16 +74,34 @@ module Spree
70
74
  if (negative = result.error&.value.try(:[], :invalid_amounts))
71
75
  return render_error(
72
76
  code: 'invalid_amount',
73
- message: Spree.t('activerecord.errors.models.spree/price_list.attributes.base.negative_price').upcase_first,
77
+ message: I18n.t('activerecord.errors.models.spree/price_list.attributes.base.negative_price').upcase_first,
74
78
  status: :unprocessable_content,
75
79
  details: { rows: negative }
76
80
  )
77
81
  end
78
82
 
83
+ if (misplaced = result.error&.value.try(:[], :quantities_on_base_prices))
84
+ return render_error(
85
+ code: 'quantity_on_base_price',
86
+ message: I18n.t('activerecord.errors.models.spree/price_list.attributes.base.quantity_on_base_price').upcase_first,
87
+ status: :unprocessable_content,
88
+ details: { rows: misplaced }
89
+ )
90
+ end
91
+
92
+ if (duplicates = result.error&.value.try(:[], :duplicate_quantities))
93
+ return render_error(
94
+ code: 'duplicate_min_quantity',
95
+ message: I18n.t('activerecord.errors.models.spree/price_list.attributes.base.duplicate_quantity').upcase_first,
96
+ status: :unprocessable_content,
97
+ details: { rows: duplicates }
98
+ )
99
+ end
100
+
79
101
  if (over_cap = result.error&.value.try(:[], :over_cap))
80
102
  return render_error(
81
103
  code: 'too_many_breaks',
82
- message: Spree.t(
104
+ message: I18n.t(
83
105
  'activerecord.errors.models.spree/price.attributes.min_quantity.too_many_breaks',
84
106
  count: Spree::Price::MAXIMUM_BREAKS_PER_VARIANT
85
107
  ).upcase_first,
@@ -88,6 +110,15 @@ module Spree
88
110
  )
89
111
  end
90
112
 
113
+ if (rising = result.error&.value.try(:[], :rising_ladders))
114
+ return render_error(
115
+ code: 'price_rises_with_quantity',
116
+ message: I18n.t('activerecord.errors.models.spree/price_list.attributes.base.price_rises_with_quantity').upcase_first,
117
+ status: :unprocessable_content,
118
+ details: { ladders: rising }
119
+ )
120
+ end
121
+
91
122
  render json: result.value
92
123
  end
93
124
 
@@ -301,7 +301,7 @@ module Spree
301
301
  end
302
302
 
303
303
  def search_provider
304
- @search_provider ||= Spree::SearchProvider::Database.new(current_store)
304
+ @search_provider ||= Spree::SearchProvider::Database.new(current_store, audience: nil)
305
305
  end
306
306
 
307
307
  # Tag changes can flip automatic-collection matches, and `Tags::Bulk*`
@@ -17,6 +17,7 @@ module Spree
17
17
  # include.
18
18
  include Spree::Api::V3::Admin::Concerns::ExternalReferences
19
19
  include Spree::Api::V3::Admin::ValidationDetails
20
+ include Spree::Api::V3::Admin::EmbedPermissions
20
21
 
21
22
  protected
22
23
 
@@ -0,0 +1,37 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ module Sellers
6
+ module Invitations
7
+ # The acceptance link of a pending invitation onto a seller's team.
8
+ #
9
+ # The link carries the token that joins the team, so reading
10
+ # sellers is not enough: the caller must be able to change the
11
+ # seller, as when sending the invitation.
12
+ class AcceptanceLinksController < Admin::BaseController
13
+ scoped_resource :sellers
14
+
15
+ # GET /api/v3/admin/sellers/:seller_id/invitations/:invitation_id/acceptance_link
16
+ def show
17
+ seller = current_store.sellers.find_by_prefix_id!(params[:seller_id])
18
+ authorize!(:update, seller)
19
+ invitation = seller.invitations.pending.find_by_prefix_id!(params[:invitation_id])
20
+
21
+ render json: Spree.api.admin_invitation_acceptance_link_serializer.new(
22
+ invitation, params: { store: current_store }
23
+ ).to_h
24
+ end
25
+
26
+ protected
27
+
28
+ def read_actions
29
+ []
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
37
+ end
@@ -41,7 +41,7 @@ module Spree
41
41
  # POST /api/v3/admin/auth/setup
42
42
  # Body: { setup_token, email, password, password_confirmation?,
43
43
  # first_name?, last_name?, store_name, country_code,
44
- # locale?, currency? }
44
+ # locale?, currency?, sample_data? }
45
45
  # Currency defaults to the country's own; an unknown code is refused
46
46
  # rather than ignored, since the token is spent in the same request.
47
47
  def create
@@ -63,6 +63,10 @@ module Spree
63
63
  adopt_default_store(user, store)
64
64
  end
65
65
 
66
+ # Outside the lock: the loader needs the admin committed, and it
67
+ # runs for minutes, so it is queued rather than awaited.
68
+ Spree::SampleData::LoadJob.perform_later(store.id) if sample_data_requested?
69
+
66
70
  refresh_token = Spree::RefreshToken.create_for(user, audience: JWT_AUDIENCE_ADMIN, request_env: request_env_for_token)
67
71
  set_refresh_cookie(refresh_token)
68
72
  render json: auth_response(user)
@@ -167,6 +171,10 @@ module Spree
167
171
  )
168
172
  end
169
173
 
174
+ def sample_data_requested?
175
+ ActiveModel::Type::Boolean.new.cast(params[:sample_data]) == true
176
+ end
177
+
170
178
  def admin_user_params
171
179
  params.permit(:email, :password, :password_confirmation, :first_name, :last_name)
172
180
  end
@@ -81,6 +81,7 @@ module Spree
81
81
  :preferred_storefront_access,
82
82
  :preferred_storefront_url,
83
83
  :preferred_guest_checkout,
84
+ :preferred_always_include_confirm_step,
84
85
  :preferred_company_field_enabled,
85
86
  :preferred_address_requires_company,
86
87
  :preferred_address_requires_phone,
@@ -104,6 +105,10 @@ module Spree
104
105
  :preferred_payout_provider,
105
106
  :preferred_default_payouts_schedule_interval,
106
107
  :preferred_default_minimum_payout_amount,
108
+ :preferred_auto_approve_sellers,
109
+ :preferred_auto_approve_seller_products,
110
+ :preferred_send_seller_transactional_emails,
111
+ :preferred_default_commission_tax_rate,
107
112
  :preferred_document_number_format,
108
113
  :preferred_order_number_prefix,
109
114
  :preferred_order_number_suffix,
@@ -39,7 +39,7 @@ module Spree
39
39
  raw = params[:translations]
40
40
  return render_empty_batch_error unless raw.is_a?(Array) && raw.any?
41
41
 
42
- batch = Spree::Translations::Batch.new(batch_params)
42
+ batch = Spree::Translations::Batch.new(batch_params, store: current_store)
43
43
  return unless require_batch_scopes!(batch)
44
44
 
45
45
  records = batch.process! { |record| authorize!(:update, record) }
@@ -43,7 +43,7 @@ module Spree
43
43
  # translation tables.
44
44
  def parent_relation
45
45
  klass = parent_lookup.klass
46
- relation = klass.respond_to?(:for_store) ? klass.for_store(current_store) : klass
46
+ relation = klass.respond_to?(:translatable_scope) ? klass.translatable_scope(current_store) : klass.for_store(current_store)
47
47
 
48
48
  children = klass.try(:translatable_children)
49
49
  children ? relation.includes(children => :translations) : relation
@@ -42,6 +42,18 @@ module Spree
42
42
  params[:webhook_endpoint_id]
43
43
  )
44
44
  end
45
+
46
+ # The payload is the record the event is about, so it is shown only
47
+ # to a caller who could read that record; the webhooks permission
48
+ # alone sees the delivery's status and response.
49
+ def serializer_params
50
+ super.merge(
51
+ payload_visible: lambda { |delivery|
52
+ key = delivery.payload_permission_key
53
+ key.nil? || holds_permission?(key)
54
+ }
55
+ )
56
+ end
45
57
  end
46
58
  end
47
59
  end
@@ -8,6 +8,8 @@ module Spree
8
8
  class WebhookEndpointsController < ResourceController
9
9
  scoped_resource :webhooks
10
10
 
11
+ before_action :reject_unauthorized_credential_subscription!, only: [:create, :update]
12
+
11
13
  # POST /api/v3/admin/webhook_endpoints/:id/send_test
12
14
  #
13
15
  # Fires a synthetic `webhook.test` delivery so admins can verify the
@@ -68,6 +70,31 @@ module Spree
68
70
  def permitted_params
69
71
  params.permit(*model_additional_permitted_attributes, :name, :url, :active, subscriptions: [])
70
72
  end
73
+
74
+ private
75
+
76
+ # Customer password reset tokens are account credentials, so the
77
+ # webhooks permission alone cannot subscribe an endpoint to them, or
78
+ # repoint an endpoint that already receives them.
79
+ def reject_unauthorized_credential_subscription!
80
+ return if holds_permission?('write_customers')
81
+
82
+ requested = Array(permitted_params[:subscriptions]) & Spree::WebhookEndpoint::CREDENTIAL_EVENTS
83
+ return if requested.empty? && !repoints_credential_endpoint?
84
+
85
+ render_error(
86
+ code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:access_denied],
87
+ message: "Receiving #{Spree::WebhookEndpoint::CREDENTIAL_EVENTS.to_sentence} requires permission to manage customers",
88
+ status: :forbidden
89
+ )
90
+ end
91
+
92
+ def repoints_credential_endpoint?
93
+ return false unless action_name == 'update'
94
+ return false unless permitted_params.key?(:url) || permitted_params.key?(:subscriptions)
95
+
96
+ current_store.webhook_endpoints.find_by_prefix_id(params[:id])&.receives_credentials?
97
+ end
71
98
  end
72
99
  end
73
100
  end
@@ -81,7 +81,10 @@ module Spree
81
81
  if errors.is_a?(ActiveModel::Errors)
82
82
  render_validation_error(errors)
83
83
  else
84
- render_service_error(error)
84
+ # The unwrapped value, so a workflow's rejection symbol reaches
85
+ # the branch that translates it rather than the one that prints
86
+ # the wrapper.
87
+ render_service_error(errors)
85
88
  end
86
89
  end
87
90
 
@@ -93,10 +96,26 @@ module Spree
93
96
 
94
97
  alias try_spree_current_user spree_current_user
95
98
 
96
- # CanCanCan ability
99
+ # Who performed this request, for the associations that record it —
100
+ # `canceler`, `approver`, `created_by` and their siblings. Answers the
101
+ # signed-in user here; the Admin branch prefers the authenticating
102
+ # secret key, because a key-made write must record the key rather than
103
+ # nobody (see docs/plans/6.0-action-actors.md).
104
+ #
105
+ # Deliberately not `current_api_key || current_user` at this level: on
106
+ # a Store API request the key is the storefront's publishable one, and
107
+ # a customer's own action is not the storefront's.
108
+ #
109
+ # @return [Object, nil] a member of Spree.actor_classes
110
+ def current_actor
111
+ try_spree_current_user
112
+ end
113
+
114
+ # CanCanCan ability, built from the swappable
115
+ # `Spree::Dependencies.ability_class` (Spree::Ability by default).
97
116
  # @return [Spree::Ability]
98
117
  def current_ability
99
- @current_ability ||= Spree::Ability.new(current_user, ability_options)
118
+ @current_ability ||= Spree.ability_class.new(current_user, ability_options)
100
119
  end
101
120
 
102
121
  # Options passed to the CanCanCan ability
@@ -176,10 +176,25 @@ module Spree
176
176
  def build_resource
177
177
  resource = resource_scope.new
178
178
  resource.assign_attributes(permitted_params) if create_workflow.nil?
179
- resource.created_by = try_spree_current_user if resource.respond_to?(:created_by_id)
179
+ resource.created_by = creator_for(resource) if resource.respond_to?(:created_by_id)
180
180
  resource
181
181
  end
182
182
 
183
+ # The actor to stamp on a new record, or nil when this model cannot
184
+ # hold it. `created_by` is polymorphic on the order operations and
185
+ # still admin-user-only elsewhere until 6.1, so a key-authenticated
186
+ # create of a gift card records nobody rather than raising — the same
187
+ # thing it recorded before keys became actors.
188
+ def creator_for(resource)
189
+ actor = current_actor
190
+ return actor if actor.nil?
191
+
192
+ association = resource.class.reflect_on_association(:created_by)
193
+ return actor if association.nil? || association.polymorphic?
194
+
195
+ actor.is_a?(association.klass) ? actor : nil
196
+ end
197
+
183
198
  # The relation a new record is built on: a nested resource's parent
184
199
  # association, the store's own association for this model, or the
185
200
  # class itself for genuinely global data (countries, roles).
@@ -239,7 +254,7 @@ module Spree
239
254
 
240
255
  @search = scope.includes(collection_includes).
241
256
  preload_associations_lazily.
242
- ransack(ransack_params)
257
+ ransack(ransack_params, auth_object: ransack_auth_object)
243
258
  result = @search.result(distinct: collection_distinct?)
244
259
  pagy_options = { limit: limit, page: page }
245
260
  result = apply_collection_sort(result)
@@ -247,6 +262,16 @@ module Spree
247
262
  @collection
248
263
  end
249
264
 
265
+ # Who is filtering, as Ransack's auth object. Nil is the back office,
266
+ # which may use every allowlisted attribute; the Store and Seller
267
+ # branches name themselves so the models narrow what they may reach
268
+ # (see Spree::RansackableAttributes). Covers sorting too.
269
+ #
270
+ # @return [Symbol, nil]
271
+ def ransack_auth_object
272
+ nil
273
+ end
274
+
250
275
  # Override in subclass to disable distinct (e.g., for custom sorting with computed columns)
251
276
  # @return [Boolean] whether to apply distinct to the collection
252
277
  def collection_distinct?
@@ -136,7 +136,7 @@ module Spree
136
136
  def auth_response(user)
137
137
  {
138
138
  token: generate_jwt(user, audience: Spree::Api::V3::JwtAuthentication::JWT_AUDIENCE_SELLER),
139
- user: Spree.api.seller_team_member_serializer.new(user, params: { store: user.sellers.first&.store }).to_h,
139
+ user: Spree.api.seller_account_serializer.new(user, params: { store: user.sellers.first&.store }).to_h,
140
140
  sellers: serialized_sellers(user)
141
141
  }
142
142
  end
@@ -94,7 +94,10 @@ module Spree
94
94
  end
95
95
 
96
96
  def authenticate_existing(user)
97
- return user if user.valid_password?(params[:password].to_s)
97
+ # Under the login lockout: otherwise this is an unthrottled way to
98
+ # guess an existing account's password with an invitation token.
99
+ verdict = Spree::Authentication::Lockout.check(user) { user.valid_password?(params[:password].to_s) }
100
+ return user if verdict == :valid
98
101
 
99
102
  render_error(
100
103
  code: ErrorHandler::ERROR_CODES[:authentication_failed],
@@ -127,7 +130,7 @@ module Spree
127
130
  def auth_response(user)
128
131
  {
129
132
  token: generate_jwt(user, audience: Spree::Api::V3::JwtAuthentication::JWT_AUDIENCE_SELLER),
130
- user: Spree.api.seller_team_member_serializer.new(
133
+ user: Spree.api.seller_account_serializer.new(
131
134
  user, params: { store: @invitation.store }
132
135
  ).to_h,
133
136
  sellers: serialized_sellers(user)
@@ -0,0 +1,34 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Seller
5
+ module Invitations
6
+ # The acceptance link of a pending invitation onto this seller's team.
7
+ #
8
+ # The link carries the token that joins the team, so it is authorized
9
+ # like inviting someone: write access to the seller profile. Rooted at
10
+ # `current_seller.invitations`, so another seller's invitation reads
11
+ # as missing.
12
+ class AcceptanceLinksController < Seller::BaseController
13
+ scoped_resource :seller_profile
14
+
15
+ # GET /api/v3/seller/invitations/:invitation_id/acceptance_link
16
+ def show
17
+ invitation = current_seller.invitations.pending.find_by_prefix_id!(params[:invitation_id])
18
+
19
+ render json: Spree.api.seller_invitation_acceptance_link_serializer.new(
20
+ invitation, params: { store: current_store }
21
+ ).to_h
22
+ end
23
+
24
+ protected
25
+
26
+ def read_actions
27
+ []
28
+ end
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
34
+ end