Files
roa2web-service-auto/deploy-package-20260223-151231/backend/config.py
Claude Agent 8bc567a9c5 fix telegram
2026-02-23 15:12:33 +00:00

297 lines
12 KiB
Python

"""
Unified Configuration for ROA2WEB Backend
Consolidates settings from Reports, Data Entry, and Telegram modules
"""
import os
import json
import logging
from pathlib import Path
from typing import List, Optional
from pydantic_settings import BaseSettings
from pydantic import BaseModel
from functools import lru_cache
logger = logging.getLogger(__name__)
class OracleServerConfig(BaseModel):
"""Configuration for a single Oracle server instance."""
id: str # Unique identifier (e.g., "romfast", "client_a")
name: str # Human-readable name (e.g., "Romfast - Producție")
host: str = "localhost"
port: int = 1521
user: str
password: str
sid: Optional[str] = None
service_name: Optional[str] = None
def get_dsn(self) -> str:
"""Build DSN string for this server."""
if self.service_name:
return f"{self.host}:{self.port}/{self.service_name}"
elif self.sid:
return f"{self.host}:{self.port}:{self.sid}"
else:
return f"{self.host}:{self.port}/ROA"
class UnifiedSettings(BaseSettings):
"""Unified application settings for all modules."""
# ============================================================================
# GENERAL APPLICATION SETTINGS
# ============================================================================
app_name: str = "ROA2WEB Unified Backend"
app_version: str = "1.0.0"
debug: bool = False
api_host: str = "0.0.0.0"
api_port: int = 8000
# ============================================================================
# ORACLE DATABASE (Shared by all modules)
# ============================================================================
# Legacy single-server configuration (backward compatible)
oracle_user: str = ""
oracle_password: str = ""
oracle_host: str = "localhost"
oracle_port: int = 1526
oracle_sid: str = "ROA"
# ============================================================================
# MULTI-ORACLE SERVER CONFIGURATION (Optional)
# ============================================================================
# JSON array of server configs. If not set, uses legacy single-server config.
# Example: ORACLE_SERVERS='[{"id": "romfast", "name": "Romfast", "host": "localhost", "port": 1521, "user": "USER", "password": "PASS", "sid": "ROA"}]'
oracle_servers: Optional[str] = None # Raw JSON string from env
# Parsed server configurations (populated in model_post_init)
_oracle_servers_parsed: List[OracleServerConfig] = []
def model_post_init(self, __context) -> None:
"""Parse ORACLE_SERVERS JSON and build server list.
Oracle passwords are loaded from:
1. secrets/{server_id}.oracle_pass file (preferred, more secure)
2. password field in ORACLE_SERVERS JSON (fallback)
"""
servers = []
secrets_dir = Path(__file__).parent / "secrets"
if self.oracle_servers:
# Parse multi-server JSON configuration
try:
servers_data = json.loads(self.oracle_servers)
if not isinstance(servers_data, list):
raise ValueError("ORACLE_SERVERS must be a JSON array")
for server_data in servers_data:
server_id = server_data.get("id", "default")
# Try to load password from secrets file
pass_file = secrets_dir / f"{server_id}.oracle_pass"
if pass_file.exists():
server_data["password"] = pass_file.read_text().strip()
logger.debug(f"Loaded Oracle password for '{server_id}' from {pass_file}")
elif "password" not in server_data:
logger.warning(f"No password found for server '{server_id}' - check secrets/{server_id}.oracle_pass")
servers.append(OracleServerConfig(**server_data))
logger.info(f"Loaded {len(servers)} Oracle servers from ORACLE_SERVERS config")
for srv in servers:
logger.info(f" - {srv.id}: {srv.name} ({srv.host}:{srv.port})")
except json.JSONDecodeError as e:
logger.error(f"Failed to parse ORACLE_SERVERS JSON: {e}")
raise ValueError(f"Invalid ORACLE_SERVERS JSON format: {e}")
else:
# Backward compatibility: build default server from legacy config
if self.oracle_user:
# Try to load password from secrets file
password = self.oracle_password
pass_file = secrets_dir / "default.oracle_pass"
if pass_file.exists():
password = pass_file.read_text().strip()
logger.debug(f"Loaded Oracle password from {pass_file}")
default_server = OracleServerConfig(
id="default",
name="Default Server",
host=self.oracle_host,
port=self.oracle_port,
user=self.oracle_user,
password=password,
sid=self.oracle_sid,
)
servers.append(default_server)
logger.info("Using legacy single-server Oracle configuration (ORACLE_USER/HOST/etc)")
logger.info(f" - default: {default_server.host}:{default_server.port}/{default_server.sid}")
object.__setattr__(self, '_oracle_servers_parsed', servers)
def get_oracle_servers(self) -> List[OracleServerConfig]:
"""Get list of configured Oracle servers."""
return self._oracle_servers_parsed
def get_oracle_server(self, server_id: str) -> Optional[OracleServerConfig]:
"""Get a specific Oracle server by ID."""
for server in self._oracle_servers_parsed:
if server.id == server_id:
return server
return None
def get_default_oracle_server(self) -> Optional[OracleServerConfig]:
"""Get the default Oracle server (first in list or 'default')."""
if not self._oracle_servers_parsed:
return None
# Try to find server with id='default', otherwise return first
for server in self._oracle_servers_parsed:
if server.id == "default":
return server
return self._oracle_servers_parsed[0]
# ============================================================================
# JWT AUTHENTICATION (Shared by all modules)
# ============================================================================
jwt_secret_key: str = "change-me-in-production"
jwt_algorithm: str = "HS256"
access_token_expire_minutes: int = 30
refresh_token_expire_days: int = 7
# ============================================================================
# SESSION SECURITY - EMAIL 2FA (Telegram module)
# ============================================================================
auth_session_secret: str = "change-me-in-production"
# ============================================================================
# CORS
# ============================================================================
cors_origins: str = "http://localhost:3000,http://localhost:5173"
# ============================================================================
# REPORTS MODULE - CACHE CONFIGURATION
# ============================================================================
reports_cache_enabled: bool = True
reports_cache_type: str = "hybrid"
reports_cache_sqlite_path: str = "./data/cache/roa2web_cache.db"
reports_cache_memory_max_size: int = 1000
reports_cache_default_ttl: int = 900
# Cache TTL per type (seconds)
reports_cache_ttl_schema: int = 86400
reports_cache_ttl_companies: int = 1800
reports_cache_ttl_dashboard_summary: int = 1800
reports_cache_ttl_dashboard_trends: int = 1800
reports_cache_ttl_invoices: int = 600
reports_cache_ttl_invoices_summary: int = 900
reports_cache_ttl_treasury: int = 600
# Cache maintenance
reports_cache_cleanup_interval: int = 3600
reports_cache_auto_invalidate: bool = False
reports_cache_check_interval: int = 300
reports_cache_track_performance: bool = True
reports_cache_benchmark_on_startup: bool = False
# ============================================================================
# DATA ENTRY MODULE - CONFIGURATION
# ============================================================================
data_entry_sqlite_database_path: str = "data/receipts/receipts.db"
data_entry_upload_path: str = "data/receipts/uploads"
data_entry_max_upload_size_mb: int = 10
data_entry_allowed_mime_types: List[str] = [
"image/jpeg",
"image/png",
"image/gif",
"image/webp",
"application/pdf",
]
# ============================================================================
# TELEGRAM MODULE - BOT CONFIGURATION
# ============================================================================
telegram_bot_token: str = ""
telegram_smtp_host: str = ""
telegram_smtp_port: int = 587
telegram_smtp_user: str = ""
telegram_smtp_password: str = ""
telegram_smtp_from_email: str = ""
telegram_smtp_from_name: str = "ROA2WEB"
telegram_smtp_use_tls: bool = True
telegram_email_max_retries: int = 3
telegram_email_retry_delay: float = 2.0
telegram_sqlite_database_path: str = "data/telegram/telegram.db"
class Config:
env_file = ".env"
env_file_encoding = "utf-8"
extra = "ignore"
case_sensitive = False
# ============================================================================
# COMPUTED PROPERTIES
# ============================================================================
@property
def oracle_dsn(self) -> str:
"""Get Oracle DSN string."""
return f"{self.oracle_host}:{self.oracle_port}/{self.oracle_sid}"
@property
def cors_origins_list(self) -> List[str]:
"""Get CORS origins as list."""
return [origin.strip() for origin in self.cors_origins.split(",")]
# Data Entry properties
@property
def data_entry_database_url(self) -> str:
"""Get SQLite database URL for async (Data Entry)."""
# Resolve to absolute path for Windows/IIS compatibility
abs_path = Path(self.data_entry_sqlite_database_path).resolve()
return f"sqlite+aiosqlite:///{abs_path}"
@property
def data_entry_sync_database_url(self) -> str:
"""Get SQLite database URL for sync operations (Alembic)."""
# Resolve to absolute path for Windows/IIS compatibility
abs_path = Path(self.data_entry_sqlite_database_path).resolve()
return f"sqlite:///{abs_path}"
@property
def data_entry_upload_path_resolved(self) -> Path:
"""Get resolved upload path."""
path = Path(self.data_entry_upload_path)
path.mkdir(parents=True, exist_ok=True)
return path
@property
def data_entry_max_upload_size_bytes(self) -> int:
"""Get max upload size in bytes."""
return self.data_entry_max_upload_size_mb * 1024 * 1024
# Reports cache properties
@property
def reports_cache_sqlite_path_resolved(self) -> Path:
"""Get resolved cache SQLite path."""
path = Path(self.reports_cache_sqlite_path)
path.parent.mkdir(parents=True, exist_ok=True)
return path
# Telegram properties
@property
def telegram_sqlite_path_resolved(self) -> Path:
"""Get resolved Telegram SQLite path."""
path = Path(self.telegram_sqlite_database_path)
path.parent.mkdir(parents=True, exist_ok=True)
return path
@lru_cache()
def get_settings() -> UnifiedSettings:
"""Get cached settings instance."""
return UnifiedSettings()
# Convenience instance
settings = get_settings()