From 7ac9e1dd0001d61b52af4fdf4c3df73de118b996 Mon Sep 17 00:00:00 2001 From: Ryan Detzel Date: Mon, 23 Sep 2024 05:57:43 -0400 Subject: [PATCH 1/3] Add a new role that will prevent users from deleting conversations --- api/models/Role.js | 2 ++ api/models/schema/roleSchema.js | 6 +++++ api/server/services/start/interface.js | 9 +++++--- client/src/components/Conversations/Convo.tsx | 22 ++++++++++++++----- .../ConvoOptions/ConvoOptions.tsx | 17 ++++++++++---- packages/data-provider/src/config.ts | 2 ++ packages/data-provider/src/roles.ts | 15 +++++++++++++ packages/data-provider/src/types.ts | 1 + 8 files changed, 62 insertions(+), 12 deletions(-) diff --git a/api/models/Role.js b/api/models/Role.js index 9c160512b7d..ebf7e809a0b 100644 --- a/api/models/Role.js +++ b/api/models/Role.js @@ -8,6 +8,7 @@ const { promptPermissionsSchema, bookmarkPermissionsSchema, multiConvoPermissionsSchema, + deleteConvoPermissionsSchema, } = require('librechat-data-provider'); const getLogStores = require('~/cache/getLogStores'); const Role = require('~/models/schema/roleSchema'); @@ -77,6 +78,7 @@ const permissionSchemas = { [PermissionTypes.PROMPTS]: promptPermissionsSchema, [PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema, [PermissionTypes.MULTI_CONVO]: multiConvoPermissionsSchema, + [PermissionTypes.DELETE_CONVO]: deleteConvoPermissionsSchema, }; /** diff --git a/api/models/schema/roleSchema.js b/api/models/schema/roleSchema.js index 36e9d3f7b6e..717a1860baa 100644 --- a/api/models/schema/roleSchema.js +++ b/api/models/schema/roleSchema.js @@ -48,6 +48,12 @@ const roleSchema = new mongoose.Schema({ default: true, }, }, + [PermissionTypes.DELETE_CONVO]: { + [Permissions.USE]: { + type: Boolean, + default: true, + }, + }, }); const Role = mongoose.model('Role', roleSchema); diff --git a/api/server/services/start/interface.js b/api/server/services/start/interface.js index bf31eb78b89..a77d32b977e 100644 --- a/api/server/services/start/interface.js +++ b/api/server/services/start/interface.js @@ -32,17 +32,20 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol bookmarks: interfaceConfig?.bookmarks ?? defaults.bookmarks, prompts: interfaceConfig?.prompts ?? defaults.prompts, multiConvo: interfaceConfig?.multiConvo ?? defaults.multiConvo, + deleteConvo: interfaceConfig?.deleteConvo ?? defaults.deleteConvo, }); await updateAccessPermissions(roleName, { [PermissionTypes.PROMPTS]: { [Permissions.USE]: loadedInterface.prompts }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: loadedInterface.bookmarks }, [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: loadedInterface.multiConvo }, + [PermissionTypes.DELETE_CONVO]: { [Permissions.USE]: loadedInterface.deleteConvo }, }); await updateAccessPermissions(SystemRoles.ADMIN, { [PermissionTypes.PROMPTS]: { [Permissions.USE]: loadedInterface.prompts }, [PermissionTypes.BOOKMARKS]: { [Permissions.USE]: loadedInterface.bookmarks }, [PermissionTypes.MULTI_CONVO]: { [Permissions.USE]: loadedInterface.multiConvo }, + [PermissionTypes.DELETE_CONVO]: { [Permissions.USE]: loadedInterface.deleteConvo }, }); let i = 0; @@ -61,7 +64,7 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol // warn about config.modelSpecs.prioritize if true and presets are enabled, that default presets will conflict with prioritizing model specs. if (config?.modelSpecs?.prioritize && loadedInterface.presets) { logger.warn( - 'Note: Prioritizing model specs can conflict with default presets if a default preset is set. It\'s recommended to disable presets from the interface or disable use of a default preset.', + "Note: Prioritizing model specs can conflict with default presets if a default preset is set. It's recommended to disable presets from the interface or disable use of a default preset.", ); i === 0 && i++; } @@ -75,14 +78,14 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol loadedInterface.parameters) ) { logger.warn( - 'Note: Enforcing model specs can conflict with the interface options: endpointsMenu, modelSelect, presets, and parameters. It\'s recommended to disable these options from the interface or disable enforcing model specs.', + "Note: Enforcing model specs can conflict with the interface options: endpointsMenu, modelSelect, presets, and parameters. It's recommended to disable these options from the interface or disable enforcing model specs.", ); i === 0 && i++; } // warn if enforce is true and prioritize is not, that enforcing model specs without prioritizing them can lead to unexpected behavior. if (config?.modelSpecs?.enforce && !config?.modelSpecs?.prioritize) { logger.warn( - 'Note: Enforcing model specs without prioritizing them can lead to unexpected behavior. It\'s recommended to enable prioritizing model specs if enforcing them.', + "Note: Enforcing model specs without prioritizing them can lead to unexpected behavior. It's recommended to enable prioritizing model specs if enforcing them.", ); i === 0 && i++; } diff --git a/client/src/components/Conversations/Convo.tsx b/client/src/components/Conversations/Convo.tsx index 9b6f504b5e9..aa4fb2a8313 100644 --- a/client/src/components/Conversations/Convo.tsx +++ b/client/src/components/Conversations/Convo.tsx @@ -14,7 +14,7 @@ import { useToastContext } from '~/Providers'; import { ConvoOptions } from './ConvoOptions'; import { cn } from '~/utils'; import store from '~/store'; -import { useLocalize } from '~/hooks' +import { useLocalize } from '~/hooks'; type KeyEvent = KeyboardEvent; @@ -151,11 +151,23 @@ export default function Conversation({ aria-label={`${localize('com_ui_rename')} ${localize('com_ui_chat')}`} />
- -
diff --git a/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx b/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx index 59c63f896ac..fbeea684191 100644 --- a/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx +++ b/client/src/components/Conversations/ConvoOptions/ConvoOptions.tsx @@ -2,11 +2,12 @@ import { useState, useId } from 'react'; import * as Ariakit from '@ariakit/react'; import { Ellipsis, Share2, Archive, Pen, Trash } from 'lucide-react'; import { useGetStartupConfig } from 'librechat-data-provider/react-query'; -import { useLocalize, useArchiveHandler } from '~/hooks'; +import { useLocalize, useArchiveHandler, useHasAccess } from '~/hooks'; import { DropdownPopup } from '~/components/ui'; import DeleteButton from './DeleteButton'; import ShareButton from './ShareButton'; import { cn } from '~/utils'; +import { PermissionTypes, Permissions } from 'librechat-data-provider'; export default function ConvoOptions({ conversation, @@ -33,6 +34,11 @@ export default function ConvoOptions({ setShowDeleteDialog(true); }; + const hasAccessToDeleteConvo = useHasAccess({ + permissionType: PermissionTypes.DELETE_CONVO, + permission: Permissions.USE, + }); + const dropdownItems = [ { label: localize('com_ui_rename'), @@ -50,12 +56,15 @@ export default function ConvoOptions({ onClick: archiveHandler, icon: , }, - { + ]; + + if (hasAccessToDeleteConvo) { + dropdownItems.push({ label: localize('com_ui_delete'), onClick: deleteHandler, icon: , - }, - ]; + }); + } const menuId = useId(); diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index be0dfd3d8ef..e459fb8c8d9 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -465,6 +465,7 @@ export const configSchema = z.object({ parameters: z.boolean().optional(), sidePanel: z.boolean().optional(), multiConvo: z.boolean().optional(), + deleteConvo: z.boolean().optional(), bookmarks: z.boolean().optional(), presets: z.boolean().optional(), prompts: z.boolean().optional(), @@ -476,6 +477,7 @@ export const configSchema = z.object({ sidePanel: true, presets: true, multiConvo: true, + deleteConvo: true, bookmarks: true, prompts: true, }), diff --git a/packages/data-provider/src/roles.ts b/packages/data-provider/src/roles.ts index fca276b00e0..1185c8896aa 100644 --- a/packages/data-provider/src/roles.ts +++ b/packages/data-provider/src/roles.ts @@ -34,6 +34,10 @@ export enum PermissionTypes { * Type for Multi-Conversation Permissions */ MULTI_CONVO = 'MULTI_CONVO', + /** + * Type for Delete Conversation Permissions + */ + DELETE_CONVO = 'DELETE_CONVO', } /** @@ -68,12 +72,17 @@ export const multiConvoPermissionsSchema = z.object({ [Permissions.USE]: z.boolean().default(false), }); +export const deleteConvoPermissionsSchema = z.object({ + [Permissions.USE]: z.boolean().default(false), +}); + export const roleSchema = z.object({ name: z.string(), [PermissionTypes.PROMPTS]: promptPermissionsSchema, [PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema, [PermissionTypes.AGENTS]: agentPermissionsSchema, [PermissionTypes.MULTI_CONVO]: multiConvoPermissionsSchema, + [PermissionTypes.DELETE_CONVO]: deleteConvoPermissionsSchema, }); export type TRole = z.infer; @@ -103,6 +112,9 @@ const defaultRolesSchema = z.object({ [PermissionTypes.MULTI_CONVO]: multiConvoPermissionsSchema.extend({ [Permissions.USE]: z.boolean().default(true), }), + [PermissionTypes.DELETE_CONVO]: deleteConvoPermissionsSchema.extend({ + [Permissions.USE]: z.boolean().default(true), + }), }), [SystemRoles.USER]: roleSchema.extend({ name: z.literal(SystemRoles.USER), @@ -110,6 +122,7 @@ const defaultRolesSchema = z.object({ [PermissionTypes.BOOKMARKS]: bookmarkPermissionsSchema, [PermissionTypes.AGENTS]: agentPermissionsSchema, [PermissionTypes.MULTI_CONVO]: multiConvoPermissionsSchema, + [PermissionTypes.DELETE_CONVO]: multiConvoPermissionsSchema, }), }); @@ -120,6 +133,7 @@ export const roleDefaults = defaultRolesSchema.parse({ [PermissionTypes.BOOKMARKS]: {}, [PermissionTypes.AGENTS]: {}, [PermissionTypes.MULTI_CONVO]: {}, + [PermissionTypes.DELETE_CONVO]: {}, }, [SystemRoles.USER]: { name: SystemRoles.USER, @@ -127,5 +141,6 @@ export const roleDefaults = defaultRolesSchema.parse({ [PermissionTypes.BOOKMARKS]: {}, [PermissionTypes.AGENTS]: {}, [PermissionTypes.MULTI_CONVO]: {}, + [PermissionTypes.DELETE_CONVO]: {}, }, }); diff --git a/packages/data-provider/src/types.ts b/packages/data-provider/src/types.ts index 41276c7557e..86174705677 100644 --- a/packages/data-provider/src/types.ts +++ b/packages/data-provider/src/types.ts @@ -319,6 +319,7 @@ export type TInterfaceConfig = { sidePanel: boolean; presets: boolean; multiConvo: boolean; + deleteConvo: boolean; bookmarks: boolean; prompts: boolean; }; From 53c057dda9ffa00e051d3c6cebf75f727e846345 Mon Sep 17 00:00:00 2001 From: Ryan Detzel Date: Mon, 23 Sep 2024 06:34:25 -0400 Subject: [PATCH 2/3] block deletes on the server --- api/server/routes/convos.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/api/server/routes/convos.js b/api/server/routes/convos.js index 104b0616f81..9afcdad739b 100644 --- a/api/server/routes/convos.js +++ b/api/server/routes/convos.js @@ -1,13 +1,13 @@ const multer = require('multer'); const express = require('express'); -const { CacheKeys } = require('librechat-data-provider'); +const { CacheKeys, PermissionTypes, Permissions } = require('librechat-data-provider'); const { initializeClient } = require('~/server/services/Endpoints/assistants'); const { getConvosByPage, deleteConvos, getConvo, saveConvo } = require('~/models/Conversation'); const { storage, importFileFilter } = require('~/server/routes/files/multer'); const requireJwtAuth = require('~/server/middleware/requireJwtAuth'); const { forkConversation } = require('~/server/utils/import/fork'); const { importConversations } = require('~/server/utils/import'); -const { createImportLimiters } = require('~/server/middleware'); +const { createImportLimiters, generateCheckAccess } = require('~/server/middleware'); const getLogStores = require('~/cache/getLogStores'); const { sleep } = require('~/server/utils'); const { logger } = require('~/config'); @@ -67,12 +67,14 @@ router.post('/gen_title', async (req, res) => { res.status(200).json({ title }); } else { res.status(404).json({ - message: 'Title not found or method not implemented for the conversation\'s endpoint', + message: "Title not found or method not implemented for the conversation's endpoint", }); } }); -router.post('/clear', async (req, res) => { +const checkDeleteConvoAccess = generateCheckAccess(PermissionTypes.DELETE_CONVO, [Permissions.USE]); + +router.post('/clear', checkDeleteConvoAccess, async (req, res) => { let filter = {}; const { conversationId, source, thread_id } = req.body.arg; if (conversationId) { From 52a27531b264649f0a6ade6b68e9dabba409aa5d Mon Sep 17 00:00:00 2001 From: Ryan Detzel Date: Mon, 23 Sep 2024 06:44:08 -0400 Subject: [PATCH 3/3] fix prettier changes --- api/server/routes/convos.js | 2 +- api/server/services/start/interface.js | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/server/routes/convos.js b/api/server/routes/convos.js index 9afcdad739b..8ca372bab7f 100644 --- a/api/server/routes/convos.js +++ b/api/server/routes/convos.js @@ -67,7 +67,7 @@ router.post('/gen_title', async (req, res) => { res.status(200).json({ title }); } else { res.status(404).json({ - message: "Title not found or method not implemented for the conversation's endpoint", + message: 'Title not found or method not implemented for the conversation\'s endpoint', }); } }); diff --git a/api/server/services/start/interface.js b/api/server/services/start/interface.js index a77d32b977e..d60dcacfa22 100644 --- a/api/server/services/start/interface.js +++ b/api/server/services/start/interface.js @@ -64,7 +64,7 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol // warn about config.modelSpecs.prioritize if true and presets are enabled, that default presets will conflict with prioritizing model specs. if (config?.modelSpecs?.prioritize && loadedInterface.presets) { logger.warn( - "Note: Prioritizing model specs can conflict with default presets if a default preset is set. It's recommended to disable presets from the interface or disable use of a default preset.", + 'Note: Prioritizing model specs can conflict with default presets if a default preset is set. It\'s recommended to disable presets from the interface or disable use of a default preset.', ); i === 0 && i++; } @@ -78,14 +78,14 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol loadedInterface.parameters) ) { logger.warn( - "Note: Enforcing model specs can conflict with the interface options: endpointsMenu, modelSelect, presets, and parameters. It's recommended to disable these options from the interface or disable enforcing model specs.", + 'Note: Enforcing model specs can conflict with the interface options: endpointsMenu, modelSelect, presets, and parameters. It\'s recommended to disable these options from the interface or disable enforcing model specs.', ); i === 0 && i++; } // warn if enforce is true and prioritize is not, that enforcing model specs without prioritizing them can lead to unexpected behavior. if (config?.modelSpecs?.enforce && !config?.modelSpecs?.prioritize) { logger.warn( - "Note: Enforcing model specs without prioritizing them can lead to unexpected behavior. It's recommended to enable prioritizing model specs if enforcing them.", + 'Note: Enforcing model specs without prioritizing them can lead to unexpected behavior. It\'s recommended to enable prioritizing model specs if enforcing them.', ); i === 0 && i++; }