admin / Synapse-Cortex
publicSelf Hosted ITSM Tool with RBAC/Tenanting and MFA
Synapse-Cortex / Synapse-Cortexv2 / app / main.py
3735 B · main
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 | import os from contextlib import asynccontextmanager from pathlib import Path from alembic import command from alembic.config import Config from fastapi import FastAPI, HTTPException, status from fastapi.responses import FileResponse from fastapi.staticfiles import StaticFiles from starlette.middleware.sessions import SessionMiddleware from .database import Base, DATABASE_URL, engine from .paths import STATIC_DIR from .routers import ( admin, ai_settings, assets, auth, credentials, dashboard, ingest, meta, playbooks, remediation, settings, setup, sla_policies, tickets, vault, ) SECRET_KEY = os.getenv("SECRET_KEY", "change-me-in-production") REPO_ROOT = Path(__file__).resolve().parent.parent SPA_DIST_DIR = REPO_ROOT / "frontend" / "dist" def run_migrations() -> None: """Bring the schema up to date. Production always runs against Postgres via real Alembic migrations (the DDL uses Postgres-only features: native ENUM types, a partial unique index). The create_all() fallback exists only so this app stays runnable against SQLite for local/dev smoke testing without a Postgres server on hand — it is never taken when DATABASE_URL points at Postgres. """ if DATABASE_URL.startswith("postgresql"): alembic_cfg = Config(str(REPO_ROOT / "alembic.ini")) alembic_cfg.set_main_option("script_location", str(REPO_ROOT / "alembic")) command.upgrade(alembic_cfg, "head") else: Base.metadata.create_all(bind=engine) @asynccontextmanager async def lifespan(app: FastAPI): run_migrations() yield app = FastAPI(title="Synapse-Cortex", version="2.0.0", lifespan=lifespan) app.add_middleware(SessionMiddleware, secret_key=SECRET_KEY, same_site="lax") app.mount("/static", StaticFiles(directory=str(STATIC_DIR)), name="static") # Pure JSON API now - there is no first-run HTML gate to enforce. The SPA # calls GET /api/v1/setup/status on boot and client-side-routes to /setup # itself when needed; every other endpoint already independently requires a # session (which can't exist until an admin has been created via setup). app.include_router(setup.router) app.include_router(auth.router) app.include_router(dashboard.router) app.include_router(meta.router) app.include_router(tickets.router) app.include_router(assets.router) app.include_router(sla_policies.router) app.include_router(settings.router) app.include_router(admin.router) app.include_router(ingest.router) app.include_router(playbooks.router) app.include_router(ai_settings.router) app.include_router(credentials.router) app.include_router(remediation.router) app.include_router(vault.router) @app.get("/health") def health(): return {"status": "ok"} # Serve the built React SPA (frontend/dist, produced by `npm run build`) for # every other path so client-side routing survives a hard refresh/deep link. # Registered last so it never shadows an API route above it. Bundle assets # are built into "_assets" (see vite.config.ts) rather than Vite's default # "assets", since the app itself has a client-side "/assets" (CMDB) route - # mounting a static handler there would shadow that route's deep links. if SPA_DIST_DIR.exists(): app.mount("/_assets", StaticFiles(directory=str(SPA_DIST_DIR / "_assets")), name="spa-assets") if (SPA_DIST_DIR / "img").exists(): app.mount("/img", StaticFiles(directory=str(SPA_DIST_DIR / "img")), name="spa-img") @app.get("/{full_path:path}") def serve_spa(full_path: str): if full_path.startswith("api/") or full_path in ("health",): raise HTTPException(status.HTTP_404_NOT_FOUND) return FileResponse(str(SPA_DIST_DIR / "index.html")) |