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>
154 lines
4.8 KiB
Python
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)}
|