from functools import lru_cache from pathlib import Path from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict def _read_version() -> str: version_file = Path(__file__).resolve().parent.parent.parent / "VERSION" try: return version_file.read_text(encoding="utf-8").strip() except (FileNotFoundError, OSError): return "1.0.0" class Settings(BaseSettings): model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore", ) # Project project_name: str = Field(default="VoIdeaAI", description="Project name") project_version: str = Field(default_factory=_read_version, description="Project version (from VERSION file)") project_env: str = Field(default="local", description="Environment") project_owner: str = Field(default="Owner", description="Project owner name") project_license: str = Field(default="AGPL-3.0", description="License type") # Server server_host: str = Field(default="0.0.0.0", description="Server host") server_port: int = Field(default=8020, description="Server port") server_external_url: str = Field(default="http://localhost:8020", description="External URL") # Database database_url: str = Field( default="postgresql+asyncpg://voidea:password@localhost:5432/voidea", description="Database URL (async, PostgreSQL with asyncpg)", ) db_echo: bool = Field(default=False, description="Echo SQL queries") @property def sync_database_url(self) -> str: return self.database_url.replace("+asyncpg", "") # Redis redis_host: str = Field(default="localhost", description="Redis host") redis_port: int = Field(default=6379, description="Redis port") redis_url: str = Field(default="redis://localhost:6379/0", description="Redis URL") # JWT jwt_secret_key: str = Field(default="", description="JWT secret key") jwt_reset_secret_key: str = Field(default="", description="JWT secret for password reset tokens (separate from access)") jwt_algorithm: str = Field(default="HS256", description="JWT algorithm") jwt_access_token_expire_minutes: int = Field(default=60, description="Access token TTL") jwt_refresh_token_expire_days: int = Field(default=30, description="Refresh token TTL") # AI Providers openai_api_key: str = Field(default="", description="OpenAI API key (Whisper, GPT)") ai_yandex_key: str = Field(default="", description="Yandex GPT API key or IAM token") ai_yandex_url: str = Field(default="https://llm.api.cloud.yandex.net", description="Yandex API URL") ai_yandex_folder_id: str = Field(default="", description="Yandex Cloud folder ID") ai_gigachat_client_id: str = Field(default="", description="GigaChat OAuth client ID") ai_gigachat_secret: str = Field(default="", description="GigaChat OAuth client secret") ai_gigachat_url: str = Field(default="https://gigachat.devices.sber.ru", description="GigaChat API URL") ai_fallback_model: str = Field(default="yandex_gpt", description="Default AI model") ai_timeout: int = Field(default=10, description="AI request timeout") ai_max_retries: int = Field(default=3, description="Max retries for AI requests") # OAuth oauth_yandex_id: str = Field(default="", description="Yandex OAuth client ID") oauth_yandex_secret: str = Field(default="", description="Yandex OAuth client secret") oauth_yandex_redirect_uri: str = Field(default="http://localhost:3000/oauth/callback") oauth_google_id: str = Field(default="", description="Google OAuth client ID") oauth_google_secret: str = Field(default="", description="Google OAuth client secret") oauth_google_redirect_uri: str = Field(default="http://localhost:8020/auth/google/callback") oauth_apple_id: str = Field(default="", description="Apple OAuth client ID") oauth_apple_secret: str = Field(default="", description="Apple OAuth client secret") oauth_apple_redirect_uri: str = Field(default="http://localhost:8020/auth/apple/callback") @property def google_oauth_enabled(self) -> bool: return bool(self.oauth_google_id) @property def apple_oauth_enabled(self) -> bool: return bool(self.oauth_apple_id) # Telegram Bot telegram_bot_token: str = Field(default="", description="Telegram bot token") # Email (SMTP) smtp_host: str = Field(default="", description="SMTP server host") smtp_port: int = Field(default=587, description="SMTP server port") smtp_user: str = Field(default="", description="SMTP username") smtp_pass: str = Field(default="", description="SMTP password") smtp_from: str = Field(default="VoIdea ") smtp_tls: bool = Field(default=True) # Security enable_2fa: bool = Field(default=False) accepted_terms_version: str = Field( default="2026-05-11", description="Current version of Terms of Service / Privacy Policy", ) # System Owner system_owner_email: str = Field( default="", description="Email of the system owner (set on VPS deploy, protected from deletion/suspension)", ) # Social Networks (empty = hidden) social_telegram: str = Field(default="voideaai", description="Telegram account name") social_vk: str = Field(default="voideaai", description="VK account name") social_youtube: str = Field(default="voideaai", description="YouTube account name") social_tiktok: str = Field(default="voideaai", description="TikTok account name") project_slogan: str = Field( default="VoIdeaAI — идеи рождаются вслух, решения приходят мгновенно!", description="Project slogan", ) # Analytics (empty = disabled) yandex_metrika_id: str = Field(default="", description="Yandex Metrika counter ID") google_analytics_id: str = Field(default="", description="Google Analytics tracking ID") # Tariffs tariffs_enabled: bool = Field(default=False, description="Enable tariff system (false = promo free-for-all)") tariffs_free_code: str = Field(default="free", description="Code of the default free tariff plan") # Push Notifications (mobile-ready stubs) fcm_server_key: str = Field(default="", description="Firebase Cloud Messaging server key (mobile push)") apns_key_id: str = Field(default="", description="Apple Push Notification Service key ID (iOS push)") # Logging log_level: str = Field(default="INFO") log_format: str = Field(default="json") log_file_path: str = Field(default="logs/app.log") log_max_bytes: int = Field(default=10485760) log_backup_count: int = Field(default=5) # CORS cors_origins: str = Field(default="http://localhost:3000,http://localhost:8020") @property def cors_origins_list(self) -> list[str]: return [origin.strip() for origin in self.cors_origins.split(",")] # Celery celery_broker_url: str = Field(default="redis://localhost:6379/0") celery_result_backend: str = Field(default="redis://localhost:6379/0") celery_task_track_started: bool = Field(default=True) celery_task_time_limit: int = Field(default=300) # Observer Agent observer_enabled: bool = Field(default=True) observer_sample_rate: float = Field(default=0.1) observer_store_raw_data: bool = Field(default=False) # Rate Limiting rate_limit_enabled: bool = Field(default=True, description="Enable rate limiting") rate_limit_default: str = Field(default="60/minute", description="Default rate limit") rate_limit_auth: str = Field(default="10/minute", description="Auth endpoints rate limit") # Crypto encryption_key: str = Field(default="", description="AES-256 encryption key (Fernet)") # Development debug: bool = Field(default=False) reload: bool = Field(default=True) def is_production(self) -> bool: return self.project_env == "production" def is_development(self) -> bool: return self.project_env in ("development", "local") @property def public_config(self) -> dict: """Non-sensitive settings exposed via GET /api/v1/config/public.""" return { "project_name": self.project_name, "project_version": self.project_version, "project_env": self.project_env, "project_slogan": self.project_slogan, "social_telegram": self.social_telegram, "social_vk": self.social_vk, "social_youtube": self.social_youtube, "social_tiktok": self.social_tiktok, "yandex_metrika_id": self.yandex_metrika_id, "google_analytics_id": self.google_analytics_id, "tariffs_enabled": self.tariffs_enabled, "tariffs_free_code": self.tariffs_free_code, "accepted_terms_version": self.accepted_terms_version, } @lru_cache def get_settings() -> Settings: return Settings() settings = get_settings()