feat(api): add import preflight validation (#1755)

* feat(api): add import preflight validation

* fix(api): harden import preflight validation
This commit is contained in:
ghost
2026-05-11 15:00:49 -07:00
committed by GitHub
parent 6b6c3bd343
commit 1fedc43f68
13 changed files with 1649 additions and 58 deletions

View File

@@ -8,6 +8,12 @@ class Api::V1::BaseController < ApplicationController
InvalidFilterError = Class.new(StandardError)
class << self
def valid_uuid?(value)
value.to_s.match?(UUID_PATTERN)
end
end
# Skip regular session-based authentication for API
skip_authentication
@@ -220,7 +226,7 @@ class Api::V1::BaseController < ApplicationController
end
def valid_uuid?(value)
value.to_s.match?(UUID_PATTERN)
self.class.valid_uuid?(value)
end
def safe_page_param

View File

@@ -4,7 +4,7 @@ class Api::V1::ImportsController < Api::V1::BaseController
include Pagy::Backend
# Ensure proper scope authorization
before_action :ensure_read_scope, only: [ :index, :show, :rows ]
before_action :ensure_read_scope, only: [ :index, :show, :rows, :preflight ]
before_action :ensure_write_scope, only: [ :create ]
before_action :set_import_with_rows, only: [ :show ]
before_action :set_import, only: [ :rows ]
@@ -77,10 +77,10 @@ class Api::V1::ImportsController < Api::V1::BaseController
if params[:file].present?
file = params[:file]
if file.size > Import::MAX_CSV_SIZE
if file.size > Import.max_csv_size
return render json: {
error: "file_too_large",
message: "File is too large. Maximum size is #{Import::MAX_CSV_SIZE / 1.megabyte}MB."
message: "File is too large. Maximum size is #{Import.max_csv_size / 1.megabyte}MB."
}, status: :unprocessable_entity
end
@@ -93,10 +93,10 @@ class Api::V1::ImportsController < Api::V1::BaseController
@import.raw_file_str = file.read
elsif params[:raw_file_content].present?
if params[:raw_file_content].bytesize > Import::MAX_CSV_SIZE
if params[:raw_file_content].bytesize > Import.max_csv_size
return render json: {
error: "content_too_large",
message: "Content is too large. Maximum size is #{Import::MAX_CSV_SIZE / 1.megabyte}MB."
message: "Content is too large. Maximum size is #{Import.max_csv_size / 1.megabyte}MB."
}, status: :unprocessable_entity
end
@@ -136,6 +136,30 @@ class Api::V1::ImportsController < Api::V1::BaseController
render json: { error: "internal_server_error", message: e.message }, status: :internal_server_error
end
def preflight
preflight_result = Import::Preflight.new(family: current_resource_owner.family, params: preflight_params).call
render json: preflight_result.payload, status: preflight_result.status
rescue ActiveRecord::RecordNotFound
render json: {
error: "record_not_found",
message: "The requested resource was not found"
}, status: :not_found
rescue CSV::MalformedCSVError => e
render json: {
error: "invalid_csv",
message: "CSV content could not be parsed",
errors: [ e.message ]
}, status: :unprocessable_entity
rescue StandardError => e
Rails.logger.error "ImportsController#preflight error: #{e.message}"
e.backtrace&.each { |line| Rails.logger.error line }
render json: {
error: "internal_server_error",
message: "Error: #{e.message}"
}, status: :internal_server_error
end
private
def set_import
@@ -186,10 +210,15 @@ class Api::V1::ImportsController < Api::V1::BaseController
:signage_convention,
:col_sep,
:amount_type_strategy,
:amount_type_inflow_value
:amount_type_inflow_value,
:rows_to_skip
)
end
def preflight_params
params.permit(*Import::Preflight::PARAM_KEYS)
end
def create_sure_import(family)
content, filename, content_type = sure_import_upload_attributes
return unless content
@@ -282,10 +311,10 @@ class Api::V1::ImportsController < Api::V1::BaseController
end
def sure_import_file_upload_attributes(file)
if file.size > SureImport::MAX_NDJSON_SIZE
if file.size > SureImport.max_ndjson_size
render json: {
error: "file_too_large",
message: "File is too large. Maximum size is #{SureImport::MAX_NDJSON_SIZE / 1.megabyte}MB."
message: "File is too large. Maximum size is #{SureImport.max_ndjson_size / 1.megabyte}MB."
}, status: :unprocessable_entity
return
end
@@ -308,10 +337,10 @@ class Api::V1::ImportsController < Api::V1::BaseController
end
def sure_import_raw_content_attributes(content)
if content.bytesize > SureImport::MAX_NDJSON_SIZE
if content.bytesize > SureImport.max_ndjson_size
render json: {
error: "content_too_large",
message: "Content is too large. Maximum size is #{SureImport::MAX_NDJSON_SIZE / 1.megabyte}MB."
message: "Content is too large. Maximum size is #{SureImport.max_ndjson_size / 1.megabyte}MB."
}, status: :unprocessable_entity
return
end