feat: refactor script template permissions — engineers manage own, add /share endpoint

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Michael Chihlas
2026-03-14 01:31:04 -04:00
parent 86110c6ace
commit 8deb78542b

View File

@@ -9,6 +9,7 @@ from sqlalchemy import select, func, or_
from app.core.database import get_db
from app.api.deps import get_current_active_user
from app.core.permissions import can_manage_script_template, can_create_content
from app.models.user import User
from app.models.script_template import ScriptCategory, ScriptTemplate, ScriptGeneration
from app.schemas.script_template import (
@@ -27,14 +28,6 @@ router = APIRouter(prefix="/scripts", tags=["scripts"])
_engine = ScriptTemplateEngine()
def _require_team_admin(user: User) -> None:
"""Raise 403 if user is not a team admin or super admin."""
if not (user.is_team_admin or user.is_super_admin):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Team admin access required",
)
# ── Categories ────────────────────────────────────────────────────────────
@@ -82,6 +75,7 @@ async def list_templates(
category_slug: Optional[str] = Query(None),
search: Optional[str] = Query(None),
tags: Optional[str] = Query(None, description="Comma-separated tags"),
managed: Optional[bool] = Query(None, description="If true, return only templates this user can edit"),
) -> list[ScriptTemplateListItem]:
query = (
select(ScriptTemplate)
@@ -108,6 +102,20 @@ async def list_templates(
)
)
if managed:
if current_user.is_super_admin:
pass # super admin can edit all
elif current_user.account_role == "owner":
query = query.where(
or_(
ScriptTemplate.created_by == current_user.id,
ScriptTemplate.team_id != None, # noqa: E711
)
)
else:
# engineers see only their own
query = query.where(ScriptTemplate.created_by == current_user.id)
result = await db.execute(query.order_by(ScriptTemplate.name))
templates = result.scalars().all()
@@ -156,7 +164,11 @@ async def create_template(
db: Annotated[AsyncSession, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_active_user)],
) -> ScriptTemplateDetail:
_require_team_admin(current_user)
if not can_create_content(current_user):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Engineer access required to create templates",
)
cat_result = await db.execute(
select(ScriptCategory).where(
@@ -202,19 +214,23 @@ async def update_template(
db: Annotated[AsyncSession, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_active_user)],
) -> ScriptTemplateDetail:
_require_team_admin(current_user)
result = await db.execute(
select(ScriptTemplate).where(
ScriptTemplate.id == template_id,
ScriptTemplate.team_id == current_user.team_id,
ScriptTemplate.is_active == True, # noqa: E712
)
)
template = result.scalar_one_or_none()
if not template:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Template not found or not editable",
detail="Template not found",
)
if not can_manage_script_template(current_user, template.created_by, template.team_id):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to edit this template",
)
update_data = data.model_dump(exclude_unset=True)
@@ -234,25 +250,66 @@ async def delete_template(
db: Annotated[AsyncSession, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_active_user)],
) -> None:
_require_team_admin(current_user)
result = await db.execute(
select(ScriptTemplate).where(
ScriptTemplate.id == template_id,
ScriptTemplate.team_id == current_user.team_id,
ScriptTemplate.is_active == True, # noqa: E712
)
)
template = result.scalar_one_or_none()
if not template:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Template not found or not deletable",
detail="Template not found",
)
if not can_manage_script_template(current_user, template.created_by, template.team_id):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="You don't have permission to delete this template",
)
template.is_active = False
await db.commit()
@router.patch("/templates/{template_id}/share", response_model=ScriptTemplateDetail)
async def share_template(
template_id: UUID,
db: Annotated[AsyncSession, Depends(get_db)],
current_user: Annotated[User, Depends(get_current_active_user)],
shared: bool = Query(..., description="true to share with team, false to make personal"),
) -> ScriptTemplateDetail:
"""Toggle team sharing for a template. Owner/admin/super_admin only."""
if not (current_user.is_super_admin or current_user.account_role == "owner"):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Only account owners and admins can share templates",
)
result = await db.execute(
select(ScriptTemplate).where(
ScriptTemplate.id == template_id,
ScriptTemplate.is_active == True, # noqa: E712
)
)
template = result.scalar_one_or_none()
if not template:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="Template not found",
)
if shared:
template.team_id = current_user.team_id
else:
template.team_id = None
await db.commit()
await db.refresh(template)
return ScriptTemplateDetail.model_validate(template)
# ── Generate ──────────────────────────────────────────────────────────────