diff --git a/backend/app/core/config.py b/backend/app/core/config.py new file mode 100644 index 0000000..02f86d4 --- /dev/null +++ b/backend/app/core/config.py @@ -0,0 +1,223 @@ +""" +Configuration module for UnitForge application. + +This module handles environment variable loading and provides +default values for configuration settings. +""" + +import os +from pathlib import Path +from typing import Optional, List +import json +import ast + +try: + from dotenv import load_dotenv + # Try to load .env file from project root + env_path = Path(__file__).parent.parent.parent.parent / '.env' + if env_path.exists(): + load_dotenv(env_path) +except ImportError: + # python-dotenv not installed, continue with os.getenv + pass + + +class Settings: + """Application settings loaded from environment variables.""" + + def __init__(self): + """Initialize settings from environment variables.""" + # Application info + self.app_name: str = os.getenv("APP_NAME", "UnitForge") + self.app_version: str = os.getenv("APP_VERSION", "1.0.0") + self.app_description: str = os.getenv( + "APP_DESCRIPTION", "Create, validate, and manage systemd unit files" + ) + + # External links + self.github_url: str = os.getenv( + "GITHUB_URL", "https://github.com/will666/unitforge" + ) + self.documentation_url: str = os.getenv( + "DOCUMENTATION_URL", "https://unitforge.readthedocs.io/" + ) + self.bug_reports_url: str = os.getenv( + "BUG_REPORTS_URL", "https://github.com/will666/unitforge/issues" + ) + + # Contact information + self.contact_email: str = os.getenv("CONTACT_EMAIL", "contact@unitforge.dev") + + # Application settings + self.environment: str = os.getenv("ENVIRONMENT", "production") + self.log_level: str = os.getenv("LOG_LEVEL", "info") + + # Server configuration + self.host: str = os.getenv("HOST", "0.0.0.0") + self.port: int = int(os.getenv("PORT", "8000")) + self.debug: bool = os.getenv("DEBUG", "").lower() in ("true", "1", "yes", "on") + self.reload: bool = os.getenv("RELOAD", "").lower() in ("true", "1", "yes", "on") + self.workers: int = int(os.getenv("WORKERS", "4")) + + # API configuration + self.api_title: str = os.getenv("API_TITLE", self.app_name) + self.api_version: str = os.getenv("API_VERSION", self.app_version) + self.api_description: str = os.getenv("API_DESCRIPTION", self.app_description) + self.api_docs_url: str = os.getenv("DOCS_URL", "/api/docs") + self.api_redoc_url: str = os.getenv("REDOC_URL", "/api/redoc") + + # Security settings + self.secret_key: Optional[str] = os.getenv("SECRET_KEY") + self.allowed_hosts: List[str] = self._parse_list("ALLOWED_HOSTS", ["*"]) + + # CORS settings + self.cors_origins: list = self._parse_cors_origins() + + # File upload settings + self.max_upload_size: int = int(os.getenv("MAX_UPLOAD_SIZE", "1048576")) + self.allowed_extensions: List[str] = self._parse_list( + "ALLOWED_EXTENSIONS", + [".service", ".timer", ".socket", ".mount", ".target", ".path"] + ) + + # Template settings + self.template_cache_ttl: int = int(os.getenv("TEMPLATE_CACHE_TTL", "300")) + self.validation_cache_ttl: int = int(os.getenv("VALIDATION_CACHE_TTL", "60")) + + # Paths + self.frontend_dir: str = os.getenv("FRONTEND_DIR", "frontend") + self.backend_dir: str = os.getenv("BACKEND_DIR", "backend") + self.static_dir: str = os.getenv("STATIC_DIR", "frontend/static") + self.templates_dir: str = os.getenv("TEMPLATES_DIR", "frontend/templates") + + # Feature flags + self.enable_api_metrics: bool = self._get_bool("ENABLE_API_METRICS", False) + self.enable_request_logging: bool = self._get_bool("ENABLE_REQUEST_LOGGING", True) + self.enable_template_caching: bool = self._get_bool("ENABLE_TEMPLATE_CACHING", True) + self.enable_validation_caching: bool = self._get_bool("ENABLE_VALIDATION_CACHING", True) + + # Performance settings + self.request_timeout: int = int(os.getenv("REQUEST_TIMEOUT", "30")) + self.keepalive_timeout: int = int(os.getenv("KEEPALIVE_TIMEOUT", "5")) + self.max_connections: int = int(os.getenv("MAX_CONNECTIONS", "100")) + + # Logging configuration + self.log_format: str = os.getenv( + "LOG_FORMAT", "%(asctime)s - %(name)s - %(levelname)s - %(message)s" + ) + self.log_date_format: str = os.getenv("LOG_DATE_FORMAT", "%Y-%m-%d %H:%M:%S") + self.access_log: bool = self._get_bool("ACCESS_LOG", True) + + # CLI configuration + self.cli_verbose: bool = self._get_bool("CLI_VERBOSE", False) + self.cli_color: bool = self._get_bool("CLI_COLOR", True) + self.cli_progress: bool = self._get_bool("CLI_PROGRESS", True) + + # Validation settings + self.strict_validation: bool = self._get_bool("STRICT_VALIDATION", False) + self.show_warnings: bool = self._get_bool("SHOW_WARNINGS", True) + self.max_validation_errors: int = int(os.getenv("MAX_VALIDATION_ERRORS", "50")) + + # Template generation defaults + self.default_user: str = os.getenv("DEFAULT_USER", "www-data") + self.default_group: str = os.getenv("DEFAULT_GROUP", "www-data") + self.default_restart_policy: str = os.getenv("DEFAULT_RESTART_POLICY", "on-failure") + self.default_wanted_by: str = os.getenv("DEFAULT_WANTED_BY", "multi-user.target") + + # Security headers + self.security_headers: bool = self._get_bool("SECURITY_HEADERS", True) + self.hsts_max_age: int = int(os.getenv("HSTS_MAX_AGE", "31536000")) + self.csp_enabled: bool = self._get_bool("CSP_ENABLED", True) + + # Monitoring + self.health_check_enabled: bool = self._get_bool("HEALTH_CHECK_ENABLED", True) + self.metrics_enabled: bool = self._get_bool("METRICS_ENABLED", False) + self.tracing_enabled: bool = self._get_bool("TRACING_ENABLED", False) + + # Asset optimization + self.hot_reload: bool = self._get_bool("HOT_RELOAD", False) + self.source_maps: bool = self._get_bool("SOURCE_MAPS", False) + self.minify_assets: bool = self._get_bool("MINIFY_ASSETS", True) + self.compress_responses: bool = self._get_bool("COMPRESS_RESPONSES", True) + + # Documentation + self.docs_auto_reload: bool = self._get_bool("DOCS_AUTO_RELOAD", False) + self.api_docs_enabled: bool = self._get_bool("API_DOCS_ENABLED", True) + self.swagger_ui_enabled: bool = self._get_bool("SWAGGER_UI_ENABLED", True) + self.redoc_enabled: bool = self._get_bool("REDOC_ENABLED", True) + + def _get_bool(self, key: str, default: bool = False) -> bool: + """Get boolean value from environment variable.""" + value = os.getenv(key, "").lower() + if value in ("true", "1", "yes", "on"): + return True + elif value in ("false", "0", "no", "off"): + return False + return default + + def _parse_list(self, key: str, default: List[str]) -> List[str]: + """Parse list from environment variable.""" + value = os.getenv(key) + if not value: + return default + + # Try to parse as JSON list first + try: + parsed = json.loads(value) + if isinstance(parsed, list): + return [str(item) for item in parsed] + except (json.JSONDecodeError, ValueError): + pass + + # Try to parse as Python literal + try: + parsed = ast.literal_eval(value) + if isinstance(parsed, list): + return [str(item) for item in parsed] + except (ValueError, SyntaxError): + pass + + # Fall back to comma-separated values + return [item.strip() for item in value.split(",") if item.strip()] + + def _parse_cors_origins(self) -> list: + """Parse CORS origins from environment variable.""" + origins_str = os.getenv("CORS_ORIGINS", "*") + if origins_str == "*": + return ["*"] + + # Try to parse as list first + try: + parsed = self._parse_list("CORS_ORIGINS", []) + if parsed: + return parsed + except Exception: + pass + + # Fall back to comma-separated values + return [origin.strip() for origin in origins_str.split(",") if origin.strip()] + + def get_template_context(self) -> dict: + """Get context variables for template rendering.""" + return { + "app_name": self.app_name, + "app_version": self.app_version, + "app_description": self.app_description, + "github_url": self.github_url, + "documentation_url": self.documentation_url, + "bug_reports_url": self.bug_reports_url, + "api_docs_url": self.api_docs_url, + "contact_email": self.contact_email, + } + + def is_development(self) -> bool: + """Check if running in development environment.""" + return self.environment.lower() == "development" or self.debug + + def is_production(self) -> bool: + """Check if running in production environment.""" + return self.environment.lower() == "production" and not self.debug + + +# Global settings instance +settings = Settings()