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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 50559a5f0c0305cee4fbf43e8d145078d0f976ffeed8a40be46f19e4d1cca64d
4
- data.tar.gz: '00688dd0b79f087aeee42271fd8678eacbaca3626115770fa29043cbbe3e3182'
3
+ metadata.gz: 9a4438212680676faa48449ff17a89b8ee1039a707cc68521a72b54aaa08be90
4
+ data.tar.gz: 0454d9e97dcebc99b1e00bef4a8793d760953f2bfffce98de7d0fb6e4064ae6e
5
5
  SHA512:
6
- metadata.gz: d56fd3fd92f8bdd5257da98f842c366aa4ad324fcb5ef64c8cf44f007011144813da32af273828604db8cc2ce237fc17df51d633de6132594b41ab3002923c4d
7
- data.tar.gz: 34e4d178767f8feccad3b96dbffb9ec02727bce30cdf106a19b9fca97c3af8f44272cf06b4a5facef4864ff3216860538c1a742cc2a1acc26a1589ea1c12b3d9
6
+ metadata.gz: d5695765e1b4f9dea758cba076d7f6fbdd0c1f9bb95e164bf9af94c5a310b989424516ed29057bb9bc6140d8acf1e327f7cd2024fba16bf6727b8089d98e2618
7
+ data.tar.gz: 3182e8e1f5b03612236658108788cb633aea6297487adb7d56aa88157301362839c423926969aa3a1205393b5630f9a923b1d8f546fe85daf7532362976446b9
@@ -27,6 +27,7 @@ module Spree
27
27
  end
28
28
 
29
29
  REFRESH_COOKIE_NAME = :spree_admin_refresh_token
30
+ OAUTH_NONCE_COOKIE_NAME = :spree_admin_oauth_nonce
30
31
  COOKIE_PATH = '/api/v3/admin/auth'.freeze
31
32
 
32
33
  private
@@ -48,6 +49,28 @@ module Spree
48
49
  cookies.signed[REFRESH_COOKIE_NAME].presence
49
50
  end
50
51
 
52
+ # Binds an SSO attempt to the browser that started it. The nonce
53
+ # signed into the state is kept here too, so a callback carrying a
54
+ # state minted in another browser — a login an attacker completed
55
+ # with their own account — is refused rather than signing the
56
+ # victim into it.
57
+ def set_oauth_nonce_cookie(nonce, expires_in:)
58
+ cookies.signed[OAUTH_NONCE_COOKIE_NAME] = base_cookie_attributes.merge(
59
+ value: nonce,
60
+ expires: expires_in.from_now,
61
+ path: COOKIE_PATH,
62
+ httponly: true
63
+ )
64
+ end
65
+
66
+ def oauth_nonce_from_cookie
67
+ cookies.signed[OAUTH_NONCE_COOKIE_NAME].presence
68
+ end
69
+
70
+ def clear_oauth_nonce_cookie
71
+ cookies.delete(OAUTH_NONCE_COOKIE_NAME, path: COOKIE_PATH)
72
+ end
73
+
51
74
  # Keyed off the request scheme, not the Rails env: Rails silently refuses to
52
75
  # write a Secure cookie on a non-SSL request, so a production app served over
53
76
  # plain http (local Docker image, http-only intranet) would never get the
@@ -0,0 +1,26 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ module Admin
5
+ # Handing money back is the `refunds` key's own authority, as it is on
6
+ # the nested refunds endpoint: a role that settles returns, claims and
7
+ # exchanges, or cancels orders, under `orders` does not thereby decide
8
+ # to refund them. The store back office's gate only — a seller refunds
9
+ # their own orders under the seller panel's `orders` key.
10
+ module RefundAuthorization
11
+ extend ActiveSupport::Concern
12
+
13
+ REFUND_PERMISSION = 'write_refunds'.freeze
14
+
15
+ private
16
+
17
+ def require_refund_permission!
18
+ return if holds_permission?(REFUND_PERMISSION)
19
+
20
+ render_missing_permission(REFUND_PERMISSION)
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -77,6 +77,7 @@ module Spree
77
77
 
78
78
  owner = token.resource_owner
79
79
  return false unless owner.is_a?(Spree.admin_user_class)
