Files
aiform_dev/backend/app/api/draft.py
AI Assistant 621c8ebf01 feat: 5 улучшений безопасности и UX
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)
- Возможность начать заново в любой момент
2025-10-24 21:34:50 +03:00

199 lines
6.1 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
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))