feat: organizations decide who sees whom; sharing only by owners
MindSift CI & Docker Release / Build, Test in Container & Push (push) Successful in 1m30s
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:
1 parent
73fad1c5db
commit
84395f3322
26 files changed
+1421
-609
No files matched your search
@@ -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))
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user