80
+ return false if account_deactivated?(owner)
80
81
 
81
82
  @current_api_key = nil
82
83
  @current_user = owner
@@ -0,0 +1,27 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ # Reads the `redirect_url` a storefront passes for an emailed link.
5
+ #
6
+ # The link carries a live token, so a page outside the store's allowed
7
+ # origins would hand that token to whoever named the page. An address
8
+ # that does not match is dropped rather than refused, so callers cannot
9
+ # probe the allow-list through error responses.
10
+ module AllowedRedirectUrl
11
+ extend ActiveSupport::Concern
12
+
13
+ private
14
+
15
+ # @return [String, nil] the redirect URL when it matches one of the
16
+ # store's allowed origins
17
+ def validated_redirect_url
18
+ redirect_url = params[:redirect_url]
19
+ return if redirect_url.blank?
20
+ return unless current_store.allowed_origin?(redirect_url)
21
+
22
+ redirect_url
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -67,7 +67,8 @@ module Spree
67
67
  return @publishable_api_key if defined?(@publishable_api_key)
68
68
 
69
69
  @publishable_api_key = Spree::StoreScopeGuard.skip do
70
- extract_api_key ? Spree::ApiKey.active.publishable.find_by(token: extract_api_key) : nil
70
+ api_key = extract_api_key ? Spree::ApiKey.active.publishable.find_by(token: extract_api_key) : nil
71
+ api_key if store_present?(api_key)
71
72
  end
72
73
  end
73
74
 
@@ -78,10 +79,21 @@ module Spree
78
79
  return @secret_api_key if defined?(@secret_api_key)
79
80
 
80
81
  @secret_api_key = Spree::StoreScopeGuard.skip do
81
- Spree::ApiKey.find_by_secret_token(extract_api_key)
82
+ api_key = Spree::ApiKey.find_by_secret_token(extract_api_key)
83
+ api_key if store_present?(api_key)
82
84
  end
83
85
  end
84
86
 
87
+ # A key selects its store, so a key whose store has been deleted no
88
+ # longer authenticates. Without this the request would run with no
89
+ # store at all, and fail deep in the first store-scoped query.
90
+ #
91
+ # @param api_key [Spree::ApiKey, nil]
92
+ # @return [Boolean]
93
+ def store_present?(api_key)
94
+ api_key&.store.present?
95
+ end
96
+
85
97
  # Marks the API key as used at most once per hour
86
98
  # to avoid unnecessary DB writes and job queue pressure on every request.
87
99
  # This follows the same approach as GitHub's personal access tokens.
@@ -11,10 +11,16 @@ module Spree
11
11
  # Find cart by prefixed ID and authorize read access (owner or guest
12
12
  # token). Completed carts are not carts anymore — they 404 unless the
13
13
  # caller opts in (idempotent completion, payment-session confirm races).
14
+ #
15
+ # Pass +token: nil+ to demand ownership. A guest token proves the
16
+ # caller holds this cart, which is the right bar for the cart's own
17
+ # contents and no higher — an endpoint reaching past the cart into
18
+ # something the account holds asks for the account instead.
19
+ #
14
20
  # @return [Spree::Cart]
15
- def find_cart(include_completed: false)
21
+ def find_cart(include_completed: false, token: cart_token)
16
22
  @cart = resolve_cart(include_completed: include_completed)
17
- authorize_storefront_read!(@cart, token: cart_token)
23
+ authorize_storefront_read!(@cart, token: token)
18
24
  @cart
19
25
  end
20
26
 
@@ -44,8 +50,15 @@ module Spree
44
50
  # Render the cart as JSON using the cart serializer. Rendering never
45
51
  # mutates: an action that wants unbuyable lines swept calls
46
52
  # +sweep_unbuyable_lines!+ itself first.
53
+ #
54
+ # Serialized from a fresh, lazily preloading read, since lazy
55
+ # preloading does not reach lines and fulfillments a write built. The
56
+ # warnings exist only in memory, so they are carried over.
47
57
  def render_cart(status: :ok)
