spree_api 6.0.0.rc4 → 6.0.0.rc6

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 (167) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/spree/api/v3/admin/auth_cookies.rb +23 -0
  3. data/app/controllers/concerns/spree/api/v3/admin/refund_authorization.rb +26 -0
  4. data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +1 -0
  5. data/app/controllers/concerns/spree/api/v3/allowed_redirect_url.rb +27 -0
  6. data/app/controllers/concerns/spree/api/v3/api_key_authentication.rb +14 -2
  7. data/app/controllers/concerns/spree/api/v3/cart_resolvable.rb +23 -4
  8. data/app/controllers/concerns/spree/api/v3/digital_assets_resource.rb +68 -0
  9. data/app/controllers/concerns/spree/api/v3/error_handler.rb +3 -0
  10. data/app/controllers/concerns/spree/api/v3/jwt_authentication.rb +27 -1
  11. data/app/controllers/concerns/spree/api/v3/orders/fulfillment_actions.rb +6 -1
  12. data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +6 -2
  13. data/app/controllers/spree/api/v3/admin/allowed_origins_controller.rb +4 -1
  14. data/app/controllers/spree/api/v3/admin/auth_controller.rb +32 -5
  15. data/app/controllers/spree/api/v3/admin/commission_lines_controller.rb +2 -2
  16. data/app/controllers/spree/api/v3/admin/companies/addresses_controller.rb +7 -1
  17. data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +27 -8
  18. data/app/controllers/spree/api/v3/admin/customer_groups_controller.rb +4 -0
  19. data/app/controllers/spree/api/v3/admin/customers/addresses_controller.rb +4 -0
  20. data/app/controllers/spree/api/v3/admin/customers/store_credits_controller.rb +1 -1
  21. data/app/controllers/spree/api/v3/admin/customers_controller.rb +14 -14
  22. data/app/controllers/spree/api/v3/admin/delivery_profiles/origin_groups_controller.rb +1 -1
  23. data/app/controllers/spree/api/v3/admin/delivery_profiles_controller.rb +1 -1
  24. data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +1 -8
  25. data/app/controllers/spree/api/v3/admin/invitation_acceptances_controller.rb +2 -0
  26. data/app/controllers/spree/api/v3/admin/me_controller.rb +2 -0
  27. data/app/controllers/spree/api/v3/admin/option_types_controller.rb +13 -0
  28. data/app/controllers/spree/api/v3/admin/orders/claims_controller.rb +2 -0
  29. data/app/controllers/spree/api/v3/admin/orders/exchanges_controller.rb +9 -0
  30. data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +1 -1
  31. data/app/controllers/spree/api/v3/admin/orders/payments_controller.rb +7 -4
  32. data/app/controllers/spree/api/v3/admin/orders/returns_controller.rb +2 -0
  33. data/app/controllers/spree/api/v3/admin/orders_controller.rb +21 -7
  34. data/app/controllers/spree/api/v3/admin/package_types_controller.rb +2 -3
  35. data/app/controllers/spree/api/v3/admin/password_resets_controller.rb +7 -1
  36. data/app/controllers/spree/api/v3/admin/price_lists_controller.rb +18 -0
  37. data/app/controllers/spree/api/v3/admin/product_types_controller.rb +12 -1
  38. data/app/controllers/spree/api/v3/admin/products/digital_assets_controller.rb +5 -109
  39. data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +7 -5
  40. data/app/controllers/spree/api/v3/admin/products_controller.rb +33 -6
  41. data/app/controllers/spree/api/v3/admin/sellers/payouts_controller.rb +4 -3
  42. data/app/controllers/spree/api/v3/admin/stock_transfers_controller.rb +3 -2
  43. data/app/controllers/spree/api/v3/admin/store_controller.rb +8 -1
  44. data/app/controllers/spree/api/v3/admin/tags_controller.rb +11 -9
  45. data/app/controllers/spree/api/v3/admin/variants_controller.rb +96 -1
  46. data/app/controllers/spree/api/v3/admin/webhook_endpoints_controller.rb +5 -2
  47. data/app/controllers/spree/api/v3/oauth/authorizations_controller.rb +17 -6
  48. data/app/controllers/spree/api/v3/resource_controller.rb +50 -7
  49. data/app/controllers/spree/api/v3/seller/auth_controller.rb +10 -0
  50. data/app/controllers/spree/api/v3/seller/invitation_acceptances_controller.rb +2 -0
  51. data/app/controllers/spree/api/v3/seller/me_controller.rb +4 -0
  52. data/app/controllers/spree/api/v3/seller/orders_controller.rb +11 -3
  53. data/app/controllers/spree/api/v3/seller/package_types_controller.rb +2 -2
  54. data/app/controllers/spree/api/v3/seller/password_resets_controller.rb +5 -1
  55. data/app/controllers/spree/api/v3/seller/products/digital_assets_controller.rb +67 -0
  56. data/app/controllers/spree/api/v3/seller/products_controller.rb +11 -63
  57. data/app/controllers/spree/api/v3/seller/resource_controller.rb +2 -6
  58. data/app/controllers/spree/api/v3/seller/team_controller.rb +1 -1
  59. data/app/controllers/spree/api/v3/store/carts/addresses_controller.rb +53 -0
  60. data/app/controllers/spree/api/v3/store/carts/store_credits_controller.rb +9 -1
  61. data/app/controllers/spree/api/v3/store/carts_controller.rb +17 -3
  62. data/app/controllers/spree/api/v3/store/companies/addresses_controller.rb +4 -0
  63. data/app/controllers/spree/api/v3/store/customer/addresses_controller.rb +4 -0
  64. data/app/controllers/spree/api/v3/store/customer/data_requests_controller.rb +19 -1
  65. data/app/controllers/spree/api/v3/store/customer/password_resets_controller.rb +3 -14
  66. data/app/controllers/spree/api/v3/store/customers_controller.rb +13 -1
  67. data/app/controllers/spree/api/v3/store/data_request_confirmations_controller.rb +23 -0
  68. data/app/controllers/spree/api/v3/store/data_requests_controller.rb +56 -0
  69. data/app/controllers/spree/api/v3/store/delivery_methods_controller.rb +6 -1
  70. data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +2 -11
  71. data/app/controllers/spree/api/v3/store/products/filters_controller.rb +19 -4
  72. data/app/request_schemas/spree/api/v3/admin/variant_request_schema.rb +21 -0
  73. data/app/request_schemas/spree/api/v3/digital_asset_request_schema.rb +25 -0
  74. data/app/request_schemas/spree/api/v3/seller/price_request_schema.rb +20 -0
  75. data/app/request_schemas/spree/api/v3/seller/variant_request_schema.rb +19 -0
  76. data/app/request_schemas/spree/api/v3/variant_request_schema.rb +8 -13
  77. data/app/serializers/concerns/spree/api/v3/digital_asset_details.rb +70 -0
  78. data/app/serializers/spree/api/v3/address_owner_serializer.rb +27 -0
  79. data/app/serializers/spree/api/v3/address_serializer.rb +5 -1
  80. data/app/serializers/spree/api/v3/admin/address_owner_serializer.rb +10 -0
  81. data/app/serializers/spree/api/v3/admin/address_serializer.rb +1 -10
  82. data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +1 -1
  83. data/app/serializers/spree/api/v3/admin/catalog_assignment_serializer.rb +1 -1
  84. data/app/serializers/spree/api/v3/admin/catalog_price_serializer.rb +1 -1
  85. data/app/serializers/spree/api/v3/admin/collection_rule_serializer.rb +1 -1
  86. data/app/serializers/spree/api/v3/admin/collection_serializer.rb +1 -1
  87. data/app/serializers/spree/api/v3/admin/commission_line_serializer.rb +2 -2
  88. data/app/serializers/spree/api/v3/admin/commission_rate_serializer.rb +1 -1
  89. data/app/serializers/spree/api/v3/admin/consent_record_serializer.rb +1 -1
  90. data/app/serializers/spree/api/v3/admin/delivery_origin_group_serializer.rb +2 -2
  91. data/app/serializers/spree/api/v3/admin/delivery_profile_serializer.rb +3 -5
  92. data/app/serializers/spree/api/v3/admin/digital_asset_serializer.rb +3 -47
  93. data/app/serializers/spree/api/v3/admin/email_template_serializer.rb +1 -1
  94. data/app/serializers/spree/api/v3/admin/newsletter_subscriber_serializer.rb +1 -0
  95. data/app/serializers/spree/api/v3/admin/order_serializer.rb +1 -1
  96. data/app/serializers/spree/api/v3/admin/package_type_serializer.rb +3 -3
  97. data/app/serializers/spree/api/v3/admin/permission_serializer.rb +1 -1
  98. data/app/serializers/spree/api/v3/admin/price_history_serializer.rb +1 -1
  99. data/app/serializers/spree/api/v3/admin/price_list_serializer.rb +9 -4
  100. data/app/serializers/spree/api/v3/admin/price_serializer.rb +1 -1
  101. data/app/serializers/spree/api/v3/admin/purchase_order_serializer.rb +1 -1
  102. data/app/serializers/spree/api/v3/admin/seller_payout_serializer.rb +2 -2
  103. data/app/serializers/spree/api/v3/admin/seller_requirement_status_serializer.rb +1 -1
  104. data/app/serializers/spree/api/v3/admin/seller_serializer.rb +3 -5
  105. data/app/serializers/spree/api/v3/admin/seller_transfer_serializer.rb +1 -1
  106. data/app/serializers/spree/api/v3/admin/shipping_label_serializer.rb +3 -3
  107. data/app/serializers/spree/api/v3/admin/stock_location_serializer.rb +1 -1
  108. data/app/serializers/spree/api/v3/admin/stock_receipt_item_serializer.rb +2 -2
  109. data/app/serializers/spree/api/v3/admin/stock_receipt_serializer.rb +1 -1
  110. data/app/serializers/spree/api/v3/admin/stock_transfer_serializer.rb +1 -1
  111. data/app/serializers/spree/api/v3/admin/store_credit_serializer.rb +3 -5
  112. data/app/serializers/spree/api/v3/admin/store_serializer.rb +1 -0
  113. data/app/serializers/spree/api/v3/admin/tax_identifier_serializer.rb +2 -2
  114. data/app/serializers/spree/api/v3/admin/tax_line_serializer.rb +1 -1
  115. data/app/serializers/spree/api/v3/admin/variant_serializer.rb +1 -1
  116. data/app/serializers/spree/api/v3/base_serializer.rb +33 -6
  117. data/app/serializers/spree/api/v3/claim_serializer.rb +1 -1
  118. data/app/serializers/spree/api/v3/collection_serializer.rb +1 -1
  119. data/app/serializers/spree/api/v3/company_invitation_serializer.rb +1 -1
  120. data/app/serializers/spree/api/v3/company_serializer.rb +1 -1
  121. data/app/serializers/spree/api/v3/data_request_confirmation_requested_event_serializer.rb +28 -0
  122. data/app/serializers/spree/api/v3/data_request_event_serializer.rb +1 -1
  123. data/app/serializers/spree/api/v3/data_request_serializer.rb +8 -4
  124. data/app/serializers/spree/api/v3/delivery_serializer.rb +1 -1
  125. data/app/serializers/spree/api/v3/delivery_zone_member_serializer.rb +1 -1
  126. data/app/serializers/spree/api/v3/discount_serializer.rb +1 -1
  127. data/app/serializers/spree/api/v3/file_upload_serializer.rb +1 -1
  128. data/app/serializers/spree/api/v3/fulfillment_serializer.rb +1 -1
  129. data/app/serializers/spree/api/v3/media_event_serializer.rb +1 -1
  130. data/app/serializers/spree/api/v3/media_serializer.rb +1 -1
  131. data/app/serializers/spree/api/v3/newsletter_subscriber_serializer.rb +5 -1
  132. data/app/serializers/spree/api/v3/option_type_serializer.rb +1 -1
  133. data/app/serializers/spree/api/v3/order_group_serializer.rb +2 -2
  134. data/app/serializers/spree/api/v3/order_serializer.rb +4 -2
  135. data/app/serializers/spree/api/v3/payment_serializer.rb +1 -1
  136. data/app/serializers/spree/api/v3/product_filter_availability_option_serializer.rb +1 -1
  137. data/app/serializers/spree/api/v3/product_filter_option_serializer.rb +1 -1
  138. data/app/serializers/spree/api/v3/seller/address_serializer.rb +3 -2
  139. data/app/serializers/spree/api/v3/seller/digital_asset_serializer.rb +16 -0
  140. data/app/serializers/spree/api/v3/seller/order_serializer.rb +3 -3
  141. data/app/serializers/spree/api/v3/seller/package_type_serializer.rb +3 -3
  142. data/app/serializers/spree/api/v3/seller/payout_serializer.rb +2 -2
  143. data/app/serializers/spree/api/v3/seller/profile_serializer.rb +2 -2
  144. data/app/serializers/spree/api/v3/seller/requirement_status_serializer.rb +1 -1
  145. data/app/serializers/spree/api/v3/seller/store_serializer.rb +18 -0
  146. data/app/serializers/spree/api/v3/seller/tax_identifier_serializer.rb +1 -1
  147. data/app/serializers/spree/api/v3/seller/variant_serializer.rb +1 -1
  148. data/app/serializers/spree/api/v3/shipping_label_event_serializer.rb +1 -1
  149. data/app/serializers/spree/api/v3/stock_movement_serializer.rb +1 -1
  150. data/app/serializers/spree/api/v3/stock_receipt_event_serializer.rb +1 -1
  151. data/app/serializers/spree/api/v3/variant_serializer.rb +2 -2
  152. data/app/services/spree/api/v3/filter_table.rb +22 -3
  153. data/app/services/spree/api/v3/filters_aggregator.rb +63 -18
  154. data/app/services/spree/api/v3/request_schema.rb +64 -17
  155. data/config/locales/en.yml +2 -0
  156. data/config/routes.rb +22 -11
  157. data/lib/spree/api/agent_write_schemas.rb +15 -3
  158. data/lib/spree/api/dependencies.rb +4 -4
  159. data/lib/spree/api/engine.rb +6 -0
  160. data/lib/spree/api/enum_types.rb +118 -0
  161. data/lib/spree/api/middleware/no_index.rb +33 -0
  162. data/lib/spree/api/openapi/filter_documentation.rb +8 -1
  163. data/lib/spree/api/openapi/schema_helper.rb +2 -1
  164. data/lib/spree/api/testing_support/v3/base.rb +18 -0
  165. data/lib/spree/api/typelizer_generation.rb +5 -0
  166. data/lib/typelizer/open_enums.rb +40 -16
  167. metadata +25 -6
