admin / Synapse-Cortex
publicSelf Hosted ITSM Tool with RBAC/Tenanting and MFA
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"} |