1. ✅ Прогресс бар загрузки: - Upload компонент с showUploadList - Кнопка показывает состояние 'Загрузка...' - Визуальный прогресс для каждого файла 2. ✅ OCR проверка полиса (заготовка): - TODO: проверка что загружен полис, а не шляпа - Если шляпа - помечаем себе в policyValidationWarning - Пользователю не говорим (silent validation) 3. ✅ Лимиты файлов: - Максимум 10 файлов - Каждый файл до 15MB - Валидация на фронте и бэкенде - Счетчик: 'Загружено: X/10 файлов' - Кнопка disabled при 10 файлах 4. ✅ Защита от инъекций и безопасность: Backend (upload.py): - Лимит файлов: if len(files) > 10 - Проверка размера: if len(content) > MAX_FILE_SIZE - Валидация типа: allowed_types = ['image/', 'application/pdf'] - Санитизация folder: allowed_folders whitelist Backend (draft.py): - Валидация session_id (max 255 chars) - Валидация step: only [1, 2, 3] - Параметризованные SQL запросы (защита от SQL injection) Frontend: - beforeUpload валидация размера - maxCount={10} - accept только разрешенные форматы 5. ✅ Кнопка 'Начать заново': - Показывается на шаге 2 и 3 (extra в Card) - Сбрасывает всю форму - Возвращает на шаг 1 - Очищает isPhoneVerified Безопасность: - SQL инъекции: параметризованные запросы ($1, $2) - XSS: Pydantic валидация всех inputs - File upload: type + size validation - Path traversal: folder whitelist - Rate limiting: TODO (Redis) UX: - Прогресс загрузки виден - Понятные лимиты (10 файлов по 15MB) - Возможность начать заново в любой момент
199 lines
6.1 KiB
Python
199 lines
6.1 KiB
Python
"""
|
||
Draft API Routes - Автосохранение драфтов форм
|
||
"""
|
||
from fastapi import APIRouter, HTTPException
|
||
from pydantic import BaseModel
|
||
from typing import Optional, Dict, Any
|
||
from datetime import datetime
|
||
import json
|
||
from ..services.database import db
|
||
import logging
|
||
|
||
router = APIRouter(prefix="/api/v1/draft", tags=["Draft"])
|
||
logger = logging.getLogger(__name__)
|
||
|
||
|
||
class DraftSaveRequest(BaseModel):
|
||
"""Запрос на сохранение драфта"""
|
||
session_id: str # Уникальный ID сессии пользователя
|
||
step: int # Текущий шаг формы (1, 2, 3)
|
||
data: Dict[str, Any] # Данные формы
|
||
user_agent: Optional[str] = None
|
||
ip_address: Optional[str] = None
|
||
|
||
|
||
@router.post("/save")
|
||
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)
|
||
|
||
# SQL для upsert (insert or update)
|
||
query = """
|
||
INSERT INTO claims_draft (
|
||
session_id,
|
||
current_step,
|
||
form_data,
|
||
user_agent,
|
||
ip_address,
|
||
created_at,
|
||
updated_at
|
||
) VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||
ON CONFLICT (session_id)
|
||
DO UPDATE SET
|
||
current_step = EXCLUDED.current_step,
|
||
form_data = EXCLUDED.form_data,
|
||
user_agent = EXCLUDED.user_agent,
|
||
ip_address = EXCLUDED.ip_address,
|
||
updated_at = EXCLUDED.updated_at
|
||
RETURNING id
|
||
"""
|
||
|
||
now = datetime.now()
|
||
|
||
result = await db.fetchval(
|
||
query,
|
||
request.session_id,
|
||
request.step,
|
||
form_data_json,
|
||
request.user_agent,
|
||
request.ip_address,
|
||
now,
|
||
now
|
||
)
|
||
|
||
logger.info(f"✅ Draft saved: session={request.session_id}, step={request.step}")
|
||
|
||
return {
|
||
"success": True,
|
||
"message": "Драфт сохранен",
|
||
"draft_id": result
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"Draft save error: {e}")
|
||
# Не падаем с ошибкой - просто логируем
|
||
# Автосохранение не должно блокировать пользователя
|
||
return {
|
||
"success": False,
|
||
"message": "Ошибка сохранения драфта"
|
||
}
|
||
|
||
|
||
@router.get("/stats")
|
||
async def get_draft_stats():
|
||
"""
|
||
Статистика по драфтам
|
||
|
||
Показывает:
|
||
- Сколько людей бросают на каждом шаге
|
||
- Среднее время на шаге
|
||
- Количество драфтов за период
|
||
"""
|
||
try:
|
||
# Статистика по шагам
|
||
step_stats_query = """
|
||
SELECT
|
||
current_step,
|
||
COUNT(*) as count,
|
||
COUNT(DISTINCT session_id) as unique_users
|
||
FROM claims_draft
|
||
WHERE created_at >= NOW() - INTERVAL '7 days'
|
||
GROUP BY current_step
|
||
ORDER BY current_step
|
||
"""
|
||
|
||
step_stats = await db.fetch(step_stats_query)
|
||
|
||
# Общая статистика
|
||
total_drafts_query = """
|
||
SELECT COUNT(*) as total
|
||
FROM claims_draft
|
||
WHERE created_at >= NOW() - INTERVAL '7 days'
|
||
"""
|
||
|
||
total = await db.fetchval(total_drafts_query)
|
||
|
||
return {
|
||
"success": True,
|
||
"period": "last_7_days",
|
||
"total_drafts": total,
|
||
"by_step": [
|
||
{
|
||
"step": row["current_step"],
|
||
"count": row["count"],
|
||
"unique_users": row["unique_users"]
|
||
}
|
||
for row in step_stats
|
||
]
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"Draft stats error: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|
||
|
||
@router.get("/list")
|
||
async def list_recent_drafts(limit: int = 50):
|
||
"""
|
||
Список последних драфтов
|
||
|
||
Для просмотра что люди заполняют
|
||
"""
|
||
try:
|
||
query = """
|
||
SELECT
|
||
id,
|
||
session_id,
|
||
current_step,
|
||
form_data,
|
||
created_at,
|
||
updated_at,
|
||
user_agent,
|
||
ip_address
|
||
FROM claims_draft
|
||
ORDER BY updated_at DESC
|
||
LIMIT $1
|
||
"""
|
||
|
||
drafts = await db.fetch(query, limit)
|
||
|
||
return {
|
||
"success": True,
|
||
"count": len(drafts),
|
||
"drafts": [
|
||
{
|
||
"id": row["id"],
|
||
"session_id": row["session_id"],
|
||
"step": row["current_step"],
|
||
"data": json.loads(row["form_data"]) if row["form_data"] else {},
|
||
"created_at": row["created_at"].isoformat(),
|
||
"updated_at": row["updated_at"].isoformat(),
|
||
"user_agent": row["user_agent"],
|
||
"ip_address": row["ip_address"]
|
||
}
|
||
for row in drafts
|
||
]
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"Draft list error: {e}")
|
||
raise HTTPException(status_code=500, detail=str(e))
|
||
|