@@ -0,0 +1,53 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Store
5
+ module Carts
6
+ # Every address this cart may be shipped or billed to, in one call:
7
+ # the buyer's own book, plus the book of the company the cart names
8
+ # — that node's entries and the ones it inherits from the nodes above
9
+ # it — limited to the countries the cart's market sells to.
10
+ #
11
+ # A checkout address step would otherwise merge two books and filter
12
+ # them by the market itself. Asking the cart keeps the picker and the
13
+ # checkout from ever disagreeing about what is usable.
14
+ #
15
+ # Signed in only, unlike the rest of the cart surface. What comes
16
+ # back is not the cart's own data but the books behind it — every
17
+ # address the buyer has saved, and their company's sites. A guest
18
+ # token says the caller holds this cart, which it may well do
19
+ # because the account holder adopted a cart the attacker started;
20
+ # it says nothing about the account. A genuine guest cart has
21
+ # neither book, so requiring the account costs nothing but the 401.
22
+ class AddressesController < Store::ResourceController
23
+ include Spree::Api::V3::CartResolvable
24
+
25
+ prepend_before_action :require_authentication!
26
+
27
+ protected
28
+
29
+ def set_parent
30
+ @parent = @cart = find_cart(token: nil)
31
+ end
32
+
33
+ def model_class
34
+ Spree::Address
35
+ end
36
+
37
+ def serializer_class
38
+ Spree.api.address_serializer
39
+ end
40
+
41
+ def collection_includes
42
+ [:owner]
43
+ end
44
+
45
+ def parent_association
46
+ :selectable_addresses
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
53
+ end
@@ -8,7 +8,7 @@ module Spree
8
8
  include Spree::Api::V3::OrderLock
