refactor: split sqlite.go and server.go by domain (FLAW-20)
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m31s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m31s
Pure moves, no behaviour change: the storage layer now has users, apikeys, thoughts, tasklists, documents, folders, tags, lists, teams and system files next to sqlite.go (store, schema, migrations); the API has handlers_*.go per area plus realtime.go and alarms.go next to server.go (server, router, middleware). Declarations were moved verbatim with an AST-based tool; imports pruned with goimports. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
7bba174454
commit
74c394efaa
26 files changed
+7702
-7467
No files matched your search
@@ -0,0 +1,326 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
"github.com/sandrodach/mindsift/internal/auth"
|
||||
"github.com/sandrodach/mindsift/internal/domain"
|
||||
"github.com/sandrodach/mindsift/internal/storage"
|
||||
)
|
||||
|
||||
func (s *Server) handleCaptureInbox(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
var req struct {
|
||||
Content string `json:"content"`
|
||||
Priority string `json:"priority"`
|
||||
Source string `json:"source"`
|
||||
DueDate string `json:"dueDate"`
|
||||
DueDate2 string `json:"due_date"`
|
||||
Target string `json:"target"` // inbox | task | list_item | document
|
||||
TargetListID string `json:"targetListId"`
|
||||
TargetList2 string `json:"target_list_id"`
|
||||
TaskListID string `json:"taskListId"`
|
||||
TaskListID2 string `json:"task_list_id"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||||
return
|
||||
}
|
||||
dueDate := req.DueDate
|
||||
if dueDate == "" {
|
||||
dueDate = req.DueDate2
|
||||
}
|
||||
targetListID := req.TargetListID
|
||||
if targetListID == "" {
|
||||
targetListID = req.TargetList2
|
||||
}
|
||||
taskListID := req.TaskListID
|
||||
if taskListID == "" {
|
||||
taskListID = req.TaskListID2
|
||||
}
|
||||
|
||||
source := strings.ToLower(strings.TrimSpace(req.Source))
|
||||
if source == "" {
|
||||
source = strings.ToLower(strings.TrimSpace(r.URL.Query().Get("source")))
|
||||
}
|
||||
if source == "" {
|
||||
source = strings.ToLower(strings.TrimSpace(r.Header.Get("X-Source")))
|
||||
}
|
||||
if source == "" {
|
||||
// When called via API key or webhook token, automatically default source to "webhook"
|
||||
if r.Header.Get("X-API-Key") != "" ||
|
||||
strings.HasPrefix(r.Header.Get("Authorization"), "Bearer mindsift_sec_") ||
|
||||
strings.HasPrefix(r.URL.Query().Get("token"), "mindsift_sec_") {
|
||||
source = "webhook"
|
||||
} else {
|
||||
source = "manual"
|
||||
}
|
||||
}
|
||||
|
||||
thought, err := s.store.CreateThoughtWithDetails(userID, req.Content, req.Priority, source, dueDate, taskListID, "todo", "")
|
||||
if err != nil {
|
||||
writeStoreError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
target := strings.TrimSpace(req.Target)
|
||||
if target == "task" || target == "document" || (target == "list_item" && targetListID != "") {
|
||||
if converted, convErr := s.store.UpdateThoughtTriage(userID, thought.ID, "keep", target, targetListID, dueDate, taskListID); convErr == nil && converted != nil {
|
||||
thought = converted
|
||||
if target == "list_item" && targetListID != "" {
|
||||
if lst, lErr := s.store.GetListByID(userID, targetListID); lErr == nil && lst != nil {
|
||||
s.wsHub.BroadcastListEvent(lst.ID, "LIST_UPDATED", lst)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
s.wsHub.BroadcastToUsers(s.taskAudience(thought), "TASK_CREATED", thought)
|
||||
writeJSON(w, http.StatusCreated, thought)
|
||||
}
|
||||
|
||||
func (s *Server) handleGetThoughts(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
status := r.URL.Query().Get("status")
|
||||
thoughts, err := s.store.GetThoughts(userID, status, 250)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, thoughts)
|
||||
}
|
||||
|
||||
func (s *Server) handleGetThought(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
thoughtID := chi.URLParam(r, "id")
|
||||
thought, err := s.store.GetThoughtByID(userID, thoughtID)
|
||||
if err != nil || thought == nil {
|
||||
writeError(w, http.StatusNotFound, "Aufgabe nicht gefunden")
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, thought)
|
||||
}
|
||||
|
||||
func (s *Server) handleGetTriageDeck(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
thoughts, err := s.store.GetThoughts(userID, "inbox", 100)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, thoughts)
|
||||
}
|
||||
|
||||
func (s *Server) handleTriageAction(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
thoughtID := chi.URLParam(r, "id")
|
||||
|
||||
var req struct {
|
||||
Action string `json:"action"` // keep | discard | restore
|
||||
ConvertTo string `json:"convertTo"`
|
||||
ConvertTo2 string `json:"convert_to"`
|
||||
TargetListID string `json:"targetListId"`
|
||||
TargetList2 string `json:"target_list_id"`
|
||||
TaskListID string `json:"taskListId"`
|
||||
TaskListID2 string `json:"task_list_id"`
|
||||
DueDate string `json:"dueDate"`
|
||||
DueDate2 string `json:"due_date"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||||
return
|
||||
}
|
||||
|
||||
convertTo := req.ConvertTo
|
||||
if convertTo == "" {
|
||||
convertTo = req.ConvertTo2
|
||||
}
|
||||
targetListID := req.TargetListID
|
||||
if targetListID == "" {
|
||||
targetListID = req.TargetList2
|
||||
}
|
||||
taskListID := req.TaskListID
|
||||
if taskListID == "" {
|
||||
taskListID = req.TaskListID2
|
||||
}
|
||||
dueDate := req.DueDate
|
||||
if dueDate == "" {
|
||||
dueDate = req.DueDate2
|
||||
}
|
||||
|
||||
updated, err := s.store.UpdateThoughtTriage(userID, thoughtID, req.Action, convertTo, targetListID, dueDate, taskListID)
|
||||
if err != nil {
|
||||
writeStoreError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
// If converted to a list item, broadcast via WebSocket to the space
|
||||
if convertTo == "list_item" && targetListID != "" {
|
||||
if lst, err := s.store.GetListByID(userID, targetListID); err == nil && lst != nil {
|
||||
s.wsHub.BroadcastListEvent(lst.ID, "LIST_UPDATED", lst)
|
||||
}
|
||||
}
|
||||
|
||||
s.wsHub.BroadcastToUsers(s.taskAudience(updated), "TASK_UPDATED", updated)
|
||||
writeJSON(w, http.StatusOK, updated)
|
||||
}
|
||||
|
||||
func (s *Server) handleGetTasks(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
taskListID := r.URL.Query().Get("taskListId")
|
||||
tasks, err := s.store.GetTasks(userID, taskListID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, tasks)
|
||||
}
|
||||
|
||||
func (s *Server) handleReorderTasks(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
taskListID := chi.URLParam(r, "id")
|
||||
if taskListID == "" {
|
||||
taskListID = r.URL.Query().Get("taskListId")
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Items []domain.ThoughtReorderItem `json:"items"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||||
return
|
||||
}
|
||||
|
||||
if taskListID != "" && !s.store.CanEditTaskList(userID, taskListID) {
|
||||
writeError(w, http.StatusForbidden, storage.ErrTaskListReadOnly.Error())
|
||||
return
|
||||
}
|
||||
// ReorderTasks updates by ID only, so every item must be a task the caller may edit
|
||||
audience := []string{userID}
|
||||
for _, it := range req.Items {
|
||||
th, err := s.store.GetThoughtByID(userID, it.ID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusNotFound, "Aufgabe nicht gefunden")
|
||||
return
|
||||
}
|
||||
if th.TaskListID != "" && !s.store.CanEditTaskList(userID, th.TaskListID) {
|
||||
writeError(w, http.StatusForbidden, storage.ErrTaskListReadOnly.Error())
|
||||
return
|
||||
}
|
||||
audience = append(audience, s.taskAudience(th)...)
|
||||
}
|
||||
|
||||
if err := s.store.ReorderTasks(taskListID, req.Items); err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
s.wsHub.BroadcastToUsers(audience, "TASK_REORDERED", map[string]interface{}{
|
||||
"taskListId": taskListID,
|
||||
"items": req.Items,
|
||||
"userId": userID,
|
||||
})
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"success": true})
|
||||
}
|
||||
|
||||
func (s *Server) handleUpdateThought(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
thoughtID := chi.URLParam(r, "id")
|
||||
|
||||
var req struct {
|
||||
Content *string `json:"content"`
|
||||
IsCompleted *bool `json:"isCompleted"`
|
||||
DueDate *string `json:"dueDate"`
|
||||
Priority *string `json:"priority"`
|
||||
TaskListID *string `json:"taskListId"`
|
||||
KanbanStatus *string `json:"kanbanStatus"`
|
||||
Notes *string `json:"notes"`
|
||||
Subtasks *[]domain.SubTask `json:"subtasks"`
|
||||
Tags *[]string `json:"tags"`
|
||||
Assignees *[]string `json:"assignees"`
|
||||
Recurrence *string `json:"recurrence"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||||
return
|
||||
}
|
||||
|
||||
before, _ := s.store.GetThoughtByID(userID, thoughtID)
|
||||
// The rule is set first, so completing a task in the same request already repeats it
|
||||
if req.Recurrence != nil {
|
||||
if err := s.store.SetTaskRecurrence(userID, thoughtID, strings.TrimSpace(*req.Recurrence)); err != nil {
|
||||
writeStoreError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
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 {
|
||||
writeStoreError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
if req.Assignees != nil {
|
||||
assignees, err := s.store.SetTaskAssignees(userID, thoughtID, *req.Assignees)
|
||||
if err != nil {
|
||||
writeStoreError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
updated.Assignees = assignees
|
||||
}
|
||||
if docID := s.store.SyncDocBlockAndTask(userID, thoughtID, updated.IsCompleted); docID != "" {
|
||||
_, head, _ := s.store.GetDocOpsSince(docID, 0, 0)
|
||||
s.notifyDocChanged(docID, head)
|
||||
}
|
||||
s.wsHub.BroadcastToUsers(append(s.taskAudience(before), s.taskAudience(updated)...), "TASK_UPDATED", updated)
|
||||
writeJSON(w, http.StatusOK, updated)
|
||||
}
|
||||
|
||||
func (s *Server) handleUnlinkEnhancedTask(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
thoughtID := chi.URLParam(r, "id")
|
||||
s.store.UnlinkTaskFromDocBlock(userID, thoughtID)
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"unlinked": true})
|
||||
}
|
||||
|
||||
func (s *Server) handleDeleteThought(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
thoughtID := chi.URLParam(r, "id")
|
||||
th, err := s.store.GetThoughtByID(userID, thoughtID)
|
||||
if err == nil && th.TaskListID != "" && !s.store.CanEditTaskList(userID, th.TaskListID) {
|
||||
writeError(w, http.StatusForbidden, storage.ErrTaskListReadOnly.Error())
|
||||
return
|
||||
}
|
||||
audience := s.taskAudience(th)
|
||||
s.store.UnlinkTaskFromDocBlock(userID, thoughtID)
|
||||
if err := s.store.DeleteThought(userID, thoughtID); err != nil {
|
||||
writeStoreError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
s.wsHub.BroadcastToUsers(audience, "TASK_DELETED", map[string]string{"id": thoughtID})
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
|
||||
}
|
||||
|
||||
func (s *Server) handlePurgeDiscarded(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
count, err := s.store.PurgeDiscardedThoughts(userID)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]int64{"purged": count})
|
||||
}
|
||||
|
||||
// --- SEARCH & EXPORT ---
|
||||
|
||||
func (s *Server) handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
q := r.URL.Query().Get("q")
|
||||
res, err := s.store.SearchGlobal(userID, q)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, res)
|
||||
}
|
||||
Reference in new issue
Block a user