refactor(collab): remove live document collaboration and enforce strictly read-only sharing
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m2s
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m2s
This commit is contained in:
1 parent
aea05e868a
commit
04fa774d10
16 files changed
+179
-3556
No files matched your search
+16
-14
@@ -414,6 +414,10 @@ func (s *Store) migrate() error {
|
||||
// Backfill username for existing accounts where username is empty
|
||||
_, _ = s.db.Exec(`UPDATE users SET username = LOWER(SUBSTR(email, 1, INSTR(email, '@') - 1)) WHERE (username IS NULL OR username = '') AND INSTR(email, '@') > 0`)
|
||||
|
||||
// Ensure all existing document shares are strictly read-only
|
||||
_, _ = s.db.Exec(`UPDATE doc_shares SET permission = 'read' WHERE permission != 'read'`)
|
||||
_, _ = s.db.Exec(`UPDATE doc_team_shares SET permission = 'read' WHERE permission != 'read'`)
|
||||
|
||||
// Default system settings (public registration disabled by default; invite or admin creation required)
|
||||
_, _ = s.db.Exec(`INSERT OR IGNORE INTO system_settings (key, value) VALUES
|
||||
('sharing_mode', 'teams_only'),
|
||||
@@ -1526,7 +1530,7 @@ func (s *Store) PatchDocumentBlocks(userID, docID string, req domain.DocBlocksPa
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if !doc.IsOwner && doc.Permission != "write" {
|
||||
if !doc.IsOwner {
|
||||
return nil, nil, fmt.Errorf("keine Schreibberechtigung für dieses Dokument (nur Lesezugriff)")
|
||||
}
|
||||
|
||||
@@ -2274,7 +2278,7 @@ func (s *Store) UpdateDocument(userID, docID string, title, icon, folderPath *st
|
||||
isOnlyPinChange := isPinned != nil && title == nil && icon == nil && folderPath == nil && blocks == nil && tags == nil
|
||||
isOnlyFolderMove := folderPath != nil && title == nil && icon == nil && blocks == nil && tags == nil && isPinned == nil
|
||||
|
||||
if !doc.IsOwner && doc.Permission != "write" && !isOnlyPinChange && !isOnlyFolderMove {
|
||||
if !doc.IsOwner && !isOnlyPinChange && !isOnlyFolderMove {
|
||||
return nil, fmt.Errorf("keine Schreibberechtigung für dieses Dokument (nur Lesezugriff)")
|
||||
}
|
||||
|
||||
@@ -2908,7 +2912,7 @@ func (s *Store) MoveDocument(userID, docID, targetFolderPath string) (*domain.Do
|
||||
// Target is NOT inside a shared folder (personal / standalone)
|
||||
if doc.SharedFolderID != "" {
|
||||
// Moving OUT of a shared folder into a personal folder
|
||||
if !doc.IsOwner && doc.Permission != "write" {
|
||||
if !doc.IsOwner {
|
||||
return nil, fmt.Errorf("keine Berechtigung, die Notiz aus dem geteilten Ordner zu verschieben")
|
||||
}
|
||||
_, err = s.db.Exec(`UPDATE documents SET shared_folder_id = '', sub_folder_path = '', folder_path = ?, updated_at = ? WHERE id = ?`,
|
||||
@@ -3564,8 +3568,8 @@ func (s *Store) CreateDocTeamShare(currentUserID, docID, teamID, permission stri
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !doc.IsOwner && doc.Permission != "write" {
|
||||
return nil, fmt.Errorf("nur der Eigentümer oder Bearbeiter kann Freigaben verwalten")
|
||||
if !doc.IsOwner {
|
||||
return nil, fmt.Errorf("nur der Eigentümer kann Freigaben verwalten")
|
||||
}
|
||||
|
||||
cleanTeamID := strings.TrimSpace(teamID)
|
||||
@@ -3575,9 +3579,8 @@ func (s *Store) CreateDocTeamShare(currentUserID, docID, teamID, permission stri
|
||||
return nil, fmt.Errorf("team nicht gefunden")
|
||||
}
|
||||
|
||||
if permission != "read" && permission != "write" {
|
||||
permission = "read"
|
||||
}
|
||||
// Document sharing is strictly read-only
|
||||
permission = "read"
|
||||
|
||||
now := domain.NowISO()
|
||||
shareID := domain.NewID("share")
|
||||
@@ -3609,20 +3612,19 @@ func (s *Store) CreateDocShare(currentUserID, docID, targetUserIdentifier, permi
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !doc.IsOwner && doc.Permission != "write" {
|
||||
return nil, fmt.Errorf("nur der Eigentümer oder Bearbeiter kann Freigaben verwalten")
|
||||
if !doc.IsOwner {
|
||||
return nil, fmt.Errorf("nur der Eigentümer kann Freigaben verwalten")
|
||||
}
|
||||
|
||||
cleanTarget := strings.TrimSpace(targetUserIdentifier)
|
||||
|
||||
// If identifier looks like a team or is prefixed with team_, route to CreateDocTeamShare
|
||||
if strings.HasPrefix(cleanTarget, "team_") {
|
||||
return s.CreateDocTeamShare(currentUserID, docID, cleanTarget, permission)
|
||||
return s.CreateDocTeamShare(currentUserID, docID, cleanTarget, "read")
|
||||
}
|
||||
|
||||
if permission != "read" && permission != "write" {
|
||||
permission = "read"
|
||||
}
|
||||
// Document sharing is strictly read-only
|
||||
permission = "read"
|
||||
|
||||
// Resolve target user by ID, username, or Email
|
||||
var targetUser *domain.User
|
||||
|
||||
@@ -173,16 +173,10 @@ func TestStoreLifecycle(t *testing.T) {
|
||||
t.Fatalf("expected UpdateDocument to fail for read-only user, but succeeded")
|
||||
}
|
||||
|
||||
// Upgrade to write permission
|
||||
_, err = store.CreateDocShare(user.ID, newDoc.ID, user2.ID, "write")
|
||||
if err != nil {
|
||||
t.Fatalf("Upgrade CreateDocShare failed: %v", err)
|
||||
}
|
||||
|
||||
// user2 edits writeable doc -> must succeed
|
||||
// user2 is a read-only collaborator -> editing must fail
|
||||
_, err = store.UpdateDocument(user2.ID, newDoc.ID, &updatedTitle, nil, nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("UpdateDocument failed for write collaborator: %v", err)
|
||||
if err == nil {
|
||||
t.Fatalf("expected UpdateDocument to fail for read-only collaborator, but succeeded")
|
||||
}
|
||||
|
||||
// Remove share
|
||||
@@ -551,8 +545,7 @@ func TestDatabaseFirstWorkspaceArchitecture(t *testing.T) {
|
||||
t.Fatalf("MoveDocument by userB failed: %v", err)
|
||||
}
|
||||
|
||||
// User B edits the document via PatchDocumentBlocks (modifying b2 and adding b3)
|
||||
// Simultaneously, User A edits b1 and adds b4 using a slightly stale order
|
||||
// User B is a read-only collaborator -> PatchDocumentBlocks must fail
|
||||
_, _, err = store.PatchDocumentBlocks(userB.ID, doc.ID, domain.DocBlocksPatchRequest{
|
||||
Upserted: []domain.DocumentBlock{
|
||||
{ID: "b2", Type: "bullet", Content: "Zeile 2 von B bearbeitet", Level: 0},
|
||||
@@ -560,8 +553,8 @@ func TestDatabaseFirstWorkspaceArchitecture(t *testing.T) {
|
||||
},
|
||||
BlockOrder: []string{"b1", "b2", "b3"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("PatchDocumentBlocks by userB failed: %v", err)
|
||||
if err == nil {
|
||||
t.Fatalf("expected PatchDocumentBlocks by read-only userB to fail, but succeeded")
|
||||
}
|
||||
|
||||
// User A's client only knew about b1, b2 when sending its patch (adds b4)
|
||||
@@ -576,9 +569,9 @@ func TestDatabaseFirstWorkspaceArchitecture(t *testing.T) {
|
||||
t.Fatalf("PatchDocumentBlocks by userA failed: %v", err)
|
||||
}
|
||||
|
||||
// Verify NO blocks were lost! All 4 blocks (b1, b2, b3, b4) must exist with both users' edits preserved
|
||||
if len(patchedDocA.Blocks) != 4 {
|
||||
t.Fatalf("expected 4 blocks after concurrent patches, got %d: %+v", len(patchedDocA.Blocks), patchedDocA.Blocks)
|
||||
// Verify User A's blocks are preserved: b1, b2, b4 (3 blocks)
|
||||
if len(patchedDocA.Blocks) != 3 {
|
||||
t.Fatalf("expected 3 blocks after patch, got %d: %+v", len(patchedDocA.Blocks), patchedDocA.Blocks)
|
||||
}
|
||||
blockContent := make(map[string]string)
|
||||
for _, b := range patchedDocA.Blocks {
|
||||
@@ -587,12 +580,6 @@ func TestDatabaseFirstWorkspaceArchitecture(t *testing.T) {
|
||||
if blockContent["b1"] != "Zeile 1 von A bearbeitet" {
|
||||
t.Fatalf("expected b1 updated by A, got %q", blockContent["b1"])
|
||||
}
|
||||
if blockContent["b2"] != "Zeile 2 von B bearbeitet" {
|
||||
t.Fatalf("expected b2 updated by B to be preserved, got %q", blockContent["b2"])
|
||||
}
|
||||
if blockContent["b3"] != "Zeile 3 neu von B" {
|
||||
t.Fatalf("expected b3 added by B to be preserved even with stale order from A, got %q", blockContent["b3"])
|
||||
}
|
||||
if blockContent["b4"] != "Zeile 4 neu von A" {
|
||||
t.Fatalf("expected b4 added by A, got %q", blockContent["b4"])
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user