9
9
 
10
10
  before_action :require_authentication!
11
- before_action :find_cart!
11
+ before_action :find_owned_cart!
12
12
  before_action :stringify_numeric_amount, only: :create
13
13
 
14
14
  request_params 'ApplyStoreCreditParams', action: :create do |body|
@@ -50,6 +50,14 @@ module Spree
50
50
 
51
51
  private
52
52
 
53
+ # The balance spent here is the cart owner's, so a guest token —
54
+ # which only proves the caller holds this cart — is not enough:
55
+ # the signed-in customer must be the owner.
56
+ def find_owned_cart!
57
+ @cart = resolve_cart(include_completed: false)
58
+ authorize_storefront_write!(@cart, token: nil)
59
+ end
60
+
53
61
  # 5.6 storefronts sent a JSON number; read it through its text so no
54
62
  # float reaches the money math.
55
63
  def stringify_numeric_amount
@@ -13,7 +13,7 @@ module Spree
13
13
 
14
14
  CART_ADDRESS = Spree::Api::V3::RequestSchema.shape('CartAddressParams') do |schema|
15
15
  schema.attribute :id, :id,
16
- description: 'An existing address of the customer to use. The other fields are then ignored.'
16
+ description: "An existing address from the buyer's address book, or from the book of the company the cart is for. The other fields are then ignored."
17
17
  schema.include_attributes Spree::Api::V3::Store::AddressRequestSchema
