refactor: all task mutations require auth — actor from JWT/token, no ?actor= or ?slug= params
Some checks failed
Deploy Tracker / deploy (push) Failing after 1s

This commit is contained in:
markov 2026-02-24 13:32:29 +01:00
parent 2f0c4d1636
commit 904c105174

View File

@ -11,6 +11,7 @@ from sqlalchemy.orm import selectinload, joinedload
from tracker.database import get_db from tracker.database import get_db
from tracker.models import Task, Step, Project, Member, Message, Chat from tracker.models import Task, Step, Project, Member, Message, Chat
from tracker.api.auth import get_current_member
router = APIRouter(tags=["tasks"]) router = APIRouter(tags=["tasks"])
@ -231,6 +232,7 @@ async def get_task(task_id: str, db: AsyncSession = Depends(get_db)):
async def create_task( async def create_task(
project_slug: str = Query(...), project_slug: str = Query(...),
req: TaskCreate = ..., req: TaskCreate = ...,
current_member: Member = Depends(get_current_member),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
# Find project # Find project
@ -298,14 +300,14 @@ async def create_task(
async def update_task( async def update_task(
task_id: str, task_id: str,
req: TaskUpdate, req: TaskUpdate,
actor: Optional[str] = Query(None, description="Who made the change"), current_member: Member = Depends(get_current_member),
db: AsyncSession = Depends(get_db), db: AsyncSession = Depends(get_db),
): ):
task = await _get_task(task_id, db) task = await _get_task(task_id, db)
slug = task.project.slug if task.project else "" slug = task.project.slug if task.project else ""
prefix = slug[:2].upper() if slug else "XX" prefix = slug[:2].upper() if slug else "XX"
key = f"{prefix}-{task.number}" key = f"{prefix}-{task.number}"
who = f"@{actor}" if actor else "Кто-то" who = f"@{current_member.slug}"
old_status = task.status old_status = task.status
old_assignee = task.assignee_slug old_assignee = task.assignee_slug
@ -338,7 +340,7 @@ async def update_task(
chat_text=f"{key}: {old_status}{req.status}", chat_text=f"{key}: {old_status}{req.status}",
task_text=f"{who} изменил статус: {old_status}{req.status} ({now_str})", task_text=f"{who} изменил статус: {old_status}{req.status} ({now_str})",
project_slug=slug, project_slug=slug,
actor_slug=actor or "system", actor_slug=current_member.slug,
) )
if req.assignee_slug is not None and req.assignee_slug != old_assignee: if req.assignee_slug is not None and req.assignee_slug != old_assignee:
if req.assignee_slug: if req.assignee_slug:
@ -347,7 +349,7 @@ async def update_task(
chat_text=f"{key}: назначена на @{req.assignee_slug}", chat_text=f"{key}: назначена на @{req.assignee_slug}",
task_text=f"{who} назначил задачу на @{req.assignee_slug} ({now_str})", task_text=f"{who} назначил задачу на @{req.assignee_slug} ({now_str})",
project_slug=slug, project_slug=slug,
actor_slug=actor or "system", actor_slug=current_member.slug,
) )
else: else:
await _system_message( await _system_message(
@ -355,7 +357,7 @@ async def update_task(
chat_text=f"{key}: исполнитель снят", chat_text=f"{key}: исполнитель снят",
task_text=f"{who} снял исполнителя @{old_assignee} ({now_str})", task_text=f"{who} снял исполнителя @{old_assignee} ({now_str})",
project_slug=slug, project_slug=slug,
actor_slug=actor or "system", actor_slug=current_member.slug,
) )
await db.commit() await db.commit()
@ -375,7 +377,7 @@ async def update_task(
@router.delete("/tasks/{task_id}") @router.delete("/tasks/{task_id}")
async def delete_task(task_id: str, db: AsyncSession = Depends(get_db)): async def delete_task(task_id: str, current_member: Member = Depends(get_current_member), db: AsyncSession = Depends(get_db)):
task = await _get_task(task_id, db) task = await _get_task(task_id, db)
project_id = str(task.project_id) project_id = str(task.project_id)
task_data = {"id": str(task.id), "project_id": project_id} task_data = {"id": str(task.id), "project_id": project_id}
@ -387,8 +389,9 @@ async def delete_task(task_id: str, db: AsyncSession = Depends(get_db)):
@router.post("/tasks/{task_id}/take", response_model=TaskOut) @router.post("/tasks/{task_id}/take", response_model=TaskOut)
async def take_task(task_id: str, slug: str = Query(...), db: AsyncSession = Depends(get_db)): async def take_task(task_id: str, current_member: Member = Depends(get_current_member), db: AsyncSession = Depends(get_db)):
"""Atomically take a task — only if unassigned and in backlog/todo.""" """Atomically take a task — only if unassigned and in backlog/todo."""
slug = current_member.slug
task = await _get_task(task_id, db) task = await _get_task(task_id, db)
if task.assignee_slug: if task.assignee_slug:
raise HTTPException(409, f"Task already assigned to {task.assignee_slug}") raise HTTPException(409, f"Task already assigned to {task.assignee_slug}")
@ -431,7 +434,7 @@ async def take_task(task_id: str, slug: str = Query(...), db: AsyncSession = Dep
@router.post("/tasks/{task_id}/reject") @router.post("/tasks/{task_id}/reject")
async def reject_task(task_id: str, req: RejectRequest, db: AsyncSession = Depends(get_db)): async def reject_task(task_id: str, req: RejectRequest, current_member: Member = Depends(get_current_member), db: AsyncSession = Depends(get_db)):
"""Reject a task with reason — unassign and return to todo.""" """Reject a task with reason — unassign and return to todo."""
task = await _get_task(task_id, db) task = await _get_task(task_id, db)
old_assignee = task.assignee_slug old_assignee = task.assignee_slug
@ -456,7 +459,7 @@ async def reject_task(task_id: str, req: RejectRequest, db: AsyncSession = Depen
@router.post("/tasks/{task_id}/assign", response_model=TaskOut) @router.post("/tasks/{task_id}/assign", response_model=TaskOut)
async def assign_task(task_id: str, req: AssignRequest, db: AsyncSession = Depends(get_db)): async def assign_task(task_id: str, req: AssignRequest, current_member: Member = Depends(get_current_member), db: AsyncSession = Depends(get_db)):
"""Assign task to a member.""" """Assign task to a member."""
# Verify assignee exists # Verify assignee exists
result = await db.execute(select(Member).where(Member.slug == req.assignee_slug)) result = await db.execute(select(Member).where(Member.slug == req.assignee_slug))
@ -495,7 +498,8 @@ async def assign_task(task_id: str, req: AssignRequest, db: AsyncSession = Depen
@router.post("/tasks/{task_id}/watch") @router.post("/tasks/{task_id}/watch")
async def watch_task(task_id: str, slug: str = Query(...), db: AsyncSession = Depends(get_db)): async def watch_task(task_id: str, current_member: Member = Depends(get_current_member), db: AsyncSession = Depends(get_db)):
slug = current_member.slug
task = await _get_task(task_id, db) task = await _get_task(task_id, db)
if slug not in (task.watchers or []): if slug not in (task.watchers or []):
task.watchers = (task.watchers or []) + [slug] task.watchers = (task.watchers or []) + [slug]
@ -504,7 +508,8 @@ async def watch_task(task_id: str, slug: str = Query(...), db: AsyncSession = De
@router.delete("/tasks/{task_id}/watch") @router.delete("/tasks/{task_id}/watch")
async def unwatch_task(task_id: str, slug: str = Query(...), db: AsyncSession = Depends(get_db)): async def unwatch_task(task_id: str, current_member: Member = Depends(get_current_member), db: AsyncSession = Depends(get_db)):
slug = current_member.slug
task = await _get_task(task_id, db) task = await _get_task(task_id, db)
task.watchers = [w for w in (task.watchers or []) if w != slug] task.watchers = [w for w in (task.watchers or []) if w != slug]
await db.commit() await db.commit()