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>
316 lines
9.9 KiB
Go
316 lines
9.9 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"net/http"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/sandrodach/mindsift/internal/auth"
|
|
"github.com/sandrodach/mindsift/internal/domain"
|
|
"github.com/sandrodach/mindsift/internal/storage"
|
|
)
|
|
|
|
// --- DOCUMENTS ---
|
|
|
|
func (s *Server) broadcastDocToAuthorizedUsers(docID, event string) {
|
|
users, err := s.store.GetAuthorizedDocUsers(docID)
|
|
if err != nil {
|
|
return
|
|
}
|
|
var firstDoc *domain.Document
|
|
for _, uid := range users {
|
|
if userDoc, err := s.store.GetDocumentByID(uid, docID); err == nil && userDoc != nil {
|
|
if firstDoc == nil {
|
|
firstDoc = userDoc
|
|
}
|
|
s.wsHub.BroadcastToUser(uid, event, userDoc)
|
|
}
|
|
}
|
|
if s.wsHub != nil && firstDoc != nil {
|
|
s.wsHub.BroadcastRoom("doc:"+docID, event, firstDoc, nil)
|
|
}
|
|
}
|
|
|
|
func (s *Server) handleGetDocuments(w http.ResponseWriter, r *http.Request) {
|
|
userID := auth.GetUserID(r.Context())
|
|
docs, err := s.store.GetDocuments(userID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, docs)
|
|
}
|
|
|
|
func (s *Server) handleCreateDocument(w http.ResponseWriter, r *http.Request) {
|
|
userID := auth.GetUserID(r.Context())
|
|
var req struct {
|
|
SpaceID string `json:"spaceId"`
|
|
Title string `json:"title"`
|
|
Icon string `json:"icon"`
|
|
FolderPath string `json:"folderPath"`
|
|
Blocks []domain.DocumentBlock `json:"blocks"`
|
|
Tags []string `json:"tags"`
|
|
IsPinned bool `json:"isPinned"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
|
return
|
|
}
|
|
|
|
doc, err := s.store.CreateDocument(userID, req.SpaceID, req.Title, req.Icon, req.FolderPath, req.Blocks, req.Tags, req.IsPinned)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
s.broadcastDocToAuthorizedUsers(doc.ID, "DOC_CREATED")
|
|
writeJSON(w, http.StatusCreated, doc)
|
|
}
|
|
|
|
func (s *Server) handleGetDocument(w http.ResponseWriter, r *http.Request) {
|
|
userID := auth.GetUserID(r.Context())
|
|
docID := chi.URLParam(r, "id")
|
|
doc, err := s.store.GetDocumentByID(userID, docID)
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "Dokument nicht gefunden")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, doc)
|
|
}
|
|
|
|
func (s *Server) handleGetBacklinks(w http.ResponseWriter, r *http.Request) {
|
|
links, err := s.store.GetBacklinks(auth.GetUserID(r.Context()), chi.URLParam(r, "id"))
|
|
if err != nil {
|
|
writeError(w, http.StatusNotFound, "Dokument nicht gefunden")
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, links)
|
|
}
|
|
|
|
func (s *Server) handleUpdateDocument(w http.ResponseWriter, r *http.Request) {
|
|
userID := auth.GetUserID(r.Context())
|
|
docID := chi.URLParam(r, "id")
|
|
|
|
var req struct {
|
|
Title *string `json:"title"`
|
|
Icon *string `json:"icon"`
|
|
FolderPath *string `json:"folderPath"`
|
|
Blocks *[]domain.DocumentBlock `json:"blocks"`
|
|
IsPinned *bool `json:"isPinned"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
|
return
|
|
}
|
|
if req.Blocks != nil {
|
|
writeError(w, http.StatusBadRequest, "Blöcke werden über POST /api/v1/docs/{id}/sync gespeichert")
|
|
return
|
|
}
|
|
|
|
doc, err := s.store.UpdateDocument(userID, docID, req.Title, req.Icon, req.FolderPath, req.IsPinned)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
if req.Title != nil {
|
|
_, head, _ := s.store.GetDocOpsSince(docID, 0, 0)
|
|
s.notifyDocChanged(docID, head)
|
|
}
|
|
if req.Title != nil || req.Icon != nil || req.FolderPath != nil {
|
|
s.broadcastDocToAuthorizedUsers(docID, "DOC_UPDATED")
|
|
}
|
|
writeJSON(w, http.StatusOK, doc)
|
|
}
|
|
|
|
func (s *Server) handleDeleteDocument(w http.ResponseWriter, r *http.Request) {
|
|
userID := auth.GetUserID(r.Context())
|
|
docID := chi.URLParam(r, "id")
|
|
authorizedUsers, _ := s.store.GetAuthorizedDocUsers(docID)
|
|
|
|
if err := s.store.DeleteDocument(userID, docID); err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
for _, uid := range authorizedUsers {
|
|
s.wsHub.BroadcastToUser(uid, "DOC_DELETED", map[string]string{"id": docID})
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
|
|
}
|
|
|
|
func (s *Server) handleGetTrash(w http.ResponseWriter, r *http.Request) {
|
|
items, err := s.store.GetTrash(auth.GetUserID(r.Context()))
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, items)
|
|
}
|
|
|
|
func (s *Server) handleRestoreDocument(w http.ResponseWriter, r *http.Request) {
|
|
userID := auth.GetUserID(r.Context())
|
|
docID := chi.URLParam(r, "id")
|
|
if err := s.store.RestoreDocument(userID, docID); err != nil {
|
|
writeTrashError(w, err)
|
|
return
|
|
}
|
|
// Everyone with access reloads their tree, so the note is back for shared users as well
|
|
authorized, _ := s.store.GetAuthorizedDocUsers(docID)
|
|
for _, uid := range append(authorized, userID) {
|
|
s.wsHub.BroadcastToUser(uid, "WORKSPACE_FOLDERS_UPDATED", map[string]string{"restored": docID})
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]bool{"restored": true})
|
|
}
|
|
|
|
func (s *Server) handlePurgeDocument(w http.ResponseWriter, r *http.Request) {
|
|
if err := s.store.PurgeDocument(auth.GetUserID(r.Context()), chi.URLParam(r, "id")); err != nil {
|
|
writeTrashError(w, err)
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
|
|
}
|
|
|
|
func (s *Server) handleEmptyTrash(w http.ResponseWriter, r *http.Request) {
|
|
ids, err := s.store.EmptyTrash(auth.GetUserID(r.Context()))
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, map[string]int{"deleted": len(ids)})
|
|
}
|
|
|
|
func writeTrashError(w http.ResponseWriter, err error) {
|
|
if errors.Is(err, storage.ErrNotInTrash) {
|
|
writeError(w, http.StatusNotFound, err.Error())
|
|
return
|
|
}
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
}
|
|
|
|
func (s *Server) handleCreateEnhancedTask(w http.ResponseWriter, r *http.Request) {
|
|
userID := auth.GetUserID(r.Context())
|
|
docID := chi.URLParam(r, "id")
|
|
blockID := chi.URLParam(r, "blockId")
|
|
|
|
var req struct {
|
|
Content string `json:"content"`
|
|
Checked bool `json:"checked"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
|
return
|
|
}
|
|
|
|
thought, err := s.store.CreateTaskFromDocBlock(userID, docID, blockID, req.Content, req.Checked)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
_, head, _ := s.store.GetDocOpsSince(docID, 0, 0)
|
|
s.notifyDocChanged(docID, head)
|
|
|
|
s.wsHub.BroadcastToUser(userID, "TASK_CREATED", thought)
|
|
writeJSON(w, http.StatusCreated, thought)
|
|
}
|
|
|
|
func (s *Server) handleMoveDocument(w http.ResponseWriter, r *http.Request) {
|
|
userID := auth.GetUserID(r.Context())
|
|
docID := chi.URLParam(r, "id")
|
|
var req struct {
|
|
FolderPath string `json:"folderPath"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
|
return
|
|
}
|
|
doc, err := s.store.MoveDocument(userID, docID, req.FolderPath)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
s.wsHub.BroadcastDocEvent(docID, "DOC_UPDATED", doc)
|
|
s.broadcastDocToAuthorizedUsers(docID, "DOC_UPDATED")
|
|
docUsers, _ := s.store.GetAuthorizedDocUsers(docID)
|
|
s.wsHub.BroadcastToUsers(append(docUsers, userID), "WORKSPACE_FOLDERS_UPDATED", map[string]string{"docId": docID})
|
|
writeJSON(w, http.StatusOK, doc)
|
|
}
|
|
|
|
// --- DOCUMENT SHARING & COLLABORATION ---
|
|
|
|
func (s *Server) handleGetDocShares(w http.ResponseWriter, r *http.Request) {
|
|
docID := chi.URLParam(r, "id")
|
|
shares, err := s.store.GetDocShares(docID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, shares)
|
|
}
|
|
|
|
func (s *Server) handleCreateDocShare(w http.ResponseWriter, r *http.Request) {
|
|
userID := auth.GetUserID(r.Context())
|
|
docID := chi.URLParam(r, "id")
|
|
var req struct {
|
|
UserID string `json:"userId"`
|
|
Email string `json:"email"`
|
|
TeamID string `json:"teamId"`
|
|
Permission string `json:"permission"` // read | write
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
|
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
|
return
|
|
}
|
|
|
|
if req.TeamID != "" {
|
|
share, err := s.store.CreateDocTeamShare(userID, docID, req.TeamID, req.Permission)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
s.broadcastDocToAuthorizedUsers(docID, "DOC_CREATED")
|
|
s.broadcastDocToAuthorizedUsers(docID, "DOC_UPDATED")
|
|
writeJSON(w, http.StatusCreated, share)
|
|
return
|
|
}
|
|
|
|
target := req.UserID
|
|
if target == "" {
|
|
target = req.Email
|
|
}
|
|
if target == "" {
|
|
writeError(w, http.StatusBadRequest, "Benutzer-ID, E-Mail oder Team erforderlich")
|
|
return
|
|
}
|
|
|
|
share, err := s.store.CreateDocShare(userID, docID, target, req.Permission)
|
|
if err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
s.broadcastDocToAuthorizedUsers(docID, "DOC_CREATED")
|
|
s.broadcastDocToAuthorizedUsers(docID, "DOC_UPDATED")
|
|
writeJSON(w, http.StatusCreated, share)
|
|
}
|
|
|
|
func (s *Server) handleDeleteDocShare(w http.ResponseWriter, r *http.Request) {
|
|
userID := auth.GetUserID(r.Context())
|
|
docID := chi.URLParam(r, "id")
|
|
targetUserID := chi.URLParam(r, "userId")
|
|
if err := s.store.DeleteDocShare(userID, docID, targetUserID); err != nil {
|
|
writeError(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
s.wsHub.BroadcastToUser(targetUserID, "DOC_DELETED", map[string]string{"id": docID})
|
|
s.broadcastDocToAuthorizedUsers(docID, "DOC_UPDATED")
|
|
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
|
|
}
|
|
|
|
func (s *Server) handleGetCollaborators(w http.ResponseWriter, r *http.Request) {
|
|
userID := auth.GetUserID(r.Context())
|
|
users, err := s.store.GetCollaborators(userID)
|
|
if err != nil {
|
|
writeError(w, http.StatusInternalServerError, err.Error())
|
|
return
|
|
}
|
|
writeJSON(w, http.StatusOK, users)
|
|
}
|