fix(tasks): audit pass — permission checks, tool gaps, hard reload
- PUT/PATCH/DELETE /api/tasks/:id now use get_note_for_user + can_write_note
so shared project editors can mutate tasks; owners unaffected
- PATCH /api/tasks/:id/status gets same treatment
- All write routes call update_note/delete_note with note.user_id (owner)
not the accessing user's uid, matching the milestone fix pattern
- create_task tool gains tags (array) and status (enum) parameters;
handler now passes tags to create_note and respects initial status
- create_task tool response now includes milestone_id and parent_id
- update_note tool gains milestone parameter; handler resolves the
milestone by title within the note's current (or newly set) project,
clears milestone_id when project is cleared
- list_tasks tool gains q keyword search parameter; passed through
to list_notes
- TaskEditorView: replace window.location.reload() with
router.push('/tasks/:id') after save
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -6,6 +6,7 @@ from quart import Blueprint, jsonify, request
|
||||
from fabledassistant.auth import login_required, get_current_user_id
|
||||
from fabledassistant.models.note import TaskPriority, TaskStatus
|
||||
from fabledassistant.routes.utils import not_found, parse_iso_date, parse_pagination
|
||||
from fabledassistant.services.access import can_write_note
|
||||
from fabledassistant.services.embeddings import upsert_note_embedding
|
||||
from fabledassistant.services.notes import (
|
||||
create_note,
|
||||
@@ -154,6 +155,13 @@ async def get_task_route(task_id: int):
|
||||
@login_required
|
||||
async def update_task_route(task_id: int):
|
||||
uid = get_current_user_id()
|
||||
result = await get_note_for_user(uid, task_id)
|
||||
if result is None:
|
||||
return not_found("Task")
|
||||
task_note, _ = result
|
||||
if not await can_write_note(uid, task_id):
|
||||
return jsonify({"error": "Permission denied"}), 403
|
||||
|
||||
data = await request.get_json()
|
||||
fields = {}
|
||||
for key in ("title",):
|
||||
@@ -198,12 +206,12 @@ async def update_task_route(task_id: int):
|
||||
if recurrence_rule is not _UNSET:
|
||||
fields["recurrence_rule"] = recurrence_rule
|
||||
|
||||
task = await update_note(uid, task_id, **fields)
|
||||
task = await update_note(task_note.user_id, task_id, **fields)
|
||||
if task is None:
|
||||
return not_found("Task")
|
||||
text = f"{task.title}\n{task.body}".strip() if task.body else (task.title or "")
|
||||
if text:
|
||||
asyncio.create_task(upsert_note_embedding(task.id, uid, text))
|
||||
asyncio.create_task(upsert_note_embedding(task.id, task_note.user_id, text))
|
||||
return jsonify(task.to_dict())
|
||||
|
||||
|
||||
@@ -211,17 +219,23 @@ async def update_task_route(task_id: int):
|
||||
@login_required
|
||||
async def patch_task_status(task_id: int):
|
||||
uid = get_current_user_id()
|
||||
result = await get_note_for_user(uid, task_id)
|
||||
if result is None:
|
||||
return not_found("Task")
|
||||
task_note, _ = result
|
||||
if not await can_write_note(uid, task_id):
|
||||
return jsonify({"error": "Permission denied"}), 403
|
||||
|
||||
data = await request.get_json()
|
||||
status_val = data.get("status")
|
||||
if not status_val:
|
||||
return jsonify({"error": "status is required"}), 400
|
||||
|
||||
try:
|
||||
TaskStatus(status_val)
|
||||
except ValueError:
|
||||
return jsonify({"error": f"Invalid status: {status_val}"}), 400
|
||||
|
||||
task = await update_note(uid, task_id, status=status_val)
|
||||
task = await update_note(task_note.user_id, task_id, status=status_val)
|
||||
if task is None:
|
||||
return not_found("Task")
|
||||
return jsonify(task.to_dict())
|
||||
@@ -252,7 +266,13 @@ async def recurrence_preview_route(task_id: int):
|
||||
@login_required
|
||||
async def delete_task_route(task_id: int):
|
||||
uid = get_current_user_id()
|
||||
deleted = await delete_note(uid, task_id)
|
||||
result = await get_note_for_user(uid, task_id)
|
||||
if result is None:
|
||||
return not_found("Task")
|
||||
task_note, _ = result
|
||||
if not await can_write_note(uid, task_id):
|
||||
return jsonify({"error": "Permission denied"}), 403
|
||||
deleted = await delete_note(task_note.user_id, task_id)
|
||||
if not deleted:
|
||||
return not_found("Task")
|
||||
return "", 204
|
||||
|
||||
Reference in New Issue
Block a user