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>
204 lines
6.6 KiB
Python
204 lines
6.6 KiB
Python
"""Admin router for team management."""
|
|
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.team import TeamModel
|
|
from models.sql.team_membership import TeamMembershipModel
|
|
from models.sql.user import UserModel
|
|
from services.database import get_async_session
|
|
from services.access_service import get_accessible_client_ids
|
|
from api.middlewares.rbac_middleware import check_team_admin
|
|
from utils.auth_dependencies import get_current_user, require_client_admin
|
|
|
|
TEAMS_ROUTER = APIRouter(prefix="/teams", tags=["Admin - Teams"])
|
|
|
|
|
|
@TEAMS_ROUTER.post("", status_code=201)
|
|
async def create_team(
|
|
name: str = Body(..., embed=True),
|
|
client_id: Optional[uuid.UUID] = Body(None, embed=True),
|
|
admin: UserModel = Depends(require_client_admin),
|
|
session: AsyncSession = Depends(get_async_session),
|
|
):
|
|
if client_id:
|
|
await check_team_admin(admin, client_id, session)
|
|
|
|
team = TeamModel(name=name, client_id=client_id, is_default=False)
|
|
session.add(team)
|
|
await session.commit()
|
|
return {
|
|
"id": str(team.id),
|
|
"name": team.name,
|
|
"client_id": str(team.client_id) if team.client_id else None,
|
|
"is_default": team.is_default,
|
|
}
|
|
|
|
|
|
@TEAMS_ROUTER.get("", response_model=List[dict])
|
|
async def list_teams(
|
|
client_id: Optional[uuid.UUID] = Query(None),
|
|
admin: UserModel = Depends(require_client_admin),
|
|
session: AsyncSession = Depends(get_async_session),
|
|
):
|
|
query = select(TeamModel)
|
|
|
|
if admin.role != "super_admin":
|
|
accessible_ids = await get_accessible_client_ids(admin, session)
|
|
query = query.where(
|
|
(TeamModel.client_id.in_(accessible_ids)) | (TeamModel.is_default == True) # noqa: E712
|
|
)
|
|
|
|
if client_id:
|
|
query = query.where(TeamModel.client_id == client_id)
|
|
|
|
result = await session.execute(query.order_by(TeamModel.created_at.desc()))
|
|
teams = result.scalars().all()
|
|
return [
|
|
{
|
|
"id": str(t.id),
|
|
"name": t.name,
|
|
"client_id": str(t.client_id) if t.client_id else None,
|
|
"is_default": t.is_default,
|
|
"created_at": t.created_at.isoformat() if t.created_at else None,
|
|
}
|
|
for t in teams
|
|
]
|
|
|
|
|
|
@TEAMS_ROUTER.get("/{team_id}")
|
|
async def get_team(
|
|
team_id: uuid.UUID,
|
|
admin: UserModel = Depends(require_client_admin),
|
|
session: AsyncSession = Depends(get_async_session),
|
|
):
|
|
team = await session.get(TeamModel, team_id)
|
|
if not team:
|
|
raise HTTPException(status_code=404, detail="Team not found")
|
|
|
|
if team.client_id and admin.role != "super_admin":
|
|
await check_team_admin(admin, team.client_id, session)
|
|
|
|
# Get members
|
|
result = await session.execute(
|
|
select(UserModel)
|
|
.join(TeamMembershipModel, TeamMembershipModel.user_id == UserModel.id)
|
|
.where(TeamMembershipModel.team_id == team_id)
|
|
)
|
|
members = result.scalars().all()
|
|
|
|
return {
|
|
"id": str(team.id),
|
|
"name": team.name,
|
|
"client_id": str(team.client_id) if team.client_id else None,
|
|
"is_default": team.is_default,
|
|
"created_at": team.created_at.isoformat() if team.created_at else None,
|
|
"members": [
|
|
{
|
|
"id": str(m.id),
|
|
"email": m.email,
|
|
"display_name": m.display_name,
|
|
"role": m.role,
|
|
}
|
|
for m in members
|
|
],
|
|
}
|
|
|
|
|
|
@TEAMS_ROUTER.post("/{team_id}/members", status_code=201)
|
|
async def add_team_member(
|
|
team_id: uuid.UUID,
|
|
user_id: uuid.UUID = Body(..., embed=True),
|
|
admin: UserModel = Depends(require_client_admin),
|
|
session: AsyncSession = Depends(get_async_session),
|
|
):
|
|
team = await session.get(TeamModel, team_id)
|
|
if not team:
|
|
raise HTTPException(status_code=404, detail="Team not found")
|
|
|
|
if team.client_id and admin.role != "super_admin":
|
|
await check_team_admin(admin, team.client_id, session)
|
|
|
|
user = await session.get(UserModel, user_id)
|
|
if not user:
|
|
raise HTTPException(status_code=404, detail="User not found")
|
|
|
|
# Check for existing membership
|
|
result = await session.execute(
|
|
select(TeamMembershipModel).where(
|
|
TeamMembershipModel.user_id == user_id,
|
|
TeamMembershipModel.team_id == team_id,
|
|
)
|
|
)
|
|
if result.scalar_one_or_none():
|
|
raise HTTPException(status_code=409, detail="User already in team")
|
|
|
|
membership = TeamMembershipModel(
|
|
user_id=user_id,
|
|
team_id=team_id,
|
|
assigned_by=admin.id,
|
|
)
|
|
session.add(membership)
|
|
await session.commit()
|
|
return {"message": "Member added", "user_id": str(user_id), "team_id": str(team_id)}
|
|
|
|
|
|
@TEAMS_ROUTER.delete("/{team_id}/members/{user_id}")
|
|
async def remove_team_member(
|
|
team_id: uuid.UUID,
|
|
user_id: uuid.UUID,
|
|
admin: UserModel = Depends(require_client_admin),
|
|
session: AsyncSession = Depends(get_async_session),
|
|
):
|
|
team = await session.get(TeamModel, team_id)
|
|
if not team:
|
|
raise HTTPException(status_code=404, detail="Team not found")
|
|
|
|
if team.client_id and admin.role != "super_admin":
|
|
await check_team_admin(admin, team.client_id, session)
|
|
|
|
result = await session.execute(
|
|
select(TeamMembershipModel).where(
|
|
TeamMembershipModel.user_id == user_id,
|
|
TeamMembershipModel.team_id == team_id,
|
|
)
|
|
)
|
|
membership = result.scalar_one_or_none()
|
|
if not membership:
|
|
raise HTTPException(status_code=404, detail="Membership not found")
|
|
|
|
await session.delete(membership)
|
|
await session.commit()
|
|
return {"message": "Member removed"}
|
|
|
|
|
|
@TEAMS_ROUTER.delete("/{team_id}")
|
|
async def delete_team(
|
|
team_id: uuid.UUID,
|
|
admin: UserModel = Depends(require_client_admin),
|
|
session: AsyncSession = Depends(get_async_session),
|
|
):
|
|
team = await session.get(TeamModel, team_id)
|
|
if not team:
|
|
raise HTTPException(status_code=404, detail="Team not found")
|
|
|
|
if team.is_default:
|
|
raise HTTPException(status_code=400, detail="Cannot delete the default team")
|
|
|
|
if team.client_id and admin.role != "super_admin":
|
|
await check_team_admin(admin, team.client_id, session)
|
|
|
|
# Remove all memberships first
|
|
result = await session.execute(
|
|
select(TeamMembershipModel).where(TeamMembershipModel.team_id == team_id)
|
|
)
|
|
memberships = result.scalars().all()
|
|
for m in memberships:
|
|
await session.delete(m)
|
|
|
|
await session.delete(team)
|
|
await session.commit()
|
|
return {"message": "Team deleted"}
|