""" Runtime configuration for AC Tool server """ import os from typing import List from dotenv import load_dotenv load_dotenv() class ServerConfig: # Server HOST: str = os.getenv('SERVER_HOST', '0.0.0.0') PORT: int = int(os.getenv('SERVER_PORT', '8000')) WORKERS: int = int(os.getenv('SERVER_WORKERS', '2')) DEBUG: bool = os.getenv('DEBUG', 'false').lower() == 'true' # Development Mode DEV_MODE: bool = os.getenv('DEV_MODE', 'true').lower() == 'true' DEV_USER_ID: str = os.getenv('DEV_USER_ID', 'dev-user-id') DEV_USER_EMAIL: str = os.getenv('DEV_USER_EMAIL', 'dev@localhost') DEV_USER_NAME: str = os.getenv('DEV_USER_NAME', 'Dev User') DEV_USER_ROLE: str = os.getenv('DEV_USER_ROLE', 'admin') # 'user' or 'admin' # CORS ALLOWED_ORIGINS: List[str] = [ origin.strip() for origin in os.getenv( 'ALLOWED_ORIGINS', 'http://localhost:3000,http://localhost:5173,https://ai-sandbox.oliver.solutions' ).split(',') ] # Azure AD / MSAL (SPA PKCE flow — no client secret needed) MSAL_CLIENT_ID: str = os.getenv('MSAL_CLIENT_ID', '9079054c-9620-4757-a256-23413042f1ef') MSAL_TENANT_ID: str = os.getenv('MSAL_TENANT_ID', 'e519c2e6-bc6d-4fdf-8d9c-923c2f002385') MSAL_REDIRECT_URI: str = os.getenv('MSAL_REDIRECT_URI', 'https://ai-sandbox.oliver.solutions/ac-helper/') MSAL_AUTHORITY: str = f'https://login.microsoftonline.com/{os.getenv("MSAL_TENANT_ID", "e519c2e6-bc6d-4fdf-8d9c-923c2f002385")}' # Admin bootstrap — emails that always get admin role on first login ADMIN_EMAIL: str = os.getenv('ADMIN_EMAIL', 'daveporter@oliver.agency') ADMIN_EMAILS: list = [ e.strip().lower() for e in os.getenv('ADMIN_EMAILS', 'daveporter@oliver.agency,vadymsamoilenko@oliver.agency').split(',') if e.strip() ] # Emergency access — set EMERGENCY_TOKEN to a long random string to allow # token-based login bypassing SSO (useful when 2FA / Azure AD is unavailable). # Leave blank to disable this bypass entirely. EMERGENCY_TOKEN: str = os.getenv('EMERGENCY_TOKEN', '') EMERGENCY_USER_EMAIL: str = os.getenv('EMERGENCY_USER_EMAIL', 'daveporter@oliver.agency') EMERGENCY_USER_NAME: str = os.getenv('EMERGENCY_USER_NAME', 'Emergency Access') # Security SESSION_SECRET: str = os.getenv('SESSION_SECRET', 'change-me-in-production') SECURE_COOKIES: bool = os.getenv('SECURE_COOKIES', 'false').lower() == 'true' HTTPS_ONLY: bool = os.getenv('HTTPS_ONLY', 'false').lower() == 'true' # File Upload MAX_UPLOAD_SIZE_MB: int = int(os.getenv('MAX_UPLOAD_SIZE_MB', '200')) MAX_CONTENT_LENGTH: int = MAX_UPLOAD_SIZE_MB * 1024 * 1024 ALLOWED_EXTENSIONS: set = {'.pdf', '.pptx', '.docx', '.xlsx', '.ppt', '.doc', '.xls'} # Job Management MAX_CONCURRENT_JOBS: int = int(os.getenv('MAX_CONCURRENT_JOBS', '2')) FILE_RETENTION_HOURS: int = int(os.getenv('FILE_RETENTION_HOURS', '24')) # WebSocket WS_PING_INTERVAL_SECONDS: int = int(os.getenv('WS_PING_INTERVAL_SECONDS', '30')) # AI GEMINI_API_KEY: str = os.getenv('GEMINI_API_KEY', '') GEMINI_MODEL: str = os.getenv('GEMINI_MODEL', 'gemini-3-flash-preview') # PostgreSQL DATABASE_URL: str = os.getenv('DATABASE_URL', 'postgresql://achelper:achelper@localhost:5432/achelper') # Data paths — mounted as Docker volume DATA_DIR: str = os.getenv( 'DATA_DIR', os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 'data') ) UPLOAD_DIR: str = os.path.join(DATA_DIR, 'uploads') OUTPUT_DIR: str = os.path.join(DATA_DIR, 'outputs') SHEETS_DIR: str = os.path.join(DATA_DIR, 'sheets') USERS_FILE: str = os.path.join(DATA_DIR, 'users.json') DROPDOWNS_FILE: str = os.path.join(DATA_DIR, 'dropdowns.json') CLIENTS_FILE: str = os.path.join(DATA_DIR, 'clients.json') CLIENTS_DROPDOWNS_DIR: str = os.path.join(DATA_DIR, 'client_dropdowns') EXPORT_TEMPLATE_FILE: str = os.path.join(DATA_DIR, 'export_template.json') USER_EXPORT_TEMPLATES_DIR: str = os.path.join(DATA_DIR, 'user_export_templates') @classmethod def ensure_directories(cls): for d in [cls.DATA_DIR, cls.UPLOAD_DIR, cls.OUTPUT_DIR, cls.SHEETS_DIR, cls.CLIENTS_DROPDOWNS_DIR, cls.USER_EXPORT_TEMPLATES_DIR]: os.makedirs(d, exist_ok=True) @classmethod def validate_auth_config(cls) -> bool: if cls.DEV_MODE: return True return bool(cls.MSAL_CLIENT_ID and cls.MSAL_TENANT_ID) @classmethod def get_cors_config(cls) -> dict: return { 'allow_origin': cls.ALLOWED_ORIGINS, 'allow_methods': ['GET', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'], 'allow_headers': ['Content-Type', 'Authorization', 'Accept'], 'allow_credentials': True, } server_config = ServerConfig()