"""Admin endpoints for multi-user Navi management."""
from typing import Annotated
import structlog
from fastapi import APIRouter, Depends, HTTPException
from navi.api.deps import (
get_session_store,
require_admin,
require_permission,
)
from navi.auth import User
from navi.config import settings
from navi.core import SessionStore
log = structlog.get_logger()
router = APIRouter(prefix="/admin", tags=["admin"])
@router.get("/sessions")
async def admin_list_sessions(
store: Annotated[SessionStore, Depends(get_session_store)],
user: Annotated[User, Depends(require_admin)],
):
"""Return all sessions across all users."""
sessions = await store.list_all(user_id=user.id, is_admin=True)
return [
{
"session_id": s.id,
"profile_id": s.profile_id,
"user_id": s.user_id,
"name": s.name,
"message_count": len(s.messages),
"pinned": s.pinned,
"created_at": s.created_at.isoformat(),
"last_active": s.last_active.isoformat(),
}
for s in sessions
]
@router.get("/users")
async def admin_list_users(
store: Annotated[SessionStore, Depends(get_session_store)],
user: Annotated[User, Depends(require_admin)],
):
"""Return all registered navi_users."""
pool = await store._get_pool()
async with pool.acquire() as conn:
rows = await conn.fetch(
"SELECT id, email, display_name, role, permissions, created_at, updated_at FROM navi_users ORDER BY created_at DESC"
)
return [
{
"id": r["id"],
"email": r["email"],
"display_name": r["display_name"],
"role": r["role"],
"permissions": r["permissions"],
"created_at": r["created_at"].isoformat(),
"updated_at": r["updated_at"].isoformat(),
}
for r in rows
]
@router.get("/memory")
async def admin_list_memory(
store: Annotated[SessionStore, Depends(get_session_store)],
user: Annotated[User, Depends(require_permission("navi.memory.read_all"))],
):
"""Return all memory facts (global view)."""
from navi.api.deps import get_memory_store
memory = get_memory_store()
facts = await memory.get_all_facts(limit=500)
return {"facts": facts, "count": len(facts)}
@router.patch("/users/{user_id}/role")
async def admin_update_user_role(
user_id: str,
body: dict,
store: Annotated[SessionStore, Depends(get_session_store)],
user: Annotated[User, Depends(require_admin)],
):
"""Update a user's cached role (requires admin)."""
role = body.get("role")
if role not in ("user", "admin"):
raise HTTPException(status_code=400, detail="Invalid role")
pool = await store._get_pool()
async with pool.acquire() as conn:
await conn.execute(
"UPDATE navi_users SET role = $1, updated_at = $2 WHERE id = $3",
role,
__import__("datetime").datetime.now(__import__("datetime").timezone.utc),
user_id,
)
log.info("admin.role_updated", target_user_id=user_id, role=role, admin_id=user.id)
return {"ok": True}
@router.get("/profiles")
async def admin_list_profiles(
user: Annotated[User, Depends(require_permission("navi.profiles.manage"))],
):
"""Return all profiles including admin-only ones."""
from navi.api.deps import get_profile_registry
profiles = get_profile_registry()
return [
{
"id": p.id,
"name": p.name,
"description": p.description,
"is_admin_only": getattr(p, "is_admin_only", False),
}
for p in profiles.all()
]
@router.patch("/profiles/{profile_id}/availability")
async def admin_update_profile_availability(
profile_id: str,
body: dict,
user: Annotated[User, Depends(require_permission("navi.profiles.manage"))],
):
"""Toggle admin-only visibility for a profile."""
is_admin_only = body.get("is_admin_only")
if not isinstance(is_admin_only, bool):
raise HTTPException(status_code=400, detail="is_admin_only must be a boolean")
# Profile configuration is loaded from navi/profiles/ config files.
# For now this is a no-op endpoint; actual persistence requires editing profile JSON.
log.info(
"admin.profile_availability",
profile_id=profile_id,
is_admin_only=is_admin_only,
admin_id=user.id,
)
return {"ok": True, "note": "Profile availability is managed via profile config files"}