mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
🔐 feat: Toggle Access to Prompts via librechat.yaml (#3735)
* 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
This commit is contained in:
parent
0c5568b80b
commit
596ecc6969
9 changed files with 176 additions and 11 deletions
|
|
@ -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,
|
||||
};
|
||||
|
|
|
|||
73
api/server/services/AppService.interface.spec.js
Normal file
73
api/server/services/AppService.interface.spec.js
Normal file
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<TCustomConfig['interface']>} 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 = () => {
|
||||
|
|
|
|||
45
api/server/services/start/interface.spec.js
Normal file
45
api/server/services/start/interface.spec.js
Normal file
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
2
package-lock.json
generated
2
package-lock.json
generated
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 */
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue