Files
nessi f925009977 chore: initial project setup with backend, frontend, Android app, and CI/CD
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
2026-06-28 09:37:51 +02:00

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()