feat: organizations decide who sees whom; sharing only by owners
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m30s

- Server admins manage organizations (one per user); new admin section,
  organization on user creation and invites, setup creates the first one
- Sharing modes private / organizations / open, enforced centrally
  (CanSee, CanShareWith) for directory, all shares and groups; unknown and
  invisible targets give the same error
- Losing visibility (org change, mode change, leaving a group) prunes shares,
  group memberships and task assignments; dry run warns before tightening
- Only owners share; editors can no longer reshare lists or remove others'
  shares; legacy task_lists.team_id and old /organizations endpoints removed
  (FLAW-06, FLAW-10, FLAW-24)
- Schema version: outdated databases are wiped and recreated on start
- Fix setup sample data (createSampleData was ignored); setup and admin
  use a shared ToggleSwitch instead of checkboxes
- Docs: sharing-and-permissions rewritten, ui-patterns, REST, DB, flaws

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
sandrodachandClaude Opus 5.5 committed 2026-10-28 22:00:33 +01:00
1 parent 73fad1c5db
commit 84395f3322
26 files changed
+1421 -609

No files matched your search

+59 -7
View File
@@ -24,6 +24,7 @@ func setupTestServer(t *testing.T) (*Server, *storage.Store, string) {
t.Helper()
dbPath := filepath.Join(t.TempDir(), "test_mindsift.db")
store, err := storage.NewStore(dbPath)
openSharing(t, store)
if err != nil {
t.Fatalf("failed to create test store: %v", err)
}
@@ -57,6 +58,7 @@ func setupTestServer(t *testing.T) (*Server, *storage.Store, string) {
func TestFirstBootSetupAndInviteRegistrationAndNonNullSlices(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "fresh_mindsift.db")
store, err := storage.NewStore(dbPath)
openSharing(t, store)
if err != nil {
t.Fatalf("failed to create fresh store: %v", err)
}
@@ -95,8 +97,8 @@ func TestFirstBootSetupAndInviteRegistrationAndNonNullSlices(t *testing.T) {
"displayName": "Server Owner",
"instanceName": "My MindSift",
"allowRegistration": false,
"sharingMode": "teams_only",
"seedSampleData": false
"sharingMode": "organizations",
"createSampleData": false
}`)
req = httptest.NewRequest(http.MethodPost, "/api/v1/auth/setup", bytes.NewReader(setupBody))
req.Header.Set("Content-Type", "application/json")
@@ -118,7 +120,7 @@ func TestFirstBootSetupAndInviteRegistrationAndNonNullSlices(t *testing.T) {
arrayEndpoints := []string{
"/api/v1/task-lists",
"/api/v1/tasks",
"/api/v1/organizations",
"/api/v1/admin/organizations",
"/api/v1/teams",
"/api/v1/lists",
}
@@ -436,6 +438,7 @@ func TestNewFullFeatures(t *testing.T) {
func TestLoginWithUsernameAndDemoMode(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "demo_api_test.db")
store, err := storage.NewStore(dbPath)
openSharing(t, store)
if err != nil {
t.Fatalf("failed to create store: %v", err)
}
@@ -523,6 +526,7 @@ func TestLoginWithUsernameAndDemoMode(t *testing.T) {
func TestSpaceMembersDirectInvitationAPI(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "space_members_test.db")
store, err := storage.NewStore(dbPath)
openSharing(t, store)
if err != nil {
t.Fatalf("failed to create store: %v", err)
}
@@ -626,6 +630,8 @@ func TestSpaceMembersDirectInvitationAPI(t *testing.T) {
t.Fatalf("failed to create team: %v", err)
}
_ = store.AddTeamMember(team.ID, normalUser.ID, "member")
// Sharing with a group requires being in it
_ = store.AddTeamMember(team.ID, adminUser.ID, "admin")
addTeamBody := []byte(`{"targetType":"team","targetId":"` + team.ID + `","role":"editor"}`)
req = httptest.NewRequest(http.MethodPost, "/api/v1/lists/"+createdList.ID+"/shares", bytes.NewReader(addTeamBody))
@@ -754,6 +760,7 @@ func TestWorkspaceTagsAPI(t *testing.T) {
func TestWebhookTriageBadgeAndIngestion(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "test_webhook.db")
store, err := storage.NewStore(dbPath)
openSharing(t, store)
if err != nil {
t.Fatalf("failed to init store: %v", err)
}
@@ -773,7 +780,7 @@ func TestWebhookTriageBadgeAndIngestion(t *testing.T) {
"displayName": "Home User",
"instanceName": "MindSift Home",
"allowRegistration": false,
"seedSampleData": false
"createSampleData": false
}`)
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/setup", bytes.NewReader(setupBody))
req.Header.Set("Content-Type", "application/json")
@@ -845,6 +852,7 @@ func TestWebhookTriageBadgeAndIngestion(t *testing.T) {
func TestSSEGlobalEndpoint(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "test_sse.db")
store, err := storage.NewStore(dbPath)
openSharing(t, store)
if err != nil {
t.Fatalf("failed to init store: %v", err)
}
@@ -864,7 +872,7 @@ func TestSSEGlobalEndpoint(t *testing.T) {
"displayName": "SSE User",
"instanceName": "MindSift SSE",
"allowRegistration": false,
"seedSampleData": false
"createSampleData": false
}`)
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/setup", bytes.NewReader(setupBody))
req.Header.Set("Content-Type", "application/json")
@@ -1309,8 +1317,52 @@ func TestDocumentSharingStrictlyReadOnly(t *testing.T) {
}
}
// openSharing lets tests that are not about visibility share freely between their users.
func openSharing(t *testing.T, store *storage.Store) {
t.Helper()
if store == nil {
return
}
sys, err := store.GetSystemSettings()
if err != nil {
t.Fatalf("GetSystemSettings: %v", err)
}
sys.SharingMode = storage.SharingOpen
if err := store.UpdateSystemSettings(sys); err != nil {
t.Fatalf("UpdateSystemSettings: %v", err)
}
}
// The setup switch "Beispieldaten anlegen" fills the new admin's account, but creates no other users
// (shared demo accounts exist only in demo mode).
func TestFirstBootSetupWithSampleData(t *testing.T) {
dbPath := filepath.Join(t.TempDir(), "sample.db")
store, err := storage.NewStore(dbPath)
if err != nil {
t.Fatalf("NewStore: %v", err)
}
defer store.Close()
srv := NewServer(&config.Config{Port: "8080", DatabasePath: dbPath, JWTSecret: "test-jwt-secret-key"}, store, ws.NewHub(), nil)
body := []byte(`{"email":"me@example.com","password":"secret","displayName":"Ich","createSampleData":true}`)
req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/setup", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
rec := httptest.NewRecorder()
srv.Router().ServeHTTP(rec, req)
if rec.Code != http.StatusCreated {
t.Fatalf("setup: %d %s", rec.Code, rec.Body.String())
}
var res struct {
User domain.User `json:"user"`
}
_ = json.Unmarshal(rec.Body.Bytes(), &res)
if n, _ := store.CountUsers(); n != 1 {
t.Fatalf("sample data must not create users, found %d", n)
}
docs, _ := store.GetDocuments(res.User.ID)
lists, _ := store.GetTaskLists(res.User.ID)
if len(docs) == 0 || len(lists) == 0 {
t.Fatalf("expected sample notes and task lists, got %d notes, %d task lists", len(docs), len(lists))
}
}