""" Admin API Endpoints =================== User management, role assignment, audit logs query, and workspace configuration. Protected with strict RBAC: require_roles(UserRole.ADMIN) """ from __future__ import annotations from typing import List, Optional import structlog from fastapi import APIRouter, Depends, HTTPException, status from pydantic import BaseModel, EmailStr from sqlalchemy import select, func from app.core.dependencies import AuthUser, DBSession from app.core.security import UserRole, require_roles, hash_password from app.models.user import User from app.models.tenant import Tenant from app.models.audit import AuditLog logger = structlog.get_logger(__name__) router = APIRouter(prefix="/admin", dependencies=[Depends(require_roles(UserRole.ADMIN))]) # ── Pydantic Schemas ────────────────────────────────────────── class AdminUserResponse(BaseModel): id: str email: str full_name: str role: str is_active: bool created_at: Optional[str] = None model_config = {"from_attributes": True} class InviteUserRequest(BaseModel): email: EmailStr full_name: str role: UserRole = UserRole.EMPLOYEE password: str = "Welcome@123!" class AuditLogResponse(BaseModel): id: str user_id: str action: str agent_name: Optional[str] outcome: str tokens_used: Optional[int] latency_ms: Optional[int] created_at: Optional[str] = None model_config = {"from_attributes": True} class TenantDetailsResponse(BaseModel): id: str name: str slug: str plan: str is_active: bool user_count: int # ── Routes ──────────────────────────────────────────────────── @router.get("/users", response_model=List[AdminUserResponse]) async def list_tenant_users( db: DBSession, current_user: AuthUser, ): """List all users within the current user's tenant (Admin only).""" result = await db.execute( select(User).where(User.tenant_id == current_user.tenant_id).order_by(User.created_at.desc()) ) users = result.scalars().all() return [ AdminUserResponse( id=u.id, email=u.email, full_name=u.full_name, role=u.role, is_active=u.is_active, created_at=u.created_at.isoformat() if u.created_at else None, ) for u in users ] @router.post("/users/invite", response_model=AdminUserResponse, status_code=status.HTTP_201_CREATED) async def invite_user( data: InviteUserRequest, db: DBSession, current_user: AuthUser, ): """Invite/Create a new user in the tenant with a specified role (Admin only).""" existing = await db.execute(select(User).where(User.email == data.email)) if existing.scalar_one_or_none(): raise HTTPException( status_code=status.HTTP_409_CONFLICT, detail={"error": "user_exists", "message": "User with this email already exists"}, ) new_user = User( email=data.email, hashed_password=hash_password(data.password), full_name=data.full_name, role=data.role.value, tenant_id=current_user.tenant_id, is_active=True, is_verified=True, ) db.add(new_user) await db.commit() logger.info("Admin invited new user", admin=current_user.email, invited=new_user.email, role=new_user.role) return AdminUserResponse( id=new_user.id, email=new_user.email, full_name=new_user.full_name, role=new_user.role, is_active=new_user.is_active, created_at=new_user.created_at.isoformat() if new_user.created_at else None, ) @router.get("/audit-logs", response_model=List[AuditLogResponse]) async def get_audit_logs( db: DBSession, current_user: AuthUser, limit: int = 50, ): """Get audit logs for current tenant (Admin only).""" result = await db.execute( select(AuditLog) .where(AuditLog.tenant_id == current_user.tenant_id) .order_by(AuditLog.created_at.desc()) .limit(limit) ) logs = result.scalars().all() return [ AuditLogResponse( id=l.id, user_id=l.user_id, action=l.action, agent_name=l.agent_name, outcome=l.outcome, tokens_used=l.tokens_used, latency_ms=l.latency_ms, created_at=l.created_at.isoformat() if l.created_at else None, ) for l in logs ] @router.get("/tenant", response_model=TenantDetailsResponse) async def get_tenant_details( db: DBSession, current_user: AuthUser, ): """Get workspace tenant info and user count (Admin only).""" tenant_res = await db.execute(select(Tenant).where(Tenant.id == current_user.tenant_id)) tenant = tenant_res.scalar_one_or_none() if not tenant: raise HTTPException(status_code=404, detail="Tenant not found") count_res = await db.execute( select(func.count(User.id)).where(User.tenant_id == current_user.tenant_id) ) user_count = count_res.scalar() or 0 return TenantDetailsResponse( id=tenant.id, name=tenant.name, slug=tenant.slug, plan=tenant.plan, is_active=tenant.is_active, user_count=user_count, )