feat(09-01): implement GET /api/admin/overview endpoint — ADMIN-11
- New backend/api/admin/overview.py: aggregate stats (user_count, total_storage_bytes, doc_status) + last 10 audit entries in one payload - Sub-router carries NO prefix (parent __init__.py carries /api/admin) - Reuses _build_filtered_query_with_handles + _audit_to_dict_with_handles from api.audit — no new serializer or query logic - Registered overview_router on admin parent router in __init__.py - All 8 ADMIN-11 tests pass; existing admin and audit suites stay green
This commit is contained in:
@@ -13,11 +13,13 @@ main.py continues to use:
|
||||
app.include_router(admin_router)
|
||||
"""
|
||||
from fastapi import APIRouter
|
||||
from api.admin.overview import router as overview_router
|
||||
from api.admin.users import router as users_router
|
||||
from api.admin.quotas import router as quotas_router
|
||||
from api.admin.ai import router as ai_router
|
||||
|
||||
router = APIRouter(prefix="/api/admin", tags=["admin"])
|
||||
router.include_router(overview_router)
|
||||
router.include_router(users_router)
|
||||
router.include_router(quotas_router)
|
||||
router.include_router(ai_router)
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi import APIRouter, Depends
|
||||
from sqlalchemy import func, select
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from db.models import AuditLog, Document, Quota, User
|
||||
from deps.auth import get_current_admin
|
||||
from deps.db import get_db
|
||||
from api.audit import _build_filtered_query_with_handles
|
||||
from api.audit import _audit_to_dict_with_handles
|
||||
|
||||
router = APIRouter() # NO prefix — parent __init__.py carries /api/admin (D-02)
|
||||
|
||||
|
||||
@router.get("/overview")
|
||||
async def get_overview(
|
||||
session: AsyncSession = Depends(get_db),
|
||||
_admin: User = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
user_count = await session.scalar(
|
||||
select(func.count(User.id)).where(User.role == "user")
|
||||
) or 0
|
||||
|
||||
total_storage_bytes = await session.scalar(
|
||||
select(func.sum(Quota.used_bytes))
|
||||
) or 0
|
||||
|
||||
status_rows = (
|
||||
await session.execute(
|
||||
select(Document.status, func.count(Document.id)).group_by(Document.status)
|
||||
)
|
||||
).all()
|
||||
doc_status = {row[0]: row[1] for row in status_rows}
|
||||
|
||||
audit_q = (
|
||||
_build_filtered_query_with_handles(None, None, None, None)
|
||||
.order_by(AuditLog.created_at.desc())
|
||||
.limit(10)
|
||||
)
|
||||
audit_rows = (await session.execute(audit_q)).all()
|
||||
recent_audit = [
|
||||
_audit_to_dict_with_handles(row[0], row[1], row[2], row[3])
|
||||
for row in audit_rows
|
||||
]
|
||||
|
||||
return {
|
||||
"user_count": user_count,
|
||||
"total_storage_bytes": total_storage_bytes,
|
||||
"doc_status": doc_status,
|
||||
"recent_audit": recent_audit,
|
||||
}
|
||||
@@ -90,7 +90,6 @@ async def admin_client(db_session: AsyncSession):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.xfail(strict=True, reason="ADMIN-11: overview endpoint not yet implemented")
|
||||
async def test_overview_requires_admin(async_client: AsyncClient):
|
||||
"""Unauthenticated GET /api/admin/overview returns 401 or 403."""
|
||||
resp = await async_client.get("/api/admin/overview")
|
||||
@@ -98,7 +97,6 @@ async def test_overview_requires_admin(async_client: AsyncClient):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.xfail(strict=True, reason="ADMIN-11: overview endpoint not yet implemented")
|
||||
async def test_overview_non_admin_forbidden(db_session: AsyncSession):
|
||||
"""Regular-user JWT returns 403 from GET /api/admin/overview."""
|
||||
from main import app
|
||||
@@ -116,7 +114,6 @@ async def test_overview_non_admin_forbidden(db_session: AsyncSession):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.xfail(strict=True, reason="ADMIN-11: overview endpoint not yet implemented")
|
||||
async def test_overview_returns_expected_keys(admin_client):
|
||||
"""Admin GET returns 200 with all four top-level keys present."""
|
||||
c, admin, session = admin_client
|
||||
@@ -128,7 +125,6 @@ async def test_overview_returns_expected_keys(admin_client):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.xfail(strict=True, reason="ADMIN-11: overview endpoint not yet implemented")
|
||||
async def test_overview_user_count_excludes_admins(admin_client):
|
||||
"""user_count reflects only role='user' accounts, not admins."""
|
||||
c, admin, session = admin_client
|
||||
@@ -146,7 +142,6 @@ async def test_overview_user_count_excludes_admins(admin_client):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.xfail(strict=True, reason="ADMIN-11: overview endpoint not yet implemented")
|
||||
async def test_overview_total_storage_sums_quotas(admin_client):
|
||||
"""total_storage_bytes sums used_bytes across all Quota rows."""
|
||||
c, admin, session = admin_client
|
||||
@@ -173,7 +168,6 @@ async def test_overview_total_storage_sums_quotas(admin_client):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.xfail(strict=True, reason="ADMIN-11: overview endpoint not yet implemented")
|
||||
async def test_overview_doc_status_groups_by_status(admin_client):
|
||||
"""doc_status maps each status to its count."""
|
||||
c, admin, session = admin_client
|
||||
@@ -204,7 +198,6 @@ async def test_overview_doc_status_groups_by_status(admin_client):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.xfail(strict=True, reason="ADMIN-11: overview endpoint not yet implemented")
|
||||
async def test_overview_recent_audit_limit_10(admin_client):
|
||||
"""recent_audit returns at most 10 entries ordered newest first."""
|
||||
c, admin, session = admin_client
|
||||
@@ -240,7 +233,6 @@ async def test_overview_recent_audit_limit_10(admin_client):
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.xfail(strict=True, reason="ADMIN-11: overview endpoint not yet implemented")
|
||||
async def test_overview_no_sensitive_fields(admin_client):
|
||||
"""Response body must not contain any sensitive field name."""
|
||||
c, admin, session = admin_client
|
||||
|
||||
Reference in New Issue
Block a user