LibreChat/api/server/routes/prompts.js
Danny Avila 81b32e400a
🔧 refactor: Organize Sharing/Agent Components and Improve Type Safety
refactor: organize Sharing/Agent components, improve type safety for resource types and access role ids, rename enums to PascalCase

refactor: organize Sharing/Agent components, improve type safety for resource types and access role ids

chore: move sharing related components to dedicated "Sharing" directory

chore: remove PublicSharingToggle component and update index exports

chore: move non-sidepanel agent components to `~/components/Agents`

chore: move AgentCategoryDisplay component with tests

chore: remove commented out code

refactor: change PERMISSION_BITS from const to enum for better type safety

refactor: reorganize imports in GenericGrantAccessDialog and update index exports for hooks

refactor: update type definitions to use ACCESS_ROLE_IDS for improved type safety

refactor: remove unused canAccessPromptResource middleware and related code

refactor: remove unused prompt access roles from createAccessRoleMethods

refactor: update resourceType in AclEntry type definition to remove unused 'prompt' value

refactor: introduce ResourceType enum and update resourceType usage across data provider files for improved type safety

refactor: update resourceType usage to ResourceType enum across sharing and permissions components for improved type safety

refactor: standardize resourceType usage to ResourceType enum across agent and prompt models, permissions controller, and middleware for enhanced type safety

refactor: update resourceType references from PROMPT_GROUP to PROMPTGROUP for consistency across models, middleware, and components

refactor: standardize access role IDs and resource type usage across agent, file, and prompt models for improved type safety and consistency

chore: add typedefs for TUpdateResourcePermissionsRequest and TUpdateResourcePermissionsResponse to enhance type definitions

chore: move SearchPicker to PeoplePicker dir

refactor: implement debouncing for query changes in SearchPicker for improved performance

chore: fix typing, import order for agent admin settings

fix: agent admin settings, prevent agent form submission

refactor: rename `ACCESS_ROLE_IDS` to `AccessRoleIds`

refactor: replace PermissionBits with PERMISSION_BITS

refactor: replace PERMISSION_BITS with PermissionBits
2025-08-13 16:24:20 -04:00

423 lines
11 KiB
JavaScript

