feat(tasks): add read-only sharing permission for task lists
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m5s

Task list shares now carry a permission (editor/viewer). The server rejects
writes from viewers with 403, and the UI hides or disables edit controls
for read-only lists.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Sonnet 5.5 committed 2026-10-27 22:30:28 +01:00
1 parent 6896a29285
commit 4e9c4ffa8c
9 files changed
+300 -61

No files matched your search

+36 -6
View File
@@ -4,6 +4,7 @@ import (
"bytes"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
@@ -243,6 +244,15 @@ func writeError(w http.ResponseWriter, status int, message string) {
writeJSON(w, status, map[string]string{"error": message})
}
// writeStoreError maps read-only task list violations to 403 and everything else to the given fallback status.
func writeStoreError(w http.ResponseWriter, fallbackStatus int, err error) {
if errors.Is(err, storage.ErrTaskListReadOnly) {
writeError(w, http.StatusForbidden, err.Error())
return
}
writeError(w, fallbackStatus, err.Error())
}
func (s *Server) apiLoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Only log API requests (/api/...)
@@ -673,7 +683,7 @@ func (s *Server) handleCaptureInbox(w http.ResponseWriter, r *http.Request) {
thought, err := s.store.CreateThoughtWithDetails(userID, req.Content, req.Priority, source, dueDate, taskListID, "todo", "")
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
writeStoreError(w, http.StatusBadRequest, err)
return
}
@@ -764,7 +774,7 @@ func (s *Server) handleTriageAction(w http.ResponseWriter, r *http.Request) {
updated, err := s.store.UpdateThoughtTriage(userID, thoughtID, req.Action, convertTo, targetListID, dueDate, taskListID)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
writeStoreError(w, http.StatusBadRequest, err)
return
}
@@ -805,6 +815,17 @@ func (s *Server) handleReorderTasks(w http.ResponseWriter, r *http.Request) {
return
}
if taskListID != "" && !s.store.CanEditTaskList(userID, taskListID) {
writeError(w, http.StatusForbidden, storage.ErrTaskListReadOnly.Error())
return
}
for _, it := range req.Items {
if th, err := s.store.GetThoughtByID(userID, it.ID); err == nil && th.TaskListID != "" && !s.store.CanEditTaskList(userID, th.TaskListID) {
writeError(w, http.StatusForbidden, storage.ErrTaskListReadOnly.Error())
return
}
}
if err := s.store.ReorderTasks(taskListID, req.Items); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
return
@@ -840,7 +861,7 @@ func (s *Server) handleUpdateThought(w http.ResponseWriter, r *http.Request) {
updated, err := s.store.UpdateThoughtDetails(userID, thoughtID, req.Content, req.IsCompleted, req.DueDate, req.Priority, req.TaskListID, req.KanbanStatus, req.Notes, req.Subtasks, req.Tags)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
writeStoreError(w, http.StatusBadRequest, err)
return
}
s.store.SyncDocBlockAndTask(userID, thoughtID, updated.IsCompleted)
@@ -858,9 +879,13 @@ func (s *Server) handleUnlinkEnhancedTask(w http.ResponseWriter, r *http.Request
func (s *Server) handleDeleteThought(w http.ResponseWriter, r *http.Request) {
userID := auth.GetUserID(r.Context())
thoughtID := chi.URLParam(r, "id")
if th, err := s.store.GetThoughtByID(userID, thoughtID); err == nil && th.TaskListID != "" && !s.store.CanEditTaskList(userID, th.TaskListID) {
writeError(w, http.StatusForbidden, storage.ErrTaskListReadOnly.Error())
return
}
s.store.UnlinkTaskFromDocBlock(userID, thoughtID)
if err := s.store.DeleteThought(userID, thoughtID); err != nil {
writeError(w, http.StatusInternalServerError, err.Error())
writeStoreError(w, http.StatusInternalServerError, err)
return
}
s.wsHub.BroadcastToAll("TASK_DELETED", map[string]string{"id": thoughtID})
@@ -2157,7 +2182,7 @@ func (s *Server) handleUpdateTaskList(w http.ResponseWriter, r *http.Request) {
}
tl, err := s.store.UpdateTaskList(userID, listID, req.Title, req.Icon, req.Color, req.TeamID, req.SpaceID, req.Columns)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
writeStoreError(w, http.StatusBadRequest, err)
return
}
s.wsHub.BroadcastToAll("TASK_LIST_UPDATED", tl)
@@ -2205,12 +2230,17 @@ func (s *Server) handleCreateTaskListShare(w http.ResponseWriter, r *http.Reques
var req struct {
TargetType string `json:"targetType"` // team | user
TargetID string `json:"targetId"`
Permission string `json:"permission"` // editor | viewer
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
return
}
share, err := s.store.AddTaskListShare(listID, req.TargetType, req.TargetID, userID)
permission := strings.ToLower(strings.TrimSpace(req.Permission))
if permission != "viewer" {
permission = "editor"
}
share, err := s.store.AddTaskListShare(listID, req.TargetType, req.TargetID, permission, userID)
if err != nil {
writeError(w, http.StatusBadRequest, err.Error())
return