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-cortex / app / main.py 3099 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
import os
from contextlib import asynccontextmanager
from pathlib import Path

from alembic import command
from alembic.config import Config
from fastapi import FastAPI, Request
from fastapi.responses import RedirectResponse
from fastapi.staticfiles import StaticFiles
from starlette.middleware.sessions import SessionMiddleware

from . import models
from .database import Base, DATABASE_URL, SessionLocal, engine
from .paths import STATIC_DIR
from .routers import admin, assets, auth, ingest, settings, setup, sla_policies, tickets, web

SECRET_KEY = os.getenv("SECRET_KEY", "change-me-in-production")

# Routes reachable before a global admin exists / before login.
OPEN_PATH_PREFIXES = ("/setup", "/login", "/static", "/health")

# Once True (an admin has been created), we never need to hit the DB again
# on every single request just to check first-run state.
_admin_exists_cache = False

REPO_ROOT = Path(__file__).resolve().parent.parent


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="1.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")


@app.middleware("http")
async def first_run_gate(request: Request, call_next):
    """Force a redirect to /setup until a global admin account exists."""
    global _admin_exists_cache

    path = request.url.path

    if not _admin_exists_cache:
        db = SessionLocal()
        try:
            admin = (
                db.query(models.User)
                .filter(models.User.role == models.UserRole.GLOBAL_ADMIN)
                .first()
            )
            _admin_exists_cache = admin is not None
        finally:
            db.close()

        if not _admin_exists_cache and not path.startswith(OPEN_PATH_PREFIXES):
            return RedirectResponse(url="/setup")

    return await call_next(request)


app.include_router(setup.router)
app.include_router(auth.router)
app.include_router(web.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.get("/health")
def health():
    return {"status": "ok"}