ppt-tool/backend/api/v1/admin/users_router.py
Vadym Samoilenko cf21ba4516 Phase 1-2: Foundation + Admin Panel & Client Management
Phase 1 (Foundation):
- Project restructure (presenton-main → backend/ + frontend/)
- Database schema (8 new models, Alembic config, seed script)
- Auth (Azure AD SSO + dev bypass, JWT sessions, AuthMiddleware)
- RBAC (access_service, rbac_middleware, admin routers)
- Audit logging (fire-and-forget, AuditMiddleware, admin router)
- i18n (react-i18next with 5 namespace files)

Phase 2 (Admin Panel & Client Management):
- Admin panel shell (sidebar layout, role guard, 12 pages)
- Redux admin slice with 18 async thunks
- User management (role changes, deactivation)
- Client management (CRUD, brand config, team management)
- Brand config editor (colors, fonts, logos, voice rules)
- Master deck upload & parser (PPTX → HTML → React pipeline)
- Audit log viewer with filters and CSV/JSON export

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-26 15:37:17 +00:00

112 lines
3.6 KiB
Python

"""Admin router for user management. Super Admin only."""
from typing import List, Optional
import uuid
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import select
from models.sql.user import UserModel
from services.database import get_async_session
from utils.auth_dependencies import require_super_admin
USERS_ROUTER = APIRouter(prefix="/users", tags=["Admin - Users"])
VALID_ROLES = {"super_admin", "client_admin", "user"}
@USERS_ROUTER.get("", response_model=List[dict])
async def list_users(
_: UserModel = Depends(require_super_admin),
is_active: Optional[bool] = Query(None),
role: Optional[str] = Query(None),
offset: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=200),
session: AsyncSession = Depends(get_async_session),
):
query = select(UserModel)
if is_active is not None:
query = query.where(UserModel.is_active == is_active)
if role:
query = query.where(UserModel.role == role)
query = query.order_by(UserModel.created_at.desc()).offset(offset).limit(limit)
result = await session.execute(query)
users = result.scalars().all()
return [
{
"id": str(u.id),
"email": u.email,
"display_name": u.display_name,
"role": u.role,
"is_active": u.is_active,
"last_login_at": u.last_login_at.isoformat() if u.last_login_at else None,
"created_at": u.created_at.isoformat() if u.created_at else None,
}
for u in users
]
@USERS_ROUTER.get("/{user_id}", response_model=dict)
async def get_user(
user_id: uuid.UUID,
_: UserModel = Depends(require_super_admin),
session: AsyncSession = Depends(get_async_session),
):
user = await session.get(UserModel, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
return {
"id": str(user.id),
"email": user.email,
"display_name": user.display_name,
"role": user.role,
"is_active": user.is_active,
"last_login_at": user.last_login_at.isoformat() if user.last_login_at else None,
"created_at": user.created_at.isoformat() if user.created_at else None,
}
@USERS_ROUTER.put("/{user_id}/role")
async def update_user_role(
user_id: uuid.UUID,
role: str = Query(...),
admin: UserModel = Depends(require_super_admin),
session: AsyncSession = Depends(get_async_session),
):
if role not in VALID_ROLES:
raise HTTPException(
status_code=400,
detail=f"Invalid role. Must be one of: {', '.join(VALID_ROLES)}",
)
user = await session.get(UserModel, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
if user.id == admin.id:
raise HTTPException(status_code=400, detail="Cannot change your own role")
user.role = role
session.add(user)
await session.commit()
return {"message": "Role updated", "user_id": str(user.id), "role": role}
@USERS_ROUTER.delete("/{user_id}")
async def deactivate_user(
user_id: uuid.UUID,
admin: UserModel = Depends(require_super_admin),
session: AsyncSession = Depends(get_async_session),
):
user = await session.get(UserModel, user_id)
if not user:
raise HTTPException(status_code=404, detail="User not found")
if user.id == admin.id:
raise HTTPException(status_code=400, detail="Cannot deactivate yourself")
user.is_active = False
session.add(user)
await session.commit()
return {"message": "User deactivated", "user_id": str(user.id)}