""" MeshBay Hub — FastAPI application factory. Usage: from meshbay_hub.app import create_app from meshbay_hub.config import load_config cfg = load_config() app = create_app(cfg) """ import asyncio from contextlib import asynccontextmanager from pathlib import Path from fastapi import FastAPI from slowapi import _rate_limit_exceeded_handler from slowapi.errors import RateLimitExceeded from meshbay_hub import __version__ from meshbay_hub.auth import generate_hub_keypair, load_hub_keypair from meshbay_hub.config import HubConfig from meshbay_hub.db.engine import close_db, init_db from meshbay_hub.api.hub import router as hub_router from meshbay_hub.api.users import router as users_router, set_config as users_set_config from meshbay_hub.api.deps import set_admin_usernames from meshbay_hub.api.nodes import router as nodes_router from meshbay_hub.api.groups import router as groups_router, swarm_router from meshbay_hub.api.revocation import router as revocation_router from meshbay_hub.api.moderation import router as moderation_router from meshbay_hub.api.federation import router as federation_router from meshbay_hub.csam import csam_router from meshbay_hub.api.health import router as health_router from meshbay_hub.api.relay import router as relay_router from meshbay_hub.api.signaling import router as signaling_router from meshbay_hub.api.admin import router as admin_router from meshbay_hub.api.notifications import router as notifications_router from meshbay_hub.api.webapp import router as webapp_router, STATIC_DIR from meshbay_hub.api.middleware import limiter async def _sync_admin_roles(admin_usernames: list[str]) -> None: """Ensure config-listed admin usernames have role='admin' in the DB.""" from sqlalchemy import select, update from meshbay_hub.db.engine import get_session_factory from meshbay_hub.db.models import User factory = get_session_factory() async with factory() as session: result = await session.execute( select(User).where(User.username.in_(admin_usernames)) ) for user in result.scalars().all(): if user.role != "admin": user.role = "admin" await session.commit() def create_app(cfg: HubConfig | None = None) -> FastAPI: from meshbay_hub.config import load_config if cfg is None: cfg = load_config() @asynccontextmanager async def lifespan(app: FastAPI): # Startup await init_db(cfg.db.url) kp = cfg.identity.private_key_path if not kp.exists(): generate_hub_keypair(kp) load_hub_keypair(kp, cfg.identity.id) users_set_config(cfg) set_admin_usernames(cfg.identity.admin_usernames) if cfg.identity.admin_usernames: await _sync_admin_roles(cfg.identity.admin_usernames) from meshbay_hub.csam import get_csam_checker get_csam_checker().load() from meshbay_hub.tasks.cleanup import cleanup_loop from meshbay_hub.db.engine import get_session_factory cleanup_task = asyncio.create_task(cleanup_loop(get_session_factory())) yield cleanup_task.cancel() try: await cleanup_task except asyncio.CancelledError: pass # Shutdown await close_db() app = FastAPI( title="MeshBay Hub", version=__version__, description="MeshBay identity authority and group registry", lifespan=lifespan, ) # Rate limiting app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) # Routers (webapp last — catches / before API routes) app.include_router(hub_router) app.include_router(users_router) app.include_router(nodes_router) app.include_router(groups_router) app.include_router(swarm_router) app.include_router(revocation_router) app.include_router(moderation_router) app.include_router(federation_router) app.include_router(csam_router) app.include_router(health_router) app.include_router(relay_router) app.include_router(signaling_router) app.include_router(admin_router) app.include_router(notifications_router) app.include_router(webapp_router) from starlette.staticfiles import StaticFiles app.mount("/", StaticFiles(directory=STATIC_DIR), name="static") return app