diff --git a/src/lib/apis/channels/index.ts b/src/lib/apis/channels/index.ts
index 225d8cd7c..5715c64e8 100644
--- a/src/lib/apis/channels/index.ts
+++ b/src/lib/apis/channels/index.ts
@@ -3,10 +3,11 @@ import { WEBUI_API_BASE_URL } from '$lib/constants';
type ChannelForm = {
type?: string;
name: string;
- is_private?: boolean;
+ is_private?: boolean | null;
data?: object;
meta?: object;
- access_control?: object;
+ access_grants?: object[];
+ group_ids?: string[];
user_ids?: string[];
};
diff --git a/src/lib/apis/groups/index.ts b/src/lib/apis/groups/index.ts
index a74c61b83..6089a6023 100644
--- a/src/lib/apis/groups/index.ts
+++ b/src/lib/apis/groups/index.ts
@@ -99,6 +99,38 @@ export const getGroupById = async (token: string, id: string) => {
return res;
};
+export const getGroupInfoById = async (token: string, id: string) => {
+ let error = null;
+
+ const res = await fetch(`${WEBUI_API_BASE_URL}/groups/id/${id}/info`, {
+ method: 'GET',
+ headers: {
+ Accept: 'application/json',
+ 'Content-Type': 'application/json',
+ authorization: `Bearer ${token}`
+ }
+ })
+ .then(async (res) => {
+ if (!res.ok) throw await res.json();
+ return res.json();
+ })
+ .then((json) => {
+ return json;
+ })
+ .catch((err) => {
+ error = err.detail;
+
+ console.error(err);
+ return null;
+ });
+
+ if (error) {
+ throw error;
+ }
+
+ return res;
+};
+
export const updateGroupById = async (token: string, id: string, group: object) => {
let error = null;
diff --git a/src/lib/apis/knowledge/index.ts b/src/lib/apis/knowledge/index.ts
index dc9dd8b88..4c7c90484 100644
--- a/src/lib/apis/knowledge/index.ts
+++ b/src/lib/apis/knowledge/index.ts
@@ -4,7 +4,7 @@ export const createNewKnowledge = async (
token: string,
name: string,
description: string,
- accessControl: null | object
+ accessGrants: object[]
) => {
let error = null;
@@ -18,7 +18,7 @@ export const createNewKnowledge = async (
body: JSON.stringify({
name: name,
description: description,
- access_control: accessControl
+ access_grants: accessGrants
})
})
.then(async (res) => {
@@ -248,7 +248,7 @@ type KnowledgeUpdateForm = {
name?: string;
description?: string;
data?: object;
- access_control?: null | object;
+ access_grants?: object[];
};
export const updateKnowledgeById = async (token: string, id: string, form: KnowledgeUpdateForm) => {
@@ -265,7 +265,7 @@ export const updateKnowledgeById = async (token: string, id: string, form: Knowl
name: form?.name ? form.name : undefined,
description: form?.description ? form.description : undefined,
data: form?.data ? form.data : undefined,
- access_control: form.access_control
+ access_grants: form.access_grants
})
})
.then(async (res) => {
diff --git a/src/lib/apis/notes/index.ts b/src/lib/apis/notes/index.ts
index 55f9427e0..341ced57e 100644
--- a/src/lib/apis/notes/index.ts
+++ b/src/lib/apis/notes/index.ts
@@ -5,7 +5,7 @@ type NoteItem = {
title: string;
data: object;
meta?: null | object;
- access_control?: null | object;
+ access_grants?: object[];
};
export const createNewNote = async (token: string, note: NoteItem) => {
diff --git a/src/lib/apis/prompts/index.ts b/src/lib/apis/prompts/index.ts
index e9cd6e848..c227c9f71 100644
--- a/src/lib/apis/prompts/index.ts
+++ b/src/lib/apis/prompts/index.ts
@@ -7,7 +7,7 @@ type PromptItem = {
content: string;
data?: object | null;
meta?: object | null;
- access_control?: null | object;
+ access_grants?: object[];
version_id?: string | null; // Active version
commit_message?: string | null; // For history tracking
is_production?: boolean; // Whether to set new version as production
@@ -23,7 +23,7 @@ type PromptHistoryItem = {
command: string;
data: object;
meta: object;
- access_control: object | null;
+ access_grants: object[];
};
user_id: string;
commit_message: string | null;
@@ -42,7 +42,7 @@ type PromptDiff = {
to_snapshot: object;
content_diff: string[];
name_changed: boolean;
- access_control_changed: boolean;
+ access_grants_changed: boolean;
};
export const createNewPrompt = async (token: string, prompt: PromptItem) => {
@@ -611,4 +611,3 @@ export const getPromptDiff = async (
return res;
};
-
diff --git a/src/lib/apis/users/index.ts b/src/lib/apis/users/index.ts
index d6da54bbf..cd3b40adc 100644
--- a/src/lib/apis/users/index.ts
+++ b/src/lib/apis/users/index.ts
@@ -327,6 +327,33 @@ export const getUserById = async (token: string, userId: string) => {
return res;
};
+export const getUserInfoById = async (token: string, userId: string) => {
+ let error = null;
+
+ const res = await fetch(`${WEBUI_API_BASE_URL}/users/${userId}/info`, {
+ method: 'GET',
+ headers: {
+ 'Content-Type': 'application/json',
+ Authorization: `Bearer ${token}`
+ }
+ })
+ .then(async (res) => {
+ if (!res.ok) throw await res.json();
+ return res.json();
+ })
+ .catch((err) => {
+ console.error(err);
+ error = err.detail;
+ return null;
+ });
+
+ if (error) {
+ throw error;
+ }
+
+ return res;
+};
+
export const updateUserStatus = async (token: string, formData: object) => {
let error = null;
diff --git a/src/lib/components/channel/ChannelInfoModal.svelte b/src/lib/components/channel/ChannelInfoModal.svelte
index 44094f780..cd1ee3524 100644
--- a/src/lib/components/channel/ChannelInfoModal.svelte
+++ b/src/lib/components/channel/ChannelInfoModal.svelte
@@ -15,13 +15,32 @@
import AddMembersModal from './ChannelInfoModal/AddMembersModal.svelte';
export let show = false;
- export let channel = null;
+ export let channel: any = null;
export let onUpdate = () => {};
let showAddMembersModal = false;
const submitHandler = async () => {};
+ const hasPublicReadGrant = (grants: any) =>
+ Array.isArray(grants) &&
+ grants.some(
+ (grant) =>
+ grant?.principal_type === 'user' &&
+ grant?.principal_id === '*' &&
+ grant?.permission === 'read'
+ );
+
+ const isPublicChannel = (channel: any): boolean => {
+ if (channel?.type === 'group') {
+ if (typeof channel?.is_private === 'boolean') {
+ return !channel.is_private;
+ }
+ return hasPublicReadGrant(channel?.access_grants);
+ }
+ return hasPublicReadGrant(channel?.access_grants);
+ };
+
const removeMemberHandler = async (userId) => {
const res = await removeMembersById(localStorage.token, channel.id, {
user_ids: [userId]
@@ -62,7 +81,7 @@
{:else}
- {#if channel?.type === 'group' ? !channel?.is_private : channel?.access_control === null}
+ {#if isPublicChannel(channel)}
{:else}
diff --git a/src/lib/components/channel/MessageInput/MentionList.svelte b/src/lib/components/channel/MessageInput/MentionList.svelte
index 4272a5650..7daf69204 100644
--- a/src/lib/components/channel/MessageInput/MentionList.svelte
+++ b/src/lib/components/channel/MessageInput/MentionList.svelte
@@ -129,6 +129,25 @@
onDestroy(() => {
window.removeEventListener('keydown', keydownListener);
});
+
+ const hasPublicReadGrant = (grants: any) =>
+ Array.isArray(grants) &&
+ grants.some(
+ (grant) =>
+ grant?.principal_type === 'user' &&
+ grant?.principal_id === '*' &&
+ grant?.permission === 'read'
+ );
+
+ const isPublicChannel = (channel: any): boolean => {
+ if (channel?.type === 'group') {
+ if (typeof channel?.is_private === 'boolean') {
+ return !channel.is_private;
+ }
+ return hasPublicReadGrant(channel?.access_grants);
+ }
+ return hasPublicReadGrant(channel?.access_grants);
+ };
{#if filteredItems.length}
@@ -165,7 +184,7 @@
>
{#if item.type === 'channel'}
- {#if item?.data?.access_control === null}
+ {#if isPublicChannel(item?.data)}
{:else}
diff --git a/src/lib/components/channel/Navbar.svelte b/src/lib/components/channel/Navbar.svelte
index 6b5d7c97c..b02193b46 100644
--- a/src/lib/components/channel/Navbar.svelte
+++ b/src/lib/components/channel/Navbar.svelte
@@ -26,6 +26,25 @@
let showChannelPinnedMessagesModal = false;
let showChannelInfoModal = false;
+ const hasPublicReadGrant = (grants: any) =>
+ Array.isArray(grants) &&
+ grants.some(
+ (grant) =>
+ grant?.principal_type === 'user' &&
+ grant?.principal_id === '*' &&
+ grant?.permission === 'read'
+ );
+
+ const isPublicChannel = (channel: any): boolean => {
+ if (channel?.type === 'group') {
+ if (typeof channel?.is_private === 'boolean') {
+ return !channel.is_private;
+ }
+ return hasPublicReadGrant(channel?.access_grants);
+ }
+ return hasPublicReadGrant(channel?.access_grants);
+ };
+
export let channel;
export let onPin = (messageId, pinned) => {};
@@ -112,7 +131,7 @@
{/if}
{:else}
- {#if channel?.type === 'group' ? !channel?.is_private : channel?.access_control === null}
+ {#if isPublicChannel(channel)}
{:else}
diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte
index 5914e7afc..968f56c2f 100644
--- a/src/lib/components/chat/MessageInput.svelte
+++ b/src/lib/components/chat/MessageInput.svelte
@@ -151,7 +151,7 @@
return {
...file,
user: undefined,
- access_control: undefined
+ access_grants: undefined
};
}),
selectedToolIds,
diff --git a/src/lib/components/layout/Sidebar.svelte b/src/lib/components/layout/Sidebar.svelte
index 03591d2b7..2a1c551d4 100644
--- a/src/lib/components/layout/Sidebar.svelte
+++ b/src/lib/components/layout/Sidebar.svelte
@@ -560,7 +560,8 @@
{
+ onSubmit={async (payload: any) => {
+ let { type, name, is_private, access_grants, group_ids, user_ids } = payload ?? {};
name = name?.trim();
if (type === 'dm') {
@@ -579,7 +580,7 @@
type: type,
name: name,
is_private: is_private,
- access_control: access_control,
+ access_grants: access_grants,
group_ids: group_ids,
user_ids: user_ids
}).catch((error) => {
diff --git a/src/lib/components/layout/Sidebar/ChannelItem.svelte b/src/lib/components/layout/Sidebar/ChannelItem.svelte
index 95be5abbf..e8cf1ff75 100644
--- a/src/lib/components/layout/Sidebar/ChannelItem.svelte
+++ b/src/lib/components/layout/Sidebar/ChannelItem.svelte
@@ -25,6 +25,25 @@
let showEditChannelModal = false;
let itemElement;
+
+ const hasPublicReadGrant = (grants: any) =>
+ Array.isArray(grants) &&
+ grants.some(
+ (grant) =>
+ grant?.principal_type === 'user' &&
+ grant?.principal_id === '*' &&
+ grant?.permission === 'read'
+ );
+
+ const isPublicChannel = (channel: any): boolean => {
+ if (channel?.type === 'group') {
+ if (typeof channel?.is_private === 'boolean') {
+ return !channel.is_private;
+ }
+ return hasPublicReadGrant(channel?.access_grants);
+ }
+ return hasPublicReadGrant(channel?.access_grants);
+ };
{
+ onSubmit={async (payload: any) => {
+ const { name, is_private, access_grants, group_ids, user_ids } = payload ?? {};
const res = await updateChannelById(localStorage.token, channel.id, {
name,
is_private,
- access_control,
+ access_grants,
group_ids,
user_ids
}).catch((error) => {
@@ -123,7 +143,7 @@
{/if}
{:else}
- {#if channel?.type === 'group' ? !channel?.is_private : channel?.access_control === null}
+ {#if isPublicChannel(channel)}
{:else}
diff --git a/src/lib/components/layout/Sidebar/ChannelModal.svelte b/src/lib/components/layout/Sidebar/ChannelModal.svelte
index 65404c056..efb62bd01 100644
--- a/src/lib/components/layout/Sidebar/ChannelModal.svelte
+++ b/src/lib/components/layout/Sidebar/ChannelModal.svelte
@@ -23,7 +23,7 @@
export let onSubmit: Function = () => {};
export let onUpdate: Function = () => {};
- export let channel = null;
+ export let channel: any = null;
export let edit = false;
let channelTypes = ['group', 'dm'];
@@ -31,7 +31,7 @@
let name = '';
let isPrivate = null;
- let accessControl = {};
+ let accessGrants = [];
let groupIds = [];
let userIds = [];
@@ -65,8 +65,8 @@
await onSubmit({
type: type,
name: name.replace(/\s/g, '-'),
- is_private: type === 'group' ? isPrivate : null,
- access_control: type === '' ? accessControl : {},
+ is_private: type === 'group' ? (isPrivate ?? true) : null,
+ access_grants: type === '' ? accessGrants : [],
group_ids: groupIds,
user_ids: userIds
});
@@ -85,8 +85,12 @@
if (channel) {
name = channel?.name ?? '';
- isPrivate = channel?.is_private ?? null;
- accessControl = channel.access_control;
+ if (type === 'group') {
+ isPrivate = typeof channel?.is_private === 'boolean' ? channel.is_private : true;
+ } else {
+ isPrivate = null;
+ }
+ accessGrants = channel?.access_grants ?? [];
userIds = channel?.user_ids ?? [];
}
};
@@ -102,8 +106,14 @@
const deleteHandler = async () => {
showDeleteConfirmDialog = false;
+ if (!channel?.id) {
+ show = false;
+ return;
+ }
- const res = await deleteChannelById(localStorage.token, channel.id).catch((error) => {
+ const channelId = channel.id;
+
+ const res = await deleteChannelById(localStorage.token, channelId).catch((error) => {
toast.error(error.message);
});
@@ -111,7 +121,7 @@
toast.success($i18n.t('Channel deleted successfully'));
onUpdate();
- if ($page.url.pathname === `/channels/${channel.id}`) {
+ if ($page.url.pathname === `/channels/${channelId}`) {
goto('/');
}
}
@@ -122,7 +132,7 @@
const resetHandler = () => {
type = '';
name = '';
- accessControl = {};
+ accessGrants = [];
userIds = [];
loading = false;
};
@@ -226,11 +236,11 @@
{#if type !== 'dm'}
{#if type === ''}
-
+
{:else if type === 'group'}
{
+ onChange={(value: string) => {
if (value === 'private') {
isPrivate = true;
} else {
diff --git a/src/lib/components/notes/NoteEditor.svelte b/src/lib/components/notes/NoteEditor.svelte
index 977d00ef7..708e7e852 100644
--- a/src/lib/components/notes/NoteEditor.svelte
+++ b/src/lib/components/notes/NoteEditor.svelte
@@ -108,9 +108,18 @@
},
// pages: [], // TODO: Implement pages for notes to allow users to create multiple pages in a note
meta: null,
- access_control: {}
+ access_grants: []
};
+ const hasPublicReadGrant = (grants) =>
+ Array.isArray(grants) &&
+ grants.some(
+ (grant) =>
+ grant?.principal_type === 'user' &&
+ grant?.principal_id === '*' &&
+ grant?.permission === 'read'
+ );
+
let files = [];
let messages = [];
@@ -161,6 +170,9 @@
if (res) {
note = res;
+ if (!Array.isArray(note?.access_grants)) {
+ note.access_grants = [];
+ }
files = res.data.files || [];
if (note?.write_access) {
@@ -193,7 +205,7 @@
data: {
files: files
},
- access_control: note?.access_control
+ access_grants: note?.access_grants ?? []
}).catch((e) => {
toast.error(`${e}`);
});
@@ -765,8 +777,8 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
console.log('noteEventHandler', _note);
if (_note.id !== id) return;
- if (_note.access_control && _note.access_control !== note.access_control) {
- note.access_control = _note.access_control;
+ if (_note.access_grants && _note.access_grants !== note.access_grants) {
+ note.access_grants = _note.access_grants;
}
if (_note.data && _note.data.files) {
@@ -851,7 +863,7 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
{#if note}
{
changeDebounceHandler();
@@ -1114,7 +1126,11 @@ Provide the enhanced notes in markdown format. Use markdown syntax for headings,
}}
disabled={note?.user_id !== $user?.id && $user?.role !== 'admin'}
>
- {note?.access_control ? $i18n.t('Private') : $i18n.t('Everyone')}
+
+ {hasPublicReadGrant(note?.access_grants)
+ ? $i18n.t('Everyone')
+ : $i18n.t('Private')}
+
{:else}
diff --git a/src/lib/components/notes/Notes.svelte b/src/lib/components/notes/Notes.svelte
index 3ca43e6ee..504bd3021 100644
--- a/src/lib/components/notes/Notes.svelte
+++ b/src/lib/components/notes/Notes.svelte
@@ -129,7 +129,7 @@
}
},
meta: null,
- access_control: {}
+ access_grants: []
}).catch((error) => {
toast.error(`${error}`);
return null;
diff --git a/src/lib/components/notes/utils.ts b/src/lib/components/notes/utils.ts
index 052c48a44..8c6bd3524 100644
--- a/src/lib/components/notes/utils.ts
+++ b/src/lib/components/notes/utils.ts
@@ -120,7 +120,7 @@ export const createNoteHandler = async (title: string, md?: string, html?: strin
}
},
meta: null,
- access_control: {}
+ access_grants: []
}).catch((error) => {
toast.error(`${error}`);
return null;
diff --git a/src/lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte b/src/lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte
index 3373e5a66..14d70106e 100644
--- a/src/lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte
+++ b/src/lib/components/workspace/Knowledge/CreateKnowledgeBase.svelte
@@ -15,7 +15,7 @@
let name = '';
let description = '';
- let accessControl = {};
+ let accessGrants = [];
const submitHandler = async () => {
loading = true;
@@ -32,7 +32,7 @@
localStorage.token,
name,
description,
- accessControl
+ accessGrants
).catch((e) => {
toast.error(`${e}`);
});
@@ -114,7 +114,7 @@
{
toast.error(`${e}`);
});
@@ -745,6 +747,9 @@
if (res) {
knowledge = res;
+ if (!Array.isArray(knowledge?.access_grants)) {
+ knowledge.access_grants = [];
+ }
knowledgeId = knowledge?.id;
} else {
goto('/workspace/knowledge');
@@ -828,9 +833,11 @@
{#if id && knowledge}
{
changeDebounceHandler();
}}
diff --git a/src/lib/components/workspace/Models/ModelEditor.svelte b/src/lib/components/workspace/Models/ModelEditor.svelte
index 18e26963a..9aee1a37c 100644
--- a/src/lib/components/workspace/Models/ModelEditor.svelte
+++ b/src/lib/components/workspace/Models/ModelEditor.svelte
@@ -108,7 +108,7 @@
let builtinTools = {};
let actionIds = [];
- let accessControl = {};
+ let accessGrants = [];
let tts = { voice: '' };
const submitHandler = async () => {
@@ -140,7 +140,7 @@
info.params = { ...info.params, ...params };
- info.access_control = accessControl;
+ info.access_grants = accessGrants;
info.meta.capabilities = capabilities;
if (enableDescription) {
@@ -301,14 +301,7 @@
builtinTools = model?.meta?.builtinTools ?? {};
tts = { voice: model?.meta?.tts?.voice ?? '' };
- if ('access_control' in model) {
- accessControl = model.access_control;
- } else {
- accessControl = {};
- }
-
- console.log(model?.access_control);
- console.log(accessControl);
+ accessGrants = model?.access_grants ?? [];
info = {
...info,
@@ -334,10 +327,10 @@
{#if loaded}
{#if onBack}
diff --git a/src/lib/components/workspace/Prompts/PromptEditor.svelte b/src/lib/components/workspace/Prompts/PromptEditor.svelte
index 6f251f36c..a9a97bd5c 100644
--- a/src/lib/components/workspace/Prompts/PromptEditor.svelte
+++ b/src/lib/components/workspace/Prompts/PromptEditor.svelte
@@ -46,7 +46,7 @@
let commitMessage = '';
let isProduction = true;
- let accessControl = {};
+ let accessGrants = [];
let showAccessControlModal = false;
let hasManualEdit = false;
@@ -87,7 +87,7 @@
command,
content,
tags: tags.map((tag) => tag.name),
- access_control: accessControl,
+ access_grants: accessGrants,
commit_message: commitMessage || undefined,
is_production: isProduction
});
@@ -259,7 +259,7 @@
command = prompt.command.at(0) === '/' ? prompt.command.slice(1) : prompt.command;
content = prompt.content;
tags = (prompt.tags || []).map((tag) => ({ name: tag }));
- accessControl = prompt?.access_control === undefined ? {} : prompt?.access_control;
+ accessGrants = prompt?.access_grants === undefined ? [] : prompt?.access_grants;
// Store originals for revert on collision
originalName = name;
@@ -286,10 +286,10 @@
diff --git a/src/lib/components/workspace/Tools/ToolkitEditor.svelte b/src/lib/components/workspace/Tools/ToolkitEditor.svelte
index dd0d4a4a1..70533099c 100644
--- a/src/lib/components/workspace/Tools/ToolkitEditor.svelte
+++ b/src/lib/components/workspace/Tools/ToolkitEditor.svelte
@@ -30,7 +30,7 @@
description: ''
};
export let content = '';
- export let accessControl = {};
+ export let accessGrants = [];
let _content = '';
@@ -161,7 +161,7 @@ class Tools:
name,
meta,
content,
- access_control: accessControl
+ access_grants: accessGrants
});
};
@@ -187,10 +187,10 @@ class Tools:
diff --git a/src/lib/components/workspace/common/AccessControl.svelte b/src/lib/components/workspace/common/AccessControl.svelte
index 1bf4bbc1f..7e691d0e3 100644
--- a/src/lib/components/workspace/common/AccessControl.svelte
+++ b/src/lib/components/workspace/common/AccessControl.svelte
@@ -3,75 +3,430 @@
const i18n = getContext('i18n');
- import { getGroups } from '$lib/apis/groups';
- import Tooltip from '$lib/components/common/Tooltip.svelte';
- import Plus from '$lib/components/icons/Plus.svelte';
- import UserCircleSolid from '$lib/components/icons/UserCircleSolid.svelte';
+ import { getGroups, getGroupById, getGroupInfoById } from '$lib/apis/groups';
+ import { getUserById, getUserInfoById } from '$lib/apis/users';
+ import { WEBUI_API_BASE_URL } from '$lib/constants';
import XMark from '$lib/components/icons/XMark.svelte';
import Badge from '$lib/components/common/Badge.svelte';
+ import GlobeAlt from '$lib/components/icons/GlobeAlt.svelte';
+ import Plus from '$lib/components/icons/Plus.svelte';
+ import AddAccessModal from './AddAccessModal.svelte';
+ import Tooltip from '$lib/components/common/Tooltip.svelte';
+
+ type AccessGrant = {
+ id?: string;
+ principal_type: 'user' | 'group';
+ principal_id: string;
+ permission: 'read' | 'write';
+ };
+
+ type LegacyAccessControl = {
+ read: { group_ids: string[]; user_ids: string[] };
+ write: { group_ids: string[]; user_ids: string[] };
+ };
export let onChange: Function = () => {};
export let accessRoles = ['read'];
- export let accessControl = {};
+ export let accessGrants: AccessGrant[] | any = [];
+ export let accessControl: any = undefined;
export let share = true;
export let sharePublic = true;
- let selectedGroupId = '';
- let groups = [];
+ let groups: any[] = [];
+ const resolvingGroupIds = new Set
();
+ let userById: Record = {};
+ const resolvingUserIds = new Set();
- $: if (!sharePublic && accessControl === null) {
- initPublicAccess();
- }
+ let showAddAccessModal = false;
- const initPublicAccess = () => {
- if (!sharePublic && accessControl === null) {
- accessControl = {
- read: {
- group_ids: [],
- user_ids: []
- },
- write: {
- group_ids: [],
- user_ids: []
+ const dedupeAccessGrants = (grants: AccessGrant[] | null | undefined): AccessGrant[] => {
+ if (!Array.isArray(grants)) return [];
+ const map = new Map();
+ for (const grant of grants) {
+ if (!grant) continue;
+ const key = `${grant.principal_type}:${grant.principal_id}:${grant.permission}`;
+ if (!grant.principal_type || !grant.principal_id || !grant.permission) continue;
+ map.set(key, {
+ id: grant.id,
+ principal_type: grant.principal_type,
+ principal_id: grant.principal_id,
+ permission: grant.permission
+ });
+ }
+ return Array.from(map.values());
+ };
+
+ const legacyAccessControlToGrants = (accessControl: any): AccessGrant[] => {
+ if (accessControl === null) {
+ return [
+ {
+ principal_type: 'user',
+ principal_id: '*',
+ permission: 'read'
}
- };
- onChange(accessControl);
+ ];
+ }
+
+ if (!accessControl || typeof accessControl !== 'object') {
+ return [];
+ }
+
+ const grants: AccessGrant[] = [];
+ for (const permission of ['read', 'write'] as const) {
+ const entry = accessControl?.[permission] ?? {};
+ for (const groupId of entry?.group_ids ?? []) {
+ grants.push({
+ principal_type: 'group',
+ principal_id: groupId,
+ permission
+ });
+ }
+ for (const userId of entry?.user_ids ?? []) {
+ grants.push({
+ principal_type: 'user',
+ principal_id: userId,
+ permission
+ });
+ }
+ }
+
+ return dedupeAccessGrants(grants);
+ };
+
+ const grantsToLegacyAccessControl = (grants: AccessGrant[]): null | LegacyAccessControl => {
+ const normalized = dedupeAccessGrants(grants);
+ if (hasPublicReadGrant(normalized)) {
+ return null;
+ }
+
+ const result: LegacyAccessControl = {
+ read: { group_ids: [], user_ids: [] },
+ write: { group_ids: [], user_ids: [] }
+ };
+
+ for (const grant of normalized) {
+ if (!['read', 'write'].includes(grant.permission)) {
+ continue;
+ }
+
+ if (grant.principal_type === 'group') {
+ if (!result[grant.permission].group_ids.includes(grant.principal_id)) {
+ result[grant.permission].group_ids = [
+ ...result[grant.permission].group_ids,
+ grant.principal_id
+ ];
+ }
+ } else if (grant.principal_type === 'user' && grant.principal_id !== '*') {
+ if (!result[grant.permission].user_ids.includes(grant.principal_id)) {
+ result[grant.permission].user_ids = [
+ ...result[grant.permission].user_ids,
+ grant.principal_id
+ ];
+ }
+ }
+ }
+
+ return result;
+ };
+
+ const normalizeInputToGrants = (value: any): AccessGrant[] => {
+ if (value === null) {
+ return legacyAccessControlToGrants(null);
+ }
+ if (Array.isArray(value)) {
+ return dedupeAccessGrants(value);
+ }
+ if (value && typeof value === 'object' && ('read' in value || 'write' in value)) {
+ return legacyAccessControlToGrants(value);
+ }
+ return [];
+ };
+
+ const stableStringify = (value: any): string => {
+ try {
+ return JSON.stringify(value ?? null);
+ } catch {
+ return '';
}
};
+ const hasPublicReadGrant = (grants: AccessGrant[]): boolean =>
+ grants.some(
+ (grant) =>
+ grant.principal_type === 'user' && grant.principal_id === '*' && grant.permission === 'read'
+ );
+
+ const currentGrants = (): AccessGrant[] =>
+ Array.isArray(accessGrants) ? (accessGrants as AccessGrant[]) : [];
+
+ const getPrincipalIdsByPermission = (
+ principalType: 'user' | 'group',
+ permission: 'read' | 'write'
+ ): string[] =>
+ Array.from(
+ new Set(
+ currentGrants()
+ .filter(
+ (grant) => grant.principal_type === principalType && grant.permission === permission
+ )
+ .map((grant) => grant.principal_id)
+ )
+ );
+
+ const hasPrincipalGrant = (
+ principalType: 'user' | 'group',
+ principalId: string,
+ permission: 'read' | 'write'
+ ): boolean =>
+ currentGrants().some(
+ (grant) =>
+ grant.principal_type === principalType &&
+ grant.principal_id === principalId &&
+ grant.permission === permission
+ );
+
+ const commitAccessGrants = (nextGrants: AccessGrant[]) => {
+ accessGrants = dedupeAccessGrants(nextGrants);
+ onChange(accessGrants);
+ };
+
+ const setPublic = (isPublic: boolean) => {
+ const filtered = currentGrants().filter(
+ (grant) =>
+ !(
+ grant.principal_type === 'user' &&
+ grant.principal_id === '*' &&
+ grant.permission === 'read'
+ )
+ );
+ if (isPublic) {
+ filtered.push({
+ principal_type: 'user',
+ principal_id: '*',
+ permission: 'read'
+ });
+ }
+ commitAccessGrants(filtered);
+ };
+
+ const upsertPrincipalGrant = (
+ principalType: 'user' | 'group',
+ principalId: string,
+ permission: 'read' | 'write',
+ grants: AccessGrant[]
+ ): AccessGrant[] => {
+ if (
+ grants.some(
+ (grant) =>
+ grant.principal_type === principalType &&
+ grant.principal_id === principalId &&
+ grant.permission === permission
+ )
+ ) {
+ return grants;
+ }
+ return [
+ ...grants,
+ {
+ principal_type: principalType,
+ principal_id: principalId,
+ permission
+ }
+ ];
+ };
+
+ const removePrincipalGrant = (
+ principalType: 'user' | 'group',
+ principalId: string,
+ permission: 'read' | 'write',
+ grants: AccessGrant[]
+ ): AccessGrant[] =>
+ grants.filter(
+ (grant) =>
+ !(
+ grant.principal_type === principalType &&
+ grant.principal_id === principalId &&
+ grant.permission === permission
+ )
+ );
+
+ const removePrincipal = (principalType: 'user' | 'group', principalId: string) => {
+ let next = [...currentGrants()];
+ next = removePrincipalGrant(principalType, principalId, 'read', next);
+ next = removePrincipalGrant(principalType, principalId, 'write', next);
+ commitAccessGrants(next);
+ };
+
+ const togglePrincipalWrite = (principalType: 'user' | 'group', principalId: string) => {
+ let next = [...currentGrants()];
+ const hasWrite = hasPrincipalGrant(principalType, principalId, 'write');
+ if (hasWrite) {
+ next = removePrincipalGrant(principalType, principalId, 'write', next);
+ } else {
+ next = upsertPrincipalGrant(principalType, principalId, 'read', next);
+ next = upsertPrincipalGrant(principalType, principalId, 'write', next);
+ }
+ commitAccessGrants(next);
+ };
+
+ const ensureUsersByIds = async (userIds: string[]) => {
+ const pendingIds = userIds.filter((id) => !userById[id] && !resolvingUserIds.has(id));
+ if (!pendingIds.length) return;
+
+ for (const id of pendingIds) {
+ resolvingUserIds.add(id);
+ }
+
+ const fetched = await Promise.all(
+ pendingIds.map(async (id) => {
+ const user = await getUserInfoById(localStorage.token, id).catch((error) => {
+ console.error(error);
+ return null;
+ });
+ return { id, user };
+ })
+ );
+
+ const nextUserById = { ...userById };
+ for (const item of fetched) {
+ if (item.user?.id) {
+ nextUserById[item.id] = item.user;
+ }
+ resolvingUserIds.delete(item.id);
+ }
+ userById = nextUserById;
+ };
+
+ const handleAddAccess = ({ userIds, groupIds }: { userIds: string[]; groupIds: string[] }) => {
+ let next = [...currentGrants()];
+
+ for (const groupId of groupIds) {
+ next = upsertPrincipalGrant('group', groupId, 'read', next);
+ }
+ for (const userId of userIds) {
+ next = upsertPrincipalGrant('user', userId, 'read', next);
+ }
+ commitAccessGrants(next);
+ };
+
+ // NOTE: We must reference `accessGrants` directly in each reactive
+ // expression so Svelte tracks the dependency.
+ const ensureGroupsByIds = async (groupIds: string[]) => {
+ const pendingIds = groupIds.filter(
+ (id) => !groups.find((g) => g.id === id) && !resolvingGroupIds.has(id)
+ );
+ if (!pendingIds.length) return;
+
+ for (const id of pendingIds) {
+ resolvingGroupIds.add(id);
+ }
+
+ const fetched = await Promise.all(
+ pendingIds.map(async (id) => {
+ const group = await getGroupInfoById(localStorage.token, id).catch((error) => {
+ console.error(error);
+ return null;
+ });
+ return group;
+ })
+ );
+
+ const newGroups = fetched.filter((g) => g);
+ if (newGroups.length > 0) {
+ groups = [...groups, ...newGroups].filter(
+ (g, index, self) => index === self.findIndex((t) => t.id === g.id)
+ );
+ }
+
+ for (const id of pendingIds) {
+ resolvingGroupIds.delete(id);
+ }
+ };
+
+ $: if (readGroupIds.length > 0 || writeGroupIds.length > 0) {
+ void ensureGroupsByIds([...readGroupIds, ...writeGroupIds]);
+ }
+ $: readGroupIds = (accessGrants, getPrincipalIdsByPermission('group', 'read'));
+ $: writeGroupIds = (accessGrants, getPrincipalIdsByPermission('group', 'write'));
+ $: readUserIds =
+ (accessGrants, getPrincipalIdsByPermission('user', 'read').filter((id) => id !== '*'));
+ $: writeUserIds =
+ (accessGrants, getPrincipalIdsByPermission('user', 'write').filter((id) => id !== '*'));
+
+ $: selectedUserIds = Array.from(new Set([...readUserIds, ...writeUserIds]));
+
+ $: selectedUsers = selectedUserIds
+ .map((id) => {
+ return userById[id] ?? { id, name: id, email: '' };
+ })
+ .sort((a, b) => a.name.localeCompare(b.name));
+
+ $: accessGroups = groups
+ .filter((group) => readGroupIds.includes(group.id) || writeGroupIds.includes(group.id))
+ .sort((a, b) => a.name.localeCompare(b.name));
+
+ $: if (selectedUserIds.length > 0) {
+ void ensureUsersByIds(selectedUserIds);
+ }
+
+ $: {
+ if (accessControl !== undefined) {
+ const normalizedGrants = normalizeInputToGrants(accessControl);
+ if (stableStringify(normalizedGrants) !== stableStringify(accessGrants)) {
+ accessGrants = normalizedGrants;
+ }
+ }
+ }
+
+ $: {
+ const normalizedGrants = normalizeInputToGrants(accessGrants);
+ if (stableStringify(normalizedGrants) !== stableStringify(accessGrants)) {
+ accessGrants = normalizedGrants;
+ }
+
+ if (accessControl !== undefined) {
+ const nextAccessControl = grantsToLegacyAccessControl(normalizedGrants);
+ if (stableStringify(nextAccessControl) !== stableStringify(accessControl)) {
+ accessControl = nextAccessControl;
+ }
+ }
+ }
+
onMount(async () => {
- groups = await getGroups(localStorage.token, true).catch((error) => {
+ console.log('AccessControl mounted', { accessGrants, accessControl });
+ const res = await getGroups(localStorage.token, true).catch((error) => {
console.error(error);
return [];
});
- if (accessControl === null) {
- initPublicAccess();
- } else {
- accessControl = {
- read: {
- group_ids: accessControl?.read?.group_ids ?? [],
- user_ids: accessControl?.read?.user_ids ?? []
- },
- write: {
- group_ids: accessControl?.write?.group_ids ?? [],
- user_ids: accessControl?.write?.user_ids ?? []
- }
- };
- }
+ console.log('getGroups res', res);
+
+ groups = [...groups, ...res].filter(
+ (g, index, self) => index === self.findIndex((t) => t.id === g.id)
+ );
+ });
+
+ $: console.log('AccessControl state', {
+ accessGrants,
+ readGroupIds,
+ writeGroupIds,
+ selectedUserIds,
+ groups,
+ accessGroups,
+ selectedUsers
});
-
-
-
{$i18n.t('Visibility')}
+
-
+
+
+
- {#if accessControl !== null}
+ {#if !hasPublicReadGrant(accessGrants ?? [])}