18
18
  end
19
19
 
@@ -25,10 +25,10 @@ module Spree
25
25
  schema.attribute :currency, :currency
26
26
  schema.attribute :locale, :locale
27
27
  schema.attribute :shipping_address_id, :id,
28
- description: 'An existing address to ship to.',
28
+ description: "An existing address to ship to, from the buyer's address book or from the book of the company the cart is for. List both with `GET /store/carts/:id/addresses`.",
29
29
  example: 'addr_abc123'
30
30
  schema.attribute :billing_address_id, :id,
31
- description: 'An existing address to bill.',
31
+ description: "An existing address to bill, from the buyer's address book or from the book of the company the cart is for. List both with `GET /store/carts/:id/addresses`.",
32
32
  example: 'addr_def456'
33
33
  schema.attribute :preferred_stock_location_id, :id
34
34
  schema.attribute :company_id, :id, nullable: true, description: <<~TEXT.squish
@@ -112,6 +112,7 @@ module Spree
112
112
  # Auto-advances to the next checkout step when possible.
113
113
  def update
114
114
  find_cart!
115
+ authorize_address_book_access!
115
116
 
116
117
  with_order_lock do
117
118
  result = Spree.carts_update_service.call(
@@ -238,6 +239,19 @@ module Spree
238
239
 
239
240
  # Find incomplete cart for associate action.
240
241
  # Only finds guest carts (no user) or carts already owned by current user (idempotent).
242
+ ADDRESS_BOOK_PARAMS = %i[shipping_address billing_address shipping_address_id billing_address_id].freeze
243
+
244
+ # A typed address on a customer's cart is saved into that customer's
245
+ # address book and may become their default, and an address id names
246
+ # an entry of that book. The guest token proves the caller holds the
247
+ # cart, not the account, so those writes need the owner.
248
+ def authorize_address_book_access!
249
+ return if @cart.customer.nil?
250
+ return unless ADDRESS_BOOK_PARAMS.any? { |key| params.key?(key) }
251
+
252
+ authorize_storefront_write!(@cart, token: nil)
253
+ end
254
+
241
255
  def find_cart_for_association
242
256
  current_store.carts.incomplete.where(customer: [nil, current_user]).find_by_prefix_id!(params[:id])
243
257
  end
@@ -69,6 +69,10 @@ module Spree
69
69
  Spree.api.address_serializer
70
70
  end
71
71
 
72
+ def collection_includes
73
+ [:owner]
74
+ end
75
+
72
76
  # Reading a book and keeping one are different rights: the listing
73
77
  # includes what this node inherits, writes stay with the node that
74
78
  # owns the row. The Store API authorizes purely by what this scope
@@ -64,6 +64,10 @@ module Spree
64
64
  Spree.api.address_serializer
65
65
  end
66
66
 
67
+ def collection_includes
68
+ [:owner]
69
+ end
70
+
67
71
  def permitted_params
68
72
  params.permit(*permitted_attributes)
69
73
  end
@@ -10,14 +10,30 @@ module Spree
10
10
  # irreversible, and an unattended session should not be enough to
11
11
  # trigger it — the same bar the account already applies to changing
12
12
  # an email address.
13
+ #
14
+ # Nothing happens until the request is confirmed from the link emailed
15
+ # to the account's address (see DataRequestConfirmationsController),
16
+ # and the export link is never shown to this session.
13
17
  class DataRequestsController < ResourceController
14
18
  include Spree::Api::V3::CurrentPasswordConfirmation
19
+ include Spree::Api::V3::AllowedRedirectUrl
15
20
 
16
21
  prepend_before_action :require_authentication!
17
22
 
23
+ # Each request while one waits sends the confirmation email again.
24
+ rate_limit to: Spree::Api::Config[:rate_limit_password_reset],
25
+ within: Spree::Api::Config[:rate_limit_window].seconds,
26
+ store: Rails.cache,
27
+ only: :create,
28
+ with: RATE_LIMIT_RESPONSE
29
+
18
30
  request_params 'CreateDataRequestParams', action: :create do |body|
19
31
  body.attribute :kind, :string, choices: Spree::DataRequest::KINDS, description: 'Defaults to `access`.'
20
32
  body.attribute :current_password, :string, description: 'Required for `erasure`.'
33
+ body.attribute :redirect_url, :string,
34
+ format: :uri,
35
+ example: 'https://myshop.com/account/data-requests/confirm',
36
+ description: "The storefront page the confirmation link opens, with the token appended. Ignored unless it matches one of the store's allowed origins."
21
37
  end
22
38
 
23
39
  # POST /api/v3/store/customers/me/data_requests
@@ -27,7 +43,9 @@ module Spree
27
43
  result = Spree::DataRequests::Create.call(
28
44
  store: current_store,
29
45
  customer: current_user,
30
- kind: requested_kind
46
+ kind: requested_kind,
47
+ confirm_by_email: true,
48
+ redirect_url: validated_redirect_url
31
49
  )
32
50
 
33
51
  return render_result_error(result) unless result.success?
@@ -6,6 +6,8 @@ module Spree
6
6
  module Store
7
7
  module Customer
8
8
  class PasswordResetsController < Store::BaseController
9
+ include Spree::Api::V3::AllowedRedirectUrl
10
+
9
11
  allow_guest_storefront_access!
10
12
  rate_limit to: Spree::Api::Config[:rate_limit_password_reset],
11
13
  within: Spree::Api::Config[:rate_limit_window].seconds,
@@ -33,26 +35,13 @@ module Spree
33
35
 
34
36
  # POST /api/v3/store/password_resets
35
37
  def create
36
- redirect_url = params[:redirect_url]
37
-
38
- # Validate redirect_url against allowed origins (secure by default).
39
- # If no allowed origins are configured, redirect_url is silently ignored
40
- # to prevent open redirect / token exfiltration attacks.
41
- if redirect_url.present?
42
- if current_store.allowed_origins.exists? && current_store.allowed_origin?(redirect_url)
43
- # redirect_url is valid — keep it
44
- else
45
- redirect_url = nil
46
- end
47
- end
48
-
49
38
  user = Spree.customer_class.find_by(email: params[:email])
50
39
 
51
40
  if user
52
41
  event_name = 'customer.password_reset_requested'
53
42
  payload = user.event_payload_for(
54
43
  event_name,
55
- reset_token: user.generate_token_for(:password_reset), store: current_store, redirect_url: redirect_url.presence
44
+ reset_token: user.generate_token_for(:password_reset), store: current_store, redirect_url: validated_redirect_url
56
45
  )
57
46
  user.publish_event(event_name, payload)
58
47
  end
@@ -31,7 +31,8 @@ module Spree
31
31
 
32
32
  request_params 'UpdateCustomerParams', action: :update do |body|
33
33
  body.attribute :email, :string, format: :email, example: 'customer@example.com'
34
- body.attribute :password, :string
34
+ body.attribute :password, :string,
35
+ description: 'Changing it signs the customer out everywhere: every refresh token is revoked, so sign in again once the access token expires.'
35
36
  body.attribute :password_confirmation, :string
36
37
  body.attribute :current_password, :string,
37
38
  description: 'Required when changing the email or the password.'
@@ -75,6 +76,7 @@ module Spree
75
76
 
76
77
  # GET /api/v3/store/customer
77
78
  def show
79
+ preload_default_addresses
78
80
  render json: serialize_resource(current_user)
79
81
  end
80
82
 
@@ -86,6 +88,10 @@ module Spree
86
88
  current_user.consent_source = Spree::ConsentRecord::ACCOUNT
87
89
 
88
90
  if current_user.update(update_params)
91
+ # A password change is how a customer takes a stolen session
92
+ # back, so no refresh token issued before it may outlive it —
93
+ # the reset flow does the same.
94
+ Spree::RefreshToken.revoke_all_for(current_user) if update_params[:password].present?
89
95
  render json: serialize_resource(current_user)
90
96
  else
91
97
  render_errors(current_user.errors)
@@ -115,6 +121,12 @@ module Spree
115
121
 
116
122
  private
117
123
 
124
+ # Both default addresses live in one table, so they are fetched in one
125
+ # query rather than one each.
126
+ def preload_default_addresses
127
+ ActiveRecord::Associations::Preloader.new(records: [current_user], associations: %i[bill_address ship_address]).call
128
+ end
129
+
118
130
  def sensitive_update?
119
131
  (params[:email].present? && params[:email] != current_user.email) ||
120
132
  params[:password].present?
@@ -0,0 +1,23 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Store
5
+ # Confirms a request a customer raised, from the page the emailed link
6
+ # opens. The page calls this when the person presses its button, never
7
+ # on load: some mail scanners open links and run the page's scripts.
8
+ class DataRequestConfirmationsController < DataRequestsController
9
+ # POST /api/v3/store/data_requests/:token/confirmation
10
+ def create
11
+ return render_confirmation_invalid(status: :unprocessable_content) unless data_request
12
+
13
+ result = Spree::DataRequests::Confirm.call(data_request: data_request)
14
+
15
+ return render_confirmation_invalid(status: :unprocessable_content) unless result.success?
16
+
17
+ render json: serialize_resource(result.value)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,56 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Store
5
+ # A data request as seen by whoever holds its confirmation token: the
6
+ # person reading mail at the address the request names.
7
+ #
8
+ # Token-addressed rather than session-authenticated, because the link
9
+ # arrives by email and may be opened on a device that was never signed
10
+ # in. Besides the export email, it is the only place the export link is
11
+ # shown, since the token is what reached the address the request names.
12
+ class DataRequestsController < Store::BaseController
13
+ allow_guest_storefront_access!
14
+
15
+ # The response can carry a live link to a person's whole export, the
16
+ # same credential the download itself keeps out of shared caches.
17
+ before_action { response.headers['Cache-Control'] = 'no-store' }
18
+
19
+ # GET /api/v3/store/data_requests/:token
20
+ def show
21
+ return render_confirmation_invalid(status: :not_found) unless data_request
22
+
23
+ render json: serialize_resource(data_request)
24
+ end
25
+
26
+ protected
27
+
28
+ def serializer_class
29
+ Spree.api.data_request_serializer
30
+ end
31
+
32
+ def serializer_params
33
+ super.merge(download_url_visible: true)
34
+ end
35
+
36
+ private
37
+
38
+ def data_request
39
+ return @data_request if defined?(@data_request)
40
+
41
+ token = params[:token].presence
42
+ @data_request = token && Spree::DataRequest.for_store(current_store).find_by_token_for(:confirmation, token)
43
+ end
44
+
45
+ def render_confirmation_invalid(status:)
46
+ render_error(
47
+ code: ERROR_CODES[:invalid_token],
48
+ message: I18n.t('spree.data_request_errors.confirmation_invalid'),
49
+ status: status
50
+ )
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -93,7 +93,7 @@ module Spree
93
93
  def covers_cart?(location, cart)
94
94
  return true if location.pickup_stock_policy == 'any'
95
95
 
96
- cart.line_items.includes(variant: :product).all? do |line_item|
96
+ pickup_line_items(cart).all? do |line_item|
97
97
  next true if line_item.variant.digital?
98
98
  next true unless line_item.variant.should_track_inventory?
99
99
 
@@ -101,6 +101,11 @@ module Spree
101
101
  stock_level.present? && stock_level.available_count >= line_item.quantity
102
102
  end
103
103
  end
104
+
105
+ # Loaded once for every location the cart is checked against.
106
+ def pickup_line_items(cart)
107
+ @pickup_line_items ||= cart.line_items.includes(variant: [:delivery_profile, { product: %i[delivery_profile store] }]).to_a
108
+ end
104
109
  end
105
110
  end
106
111
  end
@@ -3,6 +3,8 @@ module Spree
3
3
  module V3
4
4
  module Store
5
5
  class NewsletterSubscribersController < Store::BaseController
6
+ include Spree::Api::V3::AllowedRedirectUrl
7
+
6
8
  allow_guest_storefront_access!
7
9
  rate_limit to: Spree::Api::Config[:rate_limit_register],
8
10
  within: Spree::Api::Config[:rate_limit_window].seconds,
@@ -139,17 +141,6 @@ module Spree
139
141
  subscriber = Spree::NewsletterSubscriber.for_store(current_store).find_by_token_for(:unsubscribe, params[:token])
140
142
  subscriber if subscriber&.prefixed_id == params[:id]
141
143
  end
142
-
143
- # Drop redirect_url when it isn't in the store's allow-list — secure-by-default,
144
- # mirrors password_resets. Returning nil omits it from the webhook payload rather
145
- # than rejecting the request, so callers can't probe the allow-list via 4xx errors.
146
- def validated_redirect_url
147
- redirect_url = params[:redirect_url]
148
- return nil if redirect_url.blank?
149
- return nil unless current_store.allowed_origin?(redirect_url)
150
-
151
- redirect_url
152
- end
153
144
  end
154
145
  end
155
146
  end
@@ -5,6 +5,7 @@ module Spree
5
5
  module Products
6
6
  class FiltersController < Store::BaseController
7
7
  include Spree::Api::V3::Store::SearchProviderSupport
8
+ include Spree::Api::V3::Store::StorefrontProducts
8
9
 
9
10
  def index
10
11
  json = Rails.cache.fetch(filters_cache_key, expires_in: 15.minutes) do
@@ -30,8 +31,8 @@ module Spree
30
31
 
31
32
  def filters_cache_key
32
33
  products_table = Spree::Product.table_name
33
- stats = current_store.products.available(Time.current, current_currency, include_preorderable: true)
34
- .pick(Arel.sql("MAX(#{products_table}.updated_at)"), Arel.sql("COUNT(DISTINCT #{products_table}.id)"))
34
+ stats = storefront_products.
35
+ pick(Arel.sql("MAX(#{products_table}.updated_at)"), Arel.sql("COUNT(DISTINCT #{products_table}.id)"))
35
36
  max_updated = stats&.first&.to_i
36
37
  product_count = stats&.last || 0
37
38
 
@@ -41,6 +42,7 @@ module Spree
41
42
  current_currency,
42
43
  current_locale,
43
44
  (hide_prices? ? 'prices_hidden' : nil),
45
+ buyer_catalog_fragment,
44
46
  category&.cache_key_with_version,
45
47
  collection&.cache_key_with_version,
46
48
  search_query,
@@ -53,11 +55,24 @@ module Spree
53
55
  parts.compact.join('/')
54
56
  end
55
57
 
58
+ # The buyer's catalogs partition the cache: a facet payload built
59
+ # for one assortment must not be served to a buyer who sees
60
+ # another, or it would describe products their listing never
61
+ # returns.
62
+ def buyer_catalog_fragment
63
+ catalogs = Spree::Catalog.for_buyer(store: current_store, customer: current_user, channel: current_channel)
64
+ return if catalogs.empty?
65
+
66
+ # Versioned, since adding or removing a product touches the
67
+ # catalog without touching the product count or timestamps.
68
+ catalogs.map(&:cache_key_with_version).sort.join('/')
69
+ end
70
+
56
71
  def filters_scope
57
72
  # Mirror ProductsController#scope so facet counts describe the
58
73
  # same product set the listing returns (coming-soon pre-orders
59
- # included).
60
- scope = current_store.products.available(Time.current, current_currency, include_preorderable: true)
74
+ # included) — narrowed to the buyer's catalogs like the listing.
75
+ scope = storefront_products
61
76
  scope = scope.in_category(category) if category.present?
62
77
  scope = scope.in_collection(collection) if collection.present?
63
78
  scope
@@ -0,0 +1,21 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # The shared variant attributes, plus the marketplace configuration
6
+ # only an operator sets.
7
+ class VariantRequestSchema < Spree::Api::V3::VariantRequestSchema
8
+ publishes 'VariantParams'
9
+
10
+ attribute :seller_id, :id
11
+ attribute :delivery_profile_id, :id, nullable: true
12
+ attribute :tax_category_id, :id
13
+ attribute :prices, :array, of: Spree::Api::V3::PriceRequestSchema,
14
+ description: 'Per-currency prices. Upserted by currency.'
15
+ attribute :stock_levels, :array, of: Spree::Api::V3::StockLevelRequestSchema,
16
+ description: 'Stock per location. Upserted by location; omitted locations are removed.'
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,25 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ # What a downloadable file on a product carries, whichever panel writes
5
+ # it. The operator's endpoint adds the provider fields on top.
6
+ class DigitalAssetRequestSchema < Spree::Api::V3::RequestSchema
7
+ # Names a blob rather than a column, so it is attached separately.
8
+ attribute :signed_id, :string,
9
+ permit: false,
10
+ description: "A `signed_id` from `files.upload()` with `visibility: 'private'`. On update it replaces the file for everyone who already bought it. A seller's file must be of a content type the marketplace accepts, checked against the file's own signature where it has one."
11
+ # Resolved through the parent rather than assigned, so an asset can't
12
+ # be pointed at a variant of another product or store.
13
+ attribute :variant_id, :id,
14
+ permit: false,
15
+ description: "On create, defaults to the product's default variant when omitted. On update, omitting it keeps the current variant."
16
+ attribute :authorized_clicks, :integer,
17
+ nullable: true,
18
+ description: "Null means the store's download settings apply."
19
+ attribute :authorized_days, :integer,
20
+ nullable: true,
21
+ description: "Null means the store's download settings apply."
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,20 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Seller
5
+ # The operator's price shape, except `currency` may be left out: the
6
+ # store's currency is assumed (see `default_price_currencies`).
7
+ class PriceRequestSchema < Spree::Api::V3::RequestSchema
8
+ attribute :currency, :currency,
9
+ example: 'USD',
10
+ description: "Omit to price in the store's currency. A seller has no currency of their own."
11
+ attribute :amount, :decimal,
12
+ required: true,
13
+ example: '24.00',
14
+ description: 'A canonical decimal string (`"1234.56"`: period decimal, no grouping). The API does not parse localized input.'
15
+ attribute :compare_at_amount, :decimal, nullable: true, example: '30.00'
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,19 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Seller
5
+ # The shared variant attributes. Narrower than the operator's set:
6
+ # `seller_id` would let a payload hand a variant to somebody else, and
7
+ # `tax_category_id`/`delivery_profile_id` are marketplace configuration.
8
+ class VariantRequestSchema < Spree::Api::V3::VariantRequestSchema
9
+ publishes 'VariantParams'
10
+
11
+ attribute :prices, :array, of: Spree::Api::V3::Seller::PriceRequestSchema,
12
+ description: 'Per-currency prices. Upserted by currency.'
13
+ attribute :stock_levels, :array, of: Spree::Api::V3::StockLevelRequestSchema,
14
+ description: "Stock per location, at this seller's own locations only: a location belonging to anyone else is ignored."
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,9 +1,9 @@
1
1
  module Spree
2
2
  module Api
3
3
  module V3
4
- # A variant's own attributes, shared by the variant endpoints and the
5
- # variants nested in a product payload. Not published on its own: each
6
- # endpoint includes these fields into its own type.
4
+ # A variant's own attributes, the ones a seller may write too. Each API
5
+ # publishes its own `VariantParams` from a subclass, which the variant
6
+ # endpoints and the variants nested in a product payload both extend.
7
7
  class VariantRequestSchema < Spree::Api::V3::RequestSchema
8
8
  attribute :sku, :string
9
9
  attribute :barcode, :string
@@ -13,14 +13,14 @@ module Spree
13
13
  attribute :height, :number, nullable: true
14
14
  attribute :width, :number, nullable: true
15
15
  attribute :depth, :number, nullable: true
16
- attribute :weight_unit, :string, choices: Spree::Variant::WEIGHT_UNITS
17
- attribute :dimensions_unit, :string, choices: Spree::Variant::DIMENSION_UNITS
16
+ attribute :weight_unit, :weight_unit, nullable: true, description: "Null falls back to the store's weight unit."
17
+ attribute :dimensions_unit, :dimension_unit, nullable: true, description: "Null falls back to the store's unit system."
18
18
  attribute :hs_code, :string
19
19
  attribute :customs_description, :string
20
20
  attribute :country_of_origin, :country
21
21
  attribute :minimum_order_quantity, :integer, nullable: true
22
22
  attribute :order_multiple, :integer, nullable: true
23
- attribute :purchase_unit, :string
23
+ attribute :purchase_unit, :string, nullable: true, choices: Spree::Variant::PURCHASE_UNITS
24
24
  attribute :units_per_carton, :integer, nullable: true, description: 'How many units fill one carton.'
25
25
  attribute :carton_package_type_id, :id,
26
26
  nullable: true,
@@ -29,8 +29,6 @@ module Spree
29
29
  nullable: true,
30
30
  description: "What one packed carton weighs, in the variant's weight unit."
31
31
  attribute :cartons_per_pallet, :integer, nullable: true, description: 'How many cartons stack on one pallet.'
32
- attribute :seller_id, :id
33
- attribute :delivery_profile_id, :id, nullable: true
34
32
  attribute :track_inventory, :boolean
35
33
  attribute :preorderable, :boolean,
36
34
  description: "Sell this variant before it's in stock. Adds the pre-order label and ships-by promise; the cap comes from `backorder_limit`, an incoming stock count, or backorder."
@@ -40,12 +38,9 @@ module Spree
40
38
  attribute :backorder_limit, :integer,
41
39
  nullable: true,
42
40
  description: 'Maximum units sellable beyond available stock, capping backorders and pre-orders. Null is unlimited.'
43
- attribute :tax_category_id, :id
44
41
  attribute :position, :integer
45
- attribute :prices, :array, of: Spree::Api::V3::PriceRequestSchema,
46
- description: 'Per-currency prices. Upserted by currency.'
47
- attribute :stock_levels, :array, of: Spree::Api::V3::StockLevelRequestSchema,
48
- description: 'Stock per location. Upserted by location; omitted locations are removed.'
42
+ attribute :options, :array, of: Spree::Api::V3::VariantOptionRequestSchema,
43
+ description: 'One pair per option type the variant takes part in (size, color). Option types and values are created when missing.'
49
44
  end
50
45
  end
51
46
  end