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
+51
-38
@@ -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',
|
||||
|
||||
Reference in new issue
Block a user