From 596ecc696961c9b9cbb30472cf62350db4b25de5 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Wed, 21 Aug 2024 19:57:34 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=90=20feat:=20Toggle=20Access=20to=20P?= =?UTF-8?q?rompts=20via=20`librechat.yaml`=20(#3735)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: update CONFIG_VERSION to '1.1.6' * chore: update package version to 0.7.415 * feat: toggle USER role access to prompts via librechat.yaml * refactor: set prompts to true when loadDefaultInterface returns true * ci(AppService): mock updatePromptsAccess --- api/models/Role.js | 42 ++++++++++- .../services/AppService.interface.spec.js | 73 +++++++++++++++++++ api/server/services/AppService.js | 2 +- api/server/services/AppService.spec.js | 3 +- api/server/services/start/interface.js | 15 +++- api/server/services/start/interface.spec.js | 45 ++++++++++++ package-lock.json | 2 +- packages/data-provider/package.json | 2 +- packages/data-provider/src/config.ts | 3 +- 9 files changed, 176 insertions(+), 11 deletions(-) create mode 100644 api/server/services/AppService.interface.spec.js create mode 100644 api/server/services/start/interface.spec.js diff --git a/api/models/Role.js b/api/models/Role.js index af02e5cac40..fed72ce508d 100644 --- a/api/models/Role.js +++ b/api/models/Role.js @@ -1,6 +1,14 @@ -const { SystemRoles, CacheKeys, roleDefaults } = require('librechat-data-provider'); +const { + SystemRoles, + CacheKeys, + roleDefaults, + PermissionTypes, + Permissions, + promptPermissionsSchema, +} = require('librechat-data-provider'); const getLogStores = require('~/cache/getLogStores'); const Role = require('~/models/schema/roleSchema'); +const { logger } = require('~/config'); /** * Retrieve a role by name and convert the found role document to a plain object. @@ -61,6 +69,37 @@ const updateRoleByName = async function (roleName, updates) { } }; +/** + * Updates the Prompt access for a specific role. + * @param {SystemRoles} roleName - The role to update the prompt access for. + * @param {boolean | undefined} [value] - The new value for the prompt access. + */ +async function updatePromptsAccess(roleName, value) { + if (typeof value === 'undefined') { + return; + } + + try { + const parsedUpdates = promptPermissionsSchema.partial().parse({ [Permissions.USE]: value }); + const role = await getRoleByName(roleName); + if (!role) { + return; + } + + const mergedUpdates = { + [PermissionTypes.PROMPTS]: { + ...role[PermissionTypes.PROMPTS], + ...parsedUpdates, + }, + }; + + await updateRoleByName(roleName, mergedUpdates); + logger.info(`Updated '${roleName}' role prompts 'USE' permission to: ${value}`); + } catch (error) { + logger.error('Failed to update USER role prompts USE permission:', error); + } +} + /** * Initialize default roles in the system. * Creates the default roles (ADMIN, USER) if they don't exist in the database. @@ -83,4 +122,5 @@ module.exports = { getRoleByName, initializeRoles, updateRoleByName, + updatePromptsAccess, }; diff --git a/api/server/services/AppService.interface.spec.js b/api/server/services/AppService.interface.spec.js new file mode 100644 index 00000000000..dcf2a7bf2df --- /dev/null +++ b/api/server/services/AppService.interface.spec.js @@ -0,0 +1,73 @@ +jest.mock('~/models/Role', () => ({ + initializeRoles: jest.fn(), + updatePromptsAccess: jest.fn(), + getRoleByName: jest.fn(), + updateRoleByName: jest.fn(), +})); + +jest.mock('~/config', () => ({ + logger: { + info: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + }, +})); + +jest.mock('./Config/loadCustomConfig', () => jest.fn()); +jest.mock('./start/interface', () => ({ + loadDefaultInterface: jest.fn(), +})); +jest.mock('./ToolService', () => ({ + loadAndFormatTools: jest.fn().mockReturnValue({}), +})); +jest.mock('./start/checks', () => ({ + checkVariables: jest.fn(), + checkHealth: jest.fn(), + checkConfig: jest.fn(), + checkAzureVariables: jest.fn(), +})); + +const AppService = require('./AppService'); +const { loadDefaultInterface } = require('./start/interface'); + +describe('AppService interface.prompts configuration', () => { + let app; + let mockLoadCustomConfig; + + beforeEach(() => { + app = { locals: {} }; + jest.resetModules(); + jest.clearAllMocks(); + mockLoadCustomConfig = require('./Config/loadCustomConfig'); + }); + + it('should set prompts to true when loadDefaultInterface returns true', async () => { + mockLoadCustomConfig.mockResolvedValue({}); + loadDefaultInterface.mockResolvedValue({ prompts: true }); + + await AppService(app); + + expect(app.locals.interfaceConfig.prompts).toBe(true); + expect(loadDefaultInterface).toHaveBeenCalled(); + }); + + it('should set prompts to false when loadDefaultInterface returns false', async () => { + mockLoadCustomConfig.mockResolvedValue({ interface: { prompts: false } }); + loadDefaultInterface.mockResolvedValue({ prompts: false }); + + await AppService(app); + + expect(app.locals.interfaceConfig.prompts).toBe(false); + expect(loadDefaultInterface).toHaveBeenCalled(); + }); + + it('should not set prompts when loadDefaultInterface returns undefined', async () => { + mockLoadCustomConfig.mockResolvedValue({}); + loadDefaultInterface.mockResolvedValue({}); + + await AppService(app); + + expect(app.locals.interfaceConfig.prompts).toBeUndefined(); + expect(loadDefaultInterface).toHaveBeenCalled(); + }); +}); diff --git a/api/server/services/AppService.js b/api/server/services/AppService.js index d776aa63b7e..eae83bc6e09 100644 --- a/api/server/services/AppService.js +++ b/api/server/services/AppService.js @@ -45,7 +45,7 @@ const AppService = async (app) => { const socialLogins = config?.registration?.socialLogins ?? configDefaults?.registration?.socialLogins; - const interfaceConfig = loadDefaultInterface(config, configDefaults); + const interfaceConfig = await loadDefaultInterface(config, configDefaults); const defaultLocals = { paths, diff --git a/api/server/services/AppService.spec.js b/api/server/services/AppService.spec.js index cab6d8e2a46..3a0db5f2385 100644 --- a/api/server/services/AppService.spec.js +++ b/api/server/services/AppService.spec.js @@ -23,6 +23,7 @@ jest.mock('./Files/Firebase/initialize', () => ({ })); jest.mock('~/models/Role', () => ({ initializeRoles: jest.fn(), + updatePromptsAccess: jest.fn(), })); jest.mock('./ToolService', () => ({ loadAndFormatTools: jest.fn().mockReturnValue({ @@ -97,8 +98,6 @@ describe('AppService', () => { socialLogins: ['testLogin'], fileStrategy: 'testStrategy', interfaceConfig: expect.objectContaining({ - privacyPolicy: undefined, - termsOfService: undefined, endpointsMenu: true, modelSelect: true, parameters: true, diff --git a/api/server/services/start/interface.js b/api/server/services/start/interface.js index 7cda7061044..2f627ce0e52 100644 --- a/api/server/services/start/interface.js +++ b/api/server/services/start/interface.js @@ -1,17 +1,21 @@ +const { SystemRoles, removeNullishValues } = require('librechat-data-provider'); +const { updatePromptsAccess } = require('~/models/Role'); const { logger } = require('~/config'); /** * Loads the default interface object. * @param {TCustomConfig | undefined} config - The loaded custom configuration. * @param {TConfigDefaults} configDefaults - The custom configuration default values. - * @returns {TCustomConfig['interface']} The default interface object. + * @param {SystemRoles} [roleName] - The role to load the default interface for, defaults to `'USER'`. + * @returns {Promise} The default interface object. */ -function loadDefaultInterface(config, configDefaults) { +async function loadDefaultInterface(config, configDefaults, roleName = SystemRoles.USER) { const { interface: interfaceConfig } = config ?? {}; const { interface: defaults } = configDefaults; const hasModelSpecs = config?.modelSpecs?.list?.length > 0; - const loadedInterface = { + /** @type {TCustomConfig['interface']} */ + const loadedInterface = removeNullishValues({ endpointsMenu: interfaceConfig?.endpointsMenu ?? (hasModelSpecs ? false : defaults.endpointsMenu), modelSelect: interfaceConfig?.modelSelect ?? (hasModelSpecs ? false : defaults.modelSelect), @@ -20,7 +24,10 @@ function loadDefaultInterface(config, configDefaults) { sidePanel: interfaceConfig?.sidePanel ?? defaults.sidePanel, privacyPolicy: interfaceConfig?.privacyPolicy ?? defaults.privacyPolicy, termsOfService: interfaceConfig?.termsOfService ?? defaults.termsOfService, - }; + prompts: interfaceConfig?.prompts ?? defaults.prompts, + }); + + await updatePromptsAccess(roleName, loadedInterface.prompts); let i = 0; const logSettings = () => { diff --git a/api/server/services/start/interface.spec.js b/api/server/services/start/interface.spec.js new file mode 100644 index 00000000000..3cf56799b11 --- /dev/null +++ b/api/server/services/start/interface.spec.js @@ -0,0 +1,45 @@ +const { SystemRoles } = require('librechat-data-provider'); +const { updatePromptsAccess } = require('~/models/Role'); +const { loadDefaultInterface } = require('./interface'); + +jest.mock('~/models/Role', () => ({ + updatePromptsAccess: jest.fn(), +})); + +describe('loadDefaultInterface', () => { + it('should call updatePromptsAccess with the correct parameters when prompts is true', async () => { + const config = { interface: { prompts: true } }; + const configDefaults = { interface: {} }; + + await loadDefaultInterface(config, configDefaults); + + expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, true); + }); + + it('should call updatePromptsAccess with false when prompts is false', async () => { + const config = { interface: { prompts: false } }; + const configDefaults = { interface: {} }; + + await loadDefaultInterface(config, configDefaults); + + expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, false); + }); + + it('should call updatePromptsAccess with undefined when prompts is not specified in config', async () => { + const config = {}; + const configDefaults = { interface: {} }; + + await loadDefaultInterface(config, configDefaults); + + expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, undefined); + }); + + it('should call updatePromptsAccess with undefined when prompts is explicitly undefined', async () => { + const config = { interface: { prompts: undefined } }; + const configDefaults = { interface: {} }; + + await loadDefaultInterface(config, configDefaults); + + expect(updatePromptsAccess).toHaveBeenCalledWith(SystemRoles.USER, undefined); + }); +}); diff --git a/package-lock.json b/package-lock.json index 9bdd6d7c555..832555780fa 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31493,7 +31493,7 @@ }, "packages/data-provider": { "name": "librechat-data-provider", - "version": "0.7.414", + "version": "0.7.415", "license": "ISC", "dependencies": { "@types/js-yaml": "^4.0.9", diff --git a/packages/data-provider/package.json b/packages/data-provider/package.json index d349c41af7e..03f78ecd870 100644 --- a/packages/data-provider/package.json +++ b/packages/data-provider/package.json @@ -1,6 +1,6 @@ { "name": "librechat-data-provider", - "version": "0.7.414", + "version": "0.7.415", "description": "data services for librechat apps", "main": "dist/index.js", "module": "dist/index.es.js", diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 96d6a11e8bf..7d8604ca74f 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -414,6 +414,7 @@ export const configSchema = z.object({ parameters: z.boolean().optional(), sidePanel: z.boolean().optional(), presets: z.boolean().optional(), + prompts: z.boolean().optional(), }) .default({ endpointsMenu: true, @@ -944,7 +945,7 @@ export enum Constants { /** Key for the app's version. */ VERSION = 'v0.7.4', /** Key for the Custom Config's version (librechat.yaml). */ - CONFIG_VERSION = '1.1.5', + CONFIG_VERSION = '1.1.6', /** Standard value for the first message's `parentMessageId` value, to indicate no parent exists. */ NO_PARENT = '00000000-0000-0000-0000-000000000000', /** Standard value for the initial conversationId before a request is sent */