spree_api 5.5.3 → 5.6.0.rc1
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/role_grant_guard.rb +3 -1
- data/app/controllers/concerns/spree/api/v3/admin_authentication.rb +1 -1
- data/app/controllers/concerns/spree/api/v3/channel_resolution.rb +4 -2
- data/app/controllers/concerns/spree/api/v3/locale_and_currency.rb +4 -1
- data/app/controllers/concerns/spree/api/v3/storefront_gating.rb +65 -0
- data/app/controllers/spree/api/v3/admin/admin_users_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/categories/products_controller.rb +98 -0
- data/app/controllers/spree/api/v3/admin/categories_controller.rb +102 -2
- data/app/controllers/spree/api/v3/admin/customers_controller.rb +8 -2
- data/app/controllers/spree/api/v3/admin/exports_controller.rb +3 -1
- data/app/controllers/spree/api/v3/admin/import_rows_controller.rb +49 -0
- data/app/controllers/spree/api/v3/admin/imports_controller.rb +274 -0
- data/app/controllers/spree/api/v3/admin/locales_controller.rb +29 -0
- data/app/controllers/spree/api/v3/admin/me_controller.rb +39 -9
- data/app/controllers/spree/api/v3/admin/orders/fulfillments_controller.rb +62 -2
- data/app/controllers/spree/api/v3/admin/password_resets_controller.rb +105 -0
- data/app/controllers/spree/api/v3/admin/payment_methods_controller.rb +1 -8
- data/app/controllers/spree/api/v3/admin/products/variants_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/products_controller.rb +8 -7
- data/app/controllers/spree/api/v3/admin/promotions_controller.rb +1 -1
- data/app/controllers/spree/api/v3/admin/resource_controller.rb +31 -0
- data/app/controllers/spree/api/v3/admin/store_controller.rb +3 -0
- data/app/controllers/spree/api/v3/admin/translatable_resources_controller.rb +38 -0
- data/app/controllers/spree/api/v3/admin/translations/batches_controller.rb +113 -0
- data/app/controllers/spree/api/v3/admin/translations_controller.rb +106 -0
- data/app/controllers/spree/api/v3/resource_controller.rb +0 -2
- data/app/controllers/spree/api/v3/store/auth_controller.rb +3 -1
- data/app/controllers/spree/api/v3/store/base_controller.rb +5 -0
- data/app/controllers/spree/api/v3/store/carts/fulfillments_controller.rb +1 -3
- data/app/controllers/spree/api/v3/store/carts_controller.rb +4 -0
- data/app/controllers/spree/api/v3/store/countries_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/currencies_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/customer/password_resets_controller.rb +5 -1
- data/app/controllers/spree/api/v3/store/customers_controller.rb +3 -1
- data/app/controllers/spree/api/v3/store/digitals_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/locales_controller.rb +3 -8
- data/app/controllers/spree/api/v3/store/markets/countries_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/markets_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/newsletter_subscribers_controller.rb +1 -0
- data/app/controllers/spree/api/v3/store/products/filters_controller.rb +5 -2
- data/app/controllers/spree/api/v3/store/products_controller.rb +1 -1
- data/app/controllers/spree/api/v3/store/resource_controller.rb +1 -0
- data/app/serializers/concerns/spree/api/v3/admin/translatable.rb +38 -0
- data/app/serializers/spree/api/v3/admin/admin_user_serializer.rb +7 -1
- data/app/serializers/spree/api/v3/admin/category_serializer.rb +5 -3
- data/app/serializers/spree/api/v3/admin/channel_serializer.rb +5 -1
- data/app/serializers/spree/api/v3/admin/customer_serializer.rb +7 -2
- data/app/serializers/spree/api/v3/admin/discount_serializer.rb +4 -0
- data/app/serializers/spree/api/v3/admin/fulfillment_serializer.rb +9 -0
- data/app/serializers/spree/api/v3/admin/gift_card_serializer.rb +7 -0
- data/app/serializers/spree/api/v3/admin/import_mapping_serializer.rb +20 -0
- data/app/serializers/spree/api/v3/admin/import_row_serializer.rb +17 -0
- data/app/serializers/spree/api/v3/admin/import_serializer.rb +70 -0
- data/app/serializers/spree/api/v3/admin/line_item_serializer.rb +11 -0
- data/app/serializers/spree/api/v3/admin/option_type_serializer.rb +2 -0
- data/app/serializers/spree/api/v3/admin/order_serializer.rb +15 -1
- data/app/serializers/spree/api/v3/admin/payment_serializer.rb +4 -0
- data/app/serializers/spree/api/v3/admin/product_serializer.rb +1 -0
- data/app/serializers/spree/api/v3/admin/promotion_rule_serializer.rb +8 -0
- data/app/serializers/spree/api/v3/admin/resource_translations_serializer.rb +76 -0
- data/app/serializers/spree/api/v3/admin/setup_task_serializer.rb +18 -0
- data/app/serializers/spree/api/v3/admin/store_serializer.rb +27 -1
- data/app/serializers/spree/api/v3/admin/variant_serializer.rb +8 -1
- data/app/serializers/spree/api/v3/base_serializer.rb +13 -0
- data/app/serializers/spree/api/v3/cart_serializer.rb +25 -22
- data/app/serializers/spree/api/v3/discount_serializer.rb +6 -2
- data/app/serializers/spree/api/v3/fulfillment_serializer.rb +16 -13
- data/app/serializers/spree/api/v3/gift_card_serializer.rb +15 -11
- data/app/serializers/spree/api/v3/import_row_serializer.rb +1 -1
- data/app/serializers/spree/api/v3/line_item_serializer.rb +35 -17
- data/app/serializers/spree/api/v3/locale_serializer.rb +6 -1
- data/app/serializers/spree/api/v3/order_serializer.rb +25 -21
- data/app/serializers/spree/api/v3/payment_serializer.rb +6 -2
- data/app/serializers/spree/api/v3/product_serializer.rb +9 -1
- data/app/serializers/spree/api/v3/variant_serializer.rb +12 -1
- data/app/services/spree/api/v3/filters_aggregator.rb +4 -4
- data/app/subscribers/spree/webhook_event_subscriber.rb +6 -0
- data/config/initializers/typelizer.rb +10 -0
- data/config/locales/en.yml +4 -0
- data/config/routes.rb +54 -4
- data/lib/spree/api/dependencies.rb +5 -0
- data/lib/spree/api/openapi/schema_helper.rb +29 -0
- metadata +21 -6
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# See `docs/plans/5.6-admin-spa-csv-import.md`.
|
|
6
|
+
#
|
|
7
|
+
# There is no standalone imports scope: an import is a bulk write of
|
|
8
|
+
# the records it creates, so every action — including reads, which
|
|
9
|
+
# expose the uploaded data — maps to the write scope of the imported
|
|
10
|
+
# resource (Spree::Imports::Customers => `write_customers`; see
|
|
11
|
+
# Spree::Import.required_scope). The index is filtered to the types
|
|
12
|
+
# the key can write.
|
|
13
|
+
class ImportsController < ResourceController
|
|
14
|
+
include ActiveStorage::SetCurrent
|
|
15
|
+
|
|
16
|
+
# The index spans many import types — `scope` filters it to the
|
|
17
|
+
# writable ones instead of gating on a single scope.
|
|
18
|
+
skip_scope_check! only: :index
|
|
19
|
+
|
|
20
|
+
# POST /api/v3/admin/imports
|
|
21
|
+
#
|
|
22
|
+
# `attachment` is an ActiveStorage signed blob id obtained from
|
|
23
|
+
# POST /api/v3/admin/direct_uploads. On success the import advances
|
|
24
|
+
# straight into `mapping` (auto-assigning file columns from the CSV
|
|
25
|
+
# headers), so the response already carries the mapping payload.
|
|
26
|
+
def create
|
|
27
|
+
@resource = build_resource
|
|
28
|
+
authorize_resource!(@resource, :create)
|
|
29
|
+
|
|
30
|
+
if @resource.save
|
|
31
|
+
begin
|
|
32
|
+
@resource.start_mapping!
|
|
33
|
+
rescue ::CSV::MalformedCSVError, EncodingError => e
|
|
34
|
+
@resource.update_columns(status: 'failed', processing_errors: e.message, updated_at: Time.current)
|
|
35
|
+
return render_error(
|
|
36
|
+
code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:validation_error],
|
|
37
|
+
message: "Could not parse CSV: #{e.message}",
|
|
38
|
+
status: :unprocessable_content
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
render json: serialize_resource(@resource), status: :created
|
|
43
|
+
else
|
|
44
|
+
render_errors(@resource.errors)
|
|
45
|
+
end
|
|
46
|
+
rescue ActiveSupport::MessageVerifier::InvalidSignature
|
|
47
|
+
render_error(
|
|
48
|
+
code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:validation_error],
|
|
49
|
+
message: 'Invalid attachment signed id',
|
|
50
|
+
status: :unprocessable_content
|
|
51
|
+
)
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# PATCH /api/v3/admin/imports/:id/complete_mapping
|
|
55
|
+
#
|
|
56
|
+
# Applies the submitted mappings atomically, then transitions out of
|
|
57
|
+
# mapping (which enqueues row creation + processing). 422 when
|
|
58
|
+
# required schema fields remain unmapped or a file column is
|
|
59
|
+
# assigned twice.
|
|
60
|
+
def complete_mapping
|
|
61
|
+
@resource = find_resource
|
|
62
|
+
authorize_resource!(@resource, :update)
|
|
63
|
+
|
|
64
|
+
unless @resource.mapping?
|
|
65
|
+
return render_error(
|
|
66
|
+
code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:validation_error],
|
|
67
|
+
message: 'Import is not in the mapping state',
|
|
68
|
+
status: :unprocessable_content
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
apply_mappings!(@resource)
|
|
73
|
+
|
|
74
|
+
if @resource.mapping_done?
|
|
75
|
+
@resource.complete_mapping!
|
|
76
|
+
render json: serialize_resource(@resource)
|
|
77
|
+
else
|
|
78
|
+
missing = @resource.required_fields - @resource.mappings.mapped.pluck(:schema_field)
|
|
79
|
+
render_error(
|
|
80
|
+
code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:validation_error],
|
|
81
|
+
message: "Required fields are not mapped: #{missing.join(', ')}",
|
|
82
|
+
status: :unprocessable_content,
|
|
83
|
+
details: { missing_required_fields: missing }
|
|
84
|
+
)
|
|
85
|
+
end
|
|
86
|
+
rescue ActiveRecord::RecordInvalid => e
|
|
87
|
+
render_validation_error(e.record.errors)
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# PATCH /api/v3/admin/imports/:id/retry_failed_rows
|
|
91
|
+
#
|
|
92
|
+
# Re-dispatches processing over the rows still `failed` (the
|
|
93
|
+
# dispatcher's pending_and_failed scope picks them up). 422 unless
|
|
94
|
+
# the import is `completed` with failures.
|
|
95
|
+
def retry_failed_rows
|
|
96
|
+
@resource = find_resource
|
|
97
|
+
authorize_resource!(@resource, :update)
|
|
98
|
+
|
|
99
|
+
if @resource.retry_failed_rows
|
|
100
|
+
render json: serialize_resource(@resource)
|
|
101
|
+
else
|
|
102
|
+
render_error(
|
|
103
|
+
code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:validation_error],
|
|
104
|
+
message: 'Import has no failed rows to retry',
|
|
105
|
+
status: :unprocessable_content
|
|
106
|
+
)
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# GET /api/v3/admin/imports/:id/download
|
|
111
|
+
#
|
|
112
|
+
# Streams the originally uploaded CSV — the audit trail for what
|
|
113
|
+
# was actually imported. Same inline-streaming rationale as
|
|
114
|
+
# ExportsController#download: a signed ActiveStorage URL neither
|
|
115
|
+
# survives the SPA's `/api/*`-only dev proxy nor carries the JWT.
|
|
116
|
+
def download
|
|
117
|
+
@resource = find_resource
|
|
118
|
+
authorize_resource!(@resource, :show)
|
|
119
|
+
|
|
120
|
+
attachment = @resource.attachment
|
|
121
|
+
unless attachment.attached?
|
|
122
|
+
return render_error(
|
|
123
|
+
code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:validation_error],
|
|
124
|
+
message: 'Import has no attached file',
|
|
125
|
+
status: :unprocessable_content
|
|
126
|
+
)
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
send_data(
|
|
130
|
+
attachment.download,
|
|
131
|
+
filename: attachment.filename.to_s,
|
|
132
|
+
type: attachment.content_type || 'text/csv',
|
|
133
|
+
disposition: 'attachment'
|
|
134
|
+
)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# GET /api/v3/admin/imports/template?type=Spree::Imports::Products
|
|
138
|
+
#
|
|
139
|
+
# CSV header row for the type's schema (including the metafield
|
|
140
|
+
# columns available for the model) — the "Download template" link
|
|
141
|
+
# in the admin dashboard.
|
|
142
|
+
def template
|
|
143
|
+
klass = resolve_import_type(params[:type])
|
|
144
|
+
|
|
145
|
+
unless klass
|
|
146
|
+
return render_error(
|
|
147
|
+
code: Spree::Api::V3::ErrorHandler::ERROR_CODES[:validation_error],
|
|
148
|
+
message: 'Unknown import type',
|
|
149
|
+
status: :unprocessable_content
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
headers = klass.new.schema_fields.map { |field| field[:name] }
|
|
154
|
+
send_data ::CSV.generate_line(headers),
|
|
155
|
+
filename: "#{klass.name.demodulize.underscore}_import_template.csv",
|
|
156
|
+
type: 'text/csv',
|
|
157
|
+
disposition: 'attachment'
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
protected
|
|
161
|
+
|
|
162
|
+
def model_class
|
|
163
|
+
Spree::Import
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def serializer_class
|
|
167
|
+
Spree.api.admin_import_serializer
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def scope_includes
|
|
171
|
+
[:user, :mappings, { attachment_attachment: :blob }]
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def scope
|
|
175
|
+
collection = super
|
|
176
|
+
return collection unless scope_limited_principal?
|
|
177
|
+
|
|
178
|
+
collection.where(type: writable_import_types)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
# Loaded by both the scope gate (before_action) and the member
|
|
182
|
+
# actions — memoize so the record is fetched once.
|
|
183
|
+
def find_resource
|
|
184
|
+
@find_resource ||= super
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
# Every action on an import maps to the write scope of the imported
|
|
188
|
+
# resource: creating is a bulk write, and status/rows expose the
|
|
189
|
+
# uploaded data, so read-only keys get nothing.
|
|
190
|
+
def action_kind
|
|
191
|
+
'write'
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
# Unresolvable types (blank/unknown `type` on create/template) fall
|
|
195
|
+
# back to `:all`, so only `write_all` keys reach the model's own
|
|
196
|
+
# validation.
|
|
197
|
+
def scoped_resource_name
|
|
198
|
+
import_class&.required_scope || :all
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def build_resource
|
|
202
|
+
klass = resolve_import_type(permitted_params[:type]) || Spree::Import
|
|
203
|
+
attrs = permitted_params.except(:type).merge(
|
|
204
|
+
owner: current_store,
|
|
205
|
+
user: acting_admin_user
|
|
206
|
+
)
|
|
207
|
+
# The done email links back to results_url — allowed origins only.
|
|
208
|
+
attrs[:results_url] = validated_allowed_origin_url(attrs[:results_url])
|
|
209
|
+
klass.new(attrs)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def permitted_params
|
|
213
|
+
params.permit(:type, :attachment, :preferred_delimiter, :results_url)
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Returns the registered Import subclass matching `name`, or nil.
|
|
217
|
+
#
|
|
218
|
+
# The constantize target comes from `available_types` (a trusted
|
|
219
|
+
# in-process registry), not from the request — `name` is only used
|
|
220
|
+
# to *select* an entry in the allowlist (same pattern as
|
|
221
|
+
# ExportsController#resolve_export_type).
|
|
222
|
+
def resolve_import_type(name)
|
|
223
|
+
return nil if name.blank?
|
|
224
|
+
|
|
225
|
+
target = Spree::Import.available_types.map(&:to_s).find { |t| t == name.to_s }
|
|
226
|
+
target&.constantize
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
private
|
|
230
|
+
|
|
231
|
+
# Imports require a user: row processors resolve records through
|
|
232
|
+
# `import.current_ability`. JWT requests use the signed-in admin;
|
|
233
|
+
# API-key requests attribute to the key's creator when that is an
|
|
234
|
+
# admin user, otherwise the model's presence validation renders 422.
|
|
235
|
+
def acting_admin_user
|
|
236
|
+
return try_spree_current_user if try_spree_current_user
|
|
237
|
+
|
|
238
|
+
created_by = current_api_key&.created_by
|
|
239
|
+
created_by if created_by.is_a?(Spree.admin_user_class)
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def import_class
|
|
243
|
+
case action_name
|
|
244
|
+
when 'create', 'template' then resolve_import_type(params[:type])
|
|
245
|
+
else find_resource.class
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def writable_import_types
|
|
250
|
+
Spree::Import.available_types.select do |type|
|
|
251
|
+
required = type.required_scope
|
|
252
|
+
required ? current_api_key.has_scope?("write_#{required}") : current_api_key.has_scope?('write_all')
|
|
253
|
+
end.map(&:to_s)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
def apply_mappings!(import)
|
|
257
|
+
submitted = params[:mappings]
|
|
258
|
+
return if submitted.blank?
|
|
259
|
+
|
|
260
|
+
ApplicationRecord.transaction do
|
|
261
|
+
submitted.each do |entry|
|
|
262
|
+
mapping = import.mappings.find_by(schema_field: entry[:schema_field])
|
|
263
|
+
next unless mapping
|
|
264
|
+
|
|
265
|
+
mapping.update!(file_column: entry[:file_column].presence)
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
import.mappings.reload
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
module Spree
|
|
2
|
+
module Api
|
|
3
|
+
module V3
|
|
4
|
+
module Admin
|
|
5
|
+
# Lists the locales a merchant can translate content into for the
|
|
6
|
+
# current store (its supported_locales_list), so the dashboard's locale
|
|
7
|
+
# switcher enumerates locales instead of hardcoding them.
|
|
8
|
+
class LocalesController < Admin::BaseController
|
|
9
|
+
scoped_resource :settings
|
|
10
|
+
|
|
11
|
+
# GET /api/v3/admin/locales
|
|
12
|
+
def index
|
|
13
|
+
render json: {
|
|
14
|
+
data: current_store.supported_locales.map do |locale|
|
|
15
|
+
Spree.api.locale_serializer.new(locale).to_h
|
|
16
|
+
end
|
|
17
|
+
}
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def action_kind
|
|
23
|
+
'read'
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -5,6 +5,8 @@ module Spree
|
|
|
5
5
|
class MeController < Admin::BaseController
|
|
6
6
|
skip_scope_check!
|
|
7
7
|
|
|
8
|
+
before_action :require_current_user!
|
|
9
|
+
|
|
8
10
|
# GET /api/v3/admin/me
|
|
9
11
|
# Returns the current admin user along with a serialized representation
|
|
10
12
|
# of their permissions (derived from CanCanCan rules). The SPA uses
|
|
@@ -21,22 +23,50 @@ module Spree
|
|
|
21
23
|
# a 500 from serializing a nil user — mirroring how #current 404s for
|
|
22
24
|
# a JWT principal that has no single key.
|
|
23
25
|
def show
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
render json: me_response
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# PATCH /api/v3/admin/me
|
|
30
|
+
# Self-service update of the signed-in admin's own profile (display
|
|
31
|
+
# name, admin UI language, and avatar) — it operates on `current_user`
|
|
32
|
+
# directly, so it needs no per-record authorization. Distinct from
|
|
33
|
+
# PATCH /admin_users/:id, which is store-scoped staff management of
|
|
34
|
+
# *other* users. `avatar` accepts an ActiveStorage direct-upload
|
|
35
|
+
# signed id to set the photo, or `null` to remove it.
|
|
36
|
+
def update
|
|
37
|
+
if current_user.update(permitted_params)
|
|
38
|
+
render json: me_response
|
|
39
|
+
else
|
|
40
|
+
render_validation_error(current_user.errors)
|
|
30
41
|
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
# A request authenticated by a secret API key has no Spree user to
|
|
47
|
+
# describe/update, so it gets a 404 pointing at the key endpoint
|
|
48
|
+
# rather than a 500 from a nil user — mirroring ApiKeysController#current.
|
|
49
|
+
def require_current_user!
|
|
50
|
+
return if current_user
|
|
31
51
|
|
|
32
|
-
|
|
52
|
+
render_error(
|
|
53
|
+
code: ERROR_CODES[:record_not_found],
|
|
54
|
+
message: Spree.t(:me_no_current_user),
|
|
55
|
+
status: :not_found
|
|
56
|
+
)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def permitted_params
|
|
60
|
+
params.permit(:selected_locale, :first_name, :last_name, :avatar)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def me_response
|
|
64
|
+
{
|
|
33
65
|
user: admin_user_serializer.new(current_user, params: serializer_params).to_h,
|
|
34
66
|
permissions: serialize_permissions(current_ability)
|
|
35
67
|
}
|
|
36
68
|
end
|
|
37
69
|
|
|
38
|
-
private
|
|
39
|
-
|
|
40
70
|
# Serializes CanCanCan's rules into a flat, JSON-safe list of permission rules.
|
|
41
71
|
#
|
|
42
72
|
# - Rule order is preserved so the frontend matcher can apply
|
|
@@ -8,6 +8,37 @@ module Spree
|
|
|
8
8
|
|
|
9
9
|
before_action :set_resource, only: [:show, :update, :fulfill, :cancel, :resume, :split]
|
|
10
10
|
|
|
11
|
+
# POST /api/v3/admin/orders/:order_id/fulfillments
|
|
12
|
+
#
|
|
13
|
+
# Manually registers a fulfillment on a completed order (external
|
|
14
|
+
# carrier / 3PL sync), bypassing order routing. Moves the requested
|
|
15
|
+
# line item quantities out of their routed fulfillments; when
|
|
16
|
+
# `items` is omitted, everything not yet shipped is moved. An
|
|
17
|
+
# explicit `cost` persists only with `status: 'shipped'` — pending
|
|
18
|
+
# fulfillments are re-priced by the rate engine.
|
|
19
|
+
def create
|
|
20
|
+
authorize!(:create, Spree::Shipment)
|
|
21
|
+
|
|
22
|
+
with_order_lock do
|
|
23
|
+
result = Spree.fulfillment_create_service.call(
|
|
24
|
+
order: @order,
|
|
25
|
+
stock_location: stock_location_for_create,
|
|
26
|
+
items: items_for_create,
|
|
27
|
+
tracking: create_params[:tracking],
|
|
28
|
+
delivery_method: delivery_method_for_create,
|
|
29
|
+
cost: create_params[:cost],
|
|
30
|
+
status: create_params[:status],
|
|
31
|
+
metadata: create_params[:metadata]&.to_h
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
if result.success?
|
|
35
|
+
render json: serialize_resource(result.value), status: :created
|
|
36
|
+
else
|
|
37
|
+
render_result_error(result)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
|
|
11
42
|
# PATCH /api/v3/admin/orders/:order_id/fulfillments/:id
|
|
12
43
|
def update
|
|
13
44
|
with_order_lock do
|
|
@@ -61,7 +92,7 @@ module Spree
|
|
|
61
92
|
quantity = params[:quantity].to_i
|
|
62
93
|
|
|
63
94
|
stock_location = if params[:stock_location_id].present?
|
|
64
|
-
|
|
95
|
+
find_stock_location!(params[:stock_location_id])
|
|
65
96
|
else
|
|
66
97
|
@resource.stock_location
|
|
67
98
|
end
|
|
@@ -96,7 +127,36 @@ module Spree
|
|
|
96
127
|
# State changes go through the dedicated `fulfill`/`cancel`/`resume`
|
|
97
128
|
# member actions, not mass assignment.
|
|
98
129
|
def permitted_params
|
|
99
|
-
params.permit(:tracking, :
|
|
130
|
+
params.permit(:tracking, :selected_delivery_rate_id, :stock_location_id)
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def create_params
|
|
134
|
+
@create_params ||= params.permit(:stock_location_id, :tracking, :delivery_method_id, :cost, :status, metadata: {}, items: [:item_id, :quantity])
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def find_stock_location!(id)
|
|
138
|
+
Spree::StockLocation.accessible_by(current_ability, :show).find_by_prefix_id!(id)
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def stock_location_for_create
|
|
142
|
+
find_stock_location!(create_params.require(:stock_location_id))
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def delivery_method_for_create
|
|
146
|
+
return if create_params[:delivery_method_id].blank?
|
|
147
|
+
|
|
148
|
+
Spree::ShippingMethod.accessible_by(current_ability, :show).find_by_prefix_id!(create_params[:delivery_method_id])
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def items_for_create
|
|
152
|
+
return if create_params[:items].nil?
|
|
153
|
+
|
|
154
|
+
create_params[:items].map do |item|
|
|
155
|
+
{
|
|
156
|
+
line_item: @order.line_items.find_by_prefix_id!(item[:item_id]),
|
|
157
|
+
quantity: Integer(item[:quantity].to_s, exception: false)
|
|
158
|
+
}
|
|
159
|
+
end
|
|
100
160
|
end
|
|
101
161
|
end
|
|
102
162
|
end
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Spree
|
|
4
|
+
module Api
|
|
5
|
+
module V3
|
|
6
|
+
module Admin
|
|
7
|
+
class PasswordResetsController < Admin::BaseController
|
|
8
|
+
include Spree::Api::V3::Admin::AuthCookies
|
|
9
|
+
|
|
10
|
+
skip_scope_check!
|
|
11
|
+
rate_limit to: Spree::Api::Config[:rate_limit_password_reset],
|
|
12
|
+
within: Spree::Api::Config[:rate_limit_window].seconds,
|
|
13
|
+
store: Rails.cache,
|
|
14
|
+
with: RATE_LIMIT_RESPONSE
|
|
15
|
+
|
|
16
|
+
skip_before_action :authenticate_admin!
|
|
17
|
+
|
|
18
|
+
# POST /api/v3/admin/password_resets
|
|
19
|
+
def create
|
|
20
|
+
redirect_url = params[:redirect_url]
|
|
21
|
+
|
|
22
|
+
# Validate redirect_url against allowed origins (secure by default).
|
|
23
|
+
# If no allowed origins are configured, redirect_url is silently ignored
|
|
24
|
+
# to prevent open redirect / token exfiltration attacks.
|
|
25
|
+
if redirect_url.present?
|
|
26
|
+
unless current_store.allowed_origins.exists? && current_store.allowed_origin?(redirect_url)
|
|
27
|
+
redirect_url = nil
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
user = Spree.admin_user_class.find_by(email: params[:email])
|
|
32
|
+
|
|
33
|
+
if user
|
|
34
|
+
token = user.generate_token_for(:password_reset)
|
|
35
|
+
event_payload = { reset_token: token, email: user.email, store_id: current_store.prefixed_id }
|
|
36
|
+
event_payload[:redirect_url] = redirect_url if redirect_url.present?
|
|
37
|
+
user.publish_event('admin_user.password_reset_requested', event_payload)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Always return 202 to prevent email enumeration
|
|
41
|
+
render json: { message: Spree.t(:password_reset_requested, scope: :api) }, status: :accepted
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# PATCH /api/v3/admin/password_resets/:id
|
|
45
|
+
def update
|
|
46
|
+
user = Spree.admin_user_class.find_by_password_reset_token(params[:id])
|
|
47
|
+
|
|
48
|
+
unless user
|
|
49
|
+
return render_error(
|
|
50
|
+
code: ERROR_CODES[:password_reset_token_invalid],
|
|
51
|
+
message: Spree.t(:password_reset_token_invalid, scope: :api),
|
|
52
|
+
status: :unprocessable_content
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
if user.update(password: params[:password], password_confirmation: params[:password_confirmation])
|
|
57
|
+
user.publish_event('admin_user.password_reset')
|
|
58
|
+
# A password reset must kill every existing session — a stolen
|
|
59
|
+
# refresh token must not survive the victim resetting their
|
|
60
|
+
# password. Revoke first, then mint the fresh token for this
|
|
61
|
+
# browser (the auto-sign-in below).
|
|
62
|
+
Spree::RefreshToken.revoke_all_for(user)
|
|
63
|
+
refresh_token = Spree::RefreshToken.create_for(user, request_env: request_env_for_token)
|
|
64
|
+
set_refresh_cookie(refresh_token)
|
|
65
|
+
|
|
66
|
+
render json: auth_response(user)
|
|
67
|
+
else
|
|
68
|
+
render_errors(user.errors)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
private
|
|
73
|
+
|
|
74
|
+
def auth_response(user)
|
|
75
|
+
{
|
|
76
|
+
token: generate_jwt(user, audience: JWT_AUDIENCE_ADMIN),
|
|
77
|
+
user: admin_user_serializer.new(user, params: serializer_params).to_h
|
|
78
|
+
}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def serializer_params
|
|
82
|
+
{
|
|
83
|
+
store: current_store,
|
|
84
|
+
locale: current_locale,
|
|
85
|
+
currency: current_currency,
|
|
86
|
+
user: nil,
|
|
87
|
+
includes: []
|
|
88
|
+
}
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def admin_user_serializer
|
|
92
|
+
Spree.api.admin_admin_user_serializer
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def request_env_for_token
|
|
96
|
+
{
|
|
97
|
+
ip_address: request.remote_ip,
|
|
98
|
+
user_agent: request.user_agent&.truncate(255)
|
|
99
|
+
}
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
@@ -22,14 +22,7 @@ module Spree
|
|
|
22
22
|
def types
|
|
23
23
|
authorize! :create, model_class
|
|
24
24
|
|
|
25
|
-
|
|
26
|
-
# — the has_many-through association can cache stale results when
|
|
27
|
-
# `current_store` was loaded earlier in the request (e.g. by the
|
|
28
|
-
# auth layer).
|
|
29
|
-
installed_class_names = Spree::PaymentMethod
|
|
30
|
-
.joins(:store_payment_methods)
|
|
31
|
-
.where(spree_payment_methods_stores: { store_id: current_store.id })
|
|
32
|
-
.pluck(:type)
|
|
25
|
+
installed_class_names = current_store.payment_methods.pluck(:type)
|
|
33
26
|
installed_shorthands = installed_class_names.filter_map do |name|
|
|
34
27
|
name.safe_constantize&.api_type
|
|
35
28
|
end
|
|
@@ -34,7 +34,7 @@ module Spree
|
|
|
34
34
|
:sku, :barcode,
|
|
35
35
|
:cost_price, :cost_currency,
|
|
36
36
|
:weight, :height, :width, :depth, :weight_unit, :dimensions_unit,
|
|
37
|
-
:track_inventory, :tax_category_id, :position,
|
|
37
|
+
:track_inventory, :preorderable, :preorder_ships_at, :backorder_limit, :tax_category_id, :position,
|
|
38
38
|
options: [:name, :value],
|
|
39
39
|
prices: [:amount, :compare_at_amount, :currency],
|
|
40
40
|
stock_items: [:stock_location_id, :count_on_hand, :backorderable]
|
|
@@ -171,11 +171,11 @@ module Spree
|
|
|
171
171
|
prices: [:amount, :compare_at_amount, :currency],
|
|
172
172
|
# Inline custom field values keyed by definition id. The model
|
|
173
173
|
# setter (`Spree::Metafields#custom_fields=`) validates each
|
|
174
|
-
# entry against its definition. We permit `value` as
|
|
175
|
-
#
|
|
176
|
-
#
|
|
177
|
-
#
|
|
178
|
-
custom_fields: [:id, :custom_field_definition_id, :value, value: {}],
|
|
174
|
+
# entry against its definition. We permit `value` as a scalar AND
|
|
175
|
+
# as `value: []` / `value: {}` so JSON metafields round-trip
|
|
176
|
+
# whether the parsed payload is an array or an object, while
|
|
177
|
+
# text/number/boolean ship scalars.
|
|
178
|
+
custom_fields: [:id, :custom_field_definition_id, :value, { value: [] }, { value: {} }],
|
|
179
179
|
# Inline media. Entries with `id` patch an existing asset
|
|
180
180
|
# (alt, position, variant_ids). Entries with `signed_id` create
|
|
181
181
|
# + attach a fresh upload. Lets the dashboard ship media changes
|
|
@@ -187,7 +187,7 @@ module Spree
|
|
|
187
187
|
:id, :sku, :barcode,
|
|
188
188
|
:cost_price, :cost_currency,
|
|
189
189
|
:weight, :height, :width, :depth, :weight_unit, :dimensions_unit,
|
|
190
|
-
:track_inventory, :tax_category_id, :position,
|
|
190
|
+
:track_inventory, :preorderable, :preorder_ships_at, :backorder_limit, :tax_category_id, :position,
|
|
191
191
|
options: [:name, :value],
|
|
192
192
|
prices: [:amount, :compare_at_amount, :currency],
|
|
193
193
|
stock_items: [:id, :stock_location_id, :count_on_hand, :backorderable]
|
|
@@ -218,7 +218,8 @@ module Spree
|
|
|
218
218
|
authorize! :update, model_class
|
|
219
219
|
|
|
220
220
|
category_ids = decode_ids(params[:category_ids])
|
|
221
|
-
categories = current_store.
|
|
221
|
+
categories = current_store.categories.
|
|
222
|
+
accessible_by(current_ability, :update).where(id: category_ids)
|
|
222
223
|
|
|
223
224
|
service.call(taxons: categories, products: bulk_collection)
|
|
224
225
|
Spree::Product.bulk_auto_match_taxons(current_store, bulk_collection.ids)
|
|
@@ -39,6 +39,37 @@ module Spree
|
|
|
39
39
|
Spree::PrefixedId.prefixed_id?(id) ? Spree::PrefixedId.decode_prefixed_id(id) : id
|
|
40
40
|
end
|
|
41
41
|
end
|
|
42
|
+
|
|
43
|
+
# Returns +url+ only when it matches one of the store's configured
|
|
44
|
+
# allowed origins — the gate for caller-provided URLs that later
|
|
45
|
+
# surface outside the request (e.g. the results_url the import/export
|
|
46
|
+
# done emails link back to). Anything else is silently dropped,
|
|
47
|
+
# mirroring the password-reset redirect_url behavior: no configured
|
|
48
|
+
# origins means no caller URLs are honored.
|
|
49
|
+
# @return [String, nil]
|
|
50
|
+
def validated_allowed_origin_url(url)
|
|
51
|
+
return if url.blank?
|
|
52
|
+
return unless current_store.allowed_origin?(url)
|
|
53
|
+
|
|
54
|
+
url
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
# Parses a strictly-integer param, returning nil for missing/blank/
|
|
58
|
+
# non-integer values (so callers can reject rather than coerce to 0).
|
|
59
|
+
# @return [Integer, nil]
|
|
60
|
+
def integer_param(name)
|
|
61
|
+
value = params[name]
|
|
62
|
+
Integer(value, exception: false) if value.is_a?(Integer) || value.to_s.match?(/\A-?\d+\z/)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Renders a 422 for a missing/invalid +new_position+.
|
|
66
|
+
def render_invalid_position
|
|
67
|
+
render_error(
|
|
68
|
+
code: ERROR_CODES[:validation_error],
|
|
69
|
+
message: Spree.t('api.errors.invalid_position', default: 'new_position must be an integer'),
|
|
70
|
+
status: :unprocessable_content
|
|
71
|
+
)
|
|
72
|
+
end
|
|
42
73
|
end
|
|
43
74
|
end
|
|
44
75
|
end
|