aboutsummaryrefslogtreecommitdiffstats
path: root/packages/meshbay-hub/src/meshbay_hub/app.py
blob: 668ae245f5f529916b72250fe44c8b81137e9f08 (plain) (blame)
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
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
"""
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