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>
112 lines
3.6 KiB
Python
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)}
|