import json import logging import os from fastapi import FastAPI, Request from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles from sqlalchemy import text from src.api.auth import router as auth_router from src.api.dashboard import router as dashboard_router from src.api.git_repositories import router as git_repositories_router from src.api.health import router as health_router from src.api.projects import router as projects_router from src.api.ssh_keys import router as ssh_keys_router from src.api.terminal import router as terminal_router from src.api.instance_proxy import router as instance_proxy_router from src.api.config_folders import router as config_folders_router from src.api.tool_configs import router as tool_configs_router from src.api.tool_instances import router as tool_instances_router from src.api.tool_instances import sessions_router from src.api.tool_types import router as tool_types_router from src.api.user_config import router as user_config_router from src.api.users import router as users_router from src.config import Settings from src.database import init_database from src.logging_config import ( ExceptionLoggingMiddleware, RequestLoggingMiddleware, configure_logging, ) # Configure logging early log_level = os.getenv("LOG_LEVEL", "INFO").upper() configure_logging(level=getattr(logging, log_level, logging.INFO)) logger = logging.getLogger(__name__) settings = Settings() app = FastAPI(title="Headquarter API") # Configure CORS - must be before other middleware # Build allowed origins list including web and api domains cors_origins = [settings.web_base_url] if settings.api_base_url != settings.web_base_url: cors_origins.append(settings.api_base_url) logger.info("CORS configured with origins: %s", cors_origins) app.add_middleware( CORSMiddleware, allow_origins=cors_origins, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.add_middleware(RequestLoggingMiddleware) app.add_middleware(ExceptionLoggingMiddleware) def _sanitize_validation_errors(errors): """Convert validation errors to JSON-safe format.""" sanitized = [] for error in errors: safe_error = { "type": error.get("type"), "loc": error.get("loc"), "msg": error.get("msg"), "input": str(error.get("input")) if error.get("input") is not None else None, } # Convert ctx to safe format ctx = error.get("ctx") if ctx: safe_ctx = {} for key, value in ctx.items(): if isinstance(value, Exception): safe_ctx[key] = str(value) elif isinstance(value, (str, int, float, bool, type(None))): safe_ctx[key] = value else: safe_ctx[key] = str(value) safe_error["ctx"] = safe_ctx sanitized.append(safe_error) return sanitized @app.exception_handler(RequestValidationError) async def validation_exception_handler(request: Request, exc: RequestValidationError): """Log validation errors and return detailed response.""" errors = exc.errors() logger.warning( "Validation error for %s %s: %s", request.method, request.url.path, errors, ) safe_errors = _sanitize_validation_errors(errors) return JSONResponse( status_code=422, content={"detail": safe_errors}, ) @app.on_event("startup") async def on_startup(): logger.info("Starting up Headquarter API...") # Initialize database (run migrations) db_ready = await init_database() if not db_ready: logger.error("Database initialization failed. Shutting down.") import sys sys.exit(1) logger.info("Startup complete.") app.include_router(health_router) app.include_router(auth_router) app.include_router(dashboard_router) app.include_router(projects_router) app.include_router(users_router) app.include_router(ssh_keys_router) app.include_router(git_repositories_router) app.include_router(user_config_router) app.include_router(tool_types_router) app.include_router(config_folders_router) app.include_router(tool_instances_router) app.include_router(tool_configs_router) app.include_router(sessions_router) app.include_router(instance_proxy_router) app.include_router(terminal_router) app.mount("/uploads", StaticFiles(directory="uploads"), name="uploads")