Catalyst / admin/Synapse-Cortex 14.8 GB / 57.8 GB 40.0 GB free
Help Sign in

admin / Synapse-Cortex

public

Self Hosted ITSM Tool with RBAC/Tenanting and MFA

Code Issues Pull requests Pipelines Packages Security Insights Wiki Settings
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"))