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

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"}