48
- render json: Spree.api.cart_serializer.new(@cart, params: serializer_params).to_h, status: status
58
+ cart = current_store.carts.where(id: @cart.id).preload_associations_lazily.take!
59
+ cart.warnings = @cart.warnings
60
+
61
+ render json: Spree.api.cart_serializer.new(cart, params: serializer_params).to_h, status: status
49
62
  end
50
63
 
51
64
  # Drops lines the customer can no longer buy — a discontinued product,
@@ -74,7 +87,13 @@ module Spree
74
87
  # storefront that knows nothing about groups still finds every order it
75
88
  # needs there.
76
89
  def render_order(status: :ok)
77
- record = @cart.reload
90
+ # Read back through a lazily preloading relation rather than
91
+ # reloaded in place: a group's orders, and everything each of them
92
+ # renders, then load once for all of them instead of once per order.
93
+ purchases = @cart.is_a?(Spree::OrderGroup) ? current_store.order_groups : current_store.orders
94
+ # Chained through `where` first: an association collection drops the
95
+ # lazy flag when `find` rebuilds the query from the association.
96
+ record = purchases.where(id: @cart.id).preload_associations_lazily.take!
78
97
 
79
98
  render json: purchase_serializer_for(record).new(record, params: serializer_params).to_h, status: status
80
99
  end
@@ -0,0 +1,68 @@
1
+ module Spree
2
+ module Api
3
+ module V3
4
+ # The downloadable files on one product, shared by the operator's and a
5
+ # seller's branch. Writes run through the digital asset workflows; each
6
+ # includer finds and authorizes the product in `set_parent`, names its
7
+ # serializer, and may add to the workflow arguments.
8
+ module DigitalAssetsResource
9
+ extend ActiveSupport::Concern
10
+
11
+ protected
12
+
13
+ def model_class
14
+ Spree::DigitalAsset
15
+ end
16
+
17
+ # Assets reach the product through `variants`, whose ORDER BY
18
+ # PostgreSQL rejects alongside the collection's DISTINCT.
19
+ def scope
20
+ super.reorder(created_at: :asc)
21
+ end
22
+
23
+ def scope_includes
24
+ [:variant, { attachment_attachment: :blob }]
25
+ end
26
+
27
+ # Not normalized: `provider_settings` may hold values shaped like
28
+ # prefixed ids.
29
+ def permitted_params
30
+ params.permit(*declared_permitted_attributes, *model_additional_permitted_attributes)
31
+ end
32
+
33
+ def create_workflow
34
+ Spree.digital_asset_create_workflow
35
+ end
36
+
37
+ def update_workflow
38
+ Spree.digital_asset_update_workflow
39
+ end
40
+
41
+ # An asset belongs to a variant, not directly to a store; the workflow
42
+ # resolves the variant through the product.
43
+ def create_workflow_arguments
44
+ { product: @parent, attributes: workflow_attributes }
45
+ end
46
+
47
+ def update_workflow_arguments
48
+ { digital_asset: @resource, attributes: workflow_attributes }
49
+ end
50
+
51
+ private
52
+
53
+ # `signed_id` and `variant_id` are declared `permit: false` because
54
+ # neither is a column: the workflow attaches the one and resolves the
55
+ # other through the product, so they travel beside the attributes.
56
+ def workflow_attributes
57
+ permitted_params.to_h.merge(params.permit(:signed_id, :variant_id).to_h)
58
+ end
59
+
60
+ # Only authorized, never saved: the workflow builds the record it
61
+ # writes. The variant is what an ability's conditions read.
62
+ def build_resource
63
+ model_class.new(variant: @parent.default_variant)
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -16,6 +16,9 @@ module Spree
16
16
  # Distinct from authentication_failed: the person proved who they are to
17
17
  # the identity provider, but no account here is authorized for them.
18
18
  account_not_provisioned: 'account_not_provisioned',
19
+ # The person proved who they are, but the account has been switched
20
+ # off (Spree::AccountDeactivation).
21
+ account_deactivated: 'account_deactivated',
19
22
  invalid_oauth_state: 'invalid_oauth_state',
20
23
  current_password_invalid: 'current_password_invalid',
21
24
  password_reset_token_invalid: 'password_reset_token_invalid',
@@ -111,7 +111,7 @@ module Spree
111
111
 
112
112
  case user_type
113
113
  when USER_TYPE_ADMIN
