""" Environment Configuration Manager for SCA Backend Handles environment-specific settings and validates configuration """ import os import secrets import warnings from pathlib import Path from dotenv import load_dotenv from typing import Literal # Load environment variables from root .env file try: env_path = Path(__file__).resolve().parent.parent / '.env' load_dotenv(env_path) except Exception: load_dotenv() # Fallback to default behavior class Config: """Base configuration class""" # Project Root # Handle Docker flattened structure vs Local nested structure if (Path(__file__).parent / 'app.py').exists(): BASE_DIR = Path(__file__).parent else: BASE_DIR = Path(__file__).resolve().parent.parent # Deployment Environment: 'local' (Development) or 'production' (Cloud/Web) # This dictates SECURITY strictness, not functionality. DEPLOYMENT_ENV: Literal['local', 'production'] = os.environ.get('DEPLOYMENT_ENV', 'local').lower() # Flask Settings # Debug enabled in Local, disabled in Production FLASK_DEBUG = os.environ.get('FLASK_DEBUG', '1' if DEPLOYMENT_ENV == 'local' else '0') == '1' # JWT Configuration # Key MUST be at least 32 bytes (256 bits) for HS256 to avoid security warnings. # In production, if missing, we generate a random 32-byte key to stay running safely. _default_key = 'sca-local-secure-ultra-long-secret-key-32-char' JWT_SECRET_KEY = os.environ.get('JWT_SECRET_KEY') if not JWT_SECRET_KEY: if DEPLOYMENT_ENV == 'local': JWT_SECRET_KEY = _default_key else: # Generate random 32-byte hex for production if not provided JWT_SECRET_KEY = secrets.token_hex(32) print(f"⚠️ WARNING: JWT_SECRET_KEY not set. Using ephemeral key: {JWT_SECRET_KEY[:8]}...") # Suppress the warning if the key is still somehow short (e.g. user-set 27 chars) # but we've already done our best by defaulting to 32+ characters. warnings.filterwarnings("ignore", category=UserWarning, module="jwt") JWT_ALGORITHM = 'HS256' JWT_ACCESS_TOKEN_EXPIRES_HOURS = int(os.environ.get('JWT_ACCESS_TOKEN_EXPIRES_HOURS', '24')) JWT_REFRESH_TOKEN_EXPIRES_DAYS = int(os.environ.get('JWT_REFRESH_TOKEN_EXPIRES_DAYS', '7')) # CORS # In Local, allow all. In Production, strict. CORS_ORIGINS = os.environ.get('CORS_ORIGINS', '*' if DEPLOYMENT_ENV == 'local' else '').split(',') # Database DATABASE_URL = os.environ.get('DATABASE_URL', f"sqlite:///{BASE_DIR / 'outputs' / 'sca_events.db'}") # Blockchain BLOCKCHAIN_RPC_URL = os.environ.get('BLOCKCHAIN_RPC_URL') BLOCKCHAIN_PRIVATE_KEY = os.environ.get('BLOCKCHAIN_PRIVATE_KEY') CONTRACT_ADDRESS = os.environ.get('CONTRACT_ADDRESS') or os.environ.get('VITE_CONTRACT_ADDRESS') # Department TARGET_DEPT = os.environ.get('TARGET_DEPT', 'CS_DEPARTMENT') # Security FORCE_HTTPS = os.environ.get('FORCE_HTTPS', 'true' if DEPLOYMENT_ENV == 'production' else 'false').lower() == 'true' # Rate Limiting RATE_LIMIT_ENABLED = os.environ.get('RATE_LIMIT_ENABLED', 'true' if DEPLOYMENT_ENV == 'production' else 'false').lower() == 'true' RATE_LIMIT_PER_MINUTE = int(os.environ.get('RATE_LIMIT_PER_MINUTE', '60')) # Logging LOG_LEVEL = os.environ.get('LOG_LEVEL', 'INFO') @classmethod def is_local(cls) -> bool: """Check if running locally (Relaxed Security)""" return cls.DEPLOYMENT_ENV == 'local' @classmethod def is_production(cls) -> bool: """Check if running in production (Strict Security)""" return cls.DEPLOYMENT_ENV == 'production' @classmethod def validate(cls): """Validate configuration and warn about insecure settings""" warnings = [] errors = [] # Check Production Security if cls.is_production(): if not cls.JWT_SECRET_KEY or cls.JWT_SECRET_KEY == 'sca-local-secret-key': errors.append("CRITICAL: JWT_SECRET_KEY must be set to a secure value in Production!") if cls.FLASK_DEBUG: errors.append("CRITICAL: FLASK_DEBUG must be disabled in Production!") if not cls.FORCE_HTTPS: warnings.append("WARNING: FORCE_HTTPS should be enabled in Production") if not cls.DATABASE_URL or 'sqlite' in cls.DATABASE_URL: warnings.append("WARNING: Using SQLite in Production. Recommended: PostgreSQL.") # Print warnings and errors if warnings: print("\n⚠️ CONFIGURATION WARNINGS:") for warning in warnings: print(f" {warning}") if errors: print("\n❌ CONFIGURATION ERRORS:") for error in errors: print(f" {error}") print("\n Please fix these issues before deploying to Production!\n") if not warnings and not errors: print(f"\n✓ Configuration validated for {cls.DEPLOYMENT_ENV.upper()} environment") return len(errors) == 0 @classmethod def get_info(cls) -> dict: """Get configuration info (safe for logging)""" return { 'deployment': 'Production (Cloud)' if cls.is_production() else 'Local (Dev)', 'capability': 'Mainnet (Full Node)', # ALWAYS Mainnet capability 'debug': cls.FLASK_DEBUG, 'database': cls.DATABASE_URL.split('/')[-1] if '/' in cls.DATABASE_URL else cls.DATABASE_URL, 'cors_origins': 'Allow All (*)' if cls.is_local() else len(cls.CORS_ORIGINS), 'blockchain_configured': bool(cls.BLOCKCHAIN_PRIVATE_KEY and cls.CONTRACT_ADDRESS), 'https_enforced': cls.FORCE_HTTPS } # Singleton config instance config = Config() if __name__ == '__main__': """Test configuration""" print("="*60) print("SCA ENVIRONMENT CONFIGURATION") print("="*60) config.validate() print("\nConfiguration Info:") import json print(json.dumps(config.get_info(), indent=2))