ppt-tool/backend/api/v1/admin/clients_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

154 lines
4.8 KiB
Python

"""Admin router for client management."""
import re
from typing import List, Optional
import uuid
from fastapi import APIRouter, Body, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from sqlmodel import select
from models.sql.client import ClientModel
from models.sql.team import TeamModel
from models.sql.user import UserModel
from services.database import get_async_session
from services.access_service import get_accessible_clients
from api.middlewares.rbac_middleware import check_client_access
from utils.auth_dependencies import require_super_admin, require_client_admin
CLIENTS_ROUTER = APIRouter(prefix="/clients", tags=["Admin - Clients"])
def _slugify(name: str) -> str:
slug = name.lower().strip()
slug = re.sub(r"[^a-z0-9]+", "-", slug)
return slug.strip("-")
@CLIENTS_ROUTER.post("", status_code=201)
async def create_client(
name: str = Body(..., embed=True),
review_policy: str = Body("self_approve", embed=True),
_: UserModel = Depends(require_super_admin),
session: AsyncSession = Depends(get_async_session),
):
slug = _slugify(name)
# Check slug uniqueness
result = await session.execute(
select(ClientModel).where(ClientModel.slug == slug)
)
if result.scalar_one_or_none():
raise HTTPException(status_code=409, detail="A client with this name already exists")
client = ClientModel(name=name, slug=slug, review_policy=review_policy)
session.add(client)
await session.flush()
# Auto-create a team for this client
team = TeamModel(name=f"{name} Team", client_id=client.id, is_default=False)
session.add(team)
await session.commit()
return {
"id": str(client.id),
"name": client.name,
"slug": client.slug,
"review_policy": client.review_policy,
"team_id": str(team.id),
}
@CLIENTS_ROUTER.get("", response_model=List[dict])
async def list_clients(
admin: UserModel = Depends(require_client_admin),
session: AsyncSession = Depends(get_async_session),
):
clients = await get_accessible_clients(admin, session)
return [
{
"id": str(c.id),
"name": c.name,
"slug": c.slug,
"logo_path": c.logo_path,
"retention_days": c.retention_days,
"review_policy": c.review_policy,
"is_active": c.is_active,
"created_at": c.created_at.isoformat() if c.created_at else None,
}
for c in clients
]
@CLIENTS_ROUTER.get("/{client_id}")
async def get_client(
client_id: uuid.UUID,
admin: UserModel = Depends(require_client_admin),
session: AsyncSession = Depends(get_async_session),
):
await check_client_access(admin, client_id, session)
client = await session.get(ClientModel, client_id)
if not client:
raise HTTPException(status_code=404, detail="Client not found")
return {
"id": str(client.id),
"name": client.name,
"slug": client.slug,
"logo_path": client.logo_path,
"retention_days": client.retention_days,
"review_policy": client.review_policy,
"is_active": client.is_active,
"created_at": client.created_at.isoformat() if client.created_at else None,
}
@CLIENTS_ROUTER.put("/{client_id}")
async def update_client(
client_id: uuid.UUID,
name: Optional[str] = Body(None, embed=True),
review_policy: Optional[str] = Body(None, embed=True),
retention_days: Optional[int] = Body(None, embed=True),
admin: UserModel = Depends(require_client_admin),
session: AsyncSession = Depends(get_async_session),
):
await check_client_access(admin, client_id, session)
client = await session.get(ClientModel, client_id)
if not client:
raise HTTPException(status_code=404, detail="Client not found")
if name is not None:
client.name = name
client.slug = _slugify(name)
if review_policy is not None:
client.review_policy = review_policy
if retention_days is not None:
client.retention_days = retention_days
session.add(client)
await session.commit()
return {
"id": str(client.id),
"name": client.name,
"slug": client.slug,
"review_policy": client.review_policy,
"retention_days": client.retention_days,
}
@CLIENTS_ROUTER.delete("/{client_id}")
async def deactivate_client(
client_id: uuid.UUID,
_: UserModel = Depends(require_super_admin),
session: AsyncSession = Depends(get_async_session),
):
client = await session.get(ClientModel, client_id)
if not client:
raise HTTPException(status_code=404, detail="Client not found")
client.is_active = False
session.add(client)
await session.commit()
return {"message": "Client deactivated", "client_id": str(client.id)}