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

+51 -38
View File
@@ -17,6 +17,7 @@ import {
SpaceMember,
SpaceShare,
SystemSettings,
SharingMode,
Team,
TaskList,
TaskListShare,
@@ -124,10 +125,19 @@ async function request<T>(path: string, options: RequestInit = {}): Promise<T> {
return res.json();
}
// Server-wide sharing mode from /auth/status; in private mode the UI hides sharing and groups
let currentSharingMode: SharingMode = 'organizations';
export const setCurrentSharingMode = (mode: SharingMode) => {
currentSharingMode = mode;
};
export const isSharingDisabled = () => currentSharingMode === 'private';
export const api = {
// --- AUTH, FIRST-BOOT SETUP & SETTINGS ---
async getAuthStatus(): Promise<AuthStatus> {
return request<AuthStatus>('/api/v1/auth/status');
const st = await request<AuthStatus>('/api/v1/auth/status');
if (st.sharingMode) setCurrentSharingMode(st.sharingMode);
return st;
},
async logout(): Promise<void> {
@@ -147,7 +157,7 @@ export const api = {
password: string;
displayName: string;
instanceName?: string;
sharingMode?: 'open' | 'teams_only' | 'strict_private';
sharingMode?: SharingMode;
allowRegistration?: boolean;
createSampleData?: boolean;
}): Promise<{ token: string; user: User }> {
@@ -830,41 +840,7 @@ export const api = {
});
},
// --- ORGANIZATIONS & TEAMS ---
async getOrganizations(): Promise<Organization[]> {
const res = await request<Organization[]>('/api/v1/organizations');
return ensureArray(res).map((o) => ({
...o,
teams: ensureArray(o.teams).map((t) => ({
...t,
members: ensureArray(t.members),
})),
}));
},
async createOrganization(name: string): Promise<Organization> {
return request<Organization>('/api/v1/organizations', {
method: 'POST',
body: JSON.stringify({ name }),
});
},
async deleteOrganization(id: string): Promise<void> {
await request(`/api/v1/organizations/${id}`, { method: 'DELETE' });
},
async getOrgTeams(orgId: string): Promise<Team[]> {
const res = await request<Team[]>(`/api/v1/organizations/${orgId}/teams`);
return ensureArray(res).map((t) => ({ ...t, members: ensureArray(t.members) }));
},
async createOrgTeam(orgId: string, name: string, description: string): Promise<Team> {
return request<Team>(`/api/v1/organizations/${orgId}/teams`, {
method: 'POST',
body: JSON.stringify({ name, description }),
});
},
// --- GROUPS (user-managed) ---
async createTeam(name: string, description = ''): Promise<Team> {
return request<Team>('/api/v1/teams', {
method: 'POST',
@@ -924,6 +900,7 @@ export const api = {
password: string;
displayName: string;
role: string;
organizationId?: string;
}): Promise<User> {
return request<User>('/api/v1/admin/users', {
method: 'POST',
@@ -966,7 +943,7 @@ export const api = {
async adminCreateInvite(payload: {
email?: string;
role: string;
teamId?: string;
organizationId?: string;
}): Promise<UserInvite> {
return request<UserInvite>('/api/v1/admin/invites', {
method: 'POST',
@@ -989,6 +966,42 @@ export const api = {
});
},
// Counts shares and group memberships a switch to `mode` would remove
async adminSharingImpact(mode: SharingMode): Promise<number> {
const res = await request<{ affected: number }>(`/api/v1/admin/settings/sharing-impact?mode=${mode}`);
return res.affected || 0;
},
// --- ORGANIZATIONS (server admin only) ---
async adminGetOrganizations(): Promise<Organization[]> {
return ensureArray(await request<Organization[]>('/api/v1/admin/organizations'));
},
async adminCreateOrganization(name: string): Promise<Organization> {
return request<Organization>('/api/v1/admin/organizations', {
method: 'POST',
body: JSON.stringify({ name }),
});
},
async adminRenameOrganization(id: string, name: string): Promise<void> {
await request(`/api/v1/admin/organizations/${id}`, {
method: 'PUT',
body: JSON.stringify({ name }),
});
},
async adminDeleteOrganization(id: string): Promise<void> {
await request(`/api/v1/admin/organizations/${id}`, { method: 'DELETE' });
},
async adminSetUserOrganization(userId: string, organizationId: string): Promise<void> {
await request(`/api/v1/admin/users/${userId}/organization`, {
method: 'PUT',
body: JSON.stringify({ organizationId }),
});
},
async adminTestSMTP(toEmail: string): Promise<{ status: string }> {
return request<{ status: string }>('/api/v1/admin/smtp/test', {
method: 'POST',