| """ |
| 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))]) |
|
|
|
|
| |
| 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 |
|
|
|
|
| |
|
|
| @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, |
| ) |
|
|