Add complete NexaMFA push MFA system with: - FastAPI backend with PostgreSQL, Redis, OIDC provider, and Prometheus metrics - React TypeScript admin console - Android Kotlin/Jetpack Compose app with biometric authentication - Docker Compose deployment configuration - Gitea CI workflow for backend, frontend, and Android builds - Environment configuration template with security settings - Documentation for security model, deployment
58 lines
2.2 KiB
Python
58 lines
2.2 KiB
Python
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
from sqlalchemy import desc, select
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.core.deps import get_db, require_admin
|
|
from app.core.security import now_utc
|
|
from app.models.audit import AuditLog
|
|
from app.models.challenge import Challenge
|
|
from app.models.device import Device
|
|
from app.models.user import User
|
|
from app.schemas.api import AuditOut, ChallengeOut, DeviceOut, UserOut
|
|
from app.services.audit import audit
|
|
|
|
router = APIRouter(prefix="/api/admin", tags=["admin"], dependencies=[Depends(require_admin)])
|
|
|
|
|
|
@router.get("/users", response_model=list[UserOut])
|
|
async def users(session: AsyncSession = Depends(get_db)):
|
|
return (await session.scalars(select(User).order_by(User.username))).all()
|
|
|
|
|
|
@router.get("/devices", response_model=list[DeviceOut])
|
|
async def devices(session: AsyncSession = Depends(get_db)):
|
|
return (await session.scalars(select(Device).order_by(desc(Device.created_at)))).all()
|
|
|
|
|
|
@router.post("/devices/{device_id}/revoke")
|
|
async def revoke_device(device_id: UUID, request: Request, session: AsyncSession = Depends(get_db)):
|
|
device = await session.get(Device, device_id)
|
|
if not device:
|
|
raise HTTPException(status_code=404, detail="Device not found")
|
|
device.is_revoked = True
|
|
device.revoked_at = now_utc()
|
|
await audit(
|
|
session,
|
|
"device.revoked",
|
|
actor="admin",
|
|
target_type="device",
|
|
target_id=str(device.id),
|
|
ip_address=request.client.host if request.client else None,
|
|
)
|
|
await session.commit()
|
|
return {"status": "revoked"}
|
|
|
|
|
|
@router.get("/audit", response_model=list[AuditOut])
|
|
async def audit_logs(limit: int = 100, session: AsyncSession = Depends(get_db)):
|
|
limit = min(max(limit, 1), 500)
|
|
return (await session.scalars(select(AuditLog).order_by(desc(AuditLog.created_at)).limit(limit))).all()
|
|
|
|
|
|
@router.get("/challenges", response_model=list[ChallengeOut])
|
|
async def challenges(limit: int = 100, session: AsyncSession = Depends(get_db)):
|
|
limit = min(max(limit, 1), 500)
|
|
return (await session.scalars(select(Challenge).order_by(desc(Challenge.created_at)).limit(limit))).all()
|