const express = require('express');
const { logger } = require('@librechat/data-schemas');
const { generateCheckAccess } = require('@librechat/api');
const {
Permissions,
SystemRoles,
ResourceType,
AccessRoleIds,
PermissionTypes,
PermissionBits,
} = require('librechat-data-provider');
const {
makePromptProduction,
getAllPromptGroups,
updatePromptGroup,
deletePromptGroup,
createPromptGroup,
getPromptGroups,
getPromptGroup,
deletePrompt,
getPrompts,
savePrompt,
getPrompt,
} = require('~/models/Prompt');
const {
canAccessPromptGroupResource,
canAccessPromptViaGroup,
requireJwtAuth,
} = require('~/server/middleware');
const {
findPubliclyAccessibleResources,
getEffectivePermissions,
findAccessibleResources,
grantPermission,
} = require('~/server/services/PermissionService');
const { getRoleByName } = require('~/models/Role');
const router = express.Router();
const checkPromptAccess = generateCheckAccess({
permissionType: PermissionTypes.PROMPTS,
permissions: [Permissions.USE],
getRoleByName,
});
const checkPromptCreate = generateCheckAccess({
permissionType: PermissionTypes.PROMPTS,
permissions: [Permissions.USE, Permissions.CREATE],
getRoleByName,
});
const checkGlobalPromptShare = generateCheckAccess({
permissionType: PermissionTypes.PROMPTS,
permissions: [Permissions.USE, Permissions.CREATE],
bodyProps: {
[Permissions.SHARED_GLOBAL]: ['projectIds', 'removeProjectIds'],
},
getRoleByName,
});
router.use(requireJwtAuth);
router.use(checkPromptAccess);
/**
* Route to get single prompt group by its ID
* GET /groups/:groupId
*/
router.get(
'/groups/:groupId',
canAccessPromptGroupResource({
requiredPermission: PermissionBits.VIEW,
}),
async (req, res) => {
const { groupId } = req.params;
try {
const group = await getPromptGroup({ _id: groupId });
if (!group) {
return res.status(404).send({ message: 'Prompt group not found' });
}
res.status(200).send(group);
} catch (error) {
logger.error('Error getting prompt group', error);
res.status(500).send({ message: 'Error getting prompt group' });
}
},
);
/**
* Route to fetch all prompt groups (ACL-aware)
* GET /all
*/
router.get('/all', async (req, res) => {
try {
const userId = req.user.id;
// Get promptGroup IDs the user has VIEW access to via ACL
const accessibleIds = await findAccessibleResources({
userId,
resourceType: ResourceType.PROMPTGROUP,
requiredPermissions: PermissionBits.VIEW,
});
const groups = await getAllPromptGroups(req, {});
// Filter the results to only include accessible groups
const accessibleGroups = groups.filter((group) =>
accessibleIds.some((id) => id.toString() === group._id.toString()),
);
res.status(200).send(accessibleGroups);
} catch (error) {
logger.error(error);
res.status(500).send({ error: 'Error getting prompt groups' });
}
});
/**
* Route to fetch paginated prompt groups with filters (ACL-aware)
* GET /groups
*/
router.get('/groups', async (req, res) => {
try {
const userId = req.user.id;
const filter = { ...req.query };
delete filter.author; // Remove author filter as we'll use ACL
// Get promptGroup IDs the user has VIEW access to via ACL
const accessibleIds = await findAccessibleResources({
userId,
resourceType: ResourceType.PROMPTGROUP,
requiredPermissions: PermissionBits.VIEW,
});
// Get publicly accessible promptGroups
const publiclyAccessibleIds = await findPubliclyAccessibleResources({
resourceType: ResourceType.PROMPTGROUP,
requiredPermissions: PermissionBits.VIEW,
});
const groups = await getPromptGroups(req, filter);
if (groups.promptGroups && groups.promptGroups.length > 0) {
groups.promptGroups = groups.promptGroups.filter((group) =>
accessibleIds.some((id) => id.toString() === group._id.toString()),
);
// Mark public groups
groups.promptGroups = groups.promptGroups.map((group) => {
if (publiclyAccessibleIds.some((id) => id.equals(group._id))) {
group.isPublic = true;
}
return group;
});
}
res.status(200).send(groups);
} catch (error) {
logger.error(error);
res.status(500).send({ error: 'Error getting prompt groups' });
}
});
/**
* Creates a new prompt group with initial prompt
* @param {object} req
* @param {TCreatePrompt} req.body
* @param {Express.Response} res
*/
const createNewPromptGroup = async (req, res) => {
try {
const { prompt, group } = req.body;
if (!prompt || !group || !group.name) {
return res.status(400).send({ error: 'Prompt and group name are required' });
}
const saveData = {
prompt,
group,
author: req.user.id,
authorName: req.user.name,
};
const result = await createPromptGroup(saveData);
// Grant owner permissions to the creator on the new promptGroup
if (result.prompt && result.prompt._id && result.prompt.groupId) {
try {
await grantPermission({
principalType: 'user',
principalId: req.user.id,
resourceType: ResourceType.PROMPTGROUP,
resourceId: result.prompt.groupId,
accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER,
grantedBy: req.user.id,
});
logger.debug(
`[createPromptGroup] Granted owner permissions to user ${req.user.id} for promptGroup ${result.prompt.groupId}`,
);
} catch (permissionError) {
logger.error(
`[createPromptGroup] Failed to grant owner permissions for promptGroup ${result.prompt.groupId}:`,
permissionError,
);
}
}
res.status(200).send(result);
} catch (error) {
logger.error(error);
res.status(500).send({ error: 'Error creating prompt group' });
}
};
/**
* Adds a new prompt to an existing prompt group
* @param {object} req
* @param {TCreatePrompt} req.body
* @param {Express.Response} res
*/
const addPromptToGroup = async (req, res) => {
try {
const { groupId } = req.params;
const { prompt } = req.body;
if (!prompt) {
return res.status(400).send({ error: 'Prompt is required' });
}
// Ensure the prompt is associated with the correct group
prompt.groupId = groupId;
const saveData = {
prompt,
author: req.user.id,
authorName: req.user.name,
};
const result = await savePrompt(saveData);
res.status(200).send(result);
} catch (error) {
logger.error(error);
res.status(500).send({ error: 'Error adding prompt to group' });
}
};
// Create new prompt group (requires CREATE permission)
router.post('/', checkPromptCreate, createNewPromptGroup);
// Add prompt to existing group (requires EDIT permission on the group)
router.post(
'/groups/:groupId/prompts',
checkPromptAccess,
canAccessPromptGroupResource({
requiredPermission: PermissionBits.EDIT,
}),
addPromptToGroup,
);
/**
* Updates a prompt group
* @param {object} req
* @param {object} req.params - The request parameters
* @param {string} req.params.groupId - The group ID
* @param {TUpdatePromptGroupPayload} req.body - The request body
* @param {Express.Response} res
*/
const patchPromptGroup = async (req, res) => {
try {
const { groupId } = req.params;
const author = req.user.id;
const filter = { _id: groupId, author };
if (req.user.role === SystemRoles.ADMIN) {
delete filter.author;
}
const promptGroup = await updatePromptGroup(filter, req.body);
res.status(200).send(promptGroup);
} catch (error) {
logger.error(error);
res.status(500).send({ error: 'Error updating prompt group' });
}
};
router.patch(
'/groups/:groupId',
checkGlobalPromptShare,
canAccessPromptGroupResource({
requiredPermission: PermissionBits.EDIT,
}),
patchPromptGroup,
);
router.patch(
'/:promptId/tags/production',
checkPromptCreate,
canAccessPromptViaGroup({
requiredPermission: PermissionBits.EDIT,
resourceIdParam: 'promptId',
}),
async (req, res) => {
try {
const { promptId } = req.params;
const result = await makePromptProduction(promptId);
res.status(200).send(result);
} catch (error) {
logger.error(error);
res.status(500).send({ error: 'Error updating prompt production' });
}
},
);
router.get(
'/:promptId',
canAccessPromptViaGroup({
requiredPermission: PermissionBits.VIEW,
resourceIdParam: 'promptId',
}),
async (req, res) => {
const { promptId } = req.params;
const prompt = await getPrompt({ _id: promptId });
res.status(200).send(prompt);
},
);
router.get('/', async (req, res) => {
try {
const author = req.user.id;
const { groupId } = req.query;
// If requesting prompts for a specific group, check permissions
if (groupId) {
const permissions = await getEffectivePermissions({
userId: req.user.id,
resourceType: ResourceType.PROMPTGROUP,
resourceId: groupId,
});
if (!(permissions & PermissionBits.VIEW)) {
return res
.status(403)
.send({ error: 'Insufficient permissions to view prompts in this group' });
}
// If user has access, fetch all prompts in the group (not just their own)
const prompts = await getPrompts({ groupId });
return res.status(200).send(prompts);
}
// If no groupId, return user's own prompts
const query = { author };
if (req.user.role === SystemRoles.ADMIN) {
delete query.author;
}
const prompts = await getPrompts(query);
res.status(200).send(prompts);
} catch (error) {
logger.error(error);
res.status(500).send({ error: 'Error getting prompts' });
}
});
/**
* Deletes a prompt
*
* @param {Express.Request} req - The request object.
* @param {TDeletePromptVariables} req.params - The request parameters
* @param {import('mongoose').ObjectId} req.params.promptId - The prompt ID
* @param {Express.Response} res - The response object.
* @return {TDeletePromptResponse} A promise that resolves when the prompt is deleted.
*/
const deletePromptController = async (req, res) => {
try {
const { promptId } = req.params;
const { groupId } = req.query;
const author = req.user.id;
const query = { promptId, groupId, author, role: req.user.role };
const result = await deletePrompt(query);
res.status(200).send(result);
} catch (error) {
logger.error(error);
res.status(500).send({ error: 'Error deleting prompt' });
}
};
/**
* Delete a prompt group
* @param {ServerRequest} req
* @param {ServerResponse} res
* @returns {Promise<TDeletePromptGroupResponse>}
*/
const deletePromptGroupController = async (req, res) => {
try {
const { groupId: _id } = req.params;
// Don't pass author - permissions are now checked by middleware
const message = await deletePromptGroup({ _id, role: req.user.role });
res.send(message);
} catch (error) {
logger.error('Error deleting prompt group', error);
res.status(500).send({ message: 'Error deleting prompt group' });
}
};
router.delete(
'/:promptId',
checkPromptCreate,
canAccessPromptViaGroup({
requiredPermission: PermissionBits.DELETE,
resourceIdParam: 'promptId',
}),
deletePromptController,
);
router.delete(
'/groups/:groupId',
checkPromptCreate,
canAccessPromptGroupResource({
requiredPermission: PermissionBits.DELETE,
}),
deletePromptGroupController,
);
module.exports = router;