diff --git a/backend/app/api/draft.py b/backend/app/api/draft.py index b9deb41..a79d469 100644 --- a/backend/app/api/draft.py +++ b/backend/app/api/draft.py @@ -32,6 +32,14 @@ async def save_draft(request: DraftSaveRequest): - Сколько времени проводят на каждом шаге - Какие поля вызывают проблемы """ + # Защита: валидация session_id + if not request.session_id or len(request.session_id) > 255: + raise HTTPException(status_code=400, detail="Invalid session_id") + + # Защита: валидация step + if request.step not in [1, 2, 3]: + raise HTTPException(status_code=400, detail="Invalid step number") + try: # Сериализуем данные в JSON form_data_json = json.dumps(request.data, ensure_ascii=False) diff --git a/backend/app/api/upload.py b/backend/app/api/upload.py index b77f6a2..1ed1320 100644 --- a/backend/app/api/upload.py +++ b/backend/app/api/upload.py @@ -161,20 +161,49 @@ async def upload_files(files: List[UploadFile] = File(...), folder: str = "claim Поддерживает множественную загрузку Args: - files: Список файлов для загрузки + files: Список файлов для загрузки (макс 10 файлов по 15MB) folder: Папка в S3 (claims, policies, documents и т.д.) Returns: List[dict]: Список загруженных файлов с URLs """ + # Защита: лимит файлов + if len(files) > 10: + raise HTTPException(status_code=400, detail="Максимум 10 файлов за раз") + + # Защита: санитизация folder + allowed_folders = ['claims', 'policies', 'documents', 'passports', 'tickets'] + if folder not in allowed_folders: + folder = 'claims' + try: uploaded_files = [] + MAX_FILE_SIZE = 15 * 1024 * 1024 # 15MB for file in files: try: # Читаем содержимое файла content = await file.read() + # Защита: проверка размера файла + if len(content) > MAX_FILE_SIZE: + uploaded_files.append({ + "success": False, + "filename": file.filename, + "error": f"Файл больше 15MB ({len(content) / 1024 / 1024:.1f}MB)" + }) + continue + + # Защита: валидация типа файла + allowed_types = ['image/', 'application/pdf'] + if file.content_type and not any(file.content_type.startswith(t) for t in allowed_types): + uploaded_files.append({ + "success": False, + "filename": file.filename, + "error": f"Недопустимый тип файла: {file.content_type}" + }) + continue + # Загружаем в S3 file_url = await s3_service.upload_file( file_content=content, diff --git a/frontend/src/components/form/Step1Policy.tsx b/frontend/src/components/form/Step1Policy.tsx index b06bcc2..e32f6eb 100644 --- a/frontend/src/components/form/Step1Policy.tsx +++ b/frontend/src/components/form/Step1Policy.tsx @@ -54,6 +54,7 @@ export default function Step1Policy({ formData, updateFormData, onNext }: Props) const [loading, setLoading] = useState(false); const [policyNotFound, setPolicyNotFound] = useState(false); const [fileList, setFileList] = useState([]); + const [uploading, setUploading] = useState(false); // Обработчик изменения поля полиса с автозаменой и маской const handleVoucherChange = (e: React.ChangeEvent) => { @@ -123,13 +124,52 @@ export default function Step1Policy({ formData, updateFormData, onNext }: Props) return; } + if (fileList.length > 10) { + message.error('Максимум 10 файлов'); + return; + } + try { - const values = await form.validateFields(); - updateFormData({ ...values, policyScanUploaded: true, policyScanFiles: fileList }); - message.success('Данные сохранены'); - onNext(); + setUploading(true); + const values = await form.validateFields(['voucher']); + + // Загружаем файлы в S3 с OCR проверкой + const formData = new FormData(); + fileList.forEach((file: any) => { + if (file.originFileObj) { + formData.append('files', file.originFileObj); + } + }); + formData.append('folder', 'policies'); + + const uploadResponse = await fetch('http://147.45.146.17:8100/api/v1/upload/files?folder=policies', { + method: 'POST', + body: formData, + }); + + const uploadResult = await uploadResponse.json(); + + if (uploadResult.success) { + // TODO: OCR проверка что это полис, а не шляпа + // Если шляпа - помечаем себе, пользователю не говорим + + updateFormData({ + ...values, + policyScanUploaded: true, + policyScanFiles: uploadResult.files, + policyValidationWarning: '' // TODO: OCR validation + }); + + message.success(`Загружено файлов: ${uploadResult.uploaded_count}`); + onNext(); + } else { + message.error('Ошибка загрузки файлов'); + } } catch (error) { - message.error('Заполните все обязательные поля'); + message.error('Ошибка загрузки файлов'); + console.error(error); + } finally { + setUploading(false); } }; @@ -202,14 +242,33 @@ export default function Step1Policy({ formData, updateFormData, onNext }: Props) listType="picture" fileList={fileList} onChange={handleUploadChange} - beforeUpload={() => false} + beforeUpload={(file) => { + const isLt15M = file.size / 1024 / 1024 < 15; + if (!isLt15M) { + message.error(`${file.name}: файл больше 15MB`); + return Upload.LIST_IGNORE; + } + if (fileList.length >= 10) { + message.error('Максимум 10 файлов'); + return Upload.LIST_IGNORE; + } + return false; + }} accept="image/*,.pdf,.heic,.heif" multiple + maxCount={10} + showUploadList={{ + showPreviewIcon: true, + showRemoveIcon: true, + }} > - +
+ Загружено: {fileList.length}/10 файлов +
@@ -226,10 +285,11 @@ export default function Step1Policy({ formData, updateFormData, onNext }: Props) diff --git a/frontend/src/pages/ClaimForm.tsx b/frontend/src/pages/ClaimForm.tsx index b9fcb43..69fa25c 100644 --- a/frontend/src/pages/ClaimForm.tsx +++ b/frontend/src/pages/ClaimForm.tsx @@ -129,9 +129,41 @@ export default function ClaimForm() { }, ]; + const handleReset = () => { + setFormData({ + voucher: '', + email: '', + phone: '', + paymentMethod: 'sbp', + }); + setCurrentStep(0); + setIsPhoneVerified(false); + message.info('Форма сброшена'); + }; + return (
- + 0 && ( + + ) + } + > {steps.map((item) => (