spree_api 6.0.0.rc5 → 6.0.0.rc7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/app/controllers/concerns/spree/api/v3/admin/auth_cookies.rb +23 -0
- data/app/controllers/concerns/spree/api/v3/admin/refund_authorization.rb +26 -0
- data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +1 -0
- data/app/controllers/concerns/spree/api/v3/api_key_authentication.rb +14 -2
- data/app/controllers/concerns/spree/api/v3/cart_resolvable.rb +15 -2
- data/app/controllers/concerns/spree/api/v3/digital_assets_resource.rb +68 -0
- data/app/controllers/concerns/spree/api/v3/error_handler.rb +3 -0
- data/app/controllers/concerns/spree/api/v3/jwt_authentication.rb +27 -1
- data/app/controllers/concerns/spree/api/v3/orders/fulfillment_actions.rb +6 -1
- data/app/controllers/spree/api/v3/admin/allowed_origins_controller.rb +4 -1
- data/app/controllers/spree/api/v3/admin/auth_controller.rb +32 -5
- data/app/controllers/spree/api/v3/admin/commission_lines_controller.rb +2 -2
- data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +4 -7
- data/app/controllers/spree/api/v3/admin/customer_groups_controller.rb +4 -0
- data/app/controllers/spree/api/v3/admin/customers/store_credits_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/customers_controller.rb +13 -8
- data/app/controllers/spree/api/v3/admin/delivery_profiles/origin_groups_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/delivery_profiles_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/digital_links_controller.rb +3 -1
- data/app/controllers/spree/api/v3/admin/invitation_acceptances_controller.rb +2 -0
- data/app/controllers/spree/api/v3/admin/me_controller.rb +2 -0
- data/app/controllers/spree/api/v3/admin/option_types_controller.rb +13 -0
- data/app/controllers/spree/api/v3/admin/orders/claims_controller.rb +2 -0
- data/app/controllers/spree/api/v3/admin/orders/exchanges_controller.rb +9 -0
- data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/orders/payments_controller.rb +7 -4
- data/app/controllers/spree/api/v3/admin/orders/returns_controller.rb +2 -0
- data/app/controllers/spree/api/v3/admin/orders_controller.rb +10 -2
- data/app/controllers/spree/api/v3/admin/password_resets_controller.rb +7 -1
- data/app/controllers/spree/api/v3/admin/price_lists_controller.rb +18 -0
- data/app/controllers/spree/api/v3/admin/product_types_controller.rb +12 -1
- data/app/controllers/spree/api/v3/admin/products/digital_assets_controller.rb +5 -109
- data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +4 -1
- data/app/controllers/spree/api/v3/admin/products_controller.rb +28 -3
- data/app/controllers/spree/api/v3/admin/sellers/payouts_controller.rb +4 -3
- data/app/controllers/spree/api/v3/admin/stock_transfers_controller.rb +3 -2
- data/app/controllers/spree/api/v3/admin/store_controller.rb +7 -0
- data/app/controllers/spree/api/v3/admin/tags_controller.rb +11 -9
- data/app/controllers/spree/api/v3/admin/webhook_endpoints_controller.rb +5 -2
- data/app/controllers/spree/api/v3/resource_controller.rb +15 -2
- data/app/controllers/spree/api/v3/seller/auth_controller.rb +10 -0
- data/app/controllers/spree/api/v3/seller/invitation_acceptances_controller.rb +2 -0
- data/app/controllers/spree/api/v3/seller/me_controller.rb +4 -0
- data/app/controllers/spree/api/v3/seller/orders_controller.rb +11 -3
- data/app/controllers/spree/api/v3/seller/password_resets_controller.rb +5 -1
- data/app/controllers/spree/api/v3/seller/products/digital_assets_controller.rb +67 -0
- data/app/controllers/spree/api/v3/seller/products_controller.rb +4 -2
- data/app/controllers/spree/api/v3/seller/team_controller.rb +1 -1
- data/app/controllers/spree/api/v3/store/carts/store_credits_controller.rb +9 -1
- data/app/controllers/spree/api/v3/store/carts_controller.rb +14 -0
- data/app/controllers/spree/api/v3/store/customers_controller.rb +13 -1
- data/app/controllers/spree/api/v3/store/delivery_methods_controller.rb +6 -1
- data/app/controllers/spree/api/v3/store/products/filters_controller.rb +19 -4
- data/app/request_schemas/spree/api/v3/digital_asset_request_schema.rb +25 -0
- data/app/serializers/concerns/spree/api/v3/digital_asset_details.rb +70 -0
- data/app/serializers/spree/api/v3/admin/delivery_origin_group_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/delivery_profile_serializer.rb +3 -5
- data/app/serializers/spree/api/v3/admin/digital_asset_serializer.rb +3 -47
- data/app/serializers/spree/api/v3/admin/newsletter_subscriber_serializer.rb +1 -0
- data/app/serializers/spree/api/v3/admin/price_history_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/price_list_serializer.rb +8 -3
- data/app/serializers/spree/api/v3/admin/price_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/seller_payout_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/admin/seller_serializer.rb +1 -3
- data/app/serializers/spree/api/v3/admin/store_credit_serializer.rb +3 -5
- data/app/serializers/spree/api/v3/base_serializer.rb +6 -1
- data/app/serializers/spree/api/v3/newsletter_subscriber_serializer.rb +5 -1
- data/app/serializers/spree/api/v3/seller/digital_asset_serializer.rb +16 -0
- data/app/serializers/spree/api/v3/seller/payout_serializer.rb +2 -2
- data/app/serializers/spree/api/v3/seller/store_serializer.rb +18 -0
- data/app/services/spree/api/v3/filters_aggregator.rb +63 -18
- data/config/locales/en.yml +1 -0
- data/config/routes.rb +3 -0
- data/lib/spree/api/dependencies.rb +2 -2
- data/lib/spree/api/engine.rb +6 -0
- data/lib/spree/api/middleware/no_index.rb +33 -0
- data/lib/spree/api/openapi/schema_helper.rb +2 -1
- data/lib/spree/api/testing_support/v3/base.rb +18 -0
- metadata +14 -6
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 982115017cd530a3474eddb5fadd2ab6bd770236d449c9a4b93488d45c485c16
|
|
4
|
+
data.tar.gz: c333427d0e3dc56730671eb612f44f4ced415f790f5eb4557c6cf2f7da5f6d34
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: c5cff8de77ced4e66a5f935e7dd3fed912cb9b8141e0675ae6014fa594022d4db451ba0ae9ef4673aebb41d70b2592529ebf858fab1cdf94e476568b72ef0ab6
|
|
7
|
+
data.tar.gz: 2c71f0f8d3667f4e9e3ab61c7024d534abfb765a8c56f23daee05430fac53b303dac6463021f7e24c5cf690eba6226ef1c2824b1eb66cd665c4ee572214505f3
|
|
@@ -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
|
|
@@ -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.
|
|
@@ -50,8 +50,15 @@ module Spree
|
|
|
50
50
|
# Render the cart as JSON using the cart serializer. Rendering never
|
|
51
51
|
# mutates: an action that wants unbuyable lines swept calls
|
|
52
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.
|
|
53
57
|
def render_cart(status: :ok)
|
|
54
|
-
|
|
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
|
|
55
62
|
end
|
|
56
63
|
|
|
57
64
|
# Drops lines the customer can no longer buy — a discontinued product,
|
|
@@ -80,7 +87,13 @@ module Spree
|
|
|
80
87
|
# storefront that knows nothing about groups still finds every order it
|
|
81
88
|
# needs there.
|
|
82
89
|
def render_order(status: :ok)
|
|
83
|
-
|
|
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!
|
|
84
97
|
|
|
85
98
|
render json: purchase_serializer_for(record).new(record, params: serializer_params).to_h, status: status
|
|
86
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
|
-
|
|
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:
|
|
81
|
+
data: fulfillments.map { |fulfillment| serialize_resource(fulfillment) }
|
|
77
82
|
}
|
|
78
83
|
else
|
|
79
84
|
render_validation_error(changer.errors)
|
|
@@ -3,7 +3,10 @@ module Spree
|
|
|
3
3
|
module V3
|
|
4
4
|
module Admin
|
|
5
5
|
class AllowedOriginsController < ResourceController
|
|
6
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
27
|
-
|
|
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
|
|
@@ -66,18 +66,15 @@ module Spree
|
|
|
66
66
|
# no-op), so the store boundary for customer parents is enforced by
|
|
67
67
|
# the `authorize!(:update, @parent)` ability check above.
|
|
68
68
|
#
|
|
69
|
-
#
|
|
70
|
-
#
|
|
71
|
-
# store's records.
|
|
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.
|
|
72
72
|
def parent_relation
|
|
73
73
|
klass = parent_lookup.klass
|
|
74
74
|
return klass.where(id: current_store.id) if klass <= Spree::Store
|
|
75
75
|
return klass if klass <= Spree.customer_class
|
|
76
76
|
|
|
77
|
-
|
|
78
|
-
raise ActiveRecord::RecordNotFound, 'Parent resource not found' if relation.equal?(klass)
|
|
79
|
-
|
|
80
|
-
relation
|
|
77
|
+
klass.for_store(current_store)
|
|
81
78
|
end
|
|
82
79
|
|
|
83
80
|
# Product-family parents have no scope pair of their own — their
|
|
@@ -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 =
|
|
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
|
-
|
|
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(:
|
|
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(:
|
|
187
|
+
apply_groups(:remove_customers_from)
|
|
184
188
|
end
|
|
185
189
|
|
|
186
190
|
protected
|
|
@@ -291,8 +295,9 @@ module Spree
|
|
|
291
295
|
end
|
|
292
296
|
|
|
293
297
|
# Authorises bulk group mutation, decodes prefixed IDs, then dispatches
|
|
294
|
-
# to `
|
|
295
|
-
# counts of records actually affected so the UI can
|
|
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.
|
|
296
301
|
def apply_groups(method)
|
|
297
302
|
authorize! :update, model_class
|
|
298
303
|
|
|
@@ -300,9 +305,9 @@ module Spree
|
|
|
300
305
|
group_ids = decode_ids(params[:customer_group_ids])
|
|
301
306
|
|
|
302
307
|
scoped_user_ids = scope.where(id: user_ids).pluck(:id)
|
|
303
|
-
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
|
|
304
309
|
|
|
305
|
-
|
|
310
|
+
Spree::CustomerGroup.public_send(method, scoped_groups, scoped_user_ids)
|
|
306
311
|
|
|
307
312
|
render json: { customer_count: scoped_user_ids.size, customer_group_count: scoped_groups.size }
|
|
308
313
|
end
|
|
@@ -5,7 +5,9 @@ module Spree
|
|
|
5
5
|
class DigitalLinksController < ResourceController
|
|
6
6
|
scoped_resource :orders
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
# Redeclaring `set_resource` replaces the base class's filter rather
|
|
9
|
+
# than extending it, so every routed member action is listed here.
|
|
10
|
+
before_action :set_resource, only: [:show, :reset]
|
|
9
11
|
|
|
10
12
|
# PATCH /api/v3/admin/digital_links/:id/reset
|
|
11
13
|
#
|
|
@@ -37,6 +37,8 @@ module Spree
|
|
|
37
37
|
|
|
38
38
|
user = resolve_or_create_invitee(@invitation)
|
|
39
39
|
return if performed?
|
|
40
|
+
# Checked before accepting: a deactivated account gains no role.
|
|
41
|
+
return render_account_deactivated if account_deactivated?(user)
|
|
40
42
|
|
|
41
43
|
@invitation.invitee = user
|
|
42
44
|
result = Spree.invitation_accept_workflow.call(invitation: @invitation)
|
|
@@ -103,6 +103,8 @@ module Spree
|
|
|
103
103
|
# role would both disclose more than was handed over and mislead a
|
|
104
104
|
# client that reads /me to decide what to attempt.
|
|
105
105
|
def me_response
|
|
106
|
+
ActiveRecord::Associations::Preloader.new(records: [current_user], associations: { spree_roles: :resource }).call
|
|
107
|
+
|
|
106
108
|
{
|
|
107
109
|
user: admin_user_serializer.new(current_user, params: serializer_params).to_h,
|
|
108
110
|
permissions: serialize_permissions(current_ability),
|
|
@@ -42,6 +42,19 @@ module Spree
|
|
|
42
42
|
[:option_values]
|
|
43
43
|
end
|
|
44
44
|
|
|
45
|
+
# Values created or changed by this write were never loaded with
|
|
46
|
+
# their swatches, so fetch every value's image in one query before
|
|
47
|
+
# rendering.
|
|
48
|
+
def serialize_resource(resource)
|
|
49
|
+
if expand_list.include?('option_values')
|
|
50
|
+
ActiveRecord::Associations::Preloader.new(
|
|
51
|
+
records: resource.option_values.to_a,
|
|
52
|
+
associations: { image_attachment: :blob }
|
|
53
|
+
).call
|
|
54
|
+
end
|
|
55
|
+
super
|
|
56
|
+
end
|
|
57
|
+
|
|
45
58
|
def permitted_params
|
|
46
59
|
params.permit(*permitted_attributes)
|
|
47
60
|
end
|
|
@@ -6,6 +6,7 @@ module Spree
|
|
|
6
6
|
# Claims on a completed order.
|
|
7
7
|
class ClaimsController < BaseController
|
|
8
8
|
include Spree::Api::V3::Orders::ClaimActions
|
|
9
|
+
include Spree::Api::V3::Admin::RefundAuthorization
|
|
9
10
|
|
|
10
11
|
# Claims are a subject of the `orders` catalog resource, so
|
|
11
12
|
# `read_orders`/`write_orders` gate these endpoints. `:claims`
|
|
@@ -51,6 +52,7 @@ module Spree
|
|
|
51
52
|
end
|
|
52
53
|
|
|
53
54
|
before_action :set_resource, only: [:show, :update, :approve, :resolve, :deny, :cancel]
|
|
55
|
+
before_action :require_refund_permission!, only: :resolve, if: -> { params[:resolution].to_s.include?('refund') }
|
|
54
56
|
|
|
55
57
|
# PATCH /api/v3/admin/orders/:order_id/claims/:id
|
|
56
58
|
#
|
|
@@ -7,6 +7,7 @@ module Spree
|
|
|
7
7
|
# member action, because each carries different arguments.
|
|
8
8
|
class ExchangesController < BaseController
|
|
9
9
|
include Spree::Api::V3::Orders::ExchangeActions
|
|
10
|
+
include Spree::Api::V3::Admin::RefundAuthorization
|
|
10
11
|
|
|
11
12
|
# Exchanges are a subject of the `orders` catalog resource, so
|
|
12
13
|
# `read_orders`/`write_orders` gate these endpoints. `:exchanges`
|
|
@@ -49,6 +50,14 @@ module Spree
|
|
|
49
50
|
end
|
|
50
51
|
|
|
51
52
|
before_action :set_resource, only: [:show, :update, :approve, :receive, :fulfill, :cancel]
|
|
53
|
+
# Fulfilling settles the difference on the units that actually came
|
|
54
|
+
# back, a credit to the customer when the replacements cost less.
|
|
55
|
+
# That figure is only known once the tax rows are rewritten for
|
|
56
|
+
# those units, inside the fulfillment itself, so the gate cannot be
|
|
57
|
+
# narrowed on the announced difference: a line received at zero
|
|
58
|
+
# units would offset a credit still paid on another. Every
|
|
59
|
+
# fulfillment settles money, so all of them take the refund authority.
|
|
60
|
+
before_action :require_refund_permission!, only: :fulfill
|
|
52
61
|
|
|
53
62
|
# PATCH /api/v3/admin/orders/:order_id/exchanges/:id
|
|
54
63
|
#
|
|
@@ -186,7 +186,7 @@ module Spree
|
|
|
186
186
|
def delivery_method_for_create
|
|
187
187
|
return if create_params[:delivery_method_id].blank?
|
|
188
188
|
|
|
189
|
-
|
|
189
|
+
current_store.delivery_methods.accessible_by(current_ability, :show).find_by_prefix_id!(create_params[:delivery_method_id])
|
|
190
190
|
end
|
|
191
191
|
|
|
192
192
|
def source_fulfillment_for_create
|
|
@@ -28,10 +28,13 @@ module Spree
|
|
|
28
28
|
def create
|
|
29
29
|
with_order_lock do
|
|
30
30
|
payment_method = current_store.payment_methods.accessible_by(current_ability, :show).find_by_prefix_id!(params[:payment_method_id])
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
)
|
|
31
|
+
# The default is what the order still owes, which is nothing
|
|
32
|
+
# once store credit covers it — a zero payment is refused the
|
|
33
|
+
# same way a zero amount is.
|
|
34
|
+
amount = money_param(:amount, @parent.currency) || @parent.order_total_after_store_credit
|
|
35
|
+
raise Spree::Money::InvalidFormat.new('must be greater than 0', field: :amount) unless amount.positive?
|
|
36
|
+
|
|
37
|
+
@resource = @parent.payments.build(amount: amount, payment_method: payment_method)
|
|
35
38
|
|
|
36
39
|
if params[:source_id].present? && payment_method.source_required?
|
|
37
40
|
@resource.source = find_source!(payment_method, params[:source_id])
|