114
- Spree.admin_user_class.find(user_id)
114
+ admin_user_from(user_id)
115
115
  else
116
116
  customer_from(user_id)
117
117
  end
@@ -128,6 +128,32 @@ module Spree
128
128
  customer.respond_to?(:anonymized?) && customer.anonymized? ? nil : customer
129
129
  end
130
130
 
131
+ # A deactivated staff account stops answering to its old tokens on the
132
+ # next request, rather than when they expire.
133
+ def admin_user_from(user_id)
134
+ admin_user = Spree.admin_user_class.find(user_id)
135
+
136
+ account_deactivated?(admin_user) ? nil : admin_user
137
+ end
138
+
139
+ # Duck-typed, like the anonymized check above: a custom admin user
140
+ # class without Spree::AccountDeactivation is never refused.
141
+ def account_deactivated?(user)
142
+ user.respond_to?(:deactivated?) && user.deactivated?
143
+ end
144
+
145
+ # For a person who proved who they are: a password that matched, a
146
+ # provider that vouched for them, a reset or invitation link they hold.
147
+ # Anyone else gets the same answer as for any failed attempt, so this
148
+ # never tells a stranger which accounts are switched off.
149
+ def render_account_deactivated
150
+ render_error(
151
+ code: ErrorHandler::ERROR_CODES[:account_deactivated],
152
+ message: I18n.t('spree.api.account_deactivated'),
153
+ status: :forbidden
154
+ )
155
+ end
156
+
131
157
  def determine_user_type(user)
132
158
  if user.is_a?(Spree.admin_user_class)
133
159
  USER_TYPE_ADMIN
@@ -72,8 +72,13 @@ module Spree
72
72
  )
73
73
 
74
74
  if changer.run!
75
+ # Read back through a lazily preloading query: what each parcel
76
+ # renders, its number included, then loads once for all of them.
77
+ order = current_store.orders.where(id: @order.id).preload_associations_lazily.take!
78
+ fulfillments = order.fulfillments.to_a
79
+
75
80
  render json: {
76
- data: @order.reload.fulfillments.map { |fulfillment| serialize_resource(fulfillment) }
81
+ data: fulfillments.map { |fulfillment| serialize_resource(fulfillment) }
77
82
  }
78
83
  else
79
84
  render_validation_error(changer.errors)
@@ -159,11 +159,15 @@ module Spree
159
159
  "held=#{ability.permission_keys.join(',')}"
160
160
  end
161
161
 
162
+ render_missing_permission(required)
163
+ end
164
+
165
+ def render_missing_permission(permission)
162
166
  render_error(
163
167
  code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:access_denied],
164
- message: "Missing permission: #{required}",
168
+ message: "Missing permission: #{permission}",
165
169
  status: :forbidden,
166
- details: { required_permission: required }
170
+ details: { required_permission: permission }
167
171
  )
168
172
  end
169
173
 
@@ -3,7 +3,10 @@ module Spree
3
3
  module V3
4
4
  module Admin
5
5
  class AllowedOriginsController < ResourceController
6
- scoped_resource :settings
6
+ # A credential, not a setting: an allowed origin can read every
7
+ # admin's session through the cookie-authenticated refresh endpoint,
8
+ # so the list is managed under the same scope as API keys.
9
+ scoped_resource :api_keys
7
10
 
8
11
  request_params :allowed_origin do |body|
9
12
  body.attribute :origin, :string,
@@ -36,6 +36,8 @@ module Spree
36
36
 
37
37
  if result.success?
38
38
  user = result.value
39
+ return render_account_deactivated if account_deactivated?(user)
40
+
39
41
  refresh_token = Spree::RefreshToken.create_for(user, audience: JWT_AUDIENCE_ADMIN, request_env: request_env_for_token)
40
42
  set_refresh_cookie(refresh_token)
41
43
  render json: auth_response(user)
@@ -56,7 +58,16 @@ module Spree
56
58
  # anyone can log in — so it exposes only provider keys, kinds, labels
57
59
  # and authorization URLs.
58
60
  def providers
