spree_api 5.4.3 → 5.5.0.rc2
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/Rakefile +36 -0
- data/app/controllers/concerns/spree/api/v3/admin/auth_cookies.rb +62 -0
- data/app/controllers/concerns/spree/api/v3/admin/role_grant_guard.rb +52 -0
- data/app/controllers/concerns/spree/api/v3/admin/subclassed_resource.rb +149 -0
- data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +54 -0
- data/app/controllers/concerns/spree/api/v3/bulk_operations.rb +103 -0
- data/app/controllers/concerns/spree/api/v3/channel_resolution.rb +60 -0
- data/app/controllers/concerns/spree/api/v3/error_handler.rb +4 -0
- data/app/controllers/concerns/spree/api/v3/params_normalizer.rb +84 -0
- data/app/controllers/concerns/spree/api/v3/scoped_authorization.rb +104 -0
- data/app/controllers/concerns/spree/api/v3/store/search_provider_support.rb +35 -1
- data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +109 -0
- data/app/controllers/spree/api/v3/admin/allowed_origins_controller.rb +25 -0
- data/app/controllers/spree/api/v3/admin/api_keys_controller.rb +84 -0
- data/app/controllers/spree/api/v3/admin/auth_controller.rb +134 -0
- data/app/controllers/spree/api/v3/admin/base_controller.rb +3 -17
- data/app/controllers/spree/api/v3/admin/categories_controller.rb +25 -0
- data/app/controllers/spree/api/v3/admin/channels_controller.rb +65 -0
- data/app/controllers/spree/api/v3/admin/countries_controller.rb +38 -0
- data/app/controllers/spree/api/v3/admin/coupon_codes_controller.rb +33 -0
- data/app/controllers/spree/api/v3/admin/custom_field_definitions_controller.rb +34 -0
- data/app/controllers/spree/api/v3/admin/custom_fields_controller.rb +129 -0
- data/app/controllers/spree/api/v3/admin/customer_groups_controller.rb +31 -0
- data/app/controllers/spree/api/v3/admin/customers/addresses_controller.rb +83 -0
- data/app/controllers/spree/api/v3/admin/customers/base_controller.rb +33 -0
- data/app/controllers/spree/api/v3/admin/customers/credit_cards_controller.rb +25 -0
- data/app/controllers/spree/api/v3/admin/customers/store_credits_controller.rb +92 -0
- data/app/controllers/spree/api/v3/admin/customers_controller.rb +119 -0
- data/app/controllers/spree/api/v3/admin/dashboard_controller.rb +44 -0
- data/app/controllers/spree/api/v3/admin/direct_uploads_controller.rb +40 -0
- data/app/controllers/spree/api/v3/admin/exports_controller.rb +136 -0
- data/app/controllers/spree/api/v3/admin/gift_card_batches_controller.rb +31 -0
- data/app/controllers/spree/api/v3/admin/gift_cards_controller.rb +33 -0
- data/app/controllers/spree/api/v3/admin/invitation_acceptances_controller.rb +138 -0
- data/app/controllers/spree/api/v3/admin/invitations_controller.rb +81 -0
- data/app/controllers/spree/api/v3/admin/markets_controller.rb +42 -0
- data/app/controllers/spree/api/v3/admin/me_controller.rb +69 -0
- data/app/controllers/spree/api/v3/admin/media_controller.rb +119 -0
- data/app/controllers/spree/api/v3/admin/option_types_controller.rb +34 -0
- data/app/controllers/spree/api/v3/admin/orders/adjustments_controller.rb +27 -0
- data/app/controllers/spree/api/v3/admin/orders/base_controller.rb +31 -0
- data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +104 -0
- data/app/controllers/spree/api/v3/admin/orders/gift_cards_controller.rb +79 -0
- data/app/controllers/spree/api/v3/admin/orders/items_controller.rb +92 -0
- data/app/controllers/spree/api/v3/admin/orders/payments_controller.rb +90 -0
- data/app/controllers/spree/api/v3/admin/orders/refunds_controller.rb +53 -0
- data/app/controllers/spree/api/v3/admin/orders/store_credits_controller.rb +59 -0
- data/app/controllers/spree/api/v3/admin/orders_controller.rb +190 -0
- data/app/controllers/spree/api/v3/admin/payment_methods_controller.rb +73 -0
- data/app/controllers/spree/api/v3/admin/price_lists_controller.rb +179 -0
- data/app/controllers/spree/api/v3/admin/prices_controller.rb +157 -0
- data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +48 -0
- data/app/controllers/spree/api/v3/admin/products_controller.rb +237 -0
- data/app/controllers/spree/api/v3/admin/promotion_actions_controller.rb +78 -0
- data/app/controllers/spree/api/v3/admin/promotion_rules_controller.rb +56 -0
- data/app/controllers/spree/api/v3/admin/promotions_controller.rb +78 -0
- data/app/controllers/spree/api/v3/admin/resource_controller.rb +29 -11
- data/app/controllers/spree/api/v3/admin/roles_controller.rb +29 -0
- data/app/controllers/spree/api/v3/admin/stock_items_controller.rb +35 -0
- data/app/controllers/spree/api/v3/admin/stock_locations_controller.rb +36 -0
- data/app/controllers/spree/api/v3/admin/stock_reservations_controller.rb +29 -0
- data/app/controllers/spree/api/v3/admin/stock_transfers_controller.rb +75 -0
- data/app/controllers/spree/api/v3/admin/store_controller.rb +53 -0
- data/app/controllers/spree/api/v3/admin/store_credit_categories_controller.rb +21 -0
- data/app/controllers/spree/api/v3/admin/tags_controller.rb +51 -0
- data/app/controllers/spree/api/v3/admin/tax_categories_controller.rb +21 -0
- data/app/controllers/spree/api/v3/admin/variants_controller.rb +33 -0
- data/app/controllers/spree/api/v3/admin/webhook_deliveries_controller.rb +49 -0
- data/app/controllers/spree/api/v3/admin/webhook_endpoints_controller.rb +75 -0
- data/app/controllers/spree/api/v3/resource_controller.rb +117 -8
- data/app/controllers/spree/api/v3/store/auth_controller.rb +8 -28
- data/app/controllers/spree/api/v3/store/base_controller.rb +6 -0
- data/app/controllers/spree/api/v3/store/carts_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/customers_controller.rb +6 -0
- data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +77 -0
- data/app/controllers/spree/api/v3/store/products/filters_controller.rb +2 -2
- data/app/controllers/spree/api/v3/store/products_controller.rb +4 -3
- data/app/controllers/spree/api/v3/store/resource_controller.rb +10 -2
- data/app/jobs/spree/webhook_delivery_job.rb +5 -0
- data/app/models/spree/api_key_ability.rb +16 -0
- data/app/serializers/spree/api/v3/admin/address_serializer.rb +2 -6
- data/app/serializers/spree/api/v3/admin/adjustment_serializer.rb +3 -15
- data/app/serializers/spree/api/v3/admin/admin_user_serializer.rb +19 -3
- data/app/serializers/spree/api/v3/admin/allowed_origin_serializer.rb +2 -6
- data/app/serializers/spree/api/v3/admin/api_key_serializer.rb +42 -0
- data/app/serializers/spree/api/v3/admin/category_serializer.rb +4 -3
- data/app/serializers/spree/api/v3/admin/channel_serializer.rb +15 -0
- data/app/serializers/spree/api/v3/admin/country_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/admin/coupon_code_serializer.rb +30 -0
- data/app/serializers/spree/api/v3/admin/credit_card_serializer.rb +4 -2
- data/app/serializers/spree/api/v3/admin/custom_field_definition_serializer.rb +21 -0
- data/app/serializers/spree/api/v3/admin/custom_field_serializer.rb +8 -3
- data/app/serializers/spree/api/v3/admin/customer_group_serializer.rb +27 -0
- data/app/serializers/spree/api/v3/admin/customer_serializer.rb +58 -2
- data/app/serializers/spree/api/v3/admin/dashboard_analytics_serializer.rb +143 -0
- data/app/serializers/spree/api/v3/admin/export_serializer.rb +40 -0
- data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +2 -6
- data/app/serializers/spree/api/v3/admin/{asset_serializer.rb → gift_card_batch_serializer.rb} +1 -1
- data/app/serializers/spree/api/v3/admin/gift_card_serializer.rb +39 -4
- data/app/serializers/spree/api/v3/admin/invitation_serializer.rb +64 -0
- data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +4 -16
- data/app/serializers/spree/api/v3/admin/media_serializer.rb +24 -2
- data/app/serializers/spree/api/v3/admin/option_type_serializer.rb +4 -1
- data/app/serializers/spree/api/v3/admin/option_value_serializer.rb +4 -1
- data/app/serializers/spree/api/v3/admin/order_serializer.rb +21 -6
- data/app/serializers/spree/api/v3/admin/payment_method_serializer.rb +11 -2
- data/app/serializers/spree/api/v3/admin/payment_serializer.rb +2 -6
- data/app/serializers/spree/api/v3/admin/payment_source_serializer.rb +4 -1
- data/app/serializers/spree/api/v3/admin/price_list_serializer.rb +51 -0
- data/app/serializers/spree/api/v3/admin/price_rule_serializer.rb +55 -0
- data/app/serializers/spree/api/v3/admin/price_serializer.rb +4 -0
- data/app/serializers/spree/api/v3/admin/product_publication_serializer.rb +11 -0
- data/app/serializers/spree/api/v3/admin/product_serializer.rb +34 -10
- data/app/serializers/spree/api/v3/admin/promotion_action_serializer.rb +71 -0
- data/app/serializers/spree/api/v3/admin/promotion_rule_serializer.rb +85 -0
- data/app/serializers/spree/api/v3/admin/promotion_serializer.rb +41 -0
- data/app/serializers/spree/api/v3/admin/refund_serializer.rb +4 -2
- data/app/serializers/spree/api/v3/admin/role_serializer.rb +17 -0
- data/app/serializers/spree/api/v3/admin/stock_item_serializer.rb +16 -1
- data/app/serializers/spree/api/v3/admin/stock_location_serializer.rb +11 -2
- data/app/serializers/spree/api/v3/admin/stock_reservation_serializer.rb +46 -0
- data/app/serializers/spree/api/v3/admin/stock_transfer_serializer.rb +37 -0
- data/app/serializers/spree/api/v3/admin/store_credit_category_serializer.rb +19 -0
- data/app/serializers/spree/api/v3/admin/store_credit_serializer.rb +11 -5
- data/app/serializers/spree/api/v3/admin/store_serializer.rb +55 -0
- data/app/serializers/spree/api/v3/admin/tax_category_serializer.rb +4 -2
- data/app/serializers/spree/api/v3/admin/variant_serializer.rb +37 -6
- data/app/serializers/spree/api/v3/admin/webhook_delivery_serializer.rb +45 -0
- data/app/serializers/spree/api/v3/admin/webhook_endpoint_serializer.rb +69 -0
- data/app/serializers/spree/api/v3/channel_serializer.rb +14 -0
- data/app/serializers/spree/api/v3/custom_field_serializer.rb +9 -10
- data/app/serializers/spree/api/v3/customer_serializer.rb +5 -0
- data/app/serializers/spree/api/v3/market_serializer.rb +2 -1
- data/app/serializers/spree/api/v3/media_serializer.rb +8 -6
- data/app/serializers/spree/api/v3/order_serializer.rb +6 -1
- data/app/serializers/spree/api/v3/payment_method_serializer.rb +11 -2
- data/app/serializers/spree/api/v3/product_publication_serializer.rb +22 -0
- data/app/serializers/spree/api/v3/product_serializer.rb +6 -1
- data/app/serializers/spree/api/v3/stock_reservation_serializer.rb +10 -0
- data/config/locales/en.yml +2 -0
- data/config/routes.rb +235 -1
- data/lib/spree/api/configuration.rb +2 -2
- data/lib/spree/api/dependencies.rb +25 -1
- data/lib/spree/api/openapi/path_sorter.rb +126 -0
- data/lib/spree/api/openapi/schema_helper.rb +185 -6
- data/lib/spree/api/testing_support/v3/base.rb +28 -0
- metadata +98 -8
- data/app/serializers/spree/api/v3/admin/shipping_category_serializer.rb +0 -14
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# See `docs/plans/5.5-admin-spa-csv-export.md`.
|
|
6
|
+
#
|
|
7
|
+
# There is no standalone exports scope: an export is a bulk read of
|
|
8
|
+
# the records it contains, so each export type is gated by the read
|
|
9
|
+
# scope of the exported resource (Spree::Exports::Customers =>
|
|
10
|
+
# `read_customers`; see Spree::Export.required_scope), and the index
|
|
11
|
+
# is filtered to the types the key can read.
|
|
12
|
+
class ExportsController < ResourceController
|
|
13
|
+
include ActiveStorage::SetCurrent
|
|
14
|
+
|
|
15
|
+
# The index spans many export types — `scope` filters it to the
|
|
16
|
+
# readable ones instead of gating on a single scope.
|
|
17
|
+
skip_scope_check! only: :index
|
|
18
|
+
|
|
19
|
+
# We stream the CSV inline rather than redirecting to ActiveStorage's
|
|
20
|
+
# signed-URL endpoint because the SPA's Vite proxy only forwards
|
|
21
|
+
# `/api/*`. A cross-origin redirect to `/rails/active_storage/...`
|
|
22
|
+
# strips the Authorization header and the download fails silently.
|
|
23
|
+
def download
|
|
24
|
+
@resource = find_resource
|
|
25
|
+
authorize_resource!(@resource, :show)
|
|
26
|
+
|
|
27
|
+
unless @resource.done?
|
|
28
|
+
return render_error(
|
|
29
|
+
code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:export_not_ready],
|
|
30
|
+
message: 'Export is not ready yet',
|
|
31
|
+
status: :unprocessable_content
|
|
32
|
+
)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
attachment = @resource.attachment
|
|
36
|
+
send_data(
|
|
37
|
+
attachment.download,
|
|
38
|
+
filename: attachment.filename.to_s,
|
|
39
|
+
type: attachment.content_type || 'text/csv',
|
|
40
|
+
disposition: 'attachment'
|
|
41
|
+
)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
protected
|
|
45
|
+
|
|
46
|
+
def model_class
|
|
47
|
+
Spree::Export
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def serializer_class
|
|
51
|
+
Spree.api.admin_export_serializer
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def scope_includes
|
|
55
|
+
[:user, { attachment_attachment: :blob }]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def scope
|
|
59
|
+
collection = super
|
|
60
|
+
return collection unless scope_limited_principal?
|
|
61
|
+
|
|
62
|
+
collection.where(type: readable_export_types)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Loaded by both the scope gate (before_action) and the member
|
|
66
|
+
# actions — memoize so the record is fetched once.
|
|
67
|
+
def find_resource
|
|
68
|
+
@find_resource ||= super
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Exports never mutate commerce data; creating or downloading one
|
|
72
|
+
# is a bulk read, so every action maps to the read-level scope.
|
|
73
|
+
def action_kind
|
|
74
|
+
'read'
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Unresolvable types (blank/unknown `type` on create) fall back to
|
|
78
|
+
# `:all`, so only `read_all`/`write_all` keys reach the model's own
|
|
79
|
+
# validation.
|
|
80
|
+
def scoped_resource_name
|
|
81
|
+
export_class&.required_scope || :all
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def build_resource
|
|
85
|
+
klass = resolve_export_type(permitted_params[:type]) || Spree::Export
|
|
86
|
+
attrs = permitted_params.except(:type).merge(
|
|
87
|
+
store: current_store,
|
|
88
|
+
user: try_spree_current_user
|
|
89
|
+
)
|
|
90
|
+
klass.new(attrs)
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# `search_params` carries an arbitrary Ransack hash with nested
|
|
94
|
+
# groupings (`{ g: [{ name_cont: 'foo' }] }`). Rails' `permit(k: {})`
|
|
95
|
+
# rejects nested hashes, so we extract via `to_unsafe_h`. `:format`
|
|
96
|
+
# is intentionally dropped — only CSV is supported and Rails' request
|
|
97
|
+
# format would otherwise overwrite the model's enum.
|
|
98
|
+
def permitted_params
|
|
99
|
+
attrs = params.permit(:type, :record_selection)
|
|
100
|
+
raw = params[:search_params]
|
|
101
|
+
attrs[:search_params] = raw.respond_to?(:to_unsafe_h) ? raw.to_unsafe_h : raw if raw.present?
|
|
102
|
+
attrs
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# Returns the registered Export subclass matching `name`, or nil.
|
|
106
|
+
#
|
|
107
|
+
# The constantize target comes from `available_types` (a trusted
|
|
108
|
+
# in-process registry), not from the request — `name` is only used
|
|
109
|
+
# to *select* an entry in the allowlist. This keeps the data flow
|
|
110
|
+
# from user input → trusted-string → `constantize` legible to
|
|
111
|
+
# static analyzers (CodeQL otherwise flags the inverse pattern of
|
|
112
|
+
# gating user input with `include?` before calling `constantize`).
|
|
113
|
+
def resolve_export_type(name)
|
|
114
|
+
return nil if name.blank?
|
|
115
|
+
|
|
116
|
+
target = Spree::Export.available_types.map(&:to_s).find { |t| t == name.to_s }
|
|
117
|
+
target&.constantize
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
private
|
|
121
|
+
|
|
122
|
+
def export_class
|
|
123
|
+
action_name == 'create' ? resolve_export_type(params[:type]) : find_resource.class
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def readable_export_types
|
|
127
|
+
Spree::Export.available_types.select do |type|
|
|
128
|
+
required = type.required_scope
|
|
129
|
+
required ? current_api_key.has_scope?("read_#{required}") : current_api_key.has_scope?('read_all')
|
|
130
|
+
end.map(&:to_s)
|
|
131
|
+
end
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# Admin bulk-issue endpoint for `Spree::GiftCardBatch`. Creating a
|
|
6
|
+
# batch synchronously generates the `codes_count` gift cards inline
|
|
7
|
+
# (or kicks off a background job when the count exceeds
|
|
8
|
+
# `Spree.config.gift_card_batch_web_limit`, default 500). Read-only
|
|
9
|
+
# access lives behind `list`/`show` so the SPA can surface batch
|
|
10
|
+
# context on the gift cards index (filter chip, batch chip on rows).
|
|
11
|
+
class GiftCardBatchesController < ResourceController
|
|
12
|
+
scoped_resource :gift_cards
|
|
13
|
+
|
|
14
|
+
protected
|
|
15
|
+
|
|
16
|
+
def model_class
|
|
17
|
+
Spree::GiftCardBatch
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def serializer_class
|
|
21
|
+
Spree.api.admin_gift_card_batch_serializer
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def permitted_params
|
|
25
|
+
params.permit(:prefix, :codes_count, :amount, :expires_at, :currency)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# Admin CRUD for `Spree::GiftCard`. Scoped to the current store via
|
|
6
|
+
# the model's `SingleStoreResource` include — the base controller's
|
|
7
|
+
# `scope` already applies `model_class.for_store(current_store)`.
|
|
8
|
+
#
|
|
9
|
+
# `store` and `created_by` are auto-stamped by `build_resource` in
|
|
10
|
+
# `Spree::Api::V3::ResourceController`, so create requests only need
|
|
11
|
+
# to include user-facing attributes (amount, currency, expires_at,
|
|
12
|
+
# optional code, optional user_id).
|
|
13
|
+
class GiftCardsController < ResourceController
|
|
14
|
+
scoped_resource :gift_cards
|
|
15
|
+
|
|
16
|
+
protected
|
|
17
|
+
|
|
18
|
+
def model_class
|
|
19
|
+
Spree::GiftCard
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def serializer_class
|
|
23
|
+
Spree.api.admin_gift_card_serializer
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def permitted_params
|
|
27
|
+
params.permit(:code, :amount, :expires_at, :user_id, :currency)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# Public invitation acceptance — mounted under `/api/v3/admin/auth/...`
|
|
6
|
+
# so the issued refresh-token cookie's path matches `/auth/refresh`.
|
|
7
|
+
class InvitationAcceptancesController < BaseController
|
|
8
|
+
include Spree::Api::V3::Admin::AuthCookies
|
|
9
|
+
|
|
10
|
+
skip_scope_check!
|
|
11
|
+
skip_before_action :authenticate_admin!, only: [:lookup, :accept]
|
|
12
|
+
|
|
13
|
+
rate_limit to: Spree::Api::Config[:rate_limit_login],
|
|
14
|
+
within: Spree::Api::Config[:rate_limit_window].seconds,
|
|
15
|
+
store: Rails.cache,
|
|
16
|
+
only: [:lookup, :accept],
|
|
17
|
+
with: RATE_LIMIT_RESPONSE
|
|
18
|
+
|
|
19
|
+
# GET /api/v3/admin/auth/invitations/:id/lookup?token=:token
|
|
20
|
+
def lookup
|
|
21
|
+
return unless load_invitation
|
|
22
|
+
|
|
23
|
+
render json: Spree.api.admin_invitation_serializer.new(@invitation).serializable_hash
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# POST /api/v3/admin/auth/invitations/:id/accept?token=:token
|
|
27
|
+
# Body: { password?, password_confirmation?, first_name?, last_name? }
|
|
28
|
+
def accept
|
|
29
|
+
return unless load_invitation
|
|
30
|
+
|
|
31
|
+
user = resolve_or_create_invitee(@invitation)
|
|
32
|
+
return if performed?
|
|
33
|
+
|
|
34
|
+
@invitation.invitee = user
|
|
35
|
+
@invitation.accept!
|
|
36
|
+
|
|
37
|
+
refresh_token = Spree::RefreshToken.create_for(user, request_env: request_env_for_token)
|
|
38
|
+
set_refresh_cookie(refresh_token)
|
|
39
|
+
render json: auth_response(user)
|
|
40
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
41
|
+
render_validation_error(e.record.errors)
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# Token mismatch is treated identically to "not found" to avoid
|
|
47
|
+
# leaking whether an ID exists.
|
|
48
|
+
def load_invitation
|
|
49
|
+
decoded_id = Spree::Invitation.decode_prefixed_id(params[:id])
|
|
50
|
+
@invitation = Spree::Invitation.pending.not_expired.find_by(id: decoded_id, token: params[:token])
|
|
51
|
+
|
|
52
|
+
unless @invitation
|
|
53
|
+
render_error(
|
|
54
|
+
code: ERROR_CODES[:record_not_found],
|
|
55
|
+
message: 'Invitation not found, expired, or already accepted',
|
|
56
|
+
status: :not_found
|
|
57
|
+
)
|
|
58
|
+
return false
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
true
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
# Email match between the invitation and any existing account is
|
|
65
|
+
# implicit: we look the user up by `invitation.email`, never by a
|
|
66
|
+
# client-supplied email. The token is the credential.
|
|
67
|
+
def resolve_or_create_invitee(invitation)
|
|
68
|
+
existing = Spree.admin_user_class.find_by(email: invitation.email)
|
|
69
|
+
return authenticate_existing(existing) if existing
|
|
70
|
+
|
|
71
|
+
create_new_invitee(invitation)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def authenticate_existing(user)
|
|
75
|
+
return user if user.valid_password?(params[:password].to_s)
|
|
76
|
+
|
|
77
|
+
render_error(
|
|
78
|
+
code: ERROR_CODES[:authentication_failed],
|
|
79
|
+
message: 'Invalid password',
|
|
80
|
+
status: :unauthorized
|
|
81
|
+
)
|
|
82
|
+
nil
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def create_new_invitee(invitation)
|
|
86
|
+
if params[:password].blank?
|
|
87
|
+
render_error(
|
|
88
|
+
code: ERROR_CODES[:parameter_missing],
|
|
89
|
+
message: 'Password is required to create your account',
|
|
90
|
+
status: :unprocessable_content
|
|
91
|
+
)
|
|
92
|
+
return nil
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
Spree.admin_user_class.create!(signup_params(invitation))
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def signup_params(invitation)
|
|
99
|
+
params.permit(:password, :password_confirmation, :first_name, :last_name).
|
|
100
|
+
merge(email: invitation.email)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def auth_response(user)
|
|
104
|
+
{
|
|
105
|
+
token: generate_jwt(user, audience: JWT_AUDIENCE_ADMIN),
|
|
106
|
+
user: admin_user_serializer.new(user, params: serializer_params).to_h
|
|
107
|
+
}
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def serializer_params
|
|
111
|
+
{
|
|
112
|
+
store: @invitation&.store || current_store,
|
|
113
|
+
locale: current_locale,
|
|
114
|
+
currency: current_currency,
|
|
115
|
+
user: nil,
|
|
116
|
+
includes: []
|
|
117
|
+
}
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def admin_user_serializer
|
|
121
|
+
Spree.api.admin_admin_user_serializer
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def request_env_for_token
|
|
125
|
+
{
|
|
126
|
+
ip_address: request.remote_ip,
|
|
127
|
+
user_agent: request.user_agent&.truncate(255)
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def jwt_expiration
|
|
132
|
+
Spree::Api::Config[:admin_jwt_expiration]
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# Manages staff invitations for the current store. Each invitation
|
|
6
|
+
# carries an email + role; on accept, a `Spree::RoleUser` is created
|
|
7
|
+
# via the invitation's `after_accept` callback and the invitee
|
|
8
|
+
# becomes a member of the staff list for this store.
|
|
9
|
+
class InvitationsController < ResourceController
|
|
10
|
+
include Spree::Api::V3::Admin::RoleGrantGuard
|
|
11
|
+
|
|
12
|
+
scoped_resource :settings
|
|
13
|
+
|
|
14
|
+
# POST /api/v3/admin/invitations
|
|
15
|
+
# Guards against inviting a new staff member straight into the admin
|
|
16
|
+
# super-role unless the inviter already holds it.
|
|
17
|
+
def create
|
|
18
|
+
return if reject_unauthorized_role_grant!(Array(permitted_params[:role_id]))
|
|
19
|
+
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# PATCH /api/v3/admin/invitations/:id/resend
|
|
24
|
+
# Issues a fresh token + email for an existing pending invitation.
|
|
25
|
+
# The model's `resend!` is responsible for resetting `expires_at`
|
|
26
|
+
# and dispatching the mailer.
|
|
27
|
+
def resend
|
|
28
|
+
@resource = find_resource
|
|
29
|
+
authorize!(:update, @resource)
|
|
30
|
+
|
|
31
|
+
@resource.resend!
|
|
32
|
+
render json: serialize_resource(@resource)
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
# Invitations are immutable post-create — UI calls `resend` for
|
|
36
|
+
# token rotation, `destroy` to revoke. Clearing the action set
|
|
37
|
+
# keeps the surface honest if a client ever fires PATCH directly.
|
|
38
|
+
def update
|
|
39
|
+
head :method_not_allowed
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
protected
|
|
43
|
+
|
|
44
|
+
def model_class
|
|
45
|
+
Spree::Invitation
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def serializer_class
|
|
49
|
+
Spree.api.admin_invitation_serializer
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def collection_includes
|
|
53
|
+
[:role, :inviter]
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def scope
|
|
57
|
+
Spree::Invitation.
|
|
58
|
+
where(resource: current_store).
|
|
59
|
+
accessible_by(current_ability, :show)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def build_resource
|
|
63
|
+
scope.new(permitted_params).tap do |invitation|
|
|
64
|
+
invitation.resource = current_store
|
|
65
|
+
invitation.inviter = try_spree_current_user
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# `email` and `role_id` are flat — `role_id` accepts a prefixed ID.
|
|
70
|
+
def permitted_params
|
|
71
|
+
attrs = params.permit(:email, :role_id)
|
|
72
|
+
if attrs[:role_id].present? && Spree::PrefixedId.prefixed_id?(attrs[:role_id])
|
|
73
|
+
attrs[:role_id] = Spree::PrefixedId.decode_prefixed_id(attrs[:role_id])
|
|
74
|
+
end
|
|
75
|
+
attrs
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# Admin Markets surface. Markets are store-scoped (`store_id` column +
|
|
6
|
+
# `acts_as_list scope: :store_id`), so the base ResourceController's
|
|
7
|
+
# `scope` chain narrows appropriately when we restrict to
|
|
8
|
+
# `current_store.markets`.
|
|
9
|
+
#
|
|
10
|
+
# `country_isos` and `supported_locales` are accepted as arrays on the
|
|
11
|
+
# wire and translated by model setters (`Spree::Market#country_isos=`,
|
|
12
|
+
# `#supported_locales=`).
|
|
13
|
+
class MarketsController < ResourceController
|
|
14
|
+
scoped_resource :settings
|
|
15
|
+
|
|
16
|
+
protected
|
|
17
|
+
|
|
18
|
+
def model_class
|
|
19
|
+
Spree::Market
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def serializer_class
|
|
23
|
+
Spree.api.admin_market_serializer
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def collection_includes
|
|
27
|
+
[:countries]
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def permitted_params
|
|
31
|
+
normalize_params(
|
|
32
|
+
params.permit(
|
|
33
|
+
:name, :currency, :default_locale, :tax_inclusive,
|
|
34
|
+
:default, :position, supported_locales: [], country_isos: []
|
|
35
|
+
)
|
|
36
|
+
)
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
class MeController < Admin::BaseController
|
|
6
|
+
skip_scope_check!
|
|
7
|
+
|
|
8
|
+
# GET /api/v3/admin/me
|
|
9
|
+
# Returns the current admin user along with a serialized representation
|
|
10
|
+
# of their permissions (derived from CanCanCan rules). The SPA uses
|
|
11
|
+
# the permissions list to decide which UI elements to show or hide.
|
|
12
|
+
# The actual authorization check is still enforced server-side by
|
|
13
|
+
# CanCanCan — the SPA list is purely for UX.
|
|
14
|
+
def show
|
|
15
|
+
render json: {
|
|
16
|
+
user: admin_user_serializer.new(current_user, params: serializer_params).to_h,
|
|
17
|
+
permissions: serialize_permissions(current_ability)
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
# Serializes CanCanCan's rules into a flat, JSON-safe list of permission rules.
|
|
24
|
+
#
|
|
25
|
+
# - Rule order is preserved so the frontend matcher can apply
|
|
26
|
+
# CanCanCan's "last matching rule wins" semantics.
|
|
27
|
+
# - Per-record conditions are NOT serialized (they often reference
|
|
28
|
+
# scopes or blocks that don't translate to JSON). The frontend
|
|
29
|
+
# receives `has_conditions: true` as a hint that the action might
|
|
30
|
+
# be denied at the per-record level — in practice the SPA shows
|
|
31
|
+
# the action optimistically and handles 403 from the API.
|
|
32
|
+
def serialize_permissions(ability)
|
|
33
|
+
ability.send(:rules).map do |rule|
|
|
34
|
+
{
|
|
35
|
+
allow: rule.base_behavior,
|
|
36
|
+
actions: Array(rule.actions).map(&:to_s),
|
|
37
|
+
subjects: Array(rule.subjects).map { |s| s.is_a?(Class) ? s.name : s.to_s },
|
|
38
|
+
has_conditions: rule_has_conditions?(rule)
|
|
39
|
+
}
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def rule_has_conditions?(rule)
|
|
44
|
+
return true if rule.block.present?
|
|
45
|
+
conditions = rule.conditions
|
|
46
|
+
return false if conditions.nil?
|
|
47
|
+
return !conditions.empty? if conditions.respond_to?(:empty?)
|
|
48
|
+
|
|
49
|
+
true
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def admin_user_serializer
|
|
53
|
+
Spree.api.admin_admin_user_serializer
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def serializer_params
|
|
57
|
+
{
|
|
58
|
+
store: current_store,
|
|
59
|
+
locale: current_locale,
|
|
60
|
+
currency: current_currency,
|
|
61
|
+
user: current_user,
|
|
62
|
+
includes: []
|
|
63
|
+
}
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
class MediaController < ResourceController
|
|
6
|
+
scoped_resource :products
|
|
7
|
+
|
|
8
|
+
def create
|
|
9
|
+
if permitted_params[:url].present?
|
|
10
|
+
create_from_url
|
|
11
|
+
elsif permitted_params[:signed_id].present?
|
|
12
|
+
create_from_signed_id
|
|
13
|
+
else
|
|
14
|
+
@resource = build_resource
|
|
15
|
+
authorize_resource!(@resource, :create)
|
|
16
|
+
|
|
17
|
+
if @resource.save
|
|
18
|
+
render json: serialize_resource(@resource), status: :created
|
|
19
|
+
else
|
|
20
|
+
render_validation_error(@resource.errors)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
protected
|
|
26
|
+
|
|
27
|
+
def model_class
|
|
28
|
+
Spree::Asset
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def serializer_class
|
|
32
|
+
Spree.api.admin_media_serializer
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def set_parent
|
|
36
|
+
@product = current_store.products.find_by_prefix_id!(params[:product_id])
|
|
37
|
+
authorize!(:show, @product)
|
|
38
|
+
|
|
39
|
+
@parent = if params[:variant_id].present?
|
|
40
|
+
@product.variants_including_master.find_by_prefix_id!(params[:variant_id])
|
|
41
|
+
else
|
|
42
|
+
@product
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Variants store assets via the polymorphic `images` association; products own
|
|
47
|
+
# their gallery via `media`. Both resolve to `Spree::Asset` rows with different
|
|
48
|
+
# `viewable_type` values.
|
|
49
|
+
def parent_association
|
|
50
|
+
params[:variant_id].present? ? :images : :media
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# For product-scoped listings we surface BOTH product-level assets and any
|
|
54
|
+
# legacy master-pinned assets, so existing data keeps showing up while
|
|
55
|
+
# merchants migrate. New uploads land on `Spree::Product` (see #set_parent).
|
|
56
|
+
def scope
|
|
57
|
+
return super if params[:variant_id].present?
|
|
58
|
+
|
|
59
|
+
Spree::Asset.where(
|
|
60
|
+
viewable_type: 'Spree::Product', viewable_id: @product.id
|
|
61
|
+
).or(
|
|
62
|
+
Spree::Asset.where(
|
|
63
|
+
viewable_type: 'Spree::Variant', viewable_id: @product.master&.id
|
|
64
|
+
)
|
|
65
|
+
).order(:position)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
ALLOWED_MEDIA_TYPES = -> { [Spree::Asset, *Spree::Asset.descendants].map(&:name).to_set.freeze }
|
|
69
|
+
|
|
70
|
+
def build_resource
|
|
71
|
+
media_type = permitted_params[:type] || 'Spree::Image'
|
|
72
|
+
|
|
73
|
+
unless ALLOWED_MEDIA_TYPES.call.include?(media_type)
|
|
74
|
+
raise ArgumentError, "Invalid media type: #{media_type}"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
media = @parent.send(parent_association).build(permitted_params.except(:type, :url, :signed_id))
|
|
78
|
+
media.type = media_type
|
|
79
|
+
|
|
80
|
+
media
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def permitted_params
|
|
84
|
+
params.permit(:type, :alt, :position, :attachment, :url, :signed_id, variant_ids: [])
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def create_from_url
|
|
88
|
+
authorize!(:create, Spree::Asset)
|
|
89
|
+
|
|
90
|
+
url = permitted_params[:url]
|
|
91
|
+
position = permitted_params[:position]
|
|
92
|
+
|
|
93
|
+
Spree::Images::SaveFromUrlJob.perform_later(
|
|
94
|
+
@parent.id,
|
|
95
|
+
@parent.class.name,
|
|
96
|
+
url,
|
|
97
|
+
nil,
|
|
98
|
+
position
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
head :accepted
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def create_from_signed_id
|
|
105
|
+
@resource = build_resource
|
|
106
|
+
@resource.attachment.attach(permitted_params[:signed_id])
|
|
107
|
+
authorize_resource!(@resource, :create)
|
|
108
|
+
|
|
109
|
+
if @resource.save
|
|
110
|
+
render json: serialize_resource(@resource), status: :created
|
|
111
|
+
else
|
|
112
|
+
render_validation_error(@resource.errors)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
class OptionTypesController < ResourceController
|
|
6
|
+
scoped_resource :products
|
|
7
|
+
|
|
8
|
+
protected
|
|
9
|
+
|
|
10
|
+
def model_class
|
|
11
|
+
Spree::OptionType
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def serializer_class
|
|
15
|
+
Spree.api.admin_option_type_serializer
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def scope_includes
|
|
19
|
+
[:option_values]
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def permitted_params
|
|
23
|
+
params.permit(
|
|
24
|
+
:name, :label, :position, :filterable, :kind,
|
|
25
|
+
option_values: [
|
|
26
|
+
:id, :name, :label, :position, :color_code, :image
|
|
27
|
+
]
|
|
28
|
+
)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|