feat: implement live-sync for tasks, workspace note sharing, and document locking system
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 53s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 53s
This commit is contained in:
1 parent
dccf1a91e3
commit
84e0b6a4bd
10 files changed
+1773
-110
No files matched your search
@@ -61,6 +61,7 @@ func (s *Server) Router() http.Handler {
|
||||
r.Post("/api/v1/auth/login", s.handleLogin)
|
||||
r.Post("/api/v1/auth/quickstart", s.handleQuickStart)
|
||||
r.Post("/api/v1/webhooks/ingest", s.handleWebhookIngest)
|
||||
r.Get("/api/v1/ws", s.handleWebSocket)
|
||||
r.Get("/api/v1/ws/spaces/{spaceId}", s.handleWebSocket)
|
||||
|
||||
// Authenticated API Routes
|
||||
@@ -71,6 +72,7 @@ func (s *Server) Router() http.Handler {
|
||||
r.Patch("/api/v1/auth/settings", s.handleUpdateUserSettings)
|
||||
r.Post("/api/v1/auth/alarms/test", s.handleTestUserAlarm)
|
||||
r.Get("/api/v1/users/directory", s.handleGetUsersDirectory)
|
||||
r.Get("/api/v1/collaborators", s.handleGetCollaborators)
|
||||
r.Get("/api/v1/stats", s.handleStats)
|
||||
r.Get("/api/v1/search", s.handleSearch)
|
||||
r.Get("/api/v1/export", s.handleExport)
|
||||
@@ -100,6 +102,17 @@ func (s *Server) Router() http.Handler {
|
||||
r.Put("/api/v1/documents/{id}", s.handleUpdateDocument)
|
||||
r.Delete("/api/v1/documents/{id}", s.handleDeleteDocument)
|
||||
|
||||
// Document Sharing & Collaboration
|
||||
r.Get("/api/v1/documents/{id}/shares", s.handleGetDocShares)
|
||||
r.Post("/api/v1/documents/{id}/shares", s.handleCreateDocShare)
|
||||
r.Delete("/api/v1/documents/{id}/shares/{userId}", s.handleDeleteDocShare)
|
||||
|
||||
// Document Locking
|
||||
r.Get("/api/v1/documents/{id}/lock", s.handleGetDocLock)
|
||||
r.Post("/api/v1/documents/{id}/lock", s.handleLockDoc)
|
||||
r.Post("/api/v1/documents/{id}/lock/heartbeat", s.handleHeartbeatDocLock)
|
||||
r.Delete("/api/v1/documents/{id}/lock", s.handleUnlockDoc)
|
||||
|
||||
// Shared Spaces & Realtime Lists
|
||||
r.Get("/api/v1/spaces", s.handleGetSpaces)
|
||||
r.Post("/api/v1/spaces", s.handleCreateSpace)
|
||||
@@ -544,6 +557,7 @@ func (s *Server) handleCaptureInbox(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
s.wsHub.BroadcastTaskEvent(thought.UserID, "TASK_CREATED", thought)
|
||||
writeJSON(w, http.StatusCreated, thought)
|
||||
}
|
||||
|
||||
@@ -618,6 +632,7 @@ func (s *Server) handleTriageAction(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
s.wsHub.BroadcastTaskEvent(updated.UserID, "TASK_UPDATED", updated)
|
||||
writeJSON(w, http.StatusOK, updated)
|
||||
}
|
||||
|
||||
@@ -656,6 +671,7 @@ func (s *Server) handleUpdateThought(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
s.wsHub.BroadcastTaskEvent(updated.UserID, "TASK_UPDATED", updated)
|
||||
writeJSON(w, http.StatusOK, updated)
|
||||
}
|
||||
|
||||
@@ -666,6 +682,7 @@ func (s *Server) handleDeleteThought(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
s.wsHub.BroadcastTaskEvent(userID, "TASK_DELETED", map[string]string{"id": thoughtID})
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
|
||||
}
|
||||
|
||||
@@ -712,6 +729,7 @@ func (s *Server) handleCreateDocument(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
s.wsHub.BroadcastDocEvent(doc.ID, "DOC_CREATED", doc)
|
||||
writeJSON(w, http.StatusCreated, doc)
|
||||
}
|
||||
|
||||
@@ -729,6 +747,17 @@ func (s *Server) handleGetDocument(w http.ResponseWriter, r *http.Request) {
|
||||
func (s *Server) handleUpdateDocument(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
docID := chi.URLParam(r, "id")
|
||||
|
||||
// Check document lock
|
||||
if lock := s.wsHub.GetDocLock(docID); lock != nil && lock.UserID != userID {
|
||||
w.WriteHeader(http.StatusLocked)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"error": "Dokument wird aktuell bearbeitet von " + lock.UserName,
|
||||
"currentLock": lock,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Title *string `json:"title"`
|
||||
Icon *string `json:"icon"`
|
||||
@@ -747,6 +776,7 @@ func (s *Server) handleUpdateDocument(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
s.wsHub.BroadcastDocEvent(docID, "DOC_UPDATED", doc)
|
||||
writeJSON(w, http.StatusOK, doc)
|
||||
}
|
||||
|
||||
@@ -757,9 +787,135 @@ func (s *Server) handleDeleteDocument(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusInternalServerError, err.Error())
|
||||
return
|
||||
}
|
||||
s.wsHub.BroadcastDocEvent(docID, "DOC_DELETED", map[string]string{"id": docID})
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"deleted": true})
|
||||
}
|
||||
|
||||
// --- 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"`
|
||||
Permission string `json:"permission"` // read | write
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
writeError(w, http.StatusBadRequest, "Ungültiges JSON")
|
||||
return
|
||||
}
|
||||
target := req.UserID
|
||||
if target == "" {
|
||||
target = req.Email
|
||||
}
|
||||
if target == "" {
|
||||
writeError(w, http.StatusBadRequest, "Benutzer-ID oder E-Mail erforderlich")
|
||||
return
|
||||
}
|
||||
|
||||
share, err := s.store.CreateDocShare(userID, docID, target, req.Permission)
|
||||
if err != nil {
|
||||
writeError(w, http.StatusBadRequest, err.Error())
|
||||
return
|
||||
}
|
||||
s.wsHub.BroadcastToUser(share.UserID, "DOC_SHARED", share)
|
||||
s.wsHub.BroadcastDocEvent(docID, "DOC_SHARED", share)
|
||||
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_UNSHARED", map[string]string{"docId": docID})
|
||||
s.wsHub.BroadcastDocEvent(docID, "DOC_UNSHARED", map[string]string{"docId": docID, "userId": targetUserID})
|
||||
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)
|
||||
}
|
||||
|
||||
// --- DOCUMENT LOCKING ---
|
||||
|
||||
func (s *Server) handleGetDocLock(w http.ResponseWriter, r *http.Request) {
|
||||
docID := chi.URLParam(r, "id")
|
||||
lock := s.wsHub.GetDocLock(docID)
|
||||
if lock == nil {
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"locked": false})
|
||||
return
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{"locked": true, "lock": lock})
|
||||
}
|
||||
|
||||
func (s *Server) handleLockDoc(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
docID := chi.URLParam(r, "id")
|
||||
|
||||
user, err := s.store.GetUserByID(userID)
|
||||
userName := "Ein Benutzer"
|
||||
if err == nil && user != nil && user.DisplayName != "" {
|
||||
userName = user.DisplayName
|
||||
}
|
||||
|
||||
info, acquired, currentLock := s.wsHub.AcquireDocLock(docID, userID, userName, 30*time.Second)
|
||||
if !acquired {
|
||||
w.WriteHeader(http.StatusLocked)
|
||||
_ = json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"locked": true,
|
||||
"acquired": false,
|
||||
"currentLock": currentLock,
|
||||
"message": "Wird aktuell bearbeitet von " + currentLock.UserName,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
s.wsHub.BroadcastDocEvent(docID, "DOC_LOCKED", info)
|
||||
writeJSON(w, http.StatusOK, map[string]interface{}{
|
||||
"locked": true,
|
||||
"acquired": true,
|
||||
"lock": info,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Server) handleHeartbeatDocLock(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
docID := chi.URLParam(r, "id")
|
||||
ok := s.wsHub.HeartbeatDocLock(docID, userID, 30*time.Second)
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"extended": ok})
|
||||
}
|
||||
|
||||
func (s *Server) handleUnlockDoc(w http.ResponseWriter, r *http.Request) {
|
||||
userID := auth.GetUserID(r.Context())
|
||||
docID := chi.URLParam(r, "id")
|
||||
released := s.wsHub.ReleaseDocLock(docID, userID)
|
||||
if released {
|
||||
s.wsHub.BroadcastDocEvent(docID, "DOC_UNLOCKED", map[string]string{"docId": docID})
|
||||
}
|
||||
writeJSON(w, http.StatusOK, map[string]bool{"unlocked": released})
|
||||
}
|
||||
|
||||
// --- SPACES & REALTIME LISTS ---
|
||||
|
||||
func (s *Server) handleGetSpaces(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -955,6 +1111,11 @@ func (s *Server) handleClearCompletedItems(w http.ResponseWriter, r *http.Reques
|
||||
func (s *Server) handleWebSocket(w http.ResponseWriter, r *http.Request) {
|
||||
spaceID := chi.URLParam(r, "spaceId")
|
||||
token := r.URL.Query().Get("token")
|
||||
if token == "" {
|
||||
if authHdr := r.Header.Get("Authorization"); strings.HasPrefix(authHdr, "Bearer ") {
|
||||
token = strings.TrimPrefix(authHdr, "Bearer ")
|
||||
}
|
||||
}
|
||||
userID := "anonymous"
|
||||
if token != "" {
|
||||
if claims, err := auth.ValidateToken(s.cfg.JWTSecret, token); err == nil {
|
||||
|
||||
Reference in new issue
Block a user