Skip to content

Commit f86e9dd

Browse files
authored
🔖 feat: Enhance Bookmarks UX, add RBAC, toggle via librechat.yaml (danny-avila#3747)
* chore: update package version to 0.7.416 * chore: Update Role.js imports order * refactor: move updateTagsInConvo to tags route, add RBAC for tags * refactor: add updateTagsInConvoOptions * fix: loading state for bookmark form * refactor: update primaryText class in TitleButton component * refactor: remove duplicate bookmarks and theming * refactor: update EditIcon component to use React.forwardRef * refactor: add _id field to tConversationTagSchema * refactor: remove promises * refactor: move mutation logic from BookmarkForm -> BookmarkEditDialog * refactor: update button class in BookmarkForm component * fix: conversation mutations and add better logging to useConversationTagMutation * refactor: update logger message in BookmarkEditDialog component * refactor: improve UI consistency in BookmarkNav and NewChat components * refactor: update logger message in BookmarkEditDialog component * refactor: Add tags prop to BookmarkForm component * refactor: Update BookmarkForm to avoid tag mutation if the tag already exists; also close dialog on submission programmatically * refactor: general role helper function to support updating access permissions for different permission types * refactor: Update getLatestText function to handle undefined values in message.content * refactor: Update useHasAccess hook to handle null role values for authenticated users * feat: toggle bookmarks access * refactor: Update PromptsCommand to handle access permissions for prompts * feat: updateConversationSelector * refactor: rename `vars` to `tagToDelete` for clarity * fix: prevent recreation of deleted tags in BookmarkMenu on Item Click * ci: mock updateBookmarksAccess function * ci: mock updateBookmarksAccess function
1 parent 366e4c5 commit f86e9dd

39 files changed

+527
-295
lines changed

‎api/models/Role.js‎

Lines changed: 34 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,11 @@
11
const {
2-
SystemRoles,
32
CacheKeys,
3+
SystemRoles,
44
roleDefaults,
55
PermissionTypes,
6-
Permissions,
6+
removeNullishValues,
77
promptPermissionsSchema,
8+
bookmarkPermissionsSchema,
89
} = require('librechat-data-provider');
910
const getLogStores = require('~/cache/getLogStores');
1011
const Role = require('~/models/schema/roleSchema');
@@ -69,37 +70,52 @@ const updateRoleByName = async function (roleName, updates) {
6970
}
7071
};
7172