59
- described = Spree.admin_authentication_strategies.describe { |key| issue_oauth_state(key) }
61
+ # The login page may load again while a sign-in is in flight, so a
62
+ # nonce the browser already holds is kept rather than replaced —
63
+ # replacing it would fail the callback of the state minted first.
64
+ nonce = oauth_nonce_from_cookie || SecureRandom.hex(16)
65
+ minted = false
66
+ described = Spree.admin_authentication_strategies.describe do |key|
67
+ minted = true
68
+ issue_oauth_state(key, nonce)
69
+ end
70
+ set_oauth_nonce_cookie(nonce, expires_in: OAUTH_STATE_EXPIRY) if minted
60
71
 
61
72
  render json: { providers: described }
62
73
  end
@@ -73,6 +84,9 @@ module Spree
73
84
  def callback
74
85
  return redirect_to_dashboard(error: ERROR_CODES[:invalid_oauth_state]) unless valid_oauth_state?
75
86
 
87
+ # Used once: a state that signed in cannot sign in again.
88
+ clear_oauth_nonce_cookie
89
+
76
90
  strategy = Spree.admin_authentication_strategies.build(
77
91
  params[:provider],
78
92
  params: params,
@@ -87,6 +101,8 @@ module Spree
87
101
  return redirect_to_dashboard(error: callback_error_code(strategy))
88
102
  end
89
103
 
104
+ return redirect_to_dashboard(error: ERROR_CODES[:account_deactivated]) if account_deactivated?(result.value)
105
+
90
106
  set_refresh_cookie(
91
107
  Spree::RefreshToken.create_for(result.value, audience: JWT_AUDIENCE_ADMIN, request_env: request_env_for_token)
92
108
  )
@@ -119,6 +135,14 @@ module Spree
119
135
  end
120
136
 
121
137
  user = refresh_token.user
138
+ # Deactivating deletes the account's refresh tokens; this catches a
139
+ # refresh already under way when that happened.
140
+ if account_deactivated?(user)
141
+ refresh_token.destroy
142
+ clear_refresh_cookie
143
+ return render_account_deactivated
144
+ end
145
+
122
146
  new_refresh_token = refresh_token.rotate!(request_env: request_env_for_token)
123
147
  set_refresh_cookie(new_refresh_token)
124
148
 
@@ -158,10 +182,11 @@ module Spree
158
182
  # expired value fails verification.
159
183
  #
160
184
  # The provider key is signed into the payload so a state minted for one
161
- # provider cannot be replayed against another's callback.
162
- def issue_oauth_state(provider)
185
+ # provider cannot be replayed against another's callback, and the
186
+ # nonce ties it to the browser holding the matching cookie.
187
+ def issue_oauth_state(provider, nonce)
163
188
  Rails.application.message_verifier(OAUTH_STATE_PURPOSE).generate(
164
- { provider: provider.to_s, nonce: SecureRandom.hex(16) },
189
+ { provider: provider.to_s, nonce: nonce },
165
190
  expires_in: OAUTH_STATE_EXPIRY
166
191
  )
167
192
  end
@@ -173,8 +198,10 @@ module Spree
173
198
  # The verifier round-trips through JSON, so payload keys come back as strings.
174
199
  payload = Rails.application.message_verifier(OAUTH_STATE_PURPOSE).verified(state)
175
200
  return false unless payload.is_a?(Hash)
201
+ return false unless payload['provider'].to_s == params[:provider].to_s
176
202
 
177
- payload['provider'].to_s == params[:provider].to_s
203
+ nonce = oauth_nonce_from_cookie
204
+ nonce.present? && ActiveSupport::SecurityUtils.secure_compare(payload['nonce'].to_s, nonce)
178
205
  end
179
206
 
180
207
  # Sends the browser back to the dashboard. On failure the login page
@@ -23,8 +23,8 @@ module Spree
23
23
 
24
24
  # Reached through the store's own orders: the lines carry no store of
25
25
  # their own, and the order is what ties them to one.
26
- def scope
27
- super.where(order_id: current_store.orders.select(:id))
26
+ def store_scope
27
+ model_class.where(order_id: current_store.orders.select(:id))
28
28
  end
29
29
 
30
30
  # Every association the serializer reads, so a page of lines costs a
@@ -61,8 +61,14 @@ module Spree
61
61
  Spree.api.admin_address_serializer
62
62
  end
63
63
 
64
+ def collection_includes
65
+ [:owner]
66
+ end
67
+
68
+ # The listing includes what the node inherits: a division's orders may ship to its headquarters' sites,
69
+ # so staff placing one must be able to see them — while writes stay with the node that owns the row.
64
70
  def scope
65
- @parent.addresses
71
+ action_name == 'index' ? @parent.address_book : @parent.addresses
66
72
  end
67
73
  end
68
74
  end
@@ -64,16 +64,34 @@ module Spree
64
64
  # store can't read or write custom fields on another store's records.
65
65
  # Users are intentionally global in Spree (`User.for_store` is a
66
66
  # no-op), so the store boundary for customer parents is enforced by
67
- # the `authorize!(:show, @parent)` ability check above.
67
+ # the `authorize!(:update, @parent)` ability check above.
68
+ #
69
+ # `Spree::Base.for_store` answers nothing for a model the store has
70
+ # no association for, so an unexpected parent type 404s rather than
71
+ # searching every store's records.
68
72
  def parent_relation
69
73
  klass = parent_lookup.klass
70
- klass.respond_to?(:for_store) ? klass.for_store(current_store) : klass
74
+ return klass.where(id: current_store.id) if klass <= Spree::Store
75
+ return klass if klass <= Spree.customer_class
76
+
77
+ klass.for_store(current_store)
71
78
  end
72
79
 
73
80
  # Product-family parents have no scope pair of their own — their
74
81
  # custom fields are catalog data, gated by `products` like the rest
75
82
  # of the variant/option-type surface.
76
- SCOPE_OVERRIDES = { 'option_type' => :products, 'variant' => :products }.freeze
83
+ # Parents whose own controller is gated by a scope other than their
84
+ # plural name share that controller's scope.
85
+ SCOPE_OVERRIDES = {
86
+ 'option_type' => :products,
87
+ 'variant' => :products,
88
+ 'company' => :customers,
89
+ 'stock_transfer' => :stock,
90
+ 'purchase_order' => :purchasing,
91
+ 'payment_method' => :settings,
92
+ 'tax_rate' => :settings,
93
+ 'store' => :settings
94
+ }.freeze
77
95
 
78
96
  # Per-parent scope check: a key holding `write_products` may write a
79
97
  # product's custom fields, `write_orders` may write an order's, etc.
@@ -104,12 +122,13 @@ module Spree
104
122
  # code reloads — `enabled_resources` is captured at boot and its
105
123
  # class references go stale. Aliases `'customer'` because the route
106
124
  # uses `customer_id` while user_class.model_name.element is `'user'`,
107
- # and `'category'` because the routes expose taxons as categories
108
- # (5.5 rename) while the model's element is still `'taxon'`.
125
+ # `'category'` because the routes expose taxons as categories
126
+ # (5.5 rename) while the model's element is still `'taxon'`, and
127
+ # `'medium'` because Rails singularizes the `media` route to `medium_id`.
109
128
  def parent_route_map
110
129
  @parent_route_map ||= Spree.custom_fields.enabled_resources.each_with_object({}) do |klass, m|
111
130
  m[klass.model_name.element.to_s] = klass.name
112
- end.merge('customer' => Spree.customer_class.name, 'category' => 'Spree::Category')
131
+ end.merge('customer' => Spree.customer_class.name, 'category' => 'Spree::Category', 'medium' => 'Spree::Media')
113
132
  end
114
133
 
115
134
  # Returns the first segment whose `<segment>_id` is in the route,
@@ -117,8 +136,8 @@ module Spree
117
136
  # by both `set_parent` and `scoped_resource_name`.
118
137
  #
119
138
  # Read from the path only: a `store_id` or `refund_id` added to the
120
- # query string or body must not switch the parent to a type that is
121
- # never routed here and has no store scope to narrow it.
139
+ # query string or body must not switch the parent away from the record
140
+ # the route names.
122
141
  def parent_lookup
123
142
  return @parent_lookup if defined?(@parent_lookup)
124
143
 
@@ -25,6 +25,10 @@ module Spree
25
25
  Spree.api.admin_customer_group_serializer
26
26
  end
27
27
 
28
+ def collection_includes
29
+ [:customer_group_users]
30
+ end
31
+
28
32
  def permitted_params
29
33
  params.permit(*permitted_attributes)
30
34
  end
@@ -69,6 +69,10 @@ module Spree
69
69
  Spree.api.admin_address_serializer
70
70
  end
71
71
 
72
+ def collection_includes
73
+ [:owner]
74
+ end
75
+
72
76
  private
73
77
 
74
78
  def address_attrs
@@ -18,7 +18,7 @@ module Spree
18
18
  # POST /api/v3/admin/customers/:customer_id/store_credits
19
19
  def create
20
20
  @resource = @parent.store_credits.new(store_credit_attributes)
21
- @resource.created_by = try_spree_current_user
21
+ @resource.created_by = current_actor
22
22
  @resource.store ||= current_store
23
23
  authorize_resource!(@resource, :create)
24
24
 
@@ -12,9 +12,13 @@ module Spree
12
12
  body.attribute :first_name, :string
13
13
  body.attribute :last_name, :string
14
14
  body.attribute :phone, :string
15
- body.attribute :password, :string,
15
+ # On create only: once the account exists its password is the
16
+ # customer's, changed by them or through their own reset email —
17
+ # never set by staff, who would otherwise be able to sign in as
18
+ # any customer.
19
+ body.attribute :password, :string, only: :create,
16
20
  description: 'Optional: a customer created without one claims the account through a password reset.'
17
- body.attribute :password_confirmation, :string,
21
+ body.attribute :password_confirmation, :string, only: :create,
18
22
  description: 'Optional: a customer created without one claims the account through a password reset.'
19
23
  body.attribute :selected_locale, :string
20
24
  body.attribute :avatar_signed_id, :string, description: 'A `signed_id` from `files.upload()`.'
@@ -175,12 +179,12 @@ module Spree
175
179
  # Bulk add the given customers to the given groups. Idempotent —
176
180
  # customers already in a group are skipped at the model layer.
177
181
  def bulk_add_to_groups
178
- apply_groups(:add_customers)
182
+ apply_groups(:add_customers_to)
179
183
  end
180
184
 
181
185
  # Bulk remove the given customers from the given groups.
182
186
  def bulk_remove_from_groups
183
- apply_groups(:remove_customers)
187
+ apply_groups(:remove_customers_from)
184
188
  end
185
189
 
186
190
  protected
@@ -248,12 +252,7 @@ module Spree
248
252
  missing = EXPORT_PERMISSIONS.reject { |permission| holds_permission?(permission) }
249
253
  return true if missing.empty?
250
254
 
251
- render_error(
252
- code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:access_denied],
253
- message: "Missing permission: #{missing.first}",
254
- status: :forbidden,
255
- details: { required_permission: missing.first }
256
- )
255
+ render_missing_permission(missing.first)
257
256
  false
