from contextlib import asynccontextmanager from fastapi import Depends, FastAPI, Request from fastapi.middleware.cors import CORSMiddleware from sqlalchemy import text from .auth import get_current_admin, record_log from .bootstrap import seed_default_admin from .config import get_settings from .database import SessionLocal, engine from .errors import APIError, api_error_handler from .routers import ( apikeys, auth, client, customers, device, logs, nas, nasreload, radacct, radcheck, radgroupcheck, radgroupreply, radpostauth, radreply, radusergroup, users, vlan, ) settings = get_settings() @asynccontextmanager async def lifespan(app: FastAPI): # Seed the default admin/admin account on first run. seed_default_admin() yield app = FastAPI( title="FreeRADIUS REST API", description="RESTful CRUD over the FreeRADIUS SQL schema. Authenticate with a " "login session (`Authorization: Bearer`) or an admin-issued `X-API-Key`.", version="2.0.0", lifespan=lifespan, ) app.add_middleware( CORSMiddleware, allow_origins=settings.cors_origin_list, allow_credentials=True, allow_methods=["*"], allow_headers=["*"], ) app.add_exception_handler(APIError, api_error_handler) # Management routers self-log with rich detail; skip them here to avoid dupes. _SELF_LOGGED_PREFIXES = ("/auth", "/users", "/apikeys") _LOGGED_METHODS = {"POST", "PUT", "PATCH", "DELETE"} @app.middleware("http") async def activity_log_middleware(request: Request, call_next): """Record every successful state-changing request to radadmin_logs, attributed to the user resolved by the auth dependency (stashed on request.state).""" response = await call_next(request) if request.method in _LOGGED_METHODS and response.status_code < 400: path = request.url.path if not path.startswith(_SELF_LOGGED_PREFIXES): username = getattr(request.state, "username", None) if username: db = SessionLocal() try: record_log( db, username=username, action=request.method.lower(), detail=f"{request.method} {path}", request=request, ) finally: db.close() return response # ---- unauthenticated meta endpoints ---- @app.get("/", tags=["meta"]) def root(): return {"service": "freeradius-rest-api", "docs": "/docs", "health": "/health"} @app.get("/health", tags=["meta"]) def health(): try: with engine.connect() as conn: conn.execute(text("SELECT 1")) db_ok = True except Exception: db_ok = False return {"status": "ok" if db_ok else "degraded", "database": "up" if db_ok else "down"} # ---- auth + management routers (guard themselves per-endpoint) ---- app.include_router(auth.router) app.include_router(users.router) app.include_router(apikeys.router) app.include_router(logs.router) # ---- authenticated resource routers (any logged-in user or API key) ---- protected = [Depends(get_current_admin)] for module in ( customers, nas, radcheck, radreply, radgroupcheck, radgroupreply, radusergroup, vlan, client, device, radacct, radpostauth, nasreload, ): app.include_router(module.router, dependencies=protected)