""" N8N Webhook Proxy Router Безопасное проксирование запросов к n8n webhooks. Frontend не знает прямых URL webhooks! """ import httpx import logging from fastapi import APIRouter, HTTPException, File, UploadFile, Form, Request from fastapi.responses import JSONResponse from typing import Optional from ..config import settings logger = logging.getLogger(__name__) router = APIRouter(prefix="/api/n8n", tags=["n8n-proxy"]) # URL webhooks из .env (будут добавлены) N8N_POLICY_CHECK_WEBHOOK = getattr(settings, 'n8n_policy_check_webhook', None) N8N_FILE_UPLOAD_WEBHOOK = getattr(settings, 'n8n_file_upload_webhook', None) N8N_CREATE_CONTACT_WEBHOOK = getattr(settings, 'n8n_create_contact_webhook', 'https://n8n.clientright.pro/webhook/511fde97-88bb-4fb4-bea5-cafdc364be27') N8N_CREATE_CLAIM_WEBHOOK = getattr(settings, 'n8n_create_claim_webhook', 'https://n8n.clientright.pro/webhook/d5bf4ca6-9e44-44b9-9714-3186ea703e7d') @router.post("/policy/check") async def proxy_policy_check(request: Request): """ Проксирует проверку полиса к n8n webhook Frontend отправляет: POST /api/n8n/policy/check Backend проксирует к: https://n8n.clientright.pro/webhook/{uuid} """ if not N8N_POLICY_CHECK_WEBHOOK: raise HTTPException(status_code=500, detail="N8N webhook не настроен") try: # Получаем JSON body от фронтенда body = await request.json() logger.info(f"🔄 Proxy policy check: {body.get('policy_number', 'unknown')}") # Проксируем запрос к n8n async with httpx.AsyncClient(timeout=30.0) as client: response = await client.post( N8N_POLICY_CHECK_WEBHOOK, json=body, headers={"Content-Type": "application/json"} ) if response.status_code == 200: response_text = response.text logger.info(f"✅ Policy check success. Response: {response_text[:500]}") try: return response.json() except Exception as e: logger.error(f"❌ Failed to parse JSON: {e}. Response: {response_text[:500]}") raise HTTPException(status_code=500, detail=f"Ошибка парсинга ответа n8n: {str(e)}") else: logger.error(f"❌ N8N returned {response.status_code}: {response.text}") raise HTTPException( status_code=response.status_code, detail=f"N8N error: {response.text}" ) except httpx.TimeoutException: logger.error("⏱️ N8N webhook timeout") raise HTTPException(status_code=504, detail="Таймаут подключения к n8n") except Exception as e: logger.error(f"❌ Error proxying to n8n: {e}") raise HTTPException(status_code=500, detail=f"Ошибка проверки полиса: {str(e)}") @router.post("/contact/create") async def proxy_create_contact(request: Request): """ Проксирует создание контакта к n8n webhook Frontend отправляет: POST /api/n8n/contact/create Backend проксирует к: https://n8n.clientright.pro/webhook/511fde97-88bb-4fb4-bea5-cafdc364be27 """ if not N8N_CREATE_CONTACT_WEBHOOK: raise HTTPException(status_code=500, detail="N8N contact webhook не настроен") try: body = await request.json() logger.info(f"🔄 Proxy create contact: phone={body.get('phone', 'unknown')}, session_id={body.get('session_id', 'unknown')}") async with httpx.AsyncClient(timeout=30.0) as client: response = await client.post( N8N_CREATE_CONTACT_WEBHOOK, json=body, headers={"Content-Type": "application/json"} ) if response.status_code == 200: response_text = response.text logger.info(f"✅ Contact created successfully. Response: {response_text[:500]}") if not response_text or response_text.strip() == '': logger.error(f"❌ N8N returned empty response") raise HTTPException(status_code=500, detail="N8N вернул пустой ответ") try: return response.json() except Exception as e: logger.error(f"❌ Failed to parse JSON: {e}. Response: {response_text[:500]}") raise HTTPException(status_code=500, detail=f"Ошибка парсинга ответа n8n: {str(e)}") else: logger.error(f"❌ N8N returned {response.status_code}: {response.text}") raise HTTPException( status_code=response.status_code, detail=f"N8N error: {response.text}" ) except httpx.TimeoutException: logger.error("⏱️ N8N webhook timeout") raise HTTPException(status_code=504, detail="Таймаут подключения к n8n") except Exception as e: logger.error(f"❌ Error proxying to n8n: {e}") raise HTTPException(status_code=500, detail=f"Ошибка создания контакта: {str(e)}") @router.post("/upload/file") async def proxy_file_upload( file: UploadFile = File(...), claim_id: Optional[str] = Form(None), voucher: Optional[str] = Form(None), session_id: Optional[str] = Form(None), file_type: Optional[str] = Form(None), filename: Optional[str] = Form(None), upload_timestamp: Optional[str] = Form(None) ): """ Проксирует загрузку файла к n8n webhook Frontend отправляет: POST /api/n8n/upload/file (multipart/form-data) Backend проксирует к: https://n8n.clientright.pro/webhook/{uuid} """ if not N8N_FILE_UPLOAD_WEBHOOK: raise HTTPException(status_code=500, detail="N8N upload webhook не настроен") try: logger.info(f"🔄 Proxy file upload: {file.filename} for claim {claim_id}") # Читаем файл file_content = await file.read() # Формируем multipart/form-data для n8n files = { 'file': (file.filename, file_content, file.content_type) } data = {} if claim_id: data['claim_id'] = claim_id if voucher: data['voucher'] = voucher if session_id: data['session_id'] = session_id if file_type: data['file_type'] = file_type if filename: data['filename'] = filename if upload_timestamp: data['upload_timestamp'] = upload_timestamp # Проксируем запрос к n8n async with httpx.AsyncClient(timeout=60.0) as client: response = await client.post( N8N_FILE_UPLOAD_WEBHOOK, files=files, data=data ) if response.status_code == 200: logger.info(f"✅ File upload success") return response.json() else: logger.error(f"❌ N8N returned {response.status_code}: {response.text}") raise HTTPException( status_code=response.status_code, detail=f"N8N error: {response.text}" ) except httpx.TimeoutException: logger.error("⏱️ N8N webhook timeout") raise HTTPException(status_code=504, detail="Таймаут загрузки файла") except Exception as e: logger.error(f"❌ Error proxying file to n8n: {e}") raise HTTPException(status_code=500, detail=f"Ошибка загрузки файла: {str(e)}") @router.post("/claim/create") async def proxy_create_claim(request: Request): """ Проксирует создание черновика заявки к n8n webhook Frontend отправляет: POST /api/n8n/claim/create Backend проксирует к: https://n8n.clientright.pro/webhook/d5bf4ca6-9e44-44b9-9714-3186ea703e7d """ if not N8N_CREATE_CLAIM_WEBHOOK: raise HTTPException(status_code=500, detail="N8N claim webhook не настроен") try: # Получаем JSON body от фронтенда body = await request.json() logger.info(f"🔄 Proxy create claim: event_type={body.get('event_type', 'unknown')}, claim_id={body.get('claim_id', 'unknown')}") # Проксируем запрос к n8n async with httpx.AsyncClient(timeout=30.0) as client: response = await client.post( N8N_CREATE_CLAIM_WEBHOOK, json=body, headers={"Content-Type": "application/json"} ) if response.status_code == 200: response_text = response.text logger.info(f"✅ Claim created successfully. Response: {response_text[:200]}") # Проверяем что ответ не пустой if not response_text or response_text.strip() == '': logger.error(f"❌ N8N returned empty response") raise HTTPException(status_code=500, detail="N8N вернул пустой ответ") try: return response.json() except Exception as e: logger.error(f"❌ Failed to parse JSON: {e}. Response: {response_text[:500]}") raise HTTPException(status_code=500, detail=f"Ошибка парсинга ответа n8n: {str(e)}") else: logger.error(f"❌ N8N returned {response.status_code}: {response.text}") raise HTTPException( status_code=response.status_code, detail=f"N8N error: {response.text}" ) except httpx.TimeoutException: logger.error("⏱️ N8N webhook timeout") raise HTTPException(status_code=504, detail="Таймаут подключения к n8n") except Exception as e: logger.error(f"❌ Error proxying to n8n: {e}") raise HTTPException(status_code=500, detail=f"Ошибка создания заявки: {str(e)}") @router.post("/documents/attach") async def attach_document_to_crm(request: Request): """ Привязывает загруженный файл к проекту или заявке в vTiger CRM Входные данные: - contact_id: ID контакта - project_id: ID проекта (обязательно) - ticket_id: ID заявки (опционально, если указан - привязываем к заявке) - file_url: URL файла в S3 - file_name: Имя файла - file_type: Тип файла (описание, например: "flight_delay_boarding_or_ticket") Логика: - Если указан ticket_id → привязываем к HelpDesk (заявке) - Иначе → привязываем к Project (проекту) """ CRM_UPLOAD_ENDPOINT = "https://crm.clientright.ru/upload_documents_to_crm.php" try: body = await request.json() contact_id = body.get('contact_id') project_id = body.get('project_id') ticket_id = body.get('ticket_id') # Опционально file_url = body.get('file_url') file_name = body.get('file_name') file_type = body.get('file_type', 'Документ') # Валидация обязательных полей if not all([contact_id, project_id, file_url, file_name]): raise HTTPException( status_code=400, detail="Обязательные поля: contact_id, project_id, file_url, file_name" ) logger.info(f"📎 Attaching document: {file_name} (type: {file_type})") logger.info(f" Contact: {contact_id}, Project: {project_id}, Ticket: {ticket_id or 'N/A'}") # Формируем payload для upload_documents_to_crm.php upload_payload = { "documents": [ { "file_url": file_url, "file_name": file_name, "upload_description": file_type, "contactid": int(contact_id), "pages": 1 } ], "projectid": int(project_id), "ticket_id": int(ticket_id) if ticket_id else None, # Передаем ticket_id если есть "user_id": 1 } logger.info(f"📤 Sending to CRM: {upload_payload}") # Отправляем запрос к CRM async with httpx.AsyncClient(timeout=60.0) as client: response = await client.post( CRM_UPLOAD_ENDPOINT, json=upload_payload, headers={"Content-Type": "application/json"} ) if response.status_code == 200: response_text = response.text logger.info(f"✅ Document attached successfully. Response: {response_text[:300]}") try: result = response.json() # Проверяем успешность if result.get('success') and result.get('results'): first_result = result['results'][0] if first_result.get('status') == 'success': crm_result = first_result.get('crm_result', {}) return { "success": True, "result": { "document_id": crm_result.get('document_id'), "document_numeric_id": crm_result.get('document_numeric_id'), "attached_to": "ticket" if ticket_id else "project", "attached_to_id": ticket_id if ticket_id else project_id, "file_name": file_name, "file_type": file_type, "s3_bucket": crm_result.get('s3_bucket'), "s3_key": crm_result.get('s3_key'), "file_size": crm_result.get('file_size'), "message": crm_result.get('message') } } else: # Ошибка в CRM error_msg = first_result.get('crm_result', {}).get('message', 'Unknown error') logger.error(f"❌ CRM error: {error_msg}") raise HTTPException(status_code=500, detail=f"CRM error: {error_msg}") else: logger.error(f"❌ Unexpected CRM response: {result}") raise HTTPException(status_code=500, detail="Неожиданный ответ от CRM") except Exception as e: logger.error(f"❌ Failed to parse CRM response: {e}. Response: {response_text[:500]}") raise HTTPException(status_code=500, detail=f"Ошибка парсинга ответа CRM: {str(e)}") else: logger.error(f"❌ CRM returned {response.status_code}: {response.text}") raise HTTPException( status_code=response.status_code, detail=f"CRM error: {response.text}" ) except httpx.TimeoutException: logger.error("⏱️ CRM upload timeout") raise HTTPException(status_code=504, detail="Таймаут загрузки в CRM") except HTTPException: raise # Пробрасываем HTTPException как есть except Exception as e: logger.error(f"❌ Error attaching document: {e}") raise HTTPException(status_code=500, detail=f"Ошибка привязки документа: {str(e)}")