258
257
  end
259
258
 
@@ -296,8 +295,9 @@ module Spree
296
295
  end
297
296
 
298
297
  # Authorises bulk group mutation, decodes prefixed IDs, then dispatches
299
- # to `add_customers` / `remove_customers` per group. Returns the
300
- # counts of records actually affected so the UI can show a toast.
298
+ # to `add_customers_to` / `remove_customers_from` for all groups at
299
+ # once. Returns the counts of records actually affected so the UI can
300
+ # show a toast.
301
301
  def apply_groups(method)
302
302
  authorize! :update, model_class
303
303
 
@@ -305,9 +305,9 @@ module Spree
305
305
  group_ids = decode_ids(params[:customer_group_ids])
306
306
 
307
307
  scoped_user_ids = scope.where(id: user_ids).pluck(:id)
308
- scoped_groups = Spree::CustomerGroup.for_store(current_store).where(id: group_ids)
308
+ scoped_groups = Spree::CustomerGroup.for_store(current_store).where(id: group_ids).to_a
309
309
 
310
- scoped_groups.find_each { |group| group.public_send(method, scoped_user_ids) }
310
+ Spree::CustomerGroup.public_send(method, scoped_groups, scoped_user_ids)
311
311
 
312
312
  render json: { customer_count: scoped_user_ids.size, customer_group_count: scoped_groups.size }
313
313
  end