73+
const permissionSchemas = {
74+
[PermissionTypes.PROMPTS]: promptPermissionsSchema,
75+
[PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema,
76+
};
77+
7278
/**
73-
* Updates the Prompt access for a specific role.
74-
* @param {SystemRoles} roleName - The role to update the prompt access for.
75-
* @param {boolean | undefined} [value] - The new value for the prompt access.
79+
* Updates access permissions for a specific role and permission type.
80+
* @param {SystemRoles} roleName - The role to update.
81+
* @param {PermissionTypes} permissionType - The type of permission to update.
82+
* @param {Object.<Permissions, boolean>} permissions - Permissions to update and their values.
7683
*/
77-
async function updatePromptsAccess(roleName, value) {
78-
if (typeof value === 'undefined') {
84+
async function updateAccessPermissions(roleName, permissionType, _permissions) {
85+
const permissions = removeNullishValues(_permissions);
86+
if (Object.keys(permissions).length === 0) {
7987
return;
8088
}
8189

8290
try {
83-
const parsedUpdates = promptPermissionsSchema.partial().parse({ [Permissions.USE]: value });
8491
const role = await getRoleByName(roleName);
85-
if (!role) {
92+
if (!role || !permissionSchemas[permissionType]) {
8693
return;
8794
}
8895

89-
const mergedUpdates = {
90-
[PermissionTypes.PROMPTS]: {
91-
...role[PermissionTypes.PROMPTS],
92-
...parsedUpdates,
96+
await updateRoleByName(roleName, {
97+
[permissionType]: {
98+
...role[permissionType],
99+
...permissionSchemas[permissionType].partial().parse(permissions),
93100
},
94-
};
101+
});
95102

96-
await updateRoleByName(roleName, mergedUpdates);
97-
logger.info(`Updated '${roleName}' role prompts 'USE' permission to: ${value}`);
103+
Object.entries(permissions).forEach(([permission, value]) =>
104+
logger.info(
105+
`Updated '${roleName}' role ${permissionType} '${permission}' permission to: ${value}`,
106+
),
107+
);
98108
} catch (error) {
99-
logger.error('Failed to update USER role prompts USE permission:', error);
109+
logger.error(`Failed to update ${roleName} role ${permissionType} permissions:`, error);
100110
}
101111
}
102112

113+
const updatePromptsAccess = (roleName, permissions) =>
114+
updateAccessPermissions(roleName, PermissionTypes.PROMPTS, permissions);
115+
116+
const updateBookmarksAccess = (roleName, permissions) =>
117+
updateAccessPermissions(roleName, PermissionTypes.BOOKMARKS, permissions);
118+
103119
/**
104120
* Initialize default roles in the system.
105121
* Creates the default roles (ADMIN, USER) if they don't exist in the database.
@@ -123,4 +139,5 @@ module.exports = {
123139
initializeRoles,
124140
updateRoleByName,
125141
updatePromptsAccess,
142+
updateBookmarksAccess,
126143
};

‎api/models/schema/roleSchema.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@ const roleSchema = new mongoose.Schema({
88
unique: true,
99
index: true,
1010
},
11+
[PermissionTypes.BOOKMARKS]: {
12+
[Permissions.USE]: {
13+
type: Boolean,
14+
default: true,
15+
},
16+
},
1117
[PermissionTypes.PROMPTS]: {
1218
[Permissions.SHARED_GLOBAL]: {
1319
type: Boolean,

‎api/server/routes/convos.js‎

Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
88
const { forkConversation } = require('~/server/utils/import/fork');
99
const { importConversations } = require('~/server/utils/import');
1010
const { createImportLimiters } = require('~/server/middleware');
11-
const { updateTagsForConversation } = require('~/models/ConversationTag');
1211
const getLogStores = require('~/cache/getLogStores');
1312
const { sleep } = require('~/server/utils');
1413
const { logger } = require('~/config');
@@ -174,18 +173,4 @@ router.post('/fork', async (req, res) => {
174173
}
175174
});
176175

177-
router.put('/tags/:conversationId', async (req, res) => {
178-
try {
179-
const conversationTags = await updateTagsForConversation(
180-
req.user.id,
181-
req.params.conversationId,
182-
req.body.tags,
183-
);
184-
res.status(200).json(conversationTags);
185-
} catch (error) {
186-
logger.error('Error updating conversation tags', error);
187-
res.status(500).send('Error updating conversation tags');
188-
}
189-
});
190-
191176
module.exports = router;

‎api/server/routes/tags.js‎

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,21 @@
11
const express = require('express');
2+
const { PermissionTypes, Permissions } = require('librechat-data-provider');
23
const {
34
getConversationTags,
45
updateConversationTag,
56
createConversationTag,
67
deleteConversationTag,
8+
updateTagsForConversation,
79
} = require('~/models/ConversationTag');
8-
const requireJwtAuth = require('~/server/middleware/requireJwtAuth');
10+
const { requireJwtAuth, generateCheckAccess } = require('~/server/middleware');
11+
const { logger } = require('~/config');
12+
913
const router = express.Router();
14+
15+
const checkBookmarkAccess = generateCheckAccess(PermissionTypes.BOOKMARKS, [Permissions.USE]);
16+
1017
router.use(requireJwtAuth);
18+
router.use(checkBookmarkAccess);
1119

1220
/**
1321
* GET /
@@ -24,7 +32,7 @@ router.get('/', async (req, res) => {
2432
res.status(404).end();
2533
}
2634
} catch (error) {
27-
console.error('Error getting conversation tags:', error);
35+
logger.error('Error getting conversation tags:', error);
2836
res.status(500).json({ error: 'Internal server error' });
2937
}
3038
});
@@ -40,7 +48,7 @@ router.post('/', async (req, res) => {
4048
const tag = await createConversationTag(req.user.id, req.body);
4149
res.status(200).json(tag);
4250
} catch (error) {
43-
console.error('Error creating conversation tag:', error);
51+
logger.error('Error creating conversation tag:', error);
4452
res.status(500).json({ error: 'Internal server error' });
4553
}
4654
});
@@ -60,7 +68,7 @@ router.put('/:tag', async (req, res) => {
6068
res.status(404).json({ error: 'Tag not found' });
6169
}
6270
} catch (error) {
63-
console.error('Error updating conversation tag:', error);
71+
logger.error('Error updating conversation tag:', error);
6472
res.status(500).json({ error: 'Internal server error' });
6573
}
6674
});
@@ -80,9 +88,29 @@ router.delete('/:tag', async (req, res) => {
8088
res.status(404).json({ error: 'Tag not found' });
8189
}
8290
} catch (error) {
83-
console.error('Error deleting conversation tag:', error);
91+
logger.error('Error deleting conversation tag:', error);
8492
res.status(500).json({ error: 'Internal server error' });
8593
}
8694
});
8795

96+
/**
97+
* PUT /convo/:conversationId
98+
* Updates the tags for a conversation.
99+
* @param {Object} req - Express request object
100+
* @param {Object} res - Express response object
101+
*/
102+
router.put('/convo/:conversationId', async (req, res) => {
103+
try {
104+
const conversationTags = await updateTagsForConversation(
105+
req.user.id,
106+
req.params.conversationId,
107+
req.body.tags,
108+
);
109+
res.status(200).json(conversationTags);
110+
} catch (error) {
111+
logger.error('Error updating conversation tags', error);
112+
res.status(500).send('Error updating conversation tags');
113+
}
114+
});
115+
88116
module.exports = router;

‎api/server/services/AppService.spec.js‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ jest.mock('./Files/Firebase/initialize', () => ({
2424
jest.mock('~/models/Role', () => ({
2525
initializeRoles: jest.fn(),
2626
updatePromptsAccess: jest.fn(),
27+
updateBookmarksAccess: jest.fn(),
2728
}));
2829
jest.mock('./ToolService', () => ({
2930
loadAndFormatTools: jest.fn().mockReturnValue({

‎api/server/services/start/interface.js‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
const { SystemRoles, removeNullishValues } = require('librechat-data-provider');
2-
const { updatePromptsAccess } = require('~/models/Role');
1+
const { SystemRoles, Permissions, removeNullishValues } = require('librechat-data-provider');
2+
const { updatePromptsAccess, updateBookmarksAccess } = require('~/models/Role');
33
const { logger } = require('~/config');
44

55
/**
@@ -24,10 +24,12 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol
2424
sidePanel: interfaceConfig?.sidePanel ?? defaults.sidePanel,
2525
privacyPolicy: interfaceConfig?.privacyPolicy ?? defaults.privacyPolicy,
2626
termsOfService: interfaceConfig?.termsOfService ?? defaults.termsOfService,
27+
bookmarks: interfaceConfig?.bookmarks ?? defaults.bookmarks,
2728
prompts: interfaceConfig?.prompts ?? defaults.prompts,
2829
});
2930

30-
await updatePromptsAccess(roleName, loadedInterface.prompts);
31+
await updatePromptsAccess(roleName, { [Permissions.USE]: loadedInterface.prompts });
32+
await updateBookmarksAccess(roleName, { [Permissions.USE]: loadedInterface.bookmarks });
3133

3234
let i = 0;
3335
const logSettings = () => {
Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
1-
const { SystemRoles } = require('librechat-data-provider');
1+
const { SystemRoles, Permissions } = require('librechat-data-provider');
22
const { updatePromptsAccess } = require('~/models/Role');
33
const { loadDefaultInterface } = require('./interface');
44

55
jest.mock('~/models/Role', () => ({
66
updatePromptsAccess: jest.fn(),
7+
updateBookmarksAccess: jest.fn(),
78
}));
89

910
describe('loadDefaultInterface', () => {
@@ -13,7 +14,7 @@ describe('loadDefaultInterface', () => {
1314

1415
await loadDefaultInterface(config, configDefaults);
1516

16-
expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, true);
17+
expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, { [Permissions.USE]: true });
1718
});
1819

1920
it('should call updatePromptsAccess with false when prompts is false', async () => {
@@ -22,7 +23,9 @@ describe('loadDefaultInterface', () => {
2223

2324
await loadDefaultInterface(config, configDefaults);
2425

25-
expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, false);
26+
expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, {
27+
[Permissions.USE]: false,
28+
});
2629
});
2730

2831
it('should call updatePromptsAccess with undefined when prompts is not specified in config', async () => {
@@ -31,7 +34,9 @@ describe('loadDefaultInterface', () => {
3134

3235
await loadDefaultInterface(config, configDefaults);
3336

34-
expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, undefined);
37+
expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, {
38+
[Permissions.USE]: undefined,
39+
});
3540
});
3641

3742
it('should call updatePromptsAccess with undefined when prompts is explicitly undefined', async () => {
@@ -40,6 +45,8 @@ describe('loadDefaultInterface', () => {
4045

4146
await loadDefaultInterface(config, configDefaults);
4247

43-
expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, undefined);
48+
expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, {
49+
[Permissions.USE]: undefined,
50+
});
4451
});
4552
});

0 commit comments

Comments
 (0)