"""GuardDog Nexus — FastAPI application entry point.""" import os import time from contextlib import asynccontextmanager import uvicorn from fastapi import FastAPI, Request from fastapi.responses import JSONResponse from fastapi.staticfiles import StaticFiles from starlette.middleware.base import BaseHTTPMiddleware from guarddog_nexus.config import config from guarddog_nexus.constants import ( APP_DESCRIPTION, APP_NAME, APP_PACKAGE, APP_VERSION, STATIC_MOUNT_PATH, ) from guarddog_nexus.db.engine import init_db from guarddog_nexus.i18n import DEFAULT_LANG, LANGUAGES from guarddog_nexus.logging_setup import log from guarddog_nexus.routes import api_findings, api_packages, api_scans from guarddog_nexus.routes.metrics import router as metrics_router from guarddog_nexus.routes.web import router as web_router from guarddog_nexus.routes.webhooks import router as webhook_router STATIC_DIR = os.path.join(os.path.dirname(__file__), "web", "static") class LangMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): # Cookie takes priority, then query param, then default cookie_lang = request.cookies.get("lang") query_lang = request.query_params.get("lang") if query_lang and query_lang in LANGUAGES and query_lang != cookie_lang: lang = query_lang elif cookie_lang and cookie_lang in LANGUAGES: lang = cookie_lang else: lang = DEFAULT_LANG request.state.lang = lang response = await call_next(request) if query_lang and query_lang in LANGUAGES: response.set_cookie("lang", query_lang, max_age=365 * 24 * 3600, httponly=True) return response @asynccontextmanager async def lifespan(app: FastAPI): await init_db() log.info("%s started on %s:%s", APP_NAME, config.host, config.port) yield log.info("%s shutting down", APP_NAME) class RequestLoggingMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next): start = time.monotonic() response = await call_next(request) duration = (time.monotonic() - start) * 1000 log.info( "%s %s %s %.1fms", request.method, request.url.path, response.status_code, duration, ) return response app = FastAPI( title=APP_NAME, version=APP_VERSION, description=APP_DESCRIPTION, lifespan=lifespan, ) app.add_middleware(LangMiddleware) app.add_middleware(RequestLoggingMiddleware) @app.exception_handler(Exception) async def global_exception_handler(request: Request, exc: Exception): log.exception("Unhandled exception on %s %s", request.method, request.url.path) return JSONResponse(status_code=500, content={"detail": "Internal server error"}) app.include_router(webhook_router) app.include_router(metrics_router) app.include_router(api_scans.router) app.include_router(api_packages.router) app.include_router(api_findings.router) app.include_router(web_router) if os.path.isdir(STATIC_DIR): app.mount(STATIC_MOUNT_PATH, StaticFiles(directory=STATIC_DIR), name="static") @app.get("/health") async def health() -> dict: return {"status": "ok", "version": APP_VERSION} def main(): uvicorn.run( f"{APP_PACKAGE}.main:app", host=config.host, port=config.port, log_level=config.log_level.lower(), reload=False, ) if __name__ == "__main__": main()