""" Конфигурация приложения """ import os import json from pathlib import Path from pydantic_settings import BaseSettings from typing import List, Optional BASE_DIR = Path(__file__).resolve().parents[2] ENV_PATH = BASE_DIR / ".env" # Список CORS, обновляется при изменении .env (чтобы не перезапускать бэкенд) _cors_origins_live: List[str] = [] _settings_cache: Optional["Settings"] = None _env_mtime_cache: float = 0 class Settings(BaseSettings): # ============================================ # APPLICATION # ============================================ app_name: str = "Ticket Form Intake Platform" app_env: str = "development" debug: bool = True # API api_v1_prefix: str = "/api/v1" backend_url: str = "http://localhost:8200" frontend_url: str = "http://localhost:5175" # ============================================ # DATABASE (PostgreSQL) # ============================================ postgres_host: str = "147.45.189.234" postgres_port: int = 5432 postgres_db: str = "default_db" postgres_user: str = "gen_user" postgres_password: str = "2~~9_^kVsU?2\\S" # ============================================ # MYSQL (для проверки полисов ERV) # ============================================ mysql_host: str = "localhost" mysql_port: int = 3306 mysql_db: str = "u2768571_crm_db" mysql_user: str = "root" mysql_password: str = "" # ============================================ # MYSQL CRM (vtiger CRM) # ============================================ mysql_crm_host: str = "localhost" # В режиме network_mode: host используем localhost # Доступ к хосту из Docker контейнера mysql_crm_port: int = 3306 mysql_crm_db: str = "ci20465_72new" mysql_crm_user: str = "ci20465_72new" mysql_crm_password: str = "EcY979Rn" @property def database_url(self) -> str: """Формирует URL для подключения к PostgreSQL""" return f"postgresql+asyncpg://{self.postgres_user}:{self.postgres_password}@{self.postgres_host}:{self.postgres_port}/{self.postgres_db}" # ============================================ # REDIS (внешний — события, буферы, SMS и т.д.) # ============================================ redis_host: str = "localhost" redis_port: int = 6379 redis_password: str = "CRM_Redis_Pass_2025_Secure!" redis_db: int = 0 redis_prefix: str = "ticket_form:" # Redis для сессий (локальный в Docker — miniapp_redis; снаружи — localhost:6383 или свой) redis_session_host: str = "localhost" redis_session_port: int = 6383 redis_session_password: str = "" redis_session_db: int = 0 @property def redis_url(self) -> str: """Формирует URL для подключения к Redis (внешний)""" if self.redis_password: return f"redis://:{self.redis_password}@{self.redis_host}:{self.redis_port}/{self.redis_db}" return f"redis://{self.redis_host}:{self.redis_port}/{self.redis_db}" @property def redis_session_url(self) -> str: """URL для локального Redis сессий""" if self.redis_session_password: return f"redis://:{self.redis_session_password}@{self.redis_session_host}:{self.redis_session_port}/{self.redis_session_db}" return f"redis://{self.redis_session_host}:{self.redis_session_port}/{self.redis_session_db}" # ============================================ # RABBITMQ # ============================================ rabbitmq_host: str = "185.197.75.249" rabbitmq_port: int = 5672 rabbitmq_user: str = "admin" rabbitmq_password: str = "tyejvtej" rabbitmq_vhost: str = "/" @property def rabbitmq_url(self) -> str: """Формирует URL для подключения к RabbitMQ""" return f"amqp://{self.rabbitmq_user}:{self.rabbitmq_password}@{self.rabbitmq_host}:{self.rabbitmq_port}{self.rabbitmq_vhost}" # ============================================ # S3 STORAGE (Timeweb Cloud Storage) # ============================================ s3_endpoint: str = "https://s3.timeweb.com" s3_bucket: str = "erv-platform-files" s3_access_key: str = "your_access_key_here" s3_secret_key: str = "your_secret_key_here" s3_region: str = "ru-1" # ============================================ # OCR SERVICE # ============================================ ocr_api_url: str = "http://147.45.146.17:8001" ocr_api_key: str = "" # ============================================ # AI SERVICE (OpenRouter) # ============================================ openrouter_api_key: str = "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" openrouter_base_url: str = "https://openrouter.ai/api/v1" openrouter_model: str = "google/gemini-2.0-flash-001" # ============================================ # FLIGHT APIs # ============================================ # FlightAware flightaware_api_key: str = "Puz0cdxAHzAEqMRZwtdeqBUSm9naJfwK" flightaware_base_url: str = "https://aeroapi.flightaware.com/aeroapi" # AviationStack (резервный) aviationstack_api_key: str = "" aviationstack_base_url: str = "http://api.aviationstack.com/v1" # ============================================ # NSPK BANKS API (и альтернативный BANK_IP из .env) # ============================================ nspk_banks_api_url: str = "https://qr.nspk.ru/proxyapp/c2bmembers.json" bank_ip: str = "http://212.193.27.93/api/payouts/dictionaries/nspk-banks" bank_api_url: str = "http://212.193.27.93/api/payouts/dictionaries/nspk-banks" # ============================================ # DADATA (подсказки адресов в форме профиля) # ============================================ forma_dadata_api_key: str = "" # FORMA_DADATA_API_KEY forma_dadata_secret: str = "" # FORMA_DADATA_SECRET # ============================================ # SMS SERVICE (SigmaSMS) # ============================================ sms_api_url: str = "https://online.sigmasms.ru/api/" sms_login: str = "" sms_password: str = "" sms_token: str = "" sms_sender: str = "lexpriority" sms_enabled: bool = True # ============================================ # VTIGER CRM (PHP Bridge) # ============================================ crm_webservice_url: str = "http://crm.clientright.ru/webservice.php" crm_webform_url: str = "https://crm.clientright.ru/modules/Webforms/capture.php" crm_token: str = "" # ============================================ # RATE LIMITING # ============================================ rate_limit_per_minute: int = 60 rate_limit_per_hour: int = 1000 # ============================================ # FILE UPLOAD # ============================================ max_upload_size_mb: int = 50 allowed_file_extensions: str = "pdf,jpg,jpeg,png,heic,heif,webp" @property def allowed_extensions_list(self) -> List[str]: """Список разрешенных расширений файлов""" return [ext.strip() for ext in self.allowed_file_extensions.split(",")] # ============================================ # CORS # ============================================ cors_origins: str = "http://localhost:5175,http://127.0.0.1:5175,http://147.45.146.17:5175" @property def cors_origins_list(self) -> List[str]: """Список CORS origins""" if isinstance(self.cors_origins, str): return [origin.strip() for origin in self.cors_origins.split(",")] return self.cors_origins # ============================================ # N8N API & WEBHOOKS # ============================================ n8n_url: str = "https://n8n.clientright.pro" n8n_api_key: str = "" # Нужно задать в .env n8n_policy_check_webhook: str = "" n8n_file_upload_webhook: str = "" n8n_create_contact_webhook: str = "https://n8n.clientright.pro/webhook/511fde97-88bb-4fb4-bea5-cafdc364be27" n8n_create_claim_webhook: str = "https://n8n.clientright.pro/webhook/d5bf4ca6-9e44-44b9-9714-3186ea703e7d" n8n_description_webhook: str = "https://n8n.clientright.ru/webhook/ticket_form_description" # Webhook для описания проблемы (переопределяется через N8N_DESCRIPTION_WEBHOOK в .env) # Консультации: тикеты из CRM (MySQL) — тот же payload, что и у других хуков n8n_ticket_form_consultation_webhook: str = "" # N8N_TICKET_FORM_CONSULTATION_WEBHOOK в .env # Подробнее по тикету: session + ticket_id → ответ вебхука (HTML/JSON) n8n_ticket_form_podrobnee_webhook: str = "" # N8N_TICKET_FORM_PODROBNEE_WEBHOOK в .env # Wizard и финальная отправка заявки (create) — один webhook, меняется через .env n8n_ticket_form_final_webhook: str = "https://n8n.clientright.pro/webhook/ecc93306-fadc-489a-afdb-d3e981013df3" n8n_tg_auth_webhook: str = "" # Webhook для авторизации пользователей Telegram WebApp (Mini App) # Контактные данные из CRM для раздела «Профиль» (массив или пусто) n8n_contact_webhook: str = "" # N8N_CONTACT_WEBHOOK в .env n8n_profile_update_webhook: str = "" # N8N_PROFILE_UPDATE_WEBHOOK в .env — обновление профиля (verification=0) # ============================================ # TELEGRAM BOT # ============================================ telegram_bot_token: str = "" # Токен бота для проверки initData WebApp def get_telegram_bot_tokens(self) -> List[tuple]: """Список (bot_id, token) для проверки подписи Telegram initData. Один токен — [('default', token)].""" token = (self.telegram_bot_token or "").strip() if token: return [("default", token)] return [] # ============================================ # MAX (мессенджер) — Mini App auth # ============================================ max_bot_token: str = "" # Токен бота MAX (один бот) max_bot_tokens: str = "" # Мультибот: JSON {"bot_id": "token", ...}. Если задан — используется вместо max_bot_token. def get_max_bot_tokens(self) -> List[tuple]: """Список (bot_id, token) для проверки подписи MAX initData. Из MAX_BOT_TOKENS (JSON) или [('default', MAX_BOT_TOKEN)].""" s = (self.max_bot_tokens or os.environ.get("MAX_BOT_TOKENS") or "").strip() if s: try: d = json.loads(s) out = [(k, str(v).strip()) for k, v in d.items() if v and str(v).strip()] if out: return out except Exception: pass token = (self.max_bot_token or os.environ.get("MAX_BOT_TOKEN") or "").strip() if token: return [("default", token)] return [] n8n_max_auth_webhook: str = "" # Webhook n8n: max_user_id → unified_id, contact_id, has_drafts n8n_auth_webhook: str = "" # Универсальный auth: channel + channel_user_id + init_data → unified_id, phone, contact_id, has_drafts # ============================================ # LOGGING # ============================================ log_level: str = "INFO" log_file: str = "/app/logs/ticket_form_backend.log" class Config: env_file = str(ENV_PATH) case_sensitive = False extra = "ignore" # Игнорируем лишние поля из .env def get_settings() -> Settings: """Текущие настройки. При изменении .env подхватываются без перезапуска.""" global _settings_cache, _env_mtime_cache, _cors_origins_live mtime = os.path.getmtime(ENV_PATH) if ENV_PATH.exists() else 0.0 if _settings_cache is None or mtime > _env_mtime_cache: _settings_cache = Settings() _env_mtime_cache = mtime _cors_origins_live.clear() _cors_origins_live.extend(_settings_cache.cors_origins_list) return _settings_cache def get_cors_origins_live() -> List[str]: """ Список CORS origins для middleware; обновляется при изменении .env без перезапуска. Обработчики, которые используют get_settings() при каждом запросе, тоже видят новые значения. """ get_settings() # обновить кеш и _cors_origins_live при изменении .env return _cors_origins_live settings = get_settings()