From 8deb78542b4c4daa5b82d7d9aab7a8c83e1b9ed9 Mon Sep 17 00:00:00 2001 From: Michael Chihlas Date: Sat, 14 Mar 2026 01:31:04 -0400 Subject: [PATCH] =?UTF-8?q?feat:=20refactor=20script=20template=20permissi?= =?UTF-8?q?ons=20=E2=80=94=20engineers=20manage=20own,=20add=20/share=20en?= =?UTF-8?q?dpoint?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 --- backend/app/api/endpoints/scripts.py | 91 ++++++++++++++++++++++------ 1 file changed, 74 insertions(+), 17 deletions(-) diff --git a/backend/app/api/endpoints/scripts.py b/backend/app/api/endpoints/scripts.py index e7ab9158..5f6ab8df 100644 --- a/backend/app/api/endpoints/scripts.py +++ b/backend/app/api/endpoints/scripts.py @@ -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 ──────────────────────────────────────────────────────────────