""" 應用程式設定模組 使用 Pydantic Settings 管理環境變數 """ from functools import lru_cache from typing import Literal from pydantic import Field from pydantic_settings import BaseSettings, SettingsConfigDict class Settings(BaseSettings): """應用程式設定""" model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", case_sensitive=False ) # 應用程式 app_name: str = "每日報導APP" app_env: Literal["development", "staging", "production"] = "development" debug: bool = Field( default=False, # 預設為 False,更安全 description="除錯模式,僅開發環境使用" ) secret_key: str = Field( default="change-me-in-production", description="應用程式密鑰,生產環境必須透過環境變數設定" ) # 資料庫 db_host: str = "localhost" db_port: int = 3306 db_name: str = "daily_news_app" db_user: str = "root" db_password: str = "" @property def database_url(self) -> str: if self.db_host == "sqlite": return f"sqlite:///{self.db_name}.db" return f"mysql+pymysql://{self.db_user}:{self.db_password}@{self.db_host}:{self.db_port}/{self.db_name}?charset=utf8mb4" @property def async_database_url(self) -> str: if self.db_host == "sqlite": return f"sqlite+aiosqlite:///{self.db_name}.db" return f"mysql+aiomysql://{self.db_user}:{self.db_password}@{self.db_host}:{self.db_port}/{self.db_name}?charset=utf8mb4" # JWT jwt_secret_key: str = Field( default="change-me", description="JWT 簽章密鑰,生產環境必須透過環境變數設定" ) jwt_algorithm: str = "HS256" jwt_access_token_expire_minutes: int = Field( default=480, # 開發環境預設值 description="JWT Token 過期時間(分鐘),建議生產環境設為 60-120 分鐘" ) # LDAP ldap_server: str = "" ldap_port: int = 389 ldap_base_dn: str = "" ldap_bind_dn: str = "" ldap_bind_password: str = "" # LLM llm_provider: Literal["gemini", "openai", "ollama"] = "gemini" gemini_api_key: str = "" gemini_model: str = "gemini-1.5-pro" openai_api_key: str = "" openai_model: str = "gpt-4o" ollama_endpoint: str = "http://localhost:11434" ollama_model: str = "llama3" # SMTP smtp_host: str = "" smtp_port: int = 587 smtp_username: str = "" smtp_password: str = "" smtp_from_email: str = "" smtp_from_name: str = "每日報導系統" # 爬蟲 crawl_schedule_time: str = "08:00" crawl_request_delay: int = 3 crawl_max_retries: int = 3 # Digitimes digitimes_username: str = "" digitimes_password: str = "" # 資料保留 data_retention_days: int = 60 # PDF pdf_logo_path: str = "" pdf_header_text: str = "" pdf_footer_text: str = "本報告僅供內部參考使用" # CORS 設定 cors_origins: list[str] = Field( default=["http://localhost:3000", "http://localhost:8000"], description="允許的 CORS 來源列表,生產環境必須明確指定,不能使用 *" ) # 管理員預設密碼 admin_password: str = Field( default="admin123", description="管理員預設密碼" ) def validate_secrets(): """驗證生產環境的密鑰設定""" if settings.app_env == "production": if settings.secret_key == "change-me-in-production": raise ValueError("生產環境必須設定 SECRET_KEY 環境變數") if settings.jwt_secret_key == "change-me": raise ValueError("生產環境必須設定 JWT_SECRET_KEY 環境變數") if len(settings.secret_key) < 32: raise ValueError("SECRET_KEY 長度必須至少 32 字元") if len(settings.jwt_secret_key) < 32: raise ValueError("JWT_SECRET_KEY 長度必須至少 32 字元") if settings.jwt_access_token_expire_minutes > 120: import warnings warnings.warn("生產環境 JWT Token 過期時間建議不超過 120 分鐘") @lru_cache def get_settings() -> Settings: """取得設定實例(快取)""" return Settings() settings = get_settings()