diff --git a/api/models/Agent.js b/api/models/Agent.js index 508740b3d..004a51cfe 100644 --- a/api/models/Agent.js +++ b/api/models/Agent.js @@ -1,23 +1,19 @@ const mongoose = require('mongoose'); const crypto = require('node:crypto'); const { logger } = require('@librechat/data-schemas'); -const { SystemRoles, Tools, actionDelimiter } = require('librechat-data-provider'); +const { ResourceType, SystemRoles, Tools, actionDelimiter } = require('librechat-data-provider'); const { GLOBAL_PROJECT_NAME, EPHEMERAL_AGENT_ID, mcp_delimiter } = require('librechat-data-provider').Constants; const { - getProjectByName, - addAgentIdsToProject, - removeAgentIdsFromProject, removeAgentFromAllProjects, + removeAgentIdsFromProject, + addAgentIdsToProject, + getProjectByName, } = require('./Project'); -const { getCachedTools } = require('~/server/services/Config'); const { removeAllPermissions } = require('~/server/services/PermissionService'); -const { Agent } = require('~/db/models'); - -/** - * Category values are now imported from shared constants - */ +const { getCachedTools } = require('~/server/services/Config'); const { getActions } = require('./Action'); +const { Agent } = require('~/db/models'); /** * Create an agent with the provided data. @@ -511,7 +507,7 @@ const deleteAgent = async (searchParameter) => { if (agent) { await removeAgentFromAllProjects(agent.id); await removeAllPermissions({ - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, }); } diff --git a/api/models/Agent.spec.js b/api/models/Agent.spec.js index 449d8103c..0c67d9238 100644 --- a/api/models/Agent.spec.js +++ b/api/models/Agent.spec.js @@ -14,6 +14,7 @@ const mongoose = require('mongoose'); const { v4: uuidv4 } = require('uuid'); const { agentSchema } = require('@librechat/data-schemas'); const { MongoMemoryServer } = require('mongodb-memory-server'); +const { AccessRoleIds, ResourceType } = require('librechat-data-provider'); const { getAgent, loadAgent, @@ -21,14 +22,14 @@ const { updateAgent, deleteAgent, getListAgents, + revertAgentVersion, updateAgentProjects, addAgentResourceFile, removeAgentResourceFiles, generateActionMetadataHash, - revertAgentVersion, } = require('./Agent'); -const { getCachedTools } = require('~/server/services/Config'); const permissionService = require('~/server/services/PermissionService'); +const { getCachedTools } = require('~/server/services/Config'); const { AclEntry } = require('~/db/models'); /** @@ -423,10 +424,10 @@ describe('models/Agent', () => { // Create necessary access roles for agents await AccessRole.create({ - accessRoleId: 'agent_owner', + accessRoleId: AccessRoleIds.AGENT_OWNER, name: 'Owner', description: 'Full control over agents', - resourceType: 'agent', + resourceType: ResourceType.AGENT, permBits: 15, // VIEW | EDIT | DELETE | SHARE }); }, 20000); @@ -501,15 +502,15 @@ describe('models/Agent', () => { await permissionService.grantPermission({ principalType: 'user', principalId: authorId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, - accessRoleId: 'agent_owner', + accessRoleId: AccessRoleIds.AGENT_OWNER, grantedBy: authorId, }); // Verify ACL entry exists const aclEntriesBefore = await AclEntry.find({ - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, }); expect(aclEntriesBefore).toHaveLength(1); @@ -523,7 +524,7 @@ describe('models/Agent', () => { // Verify ACL entries are removed const aclEntriesAfter = await AclEntry.find({ - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, }); expect(aclEntriesAfter).toHaveLength(0); diff --git a/api/models/File.spec.js b/api/models/File.spec.js index 99464fdbd..30de61bab 100644 --- a/api/models/File.spec.js +++ b/api/models/File.spec.js @@ -1,11 +1,12 @@ const mongoose = require('mongoose'); const { v4: uuidv4 } = require('uuid'); -const { MongoMemoryServer } = require('mongodb-memory-server'); const { createModels } = require('@librechat/data-schemas'); -const { getFiles, createFile } = require('./File'); -const { createAgent } = require('./Agent'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { AccessRoleIds, ResourceType } = require('librechat-data-provider'); const { grantPermission } = require('~/server/services/PermissionService'); +const { getFiles, createFile } = require('./File'); const { seedDefaultRoles } = require('~/models'); +const { createAgent } = require('./Agent'); let File; let Agent; @@ -116,9 +117,9 @@ describe('File Access Control', () => { await grantPermission({ principalType: 'user', principalId: userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, - accessRoleId: 'agent_editor', + accessRoleId: AccessRoleIds.AGENT_EDITOR, grantedBy: authorId, }); @@ -233,9 +234,9 @@ describe('File Access Control', () => { await grantPermission({ principalType: 'user', principalId: userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, - accessRoleId: 'agent_viewer', + accessRoleId: AccessRoleIds.AGENT_VIEWER, grantedBy: authorId, }); @@ -291,9 +292,9 @@ describe('File Access Control', () => { await grantPermission({ principalType: 'user', principalId: userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, - accessRoleId: 'agent_editor', + accessRoleId: AccessRoleIds.AGENT_EDITOR, grantedBy: authorId, }); diff --git a/api/models/Prompt.js b/api/models/Prompt.js index 7394968ba..eadd60163 100644 --- a/api/models/Prompt.js +++ b/api/models/Prompt.js @@ -1,11 +1,16 @@ const { ObjectId } = require('mongodb'); const { logger } = require('@librechat/data-schemas'); -const { SystemRoles, SystemCategories, Constants } = require('librechat-data-provider'); const { - getProjectByName, - addGroupIdsToProject, - removeGroupIdsFromProject, + Constants, + SystemRoles, + ResourceType, + SystemCategories, +} = require('librechat-data-provider'); +const { removeGroupFromAllProjects, + removeGroupIdsFromProject, + addGroupIdsToProject, + getProjectByName, } = require('./Project'); const { removeAllPermissions } = require('~/server/services/PermissionService'); const { PromptGroup, Prompt } = require('~/db/models'); @@ -234,7 +239,7 @@ const deletePromptGroup = async ({ _id, author, role }) => { await removeGroupFromAllProjects(_id); try { - await removeAllPermissions({ resourceType: 'promptGroup', resourceId: _id }); + await removeAllPermissions({ resourceType: ResourceType.PROMPTGROUP, resourceId: _id }); } catch (error) { logger.error('Error removing promptGroup permissions:', error); } @@ -428,16 +433,6 @@ module.exports = { throw new Error('Failed to delete the prompt'); } - // Remove all ACL entries for this prompt - try { - await removeAllPermissions({ - resourceType: 'prompt', - resourceId: promptId, - }); - } catch (error) { - logger.error('Error removing prompt permissions:', error); - } - const remainingPrompts = await Prompt.find({ groupId }) .select('_id') .sort({ createdAt: 1 }) @@ -447,7 +442,7 @@ module.exports = { // Remove all ACL entries for the promptGroup when deleting the last prompt try { await removeAllPermissions({ - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: groupId, }); } catch (error) { diff --git a/api/models/Prompt.spec.js b/api/models/Prompt.spec.js index df45d27cd..d7e8da575 100644 --- a/api/models/Prompt.spec.js +++ b/api/models/Prompt.spec.js @@ -1,8 +1,13 @@ -const { ObjectId } = require('mongodb'); -const { MongoMemoryServer } = require('mongodb-memory-server'); const mongoose = require('mongoose'); -const { SystemRoles } = require('librechat-data-provider'); -const { logger, PermissionBits } = require('@librechat/data-schemas'); +const { ObjectId } = require('mongodb'); +const { logger } = require('@librechat/data-schemas'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { + SystemRoles, + ResourceType, + AccessRoleIds, + PermissionBits, +} = require('librechat-data-provider'); // Mock the config/connect module to prevent connection attempts during tests jest.mock('../../config/connect', () => jest.fn().mockResolvedValue(true)); @@ -49,24 +54,24 @@ async function setupTestData() { // Create access roles for promptGroups testRoles = { viewer: await AccessRole.create({ - accessRoleId: 'promptGroup_viewer', + accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, name: 'Viewer', description: 'Can view promptGroups', - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, permBits: PermissionBits.VIEW, }), editor: await AccessRole.create({ - accessRoleId: 'promptGroup_editor', + accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR, name: 'Editor', description: 'Can view and edit promptGroups', - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, permBits: PermissionBits.VIEW | PermissionBits.EDIT, }), owner: await AccessRole.create({ - accessRoleId: 'promptGroup_owner', + accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, name: 'Owner', description: 'Full control over promptGroups', - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, permBits: PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE | PermissionBits.SHARE, }), @@ -148,15 +153,15 @@ describe('Prompt ACL Permissions', () => { await permissionService.grantPermission({ principalType: 'user', principalId: testUsers.owner._id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: testGroup._id, - accessRoleId: 'promptGroup_owner', + accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, grantedBy: testUsers.owner._id, }); // Check ACL entry const aclEntry = await AclEntry.findOne({ - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: testGroup._id, principalType: 'user', principalId: testUsers.owner._id, @@ -192,9 +197,9 @@ describe('Prompt ACL Permissions', () => { await permissionService.grantPermission({ principalType: 'user', principalId: testUsers.owner._id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: testPromptGroup._id, - accessRoleId: 'promptGroup_owner', + accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, grantedBy: testUsers.owner._id, }); }); @@ -208,7 +213,7 @@ describe('Prompt ACL Permissions', () => { it('owner should have full access to their prompt', async () => { const hasAccess = await permissionService.checkPermission({ userId: testUsers.owner._id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: testPromptGroup._id, requiredPermission: PermissionBits.VIEW, }); @@ -217,7 +222,7 @@ describe('Prompt ACL Permissions', () => { const canEdit = await permissionService.checkPermission({ userId: testUsers.owner._id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: testPromptGroup._id, requiredPermission: PermissionBits.EDIT, }); @@ -230,22 +235,22 @@ describe('Prompt ACL Permissions', () => { await permissionService.grantPermission({ principalType: 'user', principalId: testUsers.viewer._id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: testPromptGroup._id, - accessRoleId: 'promptGroup_viewer', + accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, grantedBy: testUsers.owner._id, }); const canView = await permissionService.checkPermission({ userId: testUsers.viewer._id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: testPromptGroup._id, requiredPermission: PermissionBits.VIEW, }); const canEdit = await permissionService.checkPermission({ userId: testUsers.viewer._id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: testPromptGroup._id, requiredPermission: PermissionBits.EDIT, }); @@ -257,7 +262,7 @@ describe('Prompt ACL Permissions', () => { it('user without permissions should have no access', async () => { const hasAccess = await permissionService.checkPermission({ userId: testUsers.noAccess._id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: testPromptGroup._id, requiredPermission: PermissionBits.VIEW, }); @@ -270,7 +275,7 @@ describe('Prompt ACL Permissions', () => { // The middleware layer handles admin bypass, not the permission service const hasAccess = await permissionService.checkPermission({ userId: testUsers.admin._id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: testPromptGroup._id, requiredPermission: PermissionBits.VIEW, }); @@ -278,7 +283,7 @@ describe('Prompt ACL Permissions', () => { // Without explicit permissions, even admin won't have access at this layer expect(hasAccess).toBe(false); - // The actual admin bypass happens in the middleware layer (canAccessPromptResource) + // The actual admin bypass happens in the middleware layer (`canAccessPromptViaGroup`/`canAccessPromptGroupResource`) // which checks req.user.role === SystemRoles.ADMIN }); }); @@ -352,16 +357,16 @@ describe('Prompt ACL Permissions', () => { await permissionService.grantPermission({ principalType: 'group', principalId: testGroups.editors._id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: testPromptGroup._id, - accessRoleId: 'promptGroup_editor', + accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR, grantedBy: testUsers.owner._id, }); // Check if group member has access const hasAccess = await permissionService.checkPermission({ userId: testUsers.editor._id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: testPromptGroup._id, requiredPermission: PermissionBits.EDIT, }); @@ -371,7 +376,7 @@ describe('Prompt ACL Permissions', () => { // Check that non-member doesn't have access const nonMemberAccess = await permissionService.checkPermission({ userId: testUsers.viewer._id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: testPromptGroup._id, requiredPermission: PermissionBits.EDIT, }); @@ -420,9 +425,9 @@ describe('Prompt ACL Permissions', () => { await permissionService.grantPermission({ principalType: 'public', principalId: null, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: publicPromptGroup._id, - accessRoleId: 'promptGroup_viewer', + accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, grantedBy: testUsers.owner._id, }); @@ -430,9 +435,9 @@ describe('Prompt ACL Permissions', () => { await permissionService.grantPermission({ principalType: 'user', principalId: testUsers.owner._id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: privatePromptGroup._id, - accessRoleId: 'promptGroup_owner', + accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, grantedBy: testUsers.owner._id, }); }); @@ -446,7 +451,7 @@ describe('Prompt ACL Permissions', () => { it('public prompt should be accessible to any user', async () => { const hasAccess = await permissionService.checkPermission({ userId: testUsers.noAccess._id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: publicPromptGroup._id, requiredPermission: PermissionBits.VIEW, includePublic: true, @@ -458,7 +463,7 @@ describe('Prompt ACL Permissions', () => { it('private prompt should not be accessible to unauthorized users', async () => { const hasAccess = await permissionService.checkPermission({ userId: testUsers.noAccess._id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: privatePromptGroup._id, requiredPermission: PermissionBits.VIEW, includePublic: true, @@ -501,15 +506,15 @@ describe('Prompt ACL Permissions', () => { await permissionService.grantPermission({ principalType: 'user', principalId: testUsers.owner._id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: testPromptGroup._id, - accessRoleId: 'promptGroup_owner', + accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, grantedBy: testUsers.owner._id, }); // Verify ACL entry exists const beforeDelete = await AclEntry.find({ - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: testPromptGroup._id, }); expect(beforeDelete).toHaveLength(1); @@ -524,7 +529,7 @@ describe('Prompt ACL Permissions', () => { // Verify ACL entries are removed const aclEntries = await AclEntry.find({ - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: testPromptGroup._id, }); diff --git a/api/models/PromptGroupMigration.spec.js b/api/models/PromptGroupMigration.spec.js index 1f95f299f..3cb3d62e1 100644 --- a/api/models/PromptGroupMigration.spec.js +++ b/api/models/PromptGroupMigration.spec.js @@ -1,8 +1,13 @@ -const { ObjectId } = require('mongodb'); -const { MongoMemoryServer } = require('mongodb-memory-server'); const mongoose = require('mongoose'); -const { logger, PermissionBits } = require('@librechat/data-schemas'); -const { Constants } = require('librechat-data-provider'); +const { ObjectId } = require('mongodb'); +const { logger } = require('@librechat/data-schemas'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { + Constants, + ResourceType, + AccessRoleIds, + PermissionBits, +} = require('librechat-data-provider'); // Mock the config/connect module to prevent connection attempts during tests jest.mock('../../config/connect', () => jest.fn().mockResolvedValue(true)); @@ -49,27 +54,27 @@ describe('PromptGroup Migration Script', () => { // Create promptGroup access roles ownerRole = await AccessRole.create({ - accessRoleId: 'promptGroup_owner', + accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, name: 'Owner', description: 'Full control over promptGroups', - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, permBits: PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE | PermissionBits.SHARE, }); viewerRole = await AccessRole.create({ - accessRoleId: 'promptGroup_viewer', + accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, name: 'Viewer', description: 'Can view promptGroups', - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, permBits: PermissionBits.VIEW, }); await AccessRole.create({ - accessRoleId: 'promptGroup_editor', + accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR, name: 'Editor', description: 'Can view and edit promptGroups', - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, permBits: PermissionBits.VIEW | PermissionBits.EDIT, }); @@ -103,7 +108,7 @@ describe('PromptGroup Migration Script', () => { }); // Create private prompt group (not in any project) - const privatePromptGroup = await PromptGroup.create({ + await PromptGroup.create({ name: 'Private Group', author: testOwner._id, authorName: testOwner.name, @@ -151,7 +156,7 @@ describe('PromptGroup Migration Script', () => { // Check global promptGroup permissions const globalOwnerEntry = await AclEntry.findOne({ - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: globalPromptGroup._id, principalType: 'user', principalId: testOwner._id, @@ -160,7 +165,7 @@ describe('PromptGroup Migration Script', () => { expect(globalOwnerEntry.permBits).toBe(ownerRole.permBits); const globalPublicEntry = await AclEntry.findOne({ - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: globalPromptGroup._id, principalType: 'public', }); @@ -169,7 +174,7 @@ describe('PromptGroup Migration Script', () => { // Check private promptGroup permissions const privateOwnerEntry = await AclEntry.findOne({ - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: privatePromptGroup._id, principalType: 'user', principalId: testOwner._id, @@ -178,7 +183,7 @@ describe('PromptGroup Migration Script', () => { expect(privateOwnerEntry.permBits).toBe(ownerRole.permBits); const privatePublicEntry = await AclEntry.findOne({ - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: privatePromptGroup._id, principalType: 'public', }); @@ -206,7 +211,7 @@ describe('PromptGroup Migration Script', () => { principalType: 'user', principalId: testOwner._id, principalModel: 'User', - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: promptGroup1._id, permBits: ownerRole.permBits, roleId: ownerRole._id, @@ -222,7 +227,7 @@ describe('PromptGroup Migration Script', () => { // Verify promptGroup2 now has permissions const group2Entry = await AclEntry.findOne({ - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: promptGroup2._id, }); expect(group2Entry).toBeTruthy(); @@ -259,7 +264,7 @@ describe('PromptGroup Migration Script', () => { // Verify the promptGroup has permissions const groupEntry = await AclEntry.findOne({ - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: promptGroup._id, }); expect(groupEntry).toBeTruthy(); diff --git a/api/server/controllers/PermissionsController.js b/api/server/controllers/PermissionsController.js index 97d01284f..1dfadeecd 100644 --- a/api/server/controllers/PermissionsController.js +++ b/api/server/controllers/PermissionsController.js @@ -4,12 +4,13 @@ const mongoose = require('mongoose'); const { logger } = require('@librechat/data-schemas'); +const { ResourceType } = require('librechat-data-provider'); const { - getAvailableRoles, - ensurePrincipalExists, - getEffectivePermissions, - ensureGroupPrincipalExists, bulkUpdateResourcePermissions, + ensureGroupPrincipalExists, + getEffectivePermissions, + ensurePrincipalExists, + getAvailableRoles, } = require('~/server/services/PermissionService'); const { AclEntry } = require('~/db/models'); const { @@ -18,8 +19,8 @@ const { calculateRelevanceScore, } = require('~/models'); const { - searchEntraIdPrincipals, entraIdPrincipalFeatureEnabled, + searchEntraIdPrincipals, } = require('~/server/services/GraphApiService'); /** @@ -27,6 +28,18 @@ const { * Delegates validation and logic to PermissionService */ +/** + * Validates that the resourceType is one of the supported enum values + * @param {string} resourceType - The resource type to validate + * @throws {Error} If resourceType is not valid + */ +const validateResourceType = (resourceType) => { + const validTypes = Object.values(ResourceType); + if (!validTypes.includes(resourceType)) { + throw new Error(`Invalid resourceType: ${resourceType}. Valid types: ${validTypes.join(', ')}`); + } +}; + /** * Bulk update permissions for a resource (grant, update, remove) * @route PUT /api/{resourceType}/{resourceId}/permissions @@ -41,6 +54,8 @@ const { const updateResourcePermissions = async (req, res) => { try { const { resourceType, resourceId } = req.params; + validateResourceType(resourceType); + /** @type {TUpdateResourcePermissionsRequest} */ const { updated, removed, public: isPublic, publicAccessRoleId } = req.body; const { id: userId } = req.user; @@ -163,6 +178,7 @@ const updateResourcePermissions = async (req, res) => { const getResourcePermissions = async (req, res) => { try { const { resourceType, resourceId } = req.params; + validateResourceType(resourceType); // Use aggregation pipeline for efficient single-query data retrieval const results = await AclEntry.aggregate([ @@ -278,6 +294,7 @@ const getResourcePermissions = async (req, res) => { const getResourceRoles = async (req, res) => { try { const { resourceType } = req.params; + validateResourceType(resourceType); const roles = await getAvailableRoles({ resourceType }); @@ -305,6 +322,8 @@ const getResourceRoles = async (req, res) => { const getUserEffectivePermissions = async (req, res) => { try { const { resourceType, resourceId } = req.params; + validateResourceType(resourceType); + const { id: userId } = req.user; const permissionBits = await getEffectivePermissions({ diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index ce5b36589..acac6298c 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -1,30 +1,33 @@ const { z } = require('zod'); const fs = require('fs').promises; const { nanoid } = require('nanoid'); -const { logger, PermissionBits } = require('@librechat/data-schemas'); +const { logger } = require('@librechat/data-schemas'); const { agentCreateSchema, agentUpdateSchema } = require('@librechat/api'); const { Tools, SystemRoles, FileSources, + ResourceType, + AccessRoleIds, EToolResources, actionDelimiter, + PermissionBits, removeNullishValues, } = require('librechat-data-provider'); const { - getAgent, - createAgent, - updateAgent, - deleteAgent, getListAgentsByAccess, countPromotedAgents, revertAgentVersion, + createAgent, + updateAgent, + deleteAgent, + getAgent, } = require('~/models/Agent'); const { - grantPermission, - findAccessibleResources, findPubliclyAccessibleResources, + findAccessibleResources, hasPublicPermission, + grantPermission, } = require('~/server/services/PermissionService'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { resizeAvatar } = require('~/server/services/Files/images/avatar'); @@ -79,9 +82,9 @@ const createAgentHandler = async (req, res) => { await grantPermission({ principalType: 'user', principalId: userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, - accessRoleId: 'agent_owner', + accessRoleId: AccessRoleIds.AGENT_OWNER, grantedBy: userId, }); logger.debug( @@ -146,7 +149,7 @@ const getAgentHandler = async (req, res, expandProperties = false) => { // Check if agent is public const isPublic = await hasPublicPermission({ - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, requiredPermissions: PermissionBits.VIEW, }); @@ -345,9 +348,9 @@ const duplicateAgentHandler = async (req, res) => { await grantPermission({ principalType: 'user', principalId: userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: newAgent._id, - accessRoleId: 'agent_owner', + accessRoleId: AccessRoleIds.AGENT_OWNER, grantedBy: userId, }); logger.debug( @@ -440,11 +443,11 @@ const getListAgentsHandler = async (req, res) => { // Get agent IDs the user has VIEW access to via ACL const accessibleIds = await findAccessibleResources({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, requiredPermissions: requiredPermission, }); const publiclyAccessibleIds = await findPubliclyAccessibleResources({ - resourceType: 'agent', + resourceType: ResourceType.AGENT, requiredPermissions: PermissionBits.VIEW, }); // Use the new ACL-aware function diff --git a/api/server/middleware/accessResources/canAccessAgentFromBody.js b/api/server/middleware/accessResources/canAccessAgentFromBody.js index 9c2d24ca8..e2b20d488 100644 --- a/api/server/middleware/accessResources/canAccessAgentFromBody.js +++ b/api/server/middleware/accessResources/canAccessAgentFromBody.js @@ -1,5 +1,5 @@ const { logger } = require('@librechat/data-schemas'); -const { Constants, isAgentsEndpoint } = require('librechat-data-provider'); +const { Constants, isAgentsEndpoint, ResourceType } = require('librechat-data-provider'); const { canAccessResource } = require('./canAccessResource'); const { getAgent } = require('~/models/Agent'); @@ -67,7 +67,7 @@ const canAccessAgentFromBody = (options) => { } const agentAccessMiddleware = canAccessResource({ - resourceType: 'agent', + resourceType: ResourceType.AGENT, requiredPermission, resourceIdParam: 'agent_id', // This will be ignored since we use custom resolver idResolver: () => resolveAgentIdFromBody(agentId), diff --git a/api/server/middleware/accessResources/canAccessAgentResource.js b/api/server/middleware/accessResources/canAccessAgentResource.js index 4bb6af5a7..62d9f248c 100644 --- a/api/server/middleware/accessResources/canAccessAgentResource.js +++ b/api/server/middleware/accessResources/canAccessAgentResource.js @@ -1,5 +1,6 @@ -const { getAgent } = require('~/models/Agent'); +const { ResourceType } = require('librechat-data-provider'); const { canAccessResource } = require('./canAccessResource'); +const { getAgent } = require('~/models/Agent'); /** * Agent ID resolver function @@ -46,7 +47,7 @@ const canAccessAgentResource = (options) => { } return canAccessResource({ - resourceType: 'agent', + resourceType: ResourceType.AGENT, requiredPermission, resourceIdParam, idResolver: resolveAgentId, diff --git a/api/server/middleware/accessResources/canAccessAgentResource.spec.js b/api/server/middleware/accessResources/canAccessAgentResource.spec.js index 4e4a0b7de..4656f9735 100644 --- a/api/server/middleware/accessResources/canAccessAgentResource.spec.js +++ b/api/server/middleware/accessResources/canAccessAgentResource.spec.js @@ -1,4 +1,5 @@ const mongoose = require('mongoose'); +const { ResourceType } = require('librechat-data-provider'); const { MongoMemoryServer } = require('mongodb-memory-server'); const { canAccessAgentResource } = require('./canAccessAgentResource'); const { User, Role, AclEntry } = require('~/db/models'); @@ -99,7 +100,7 @@ describe('canAccessAgentResource middleware', () => { principalType: 'user', principalId: testUser._id, principalModel: 'User', - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, permBits: 15, // All permissions (1+2+4+8) grantedBy: testUser._id, @@ -136,7 +137,7 @@ describe('canAccessAgentResource middleware', () => { principalType: 'user', principalId: otherUser._id, principalModel: 'User', - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, permBits: 15, // All permissions grantedBy: otherUser._id, @@ -177,7 +178,7 @@ describe('canAccessAgentResource middleware', () => { principalType: 'user', principalId: testUser._id, principalModel: 'User', - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, permBits: 1, // VIEW permission grantedBy: otherUser._id, @@ -214,7 +215,7 @@ describe('canAccessAgentResource middleware', () => { principalType: 'user', principalId: testUser._id, principalModel: 'User', - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, permBits: 1, // VIEW permission only grantedBy: otherUser._id, @@ -261,7 +262,7 @@ describe('canAccessAgentResource middleware', () => { principalType: 'user', principalId: testUser._id, principalModel: 'User', - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, permBits: 15, // All permissions grantedBy: testUser._id, @@ -297,7 +298,7 @@ describe('canAccessAgentResource middleware', () => { principalType: 'user', principalId: testUser._id, principalModel: 'User', - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, permBits: 15, // All permissions (1+2+4+8) grantedBy: testUser._id, @@ -357,7 +358,7 @@ describe('canAccessAgentResource middleware', () => { principalType: 'user', principalId: testUser._id, principalModel: 'User', - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, permBits: 15, // All permissions grantedBy: testUser._id, diff --git a/api/server/middleware/accessResources/canAccessPromptGroupResource.js b/api/server/middleware/accessResources/canAccessPromptGroupResource.js index b18d49fbf..90aa28077 100644 --- a/api/server/middleware/accessResources/canAccessPromptGroupResource.js +++ b/api/server/middleware/accessResources/canAccessPromptGroupResource.js @@ -1,5 +1,6 @@ -const { getPromptGroup } = require('~/models/Prompt'); +const { ResourceType } = require('librechat-data-provider'); const { canAccessResource } = require('./canAccessResource'); +const { getPromptGroup } = require('~/models/Prompt'); /** * PromptGroup ID resolver function @@ -48,7 +49,7 @@ const canAccessPromptGroupResource = (options) => { } return canAccessResource({ - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, requiredPermission, resourceIdParam, idResolver: resolvePromptGroupId, diff --git a/api/server/middleware/accessResources/canAccessPromptResource.js b/api/server/middleware/accessResources/canAccessPromptResource.js deleted file mode 100644 index 730abcb7d..000000000 --- a/api/server/middleware/accessResources/canAccessPromptResource.js +++ /dev/null @@ -1,58 +0,0 @@ -const { getPrompt } = require('~/models/Prompt'); -const { canAccessResource } = require('./canAccessResource'); - -/** - * Prompt ID resolver function - * Resolves prompt ID to MongoDB ObjectId - * - * @param {string} promptId - Prompt ID from route parameter - * @returns {Promise} Prompt document with _id field, or null if not found - */ -const resolvePromptId = async (promptId) => { - return await getPrompt({ _id: promptId }); -}; - -/** - * Prompt-specific middleware factory that creates middleware to check prompt access permissions. - * This middleware extends the generic canAccessResource to handle prompt ID resolution. - * - * @param {Object} options - Configuration options - * @param {number} options.requiredPermission - The permission bit required (1=view, 2=edit, 4=delete, 8=share) - * @param {string} [options.resourceIdParam='promptId'] - The name of the route parameter containing the prompt ID - * @returns {Function} Express middleware function - * - * @example - * // Basic usage for viewing prompts - * router.get('/prompts/:promptId', - * canAccessPromptResource({ requiredPermission: 1 }), - * getPrompt - * ); - * - * @example - * // Custom resource ID parameter and edit permission - * router.patch('/prompts/:id', - * canAccessPromptResource({ - * requiredPermission: 2, - * resourceIdParam: 'id' - * }), - * updatePrompt - * ); - */ -const canAccessPromptResource = (options) => { - const { requiredPermission, resourceIdParam = 'promptId' } = options; - - if (!requiredPermission || typeof requiredPermission !== 'number') { - throw new Error('canAccessPromptResource: requiredPermission is required and must be a number'); - } - - return canAccessResource({ - resourceType: 'prompt', - requiredPermission, - resourceIdParam, - idResolver: resolvePromptId, - }); -}; - -module.exports = { - canAccessPromptResource, -}; diff --git a/api/server/middleware/accessResources/canAccessPromptViaGroup.js b/api/server/middleware/accessResources/canAccessPromptViaGroup.js index 9be82dc9e..0bb0a804a 100644 --- a/api/server/middleware/accessResources/canAccessPromptViaGroup.js +++ b/api/server/middleware/accessResources/canAccessPromptViaGroup.js @@ -1,5 +1,6 @@ -const { getPrompt } = require('~/models/Prompt'); +const { ResourceType } = require('librechat-data-provider'); const { canAccessResource } = require('./canAccessResource'); +const { getPrompt } = require('~/models/Prompt'); /** * Prompt to PromptGroup ID resolver function @@ -42,7 +43,7 @@ const canAccessPromptViaGroup = (options) => { } return canAccessResource({ - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, requiredPermission, resourceIdParam, idResolver: resolvePromptToGroupId, diff --git a/api/server/middleware/accessResources/fileAccess.js b/api/server/middleware/accessResources/fileAccess.js index 71b0a7838..d2d084f3a 100644 --- a/api/server/middleware/accessResources/fileAccess.js +++ b/api/server/middleware/accessResources/fileAccess.js @@ -1,8 +1,8 @@ const { logger } = require('@librechat/data-schemas'); -const { PERMISSION_BITS, hasPermissions } = require('librechat-data-provider'); +const { PermissionBits, hasPermissions, ResourceType } = require('librechat-data-provider'); const { getEffectivePermissions } = require('~/server/services/PermissionService'); -const { getFiles } = require('~/models/File'); const { getAgent } = require('~/models/Agent'); +const { getFiles } = require('~/models/File'); /** * Checks if user has access to a file through agent permissions @@ -35,11 +35,11 @@ const checkAgentBasedFileAccess = async (userId, fileId) => { try { const permissions = await getEffectivePermissions({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id || agent.id, }); - if (hasPermissions(permissions, PERMISSION_BITS.VIEW)) { + if (hasPermissions(permissions, PermissionBits.VIEW)) { logger.debug(`[fileAccess] User ${userId} has VIEW permissions on agent ${agent.id}`); return true; } diff --git a/api/server/middleware/accessResources/index.js b/api/server/middleware/accessResources/index.js index 650feb575..e1c5def82 100644 --- a/api/server/middleware/accessResources/index.js +++ b/api/server/middleware/accessResources/index.js @@ -1,7 +1,6 @@ const { canAccessResource } = require('./canAccessResource'); const { canAccessAgentResource } = require('./canAccessAgentResource'); const { canAccessAgentFromBody } = require('./canAccessAgentFromBody'); -const { canAccessPromptResource } = require('./canAccessPromptResource'); const { canAccessPromptViaGroup } = require('./canAccessPromptViaGroup'); const { canAccessPromptGroupResource } = require('./canAccessPromptGroupResource'); @@ -9,7 +8,6 @@ module.exports = { canAccessResource, canAccessAgentResource, canAccessAgentFromBody, - canAccessPromptResource, canAccessPromptViaGroup, canAccessPromptGroupResource, }; diff --git a/api/server/routes/accessPermissions.js b/api/server/routes/accessPermissions.js index 32979e36c..532f3bc50 100644 --- a/api/server/routes/accessPermissions.js +++ b/api/server/routes/accessPermissions.js @@ -1,5 +1,5 @@ const express = require('express'); -const { PermissionBits } = require('@librechat/data-schemas'); +const { ResourceType, PermissionBits } = require('librechat-data-provider'); const { getUserEffectivePermissions, updateResourcePermissions, @@ -49,19 +49,17 @@ router.put( // Use middleware that dynamically handles resource type and permissions (req, res, next) => { const { resourceType } = req.params; - - // Define resource-specific middleware based on resourceType let middleware; - if (resourceType === 'agent') { + if (resourceType === ResourceType.AGENT) { middleware = canAccessResource({ - resourceType: 'agent', + resourceType: ResourceType.AGENT, requiredPermission: PermissionBits.SHARE, resourceIdParam: 'resourceId', }); - } else if (resourceType === 'promptGroup') { + } else if (resourceType === ResourceType.PROMPTGROUP) { middleware = canAccessResource({ - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, requiredPermission: PermissionBits.SHARE, resourceIdParam: 'resourceId', }); diff --git a/api/server/routes/agents/actions.js b/api/server/routes/agents/actions.js index 1f170b1d6..8b7bf74e5 100644 --- a/api/server/routes/agents/actions.js +++ b/api/server/routes/agents/actions.js @@ -1,21 +1,22 @@ const express = require('express'); const { nanoid } = require('nanoid'); +const { logger } = require('@librechat/data-schemas'); const { generateCheckAccess } = require('@librechat/api'); -const { logger, PermissionBits } = require('@librechat/data-schemas'); const { Permissions, + ResourceType, PermissionTypes, actionDelimiter, + PermissionBits, removeNullishValues, } = require('librechat-data-provider'); const { encryptMetadata, domainParser } = require('~/server/services/ActionService'); const { findAccessibleResources } = require('~/server/services/PermissionService'); +const { getAgent, updateAgent, getListAgentsByAccess } = require('~/models/Agent'); const { updateAction, getActions, deleteAction } = require('~/models/Action'); const { isActionDomainAllowed } = require('~/server/services/domains'); const { canAccessAgentResource } = require('~/server/middleware'); -const { getAgent, updateAgent } = require('~/models/Agent'); const { getRoleByName } = require('~/models/Role'); -const { getListAgentsByAccess } = require('~/models/Agent'); const router = express.Router(); @@ -36,7 +37,7 @@ router.get('/', async (req, res) => { const userId = req.user.id; const editableAgentObjectIds = await findAccessibleResources({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, requiredPermissions: PermissionBits.EDIT, }); diff --git a/api/server/routes/agents/chat.js b/api/server/routes/agents/chat.js index 6d533629e..7ac4ce811 100644 --- a/api/server/routes/agents/chat.js +++ b/api/server/routes/agents/chat.js @@ -1,7 +1,6 @@ const express = require('express'); -const { PermissionBits } = require('@librechat/data-schemas'); const { generateCheckAccess, skipAgentCheck } = require('@librechat/api'); -const { PermissionTypes, Permissions } = require('librechat-data-provider'); +const { PermissionTypes, Permissions, PermissionBits } = require('librechat-data-provider'); const { setHeaders, moderateText, diff --git a/api/server/routes/agents/v1.js b/api/server/routes/agents/v1.js index 9073decd5..3e7a877ad 100644 --- a/api/server/routes/agents/v1.js +++ b/api/server/routes/agents/v1.js @@ -1,7 +1,6 @@ const express = require('express'); const { generateCheckAccess } = require('@librechat/api'); -const { PermissionBits } = require('@librechat/data-schemas'); -const { PermissionTypes, Permissions } = require('librechat-data-provider'); +const { PermissionTypes, Permissions, PermissionBits } = require('librechat-data-provider'); const { requireJwtAuth, canAccessAgentResource } = require('~/server/middleware'); const v1 = require('~/server/controllers/agents/v1'); const { getRoleByName } = require('~/models/Role'); diff --git a/api/server/routes/files/files.agents.test.js b/api/server/routes/files/files.agents.test.js index 0ef5580eb..25e298aa5 100644 --- a/api/server/routes/files/files.agents.test.js +++ b/api/server/routes/files/files.agents.test.js @@ -4,6 +4,7 @@ const mongoose = require('mongoose'); const { v4: uuidv4 } = require('uuid'); const { MongoMemoryServer } = require('mongodb-memory-server'); const { createMethods } = require('@librechat/data-schemas'); +const { AccessRoleIds, ResourceType } = require('librechat-data-provider'); const { createAgent } = require('~/models/Agent'); const { createFile } = require('~/models/File'); @@ -186,9 +187,9 @@ describe('File Routes - Agent Files Endpoint', () => { await grantPermission({ principalType: 'user', principalId: otherUserId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, - accessRoleId: 'agent_editor', + accessRoleId: AccessRoleIds.AGENT_EDITOR, grantedBy: authorId, }); @@ -241,9 +242,9 @@ describe('File Routes - Agent Files Endpoint', () => { await grantPermission({ principalType: 'user', principalId: otherUserId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, - accessRoleId: 'agent_viewer', + accessRoleId: AccessRoleIds.AGENT_VIEWER, grantedBy: authorId, }); diff --git a/api/server/routes/files/files.js b/api/server/routes/files/files.js index a52287f9f..a5bda3100 100644 --- a/api/server/routes/files/files.js +++ b/api/server/routes/files/files.js @@ -6,8 +6,9 @@ const { isUUID, CacheKeys, FileSources, - PERMISSION_BITS, + ResourceType, EModelEndpoint, + PermissionBits, isAgentsEndpoint, checkOpenAIStorage, } = require('librechat-data-provider'); @@ -17,6 +18,7 @@ const { processDeleteRequest, processAgentFileUpload, } = require('~/server/services/Files/process'); +const { fileAccess } = require('~/server/middleware/accessResources/fileAccess'); const { getStrategyFunctions } = require('~/server/services/Files/strategies'); const { getOpenAIClient } = require('~/server/controllers/assistants/helpers'); const { checkPermission } = require('~/server/services/PermissionService'); @@ -24,12 +26,11 @@ const { loadAuthValues } = require('~/server/services/Tools/credentials'); const { refreshS3FileUrls } = require('~/server/services/Files/S3/crud'); const { hasAccessToFilesViaAgent } = require('~/server/services/Files'); const { getFiles, batchUpdateFiles } = require('~/models/File'); +const { cleanFileName } = require('~/server/utils/files'); const { getAssistant } = require('~/models/Assistant'); const { getAgent } = require('~/models/Agent'); -const { cleanFileName } = require('~/server/utils/files'); const { getLogStores } = require('~/cache'); const { logger } = require('~/config'); -const { fileAccess } = require('~/server/middleware/accessResources/fileAccess'); const router = express.Router(); @@ -78,9 +79,9 @@ router.get('/agent/:agent_id', async (req, res) => { if (agent.author.toString() !== userId) { const hasEditPermission = await checkPermission({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, - requiredPermission: PERMISSION_BITS.EDIT, + requiredPermission: PermissionBits.EDIT, }); if (!hasEditPermission) { diff --git a/api/server/routes/files/files.test.js b/api/server/routes/files/files.test.js index d02caf79f..350bb3281 100644 --- a/api/server/routes/files/files.test.js +++ b/api/server/routes/files/files.test.js @@ -4,6 +4,7 @@ const mongoose = require('mongoose'); const { v4: uuidv4 } = require('uuid'); const { createMethods } = require('@librechat/data-schemas'); const { MongoMemoryServer } = require('mongodb-memory-server'); +const { AccessRoleIds, ResourceType } = require('librechat-data-provider'); const { createAgent } = require('~/models/Agent'); const { createFile } = require('~/models/File'); @@ -228,9 +229,9 @@ describe('File Routes - Delete with Agent Access', () => { await grantPermission({ principalType: 'user', principalId: otherUserId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, - accessRoleId: 'agent_editor', + accessRoleId: AccessRoleIds.AGENT_EDITOR, grantedBy: authorId, }); @@ -282,9 +283,9 @@ describe('File Routes - Delete with Agent Access', () => { await grantPermission({ principalType: 'user', principalId: otherUserId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, - accessRoleId: 'agent_editor', + accessRoleId: AccessRoleIds.AGENT_EDITOR, grantedBy: authorId, }); @@ -348,9 +349,9 @@ describe('File Routes - Delete with Agent Access', () => { await grantPermission({ principalType: 'user', principalId: otherUserId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, - accessRoleId: 'agent_editor', + accessRoleId: AccessRoleIds.AGENT_EDITOR, grantedBy: authorId, }); @@ -391,9 +392,9 @@ describe('File Routes - Delete with Agent Access', () => { await grantPermission({ principalType: 'user', principalId: otherUserId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, - accessRoleId: 'agent_viewer', + accessRoleId: AccessRoleIds.AGENT_VIEWER, grantedBy: authorId, }); diff --git a/api/server/routes/prompts.js b/api/server/routes/prompts.js index 8f01d9bee..ec14bfd14 100644 --- a/api/server/routes/prompts.js +++ b/api/server/routes/prompts.js @@ -1,20 +1,26 @@ const express = require('express'); -const { logger, PermissionBits } = require('@librechat/data-schemas'); +const { logger } = require('@librechat/data-schemas'); const { generateCheckAccess } = require('@librechat/api'); -const { Permissions, SystemRoles, PermissionTypes } = require('librechat-data-provider'); const { - getPrompt, - getPrompts, - savePrompt, - deletePrompt, - getPromptGroup, - getPromptGroups, + Permissions, + SystemRoles, + ResourceType, + AccessRoleIds, + PermissionTypes, + PermissionBits, +} = require('librechat-data-provider'); +const { + makePromptProduction, + getAllPromptGroups, updatePromptGroup, deletePromptGroup, createPromptGroup, - getAllPromptGroups, - // updatePromptLabels, - makePromptProduction, + getPromptGroups, + getPromptGroup, + deletePrompt, + getPrompts, + savePrompt, + getPrompt, } = require('~/models/Prompt'); const { canAccessPromptGroupResource, @@ -22,10 +28,10 @@ const { requireJwtAuth, } = require('~/server/middleware'); const { - grantPermission, + findPubliclyAccessibleResources, getEffectivePermissions, findAccessibleResources, - findPubliclyAccessibleResources, + grantPermission, } = require('~/server/services/PermissionService'); const { getRoleByName } = require('~/models/Role'); @@ -92,7 +98,7 @@ router.get('/all', async (req, res) => { // Get promptGroup IDs the user has VIEW access to via ACL const accessibleIds = await findAccessibleResources({ userId, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, requiredPermissions: PermissionBits.VIEW, }); @@ -123,13 +129,13 @@ router.get('/groups', async (req, res) => { // Get promptGroup IDs the user has VIEW access to via ACL const accessibleIds = await findAccessibleResources({ userId, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, requiredPermissions: PermissionBits.VIEW, }); // Get publicly accessible promptGroups const publiclyAccessibleIds = await findPubliclyAccessibleResources({ - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, requiredPermissions: PermissionBits.VIEW, }); @@ -185,9 +191,9 @@ const createNewPromptGroup = async (req, res) => { await grantPermission({ principalType: 'user', principalId: req.user.id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: result.prompt.groupId, - accessRoleId: 'promptGroup_owner', + accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, grantedBy: req.user.id, }); logger.debug( @@ -327,7 +333,7 @@ router.get('/', async (req, res) => { if (groupId) { const permissions = await getEffectivePermissions({ userId: req.user.id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: groupId, }); diff --git a/api/server/routes/prompts.test.js b/api/server/routes/prompts.test.js index 20c4e435a..5654f64fd 100644 --- a/api/server/routes/prompts.test.js +++ b/api/server/routes/prompts.test.js @@ -1,10 +1,14 @@ -const request = require('supertest'); const express = require('express'); -const { MongoMemoryServer } = require('mongodb-memory-server'); +const request = require('supertest'); const mongoose = require('mongoose'); const { ObjectId } = require('mongodb'); -const { SystemRoles } = require('librechat-data-provider'); -const { PermissionBits } = require('@librechat/data-schemas'); +const { MongoMemoryServer } = require('mongodb-memory-server'); +const { + SystemRoles, + ResourceType, + AccessRoleIds, + PermissionBits, +} = require('librechat-data-provider'); // Mock modules before importing jest.mock('~/server/services/Config', () => ({ @@ -18,7 +22,6 @@ jest.mock('~/models/Role', () => ({ jest.mock('~/server/middleware', () => ({ requireJwtAuth: (req, res, next) => next(), - canAccessPromptResource: jest.requireActual('~/server/middleware').canAccessPromptResource, canAccessPromptViaGroup: jest.requireActual('~/server/middleware').canAccessPromptViaGroup, canAccessPromptGroupResource: jest.requireActual('~/server/middleware').canAccessPromptGroupResource, @@ -90,21 +93,21 @@ async function setupTestData() { // Create access roles for promptGroups testRoles = { viewer: await AccessRole.create({ - accessRoleId: 'promptGroup_viewer', + accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, name: 'Viewer', - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, permBits: PermissionBits.VIEW, }), editor: await AccessRole.create({ - accessRoleId: 'promptGroup_editor', + accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR, name: 'Editor', - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, permBits: PermissionBits.VIEW | PermissionBits.EDIT, }), owner: await AccessRole.create({ - accessRoleId: 'promptGroup_owner', + accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, name: 'Owner', - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, permBits: PermissionBits.VIEW | PermissionBits.EDIT | PermissionBits.DELETE | PermissionBits.SHARE, }), @@ -218,7 +221,7 @@ describe('Prompt Routes - ACL Permissions', () => { // Check ACL entry was created const aclEntry = await AclEntry.findOne({ - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: response.body.prompt.groupId, principalType: 'user', principalId: testUsers.owner._id, @@ -248,7 +251,7 @@ describe('Prompt Routes - ACL Permissions', () => { // Check ACL entry was created for the promptGroup const aclEntry = await AclEntry.findOne({ - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: response.body.group._id, principalType: 'user', principalId: testUsers.owner._id, @@ -293,9 +296,9 @@ describe('Prompt Routes - ACL Permissions', () => { await grantPermission({ principalType: 'user', principalId: testUsers.owner._id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: testGroup._id, - accessRoleId: 'promptGroup_viewer', + accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, grantedBy: testUsers.owner._id, }); @@ -378,9 +381,9 @@ describe('Prompt Routes - ACL Permissions', () => { await grantPermission({ principalType: 'user', principalId: testUsers.owner._id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: testGroup._id, - accessRoleId: 'promptGroup_owner', + accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER, grantedBy: testUsers.owner._id, }); }); @@ -405,7 +408,7 @@ describe('Prompt Routes - ACL Permissions', () => { // Verify ACL entries were removed const aclEntries = await AclEntry.find({ - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: testGroup._id, }); expect(aclEntries).toHaveLength(0); @@ -425,9 +428,9 @@ describe('Prompt Routes - ACL Permissions', () => { await grantPermission({ principalType: 'user', principalId: testUsers.viewer._id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: testGroup._id, - accessRoleId: 'promptGroup_viewer', + accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, grantedBy: testUsers.editor._id, }); @@ -492,9 +495,9 @@ describe('Prompt Routes - ACL Permissions', () => { await grantPermission({ principalType: 'user', principalId: testUsers.owner._id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: testGroup._id, - accessRoleId: 'promptGroup_editor', + accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR, grantedBy: testUsers.owner._id, }); @@ -530,9 +533,9 @@ describe('Prompt Routes - ACL Permissions', () => { await grantPermission({ principalType: 'user', principalId: testUsers.viewer._id, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: testGroup._id, - accessRoleId: 'promptGroup_viewer', + accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, grantedBy: testUsers.owner._id, }); @@ -587,9 +590,9 @@ describe('Prompt Routes - ACL Permissions', () => { await grantPermission({ principalType: 'public', principalId: null, - resourceType: 'promptGroup', + resourceType: ResourceType.PROMPTGROUP, resourceId: publicGroup._id, - accessRoleId: 'promptGroup_viewer', + accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER, grantedBy: testUsers.owner._id, }); }); diff --git a/api/server/services/Files/permissions.js b/api/server/services/Files/permissions.js index f71a707ca..37eaf50cc 100644 --- a/api/server/services/Files/permissions.js +++ b/api/server/services/Files/permissions.js @@ -1,5 +1,5 @@ const { logger } = require('@librechat/data-schemas'); -const { PERMISSION_BITS } = require('librechat-data-provider'); +const { PermissionBits, ResourceType } = require('librechat-data-provider'); const { checkPermission } = require('~/server/services/PermissionService'); const { getAgent } = require('~/models/Agent'); @@ -32,9 +32,9 @@ const hasAccessToFilesViaAgent = async (userId, fileIds, agentId) => { // Check if user has at least VIEW permission on the agent const hasViewPermission = await checkPermission({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, - requiredPermission: PERMISSION_BITS.VIEW, + requiredPermission: PermissionBits.VIEW, }); if (!hasViewPermission) { @@ -44,9 +44,9 @@ const hasAccessToFilesViaAgent = async (userId, fileIds, agentId) => { // Check if user has EDIT permission (which would indicate collaborative access) const hasEditPermission = await checkPermission({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: agent._id, - requiredPermission: PERMISSION_BITS.EDIT, + requiredPermission: PermissionBits.EDIT, }); // If user only has VIEW permission, they can't access files diff --git a/api/server/services/PermissionService.js b/api/server/services/PermissionService.js index 4057c2c5b..31e91dc4d 100644 --- a/api/server/services/PermissionService.js +++ b/api/server/services/PermissionService.js @@ -1,32 +1,45 @@ const mongoose = require('mongoose'); +const { isEnabled } = require('@librechat/api'); +const { ResourceType } = require('librechat-data-provider'); const { getTransactionSupport, logger } = require('@librechat/data-schemas'); -const { isEnabled } = require('~/server/utils'); const { entraIdPrincipalFeatureEnabled, - getUserEntraGroups, getUserOwnedEntraGroups, + getUserEntraGroups, getGroupMembers, getGroupOwners, } = require('~/server/services/GraphApiService'); const { + findAccessibleResources: findAccessibleResourcesACL, + getEffectivePermissions: getEffectivePermissionsACL, + grantPermission: grantPermissionACL, + findEntriesByPrincipalsAndResource, findGroupByExternalId, findRoleByIdentifier, getUserPrincipals, + hasPermission, createGroup, createUser, updateUser, findUser, - grantPermission: grantPermissionACL, - findAccessibleResources: findAccessibleResourcesACL, - hasPermission, - getEffectivePermissions: getEffectivePermissionsACL, - findEntriesByPrincipalsAndResource, } = require('~/models'); const { AclEntry, AccessRole, Group } = require('~/db/models'); /** @type {boolean|null} */ let transactionSupportCache = null; +/** + * Validates that the resourceType is one of the supported enum values + * @param {string} resourceType - The resource type to validate + * @throws {Error} If resourceType is not valid + */ +const validateResourceType = (resourceType) => { + const validTypes = Object.values(ResourceType); + if (!validTypes.includes(resourceType)) { + throw new Error(`Invalid resourceType: ${resourceType}. Valid types: ${validTypes.join(', ')}`); + } +}; + /** * @import { TPrincipal } from 'librechat-data-provider' */ @@ -37,7 +50,7 @@ let transactionSupportCache = null; * @param {string|mongoose.Types.ObjectId|null} params.principalId - The ID of the principal (null for 'public') * @param {string} params.resourceType - Type of resource (e.g., 'agent') * @param {string|mongoose.Types.ObjectId} params.resourceId - The ID of the resource - * @param {string} params.accessRoleId - The ID of the role (e.g., 'agent_viewer', 'agent_editor') + * @param {string} params.accessRoleId - The ID of the role (e.g., AccessRoleIds.AGENT_VIEWER, AccessRoleIds.AGENT_EDITOR) * @param {string|mongoose.Types.ObjectId} params.grantedBy - User ID granting the permission * @param {mongoose.ClientSession} [params.session] - Optional MongoDB session for transactions * @returns {Promise} The created or updated ACL entry @@ -68,6 +81,8 @@ const grantPermission = async ({ throw new Error(`Invalid resource ID: ${resourceId}`); } + validateResourceType(resourceType); + // Get the role to determine permission bits const role = await findRoleByIdentifier(accessRoleId); if (!role) { @@ -111,6 +126,8 @@ const checkPermission = async ({ userId, resourceType, resourceId, requiredPermi throw new Error('requiredPermission must be a positive number'); } + validateResourceType(resourceType); + // Get all principals for the user (user + groups + public) const principals = await getUserPrincipals(userId); @@ -139,6 +156,8 @@ const checkPermission = async ({ userId, resourceType, resourceId, requiredPermi */ const getEffectivePermissions = async ({ userId, resourceType, resourceId }) => { try { + validateResourceType(resourceType); + // Get all principals for the user (user + groups + public) const principals = await getUserPrincipals(userId); @@ -166,6 +185,8 @@ const findAccessibleResources = async ({ userId, resourceType, requiredPermissio throw new Error('requiredPermissions must be a positive number'); } + validateResourceType(resourceType); + // Get all principals for the user (user + groups + public) const principalsList = await getUserPrincipals(userId); @@ -196,6 +217,8 @@ const findPubliclyAccessibleResources = async ({ resourceType, requiredPermissio throw new Error('requiredPermissions must be a positive number'); } + validateResourceType(resourceType); + // Find all public ACL entries where the public principal has at least the required permission bits const entries = await AclEntry.find({ principalType: 'public', @@ -221,12 +244,9 @@ const findPubliclyAccessibleResources = async ({ resourceType, requiredPermissio * @returns {Promise} Array of role definitions */ const getAvailableRoles = async ({ resourceType }) => { - try { - return await AccessRole.find({ resourceType }).lean(); - } catch (error) { - logger.error(`[PermissionService.getAvailableRoles] Error: ${error.message}`); - return []; - } + validateResourceType(resourceType); + + return await AccessRole.find({ resourceType }).lean(); }; /** @@ -482,6 +502,8 @@ const hasPublicPermission = async ({ resourceType, resourceId, requiredPermissio throw new Error('requiredPermissions must be a positive number'); } + validateResourceType(resourceType); + // Use public principal to check permissions const publicPrincipal = [{ principalType: 'public' }]; @@ -707,14 +729,16 @@ const bulkUpdateResourcePermissions = async ({ }; /** - * Remove all permissions for a specific resource - * @param {Object} params - Parameters for removing permissions + * Remove all permissions for a resource (cleanup when resource is deleted) + * @param {Object} params - Parameters for removing all permissions * @param {string} params.resourceType - Type of resource (e.g., 'agent', 'prompt') * @param {string|mongoose.Types.ObjectId} params.resourceId - The ID of the resource - * @returns {Promise} Delete result + * @returns {Promise} Result of the deletion operation */ const removeAllPermissions = async ({ resourceType, resourceId }) => { try { + validateResourceType(resourceType); + if (!resourceId || !mongoose.Types.ObjectId.isValid(resourceId)) { throw new Error(`Invalid resource ID: ${resourceId}`); } diff --git a/api/server/services/PermissionService.spec.js b/api/server/services/PermissionService.spec.js index 13adc47dd..207527ed3 100644 --- a/api/server/services/PermissionService.spec.js +++ b/api/server/services/PermissionService.spec.js @@ -1,6 +1,7 @@ const mongoose = require('mongoose'); const { RoleBits } = require('@librechat/data-schemas'); const { MongoMemoryServer } = require('mongodb-memory-server'); +const { AccessRoleIds, ResourceType } = require('librechat-data-provider'); const { bulkUpdateResourcePermissions, getEffectivePermissions, @@ -48,49 +49,49 @@ beforeEach(async () => { // Seed some roles for testing await AccessRole.create([ { - accessRoleId: 'agent_viewer', + accessRoleId: AccessRoleIds.AGENT_VIEWER, name: 'Agent Viewer', description: 'Can view agents', - resourceType: 'agent', + resourceType: ResourceType.AGENT, permBits: RoleBits.VIEWER, // VIEW permission }, { - accessRoleId: 'agent_editor', + accessRoleId: AccessRoleIds.AGENT_EDITOR, name: 'Agent Editor', description: 'Can edit agents', - resourceType: 'agent', + resourceType: ResourceType.AGENT, permBits: RoleBits.EDITOR, // VIEW + EDIT permissions }, { - accessRoleId: 'agent_owner', + accessRoleId: AccessRoleIds.AGENT_OWNER, name: 'Agent Owner', description: 'Full control over agents', - resourceType: 'agent', + resourceType: ResourceType.AGENT, permBits: RoleBits.OWNER, // VIEW + EDIT + DELETE + SHARE permissions }, { - accessRoleId: 'project_viewer', + accessRoleId: AccessRoleIds.PROJECT_VIEWER, name: 'Project Viewer', description: 'Can view projects', resourceType: 'project', permBits: RoleBits.VIEWER, }, { - accessRoleId: 'project_editor', + accessRoleId: AccessRoleIds.PROJECT_EDITOR, name: 'Project Editor', description: 'Can edit projects', resourceType: 'project', permBits: RoleBits.EDITOR, }, { - accessRoleId: 'project_manager', + accessRoleId: AccessRoleIds.PROJECT_MANAGER, name: 'Project Manager', description: 'Can manage projects', resourceType: 'project', permBits: RoleBits.MANAGER, }, { - accessRoleId: 'project_owner', + accessRoleId: AccessRoleIds.PROJECT_OWNER, name: 'Project Owner', description: 'Full control over projects', resourceType: 'project', @@ -117,9 +118,9 @@ describe('PermissionService', () => { const entry = await grantPermission({ principalType: 'user', principalId: userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, - accessRoleId: 'agent_viewer', + accessRoleId: AccessRoleIds.AGENT_VIEWER, grantedBy: grantedById, }); @@ -131,7 +132,7 @@ describe('PermissionService', () => { expect(entry.resourceId.toString()).toBe(resourceId.toString()); // Get the role to verify the permission bits are correctly set - const role = await findRoleByIdentifier('agent_viewer'); + const role = await findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER); expect(entry.permBits).toBe(role.permBits); expect(entry.roleId.toString()).toBe(role._id.toString()); expect(entry.grantedBy.toString()).toBe(grantedById.toString()); @@ -142,9 +143,9 @@ describe('PermissionService', () => { const entry = await grantPermission({ principalType: 'group', principalId: groupId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, - accessRoleId: 'agent_editor', + accessRoleId: AccessRoleIds.AGENT_EDITOR, grantedBy: grantedById, }); @@ -154,7 +155,7 @@ describe('PermissionService', () => { expect(entry.principalModel).toBe('Group'); // Get the role to verify the permission bits are correctly set - const role = await findRoleByIdentifier('agent_editor'); + const role = await findRoleByIdentifier(AccessRoleIds.AGENT_EDITOR); expect(entry.permBits).toBe(role.permBits); expect(entry.roleId.toString()).toBe(role._id.toString()); }); @@ -163,9 +164,9 @@ describe('PermissionService', () => { const entry = await grantPermission({ principalType: 'public', principalId: null, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, - accessRoleId: 'agent_viewer', + accessRoleId: AccessRoleIds.AGENT_VIEWER, grantedBy: grantedById, }); @@ -175,7 +176,7 @@ describe('PermissionService', () => { expect(entry.principalModel).toBeUndefined(); // Get the role to verify the permission bits are correctly set - const role = await findRoleByIdentifier('agent_viewer'); + const role = await findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER); expect(entry.permBits).toBe(role.permBits); expect(entry.roleId.toString()).toBe(role._id.toString()); }); @@ -185,9 +186,9 @@ describe('PermissionService', () => { grantPermission({ principalType: 'invalid', principalId: userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, - accessRoleId: 'agent_viewer', + accessRoleId: AccessRoleIds.AGENT_VIEWER, grantedBy: grantedById, }), ).rejects.toThrow('Invalid principal type: invalid'); @@ -198,9 +199,9 @@ describe('PermissionService', () => { grantPermission({ principalType: 'user', principalId: null, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, - accessRoleId: 'agent_viewer', + accessRoleId: AccessRoleIds.AGENT_VIEWER, grantedBy: grantedById, }), ).rejects.toThrow('Principal ID is required for user and group principals'); @@ -211,7 +212,7 @@ describe('PermissionService', () => { grantPermission({ principalType: 'user', principalId: userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, accessRoleId: 'non_existent_role', grantedBy: grantedById, @@ -224,9 +225,9 @@ describe('PermissionService', () => { grantPermission({ principalType: 'user', principalId: userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, - accessRoleId: 'project_viewer', // Project role for agent resource + accessRoleId: AccessRoleIds.PROJECT_VIEWER, // Project role for agent resource grantedBy: grantedById, }), ).rejects.toThrow('Role project_viewer is for project resources, not agent'); @@ -237,9 +238,9 @@ describe('PermissionService', () => { await grantPermission({ principalType: 'user', principalId: userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, - accessRoleId: 'agent_viewer', + accessRoleId: AccessRoleIds.AGENT_VIEWER, grantedBy: grantedById, }); @@ -247,13 +248,13 @@ describe('PermissionService', () => { const updated = await grantPermission({ principalType: 'user', principalId: userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, - accessRoleId: 'agent_editor', + accessRoleId: AccessRoleIds.AGENT_EDITOR, grantedBy: grantedById, }); - const editorRole = await findRoleByIdentifier('agent_editor'); + const editorRole = await findRoleByIdentifier(AccessRoleIds.AGENT_EDITOR); expect(updated.permBits).toBe(editorRole.permBits); expect(updated.roleId.toString()).toBe(editorRole._id.toString()); @@ -261,7 +262,7 @@ describe('PermissionService', () => { const entries = await AclEntry.find({ principalType: 'user', principalId: userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, }); expect(entries).toHaveLength(1); @@ -279,9 +280,9 @@ describe('PermissionService', () => { await grantPermission({ principalType: 'user', principalId: userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, - accessRoleId: 'agent_viewer', + accessRoleId: AccessRoleIds.AGENT_VIEWER, grantedBy: grantedById, }); @@ -289,9 +290,9 @@ describe('PermissionService', () => { await grantPermission({ principalType: 'group', principalId: groupId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: otherResourceId, - accessRoleId: 'agent_editor', + accessRoleId: AccessRoleIds.AGENT_EDITOR, grantedBy: grantedById, }); }); @@ -302,7 +303,7 @@ describe('PermissionService', () => { const hasViewPermission = await checkPermission({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, requiredPermission: 1, // RoleBits.VIEWER // 1 = VIEW }); @@ -312,7 +313,7 @@ describe('PermissionService', () => { // Check higher permission level that user doesn't have const hasEditPermission = await checkPermission({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, requiredPermission: 3, // RoleBits.EDITOR = VIEW + EDIT }); @@ -330,7 +331,7 @@ describe('PermissionService', () => { // Check original resource (user has access) const hasViewOnOriginal = await checkPermission({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, requiredPermission: 1, // RoleBits.VIEWER // 1 = VIEW }); @@ -340,7 +341,7 @@ describe('PermissionService', () => { // Check other resource (group has access) const hasViewOnOther = await checkPermission({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: otherResourceId, requiredPermission: 1, // RoleBits.VIEWER // 1 = VIEW }); @@ -356,9 +357,9 @@ describe('PermissionService', () => { await grantPermission({ principalType: 'public', principalId: null, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: publicResourceId, - accessRoleId: 'agent_viewer', + accessRoleId: AccessRoleIds.AGENT_VIEWER, grantedBy: grantedById, }); @@ -371,7 +372,7 @@ describe('PermissionService', () => { const hasPublicAccess = await checkPermission({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: publicResourceId, requiredPermission: 1, // RoleBits.VIEWER // 1 = VIEW }); @@ -385,7 +386,7 @@ describe('PermissionService', () => { await expect( checkPermission({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, requiredPermission: 'invalid', }), @@ -393,7 +394,7 @@ describe('PermissionService', () => { const nonExistentResource = await checkPermission({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: new mongoose.Types.ObjectId(), requiredPermission: 1, // RoleBits.VIEWER }); @@ -406,7 +407,7 @@ describe('PermissionService', () => { const hasPermission = await checkPermission({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, requiredPermission: 1, // RoleBits.VIEWER }); @@ -424,18 +425,18 @@ describe('PermissionService', () => { await grantPermission({ principalType: 'user', principalId: userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, - accessRoleId: 'agent_viewer', + accessRoleId: AccessRoleIds.AGENT_VIEWER, grantedBy: grantedById, }); await grantPermission({ principalType: 'group', principalId: groupId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, - accessRoleId: 'agent_editor', + accessRoleId: AccessRoleIds.AGENT_EDITOR, grantedBy: grantedById, }); @@ -444,9 +445,9 @@ describe('PermissionService', () => { await grantPermission({ principalType: 'public', principalId: null, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: publicResourceId, - accessRoleId: 'agent_viewer', + accessRoleId: AccessRoleIds.AGENT_VIEWER, grantedBy: grantedById, }); @@ -459,7 +460,7 @@ describe('PermissionService', () => { principalId: userId, resourceType: 'project', resourceId: projectId, - accessRoleId: 'project_viewer', + accessRoleId: AccessRoleIds.PROJECT_VIEWER, grantedBy: grantedById, }); @@ -467,10 +468,10 @@ describe('PermissionService', () => { principalType: 'user', principalId: userId, principalModel: 'User', - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: childResourceId, permBits: RoleBits.VIEWER, - roleId: (await findRoleByIdentifier('agent_viewer'))._id, + roleId: (await findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER))._id, grantedBy: grantedById, grantedAt: new Date(), inheritedFrom: projectId, @@ -486,7 +487,7 @@ describe('PermissionService', () => { const effective = await getEffectivePermissions({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, }); @@ -505,7 +506,7 @@ describe('PermissionService', () => { const effective = await getEffectivePermissions({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: childResourceId, }); @@ -519,7 +520,7 @@ describe('PermissionService', () => { const nonExistentResource = new mongoose.Types.ObjectId(); const effective = await getEffectivePermissions({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: nonExistentResource, }); @@ -532,7 +533,7 @@ describe('PermissionService', () => { const effective = await getEffectivePermissions({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, }); @@ -555,9 +556,9 @@ describe('PermissionService', () => { await grantPermission({ principalType: 'user', principalId: userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: resource1, - accessRoleId: 'agent_viewer', + accessRoleId: AccessRoleIds.AGENT_VIEWER, grantedBy: grantedById, }); @@ -565,9 +566,9 @@ describe('PermissionService', () => { await grantPermission({ principalType: 'user', principalId: userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: resource2, - accessRoleId: 'agent_editor', + accessRoleId: AccessRoleIds.AGENT_EDITOR, grantedBy: grantedById, }); @@ -575,9 +576,9 @@ describe('PermissionService', () => { await grantPermission({ principalType: 'group', principalId: groupId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: resource3, - accessRoleId: 'agent_viewer', + accessRoleId: AccessRoleIds.AGENT_VIEWER, grantedBy: grantedById, }); }); @@ -588,7 +589,7 @@ describe('PermissionService', () => { const viewableResources = await findAccessibleResources({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, requiredPermissions: 1, // RoleBits.VIEWER // 1 = VIEW }); @@ -602,7 +603,7 @@ describe('PermissionService', () => { const editableResources = await findAccessibleResources({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, requiredPermissions: 3, // RoleBits.EDITOR = VIEW + EDIT }); @@ -619,7 +620,7 @@ describe('PermissionService', () => { const viewableResources = await findAccessibleResources({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, requiredPermissions: 1, // RoleBits.VIEWER // 1 = VIEW }); @@ -633,7 +634,7 @@ describe('PermissionService', () => { await expect( findAccessibleResources({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, requiredPermissions: 'invalid', }), ).rejects.toThrow('requiredPermissions must be a positive number'); @@ -652,7 +653,7 @@ describe('PermissionService', () => { const resources = await findAccessibleResources({ userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, requiredPermissions: 1, // RoleBits.VIEWER }); @@ -663,12 +664,12 @@ describe('PermissionService', () => { describe('getAvailableRoles', () => { test('should get all roles for a resource type', async () => { const roles = await getAvailableRoles({ - resourceType: 'agent', + resourceType: ResourceType.AGENT, }); expect(roles).toHaveLength(3); expect(roles.map((r) => r.accessRoleId).sort()).toEqual( - ['agent_editor', 'agent_owner', 'agent_viewer'].sort(), + [AccessRoleIds.AGENT_EDITOR, AccessRoleIds.AGENT_OWNER, AccessRoleIds.AGENT_VIEWER].sort(), ); }); @@ -689,27 +690,27 @@ describe('PermissionService', () => { await grantPermission({ principalType: 'user', principalId: userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, - accessRoleId: 'agent_viewer', + accessRoleId: AccessRoleIds.AGENT_VIEWER, grantedBy: grantedById, }); await grantPermission({ principalType: 'group', principalId: groupId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, - accessRoleId: 'agent_editor', + accessRoleId: AccessRoleIds.AGENT_EDITOR, grantedBy: grantedById, }); await grantPermission({ principalType: 'public', principalId: null, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, - accessRoleId: 'agent_viewer', + accessRoleId: AccessRoleIds.AGENT_VIEWER, grantedBy: grantedById, }); }); @@ -720,22 +721,22 @@ describe('PermissionService', () => { { type: 'user', id: userId, - accessRoleId: 'agent_viewer', + accessRoleId: AccessRoleIds.AGENT_VIEWER, }, { type: 'user', id: otherUserId, - accessRoleId: 'agent_editor', + accessRoleId: AccessRoleIds.AGENT_EDITOR, }, { type: 'group', id: groupId, - accessRoleId: 'agent_owner', + accessRoleId: AccessRoleIds.AGENT_OWNER, }, ]; const results = await bulkUpdateResourcePermissions({ - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: newResourceId, updatedPrincipals, grantedBy: grantedById, @@ -748,7 +749,7 @@ describe('PermissionService', () => { // Verify permissions were created const aclEntries = await AclEntry.find({ - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: newResourceId, }); expect(aclEntries).toHaveLength(3); @@ -759,21 +760,21 @@ describe('PermissionService', () => { { type: 'user', id: userId, - accessRoleId: 'agent_editor', // Upgrade from viewer to editor + accessRoleId: AccessRoleIds.AGENT_EDITOR, // Upgrade from viewer to editor }, { type: 'group', id: groupId, - accessRoleId: 'agent_owner', // Upgrade from editor to owner + accessRoleId: AccessRoleIds.AGENT_OWNER, // Upgrade from editor to owner }, { type: 'public', - accessRoleId: 'agent_viewer', // Keep same role + accessRoleId: AccessRoleIds.AGENT_VIEWER, // Keep same role }, ]; const results = await bulkUpdateResourcePermissions({ - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, updatedPrincipals, grantedBy: grantedById, @@ -789,18 +790,18 @@ describe('PermissionService', () => { const userEntry = await AclEntry.findOne({ principalType: 'user', principalId: userId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, }).populate('roleId', 'accessRoleId'); - expect(userEntry.roleId.accessRoleId).toBe('agent_editor'); + expect(userEntry.roleId.accessRoleId).toBe(AccessRoleIds.AGENT_EDITOR); const groupEntry = await AclEntry.findOne({ principalType: 'group', principalId: groupId, - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, }).populate('roleId', 'accessRoleId'); - expect(groupEntry.roleId.accessRoleId).toBe('agent_owner'); + expect(groupEntry.roleId.accessRoleId).toBe(AccessRoleIds.AGENT_OWNER); }); test('should revoke specified permissions', async () => { @@ -815,7 +816,7 @@ describe('PermissionService', () => { ]; const results = await bulkUpdateResourcePermissions({ - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, revokedPrincipals, grantedBy: grantedById, @@ -828,7 +829,7 @@ describe('PermissionService', () => { // Verify only user permission remains const remainingEntries = await AclEntry.find({ - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, }); expect(remainingEntries).toHaveLength(1); @@ -841,12 +842,12 @@ describe('PermissionService', () => { { type: 'user', id: userId, - accessRoleId: 'agent_owner', // Update existing + accessRoleId: AccessRoleIds.AGENT_OWNER, // Update existing }, { type: 'user', id: otherUserId, - accessRoleId: 'agent_viewer', // New permission + accessRoleId: AccessRoleIds.AGENT_VIEWER, // New permission }, ]; @@ -861,7 +862,7 @@ describe('PermissionService', () => { ]; const results = await bulkUpdateResourcePermissions({ - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, updatedPrincipals, revokedPrincipals, @@ -875,19 +876,19 @@ describe('PermissionService', () => { // Verify final state const finalEntries = await AclEntry.find({ - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, }).populate('roleId', 'accessRoleId'); expect(finalEntries).toHaveLength(2); const userEntry = finalEntries.find((e) => e.principalId.toString() === userId.toString()); - expect(userEntry.roleId.accessRoleId).toBe('agent_owner'); + expect(userEntry.roleId.accessRoleId).toBe(AccessRoleIds.AGENT_OWNER); const otherUserEntry = finalEntries.find( (e) => e.principalId.toString() === otherUserId.toString(), ); - expect(otherUserEntry.roleId.accessRoleId).toBe('agent_viewer'); + expect(otherUserEntry.roleId.accessRoleId).toBe(AccessRoleIds.AGENT_VIEWER); }); test('should handle errors for invalid roles gracefully', async () => { @@ -895,7 +896,7 @@ describe('PermissionService', () => { { type: 'user', id: userId, - accessRoleId: 'agent_viewer', // Valid + accessRoleId: AccessRoleIds.AGENT_VIEWER, // Valid }, { type: 'user', @@ -905,12 +906,12 @@ describe('PermissionService', () => { { type: 'group', id: groupId, - accessRoleId: 'project_viewer', // Wrong resource type + accessRoleId: AccessRoleIds.PROJECT_VIEWER, // Wrong resource type }, ]; const results = await bulkUpdateResourcePermissions({ - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, updatedPrincipals, grantedBy: grantedById, @@ -928,7 +929,7 @@ describe('PermissionService', () => { test('should handle empty arrays (no operations)', async () => { const results = await bulkUpdateResourcePermissions({ - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, updatedPrincipals: [], revokedPrincipals: [], @@ -942,7 +943,7 @@ describe('PermissionService', () => { // Verify no changes to existing permissions (since no operations were performed) const remainingEntries = await AclEntry.find({ - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, }); expect(remainingEntries).toHaveLength(3); // Original permissions still exist @@ -951,7 +952,7 @@ describe('PermissionService', () => { test('should throw error for invalid updatedPrincipals array', async () => { await expect( bulkUpdateResourcePermissions({ - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, updatedPrincipals: 'not an array', grantedBy: grantedById, @@ -962,7 +963,7 @@ describe('PermissionService', () => { test('should throw error for invalid resource ID', async () => { await expect( bulkUpdateResourcePermissions({ - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId: 'invalid-id', permissions: [], grantedBy: grantedById, @@ -974,12 +975,12 @@ describe('PermissionService', () => { const updatedPrincipals = [ { type: 'public', - accessRoleId: 'agent_editor', // Update public permission + accessRoleId: AccessRoleIds.AGENT_EDITOR, // Update public permission }, { type: 'user', id: otherUserId, - accessRoleId: 'agent_viewer', // New user permission + accessRoleId: AccessRoleIds.AGENT_VIEWER, // New user permission }, ]; @@ -995,7 +996,7 @@ describe('PermissionService', () => { ]; const results = await bulkUpdateResourcePermissions({ - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, updatedPrincipals, revokedPrincipals, @@ -1010,12 +1011,12 @@ describe('PermissionService', () => { // Verify public permission was updated const publicEntry = await AclEntry.findOne({ principalType: 'public', - resourceType: 'agent', + resourceType: ResourceType.AGENT, resourceId, }).populate('roleId', 'accessRoleId'); expect(publicEntry).toBeDefined(); - expect(publicEntry.roleId.accessRoleId).toBe('agent_editor'); + expect(publicEntry.roleId.accessRoleId).toBe(AccessRoleIds.AGENT_EDITOR); }); test('should work with different resource types', async () => { @@ -1025,12 +1026,12 @@ describe('PermissionService', () => { { type: 'user', id: userId, - accessRoleId: 'project_viewer', + accessRoleId: AccessRoleIds.PROJECT_VIEWER, }, { type: 'group', id: groupId, - accessRoleId: 'project_editor', + accessRoleId: AccessRoleIds.PROJECT_EDITOR, }, ]; diff --git a/api/typedefs.js b/api/typedefs.js index 5205503fc..deaa210e8 100644 --- a/api/typedefs.js +++ b/api/typedefs.js @@ -1072,6 +1072,19 @@ * @memberof typedefs */ +/** Permissions */ +/** + * @exports TUpdateResourcePermissionsRequest + * @typedef {import('librechat-data-provider').TUpdateResourcePermissionsRequest} TUpdateResourcePermissionsRequest + * @memberof typedefs + */ + +/** + * @exports TUpdateResourcePermissionsResponse + * @typedef {import('librechat-data-provider').TUpdateResourcePermissionsResponse} TUpdateResourcePermissionsResponse + * @memberof typedefs + */ + /** * @exports JsonSchemaType * @typedef {import('@librechat/api').JsonSchemaType} JsonSchemaType diff --git a/client/src/components/SidePanel/Agents/AgentCard.tsx b/client/src/components/Agents/AgentCard.tsx similarity index 96% rename from client/src/components/SidePanel/Agents/AgentCard.tsx rename to client/src/components/Agents/AgentCard.tsx index 292aa5957..01925e99e 100644 --- a/client/src/components/SidePanel/Agents/AgentCard.tsx +++ b/client/src/components/Agents/AgentCard.tsx @@ -1,10 +1,7 @@ import React from 'react'; - import type t from 'librechat-data-provider'; - import useLocalize from '~/hooks/useLocalize'; -import { renderAgentAvatar, getContactDisplayName } from '~/utils/agents'; -import { cn } from '~/utils'; +import { cn, renderAgentAvatar, getContactDisplayName } from '~/utils'; interface AgentCardProps { agent: t.Agent; // The agent data to display diff --git a/client/src/components/SidePanel/Agents/AgentCategoryDisplay.tsx b/client/src/components/Agents/AgentCategoryDisplay.tsx similarity index 100% rename from client/src/components/SidePanel/Agents/AgentCategoryDisplay.tsx rename to client/src/components/Agents/AgentCategoryDisplay.tsx diff --git a/client/src/components/SidePanel/Agents/AgentDetail.tsx b/client/src/components/Agents/AgentDetail.tsx similarity index 98% rename from client/src/components/SidePanel/Agents/AgentDetail.tsx rename to client/src/components/Agents/AgentDetail.tsx index 57a05ec52..63c368357 100644 --- a/client/src/components/SidePanel/Agents/AgentDetail.tsx +++ b/client/src/components/Agents/AgentDetail.tsx @@ -6,7 +6,7 @@ import { QueryKeys, Constants, EModelEndpoint, - PERMISSION_BITS, + PermissionBits, LocalStorageKeys, AgentListResponse, } from 'librechat-data-provider'; @@ -45,7 +45,7 @@ const AgentDetail: React.FC = ({ agent, isOpen, onClose }) => */ const handleStartChat = () => { if (agent) { - const keys = [QueryKeys.agents, { requiredPermission: PERMISSION_BITS.EDIT }]; + const keys = [QueryKeys.agents, { requiredPermission: PermissionBits.EDIT }]; const listResp = queryClient.getQueryData(keys); if (listResp != null) { if (!listResp.data.some((a) => a.id === agent.id)) { diff --git a/client/src/components/SidePanel/Agents/AgentGrid.tsx b/client/src/components/Agents/AgentGrid.tsx similarity index 98% rename from client/src/components/SidePanel/Agents/AgentGrid.tsx rename to client/src/components/Agents/AgentGrid.tsx index 1b07b3969..26406504f 100644 --- a/client/src/components/SidePanel/Agents/AgentGrid.tsx +++ b/client/src/components/Agents/AgentGrid.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from 'react'; import { Button, Spinner } from '@librechat/client'; -import { PERMISSION_BITS } from 'librechat-data-provider'; +import { PermissionBits } from 'librechat-data-provider'; import type t from 'librechat-data-provider'; import { useMarketplaceAgentsInfiniteQuery } from '~/data-provider/Agents'; import { useAgentCategories, useLocalize } from '~/hooks'; @@ -33,7 +33,7 @@ const AgentGrid: React.FC = ({ category, searchQuery, onSelectAg limit: number; promoted?: 0 | 1; } = { - requiredPermission: PERMISSION_BITS.VIEW, // View permission for marketplace viewing + requiredPermission: PermissionBits.VIEW, // View permission for marketplace viewing limit: 6, }; diff --git a/client/src/components/SidePanel/Agents/CategoryTabs.tsx b/client/src/components/Agents/CategoryTabs.tsx similarity index 100% rename from client/src/components/SidePanel/Agents/CategoryTabs.tsx rename to client/src/components/Agents/CategoryTabs.tsx diff --git a/client/src/components/SidePanel/Agents/ErrorDisplay.tsx b/client/src/components/Agents/ErrorDisplay.tsx similarity index 100% rename from client/src/components/SidePanel/Agents/ErrorDisplay.tsx rename to client/src/components/Agents/ErrorDisplay.tsx diff --git a/client/src/components/SidePanel/Agents/AgentMarketplace.tsx b/client/src/components/Agents/Marketplace.tsx similarity index 100% rename from client/src/components/SidePanel/Agents/AgentMarketplace.tsx rename to client/src/components/Agents/Marketplace.tsx diff --git a/client/src/components/SidePanel/Agents/MarketplaceContext.tsx b/client/src/components/Agents/MarketplaceContext.tsx similarity index 100% rename from client/src/components/SidePanel/Agents/MarketplaceContext.tsx rename to client/src/components/Agents/MarketplaceContext.tsx diff --git a/client/src/components/SidePanel/Agents/SearchBar.tsx b/client/src/components/Agents/SearchBar.tsx similarity index 100% rename from client/src/components/SidePanel/Agents/SearchBar.tsx rename to client/src/components/Agents/SearchBar.tsx diff --git a/client/src/components/SidePanel/Agents/SmartLoader.tsx b/client/src/components/Agents/SmartLoader.tsx similarity index 100% rename from client/src/components/SidePanel/Agents/SmartLoader.tsx rename to client/src/components/Agents/SmartLoader.tsx diff --git a/client/src/components/SidePanel/Agents/__tests__/Accessibility.spec.tsx b/client/src/components/Agents/tests/Accessibility.spec.tsx similarity index 100% rename from client/src/components/SidePanel/Agents/__tests__/Accessibility.spec.tsx rename to client/src/components/Agents/tests/Accessibility.spec.tsx diff --git a/client/src/components/SidePanel/Agents/__tests__/AgentCard.spec.tsx b/client/src/components/Agents/tests/AgentCard.spec.tsx similarity index 100% rename from client/src/components/SidePanel/Agents/__tests__/AgentCard.spec.tsx rename to client/src/components/Agents/tests/AgentCard.spec.tsx diff --git a/client/src/components/SidePanel/Agents/__tests__/AgentCategoryDisplay.spec.tsx b/client/src/components/Agents/tests/AgentCategoryDisplay.spec.tsx similarity index 100% rename from client/src/components/SidePanel/Agents/__tests__/AgentCategoryDisplay.spec.tsx rename to client/src/components/Agents/tests/AgentCategoryDisplay.spec.tsx diff --git a/client/src/components/SidePanel/Agents/__tests__/AgentDetail.spec.tsx b/client/src/components/Agents/tests/AgentDetail.spec.tsx similarity index 100% rename from client/src/components/SidePanel/Agents/__tests__/AgentDetail.spec.tsx rename to client/src/components/Agents/tests/AgentDetail.spec.tsx diff --git a/client/src/components/SidePanel/Agents/__tests__/AgentGrid.integration.spec.tsx b/client/src/components/Agents/tests/AgentGrid.integration.spec.tsx similarity index 100% rename from client/src/components/SidePanel/Agents/__tests__/AgentGrid.integration.spec.tsx rename to client/src/components/Agents/tests/AgentGrid.integration.spec.tsx diff --git a/client/src/components/SidePanel/Agents/__tests__/CategoryTabs.spec.tsx b/client/src/components/Agents/tests/CategoryTabs.spec.tsx similarity index 100% rename from client/src/components/SidePanel/Agents/__tests__/CategoryTabs.spec.tsx rename to client/src/components/Agents/tests/CategoryTabs.spec.tsx diff --git a/client/src/components/SidePanel/Agents/__tests__/ErrorDisplay.spec.tsx b/client/src/components/Agents/tests/ErrorDisplay.spec.tsx similarity index 100% rename from client/src/components/SidePanel/Agents/__tests__/ErrorDisplay.spec.tsx rename to client/src/components/Agents/tests/ErrorDisplay.spec.tsx diff --git a/client/src/components/SidePanel/Agents/__tests__/MarketplaceContext.spec.tsx b/client/src/components/Agents/tests/MarketplaceContext.spec.tsx similarity index 100% rename from client/src/components/SidePanel/Agents/__tests__/MarketplaceContext.spec.tsx rename to client/src/components/Agents/tests/MarketplaceContext.spec.tsx diff --git a/client/src/components/SidePanel/Agents/__tests__/SearchBar.spec.tsx b/client/src/components/Agents/tests/SearchBar.spec.tsx similarity index 100% rename from client/src/components/SidePanel/Agents/__tests__/SearchBar.spec.tsx rename to client/src/components/Agents/tests/SearchBar.spec.tsx diff --git a/client/src/components/SidePanel/Agents/__tests__/SmartLoader.spec.tsx b/client/src/components/Agents/tests/SmartLoader.spec.tsx similarity index 100% rename from client/src/components/SidePanel/Agents/__tests__/SmartLoader.spec.tsx rename to client/src/components/Agents/tests/SmartLoader.spec.tsx diff --git a/client/src/components/Prompts/Groups/ChatGroupItem.tsx b/client/src/components/Prompts/Groups/ChatGroupItem.tsx index 9fb27ad22..a69ff16be 100644 --- a/client/src/components/Prompts/Groups/ChatGroupItem.tsx +++ b/client/src/components/Prompts/Groups/ChatGroupItem.tsx @@ -7,7 +7,7 @@ import { DropdownMenuContent, DropdownMenuTrigger, } from '@librechat/client'; -import { PERMISSION_BITS } from 'librechat-data-provider'; +import { PermissionBits } from 'librechat-data-provider'; import type { TPromptGroup } from 'librechat-data-provider'; import { useLocalize, useSubmitMessage, useCustomLink, useResourcePermissions } from '~/hooks'; import VariableDialog from '~/components/Prompts/Groups/VariableDialog'; @@ -35,7 +35,7 @@ function ChatGroupItem({ // Check permissions for the promptGroup const { hasPermission } = useResourcePermissions('promptGroup', group._id || ''); - const canEdit = hasPermission(PERMISSION_BITS.EDIT); + const canEdit = hasPermission(PermissionBits.EDIT); const onCardClick: React.MouseEventHandler = () => { const text = group.productionPrompt?.prompt; diff --git a/client/src/components/Prompts/Groups/DashGroupItem.tsx b/client/src/components/Prompts/Groups/DashGroupItem.tsx index 756e281f9..3e906fba8 100644 --- a/client/src/components/Prompts/Groups/DashGroupItem.tsx +++ b/client/src/components/Prompts/Groups/DashGroupItem.tsx @@ -1,7 +1,7 @@ import { memo, useState, useRef, useMemo, useCallback, KeyboardEvent } from 'react'; import { EarthIcon, Pen } from 'lucide-react'; import { useNavigate, useParams } from 'react-router-dom'; -import { PERMISSION_BITS, type TPromptGroup } from 'librechat-data-provider'; +import { PermissionBits, type TPromptGroup } from 'librechat-data-provider'; import { Input, Label, @@ -30,8 +30,8 @@ function DashGroupItemComponent({ group, instanceProjectId }: DashGroupItemProps const [nameInputValue, setNameInputValue] = useState(group.name); const { hasPermission } = useResourcePermissions('promptGroup', group._id || ''); - const canEdit = hasPermission(PERMISSION_BITS.EDIT); - const canDelete = hasPermission(PERMISSION_BITS.DELETE); + const canEdit = hasPermission(PermissionBits.EDIT); + const canDelete = hasPermission(PermissionBits.DELETE); const isGlobalGroup = useMemo( () => instanceProjectId && group.projectIds?.includes(instanceProjectId), diff --git a/client/src/components/Prompts/PromptForm.tsx b/client/src/components/Prompts/PromptForm.tsx index 6f305f0f5..60501ee0a 100644 --- a/client/src/components/Prompts/PromptForm.tsx +++ b/client/src/components/Prompts/PromptForm.tsx @@ -6,7 +6,7 @@ import { Menu, Rocket } from 'lucide-react'; import { useForm, FormProvider } from 'react-hook-form'; import { useParams, useOutletContext } from 'react-router-dom'; import { Button, Skeleton, useToastContext } from '@librechat/client'; -import { Permissions, PermissionTypes, PERMISSION_BITS } from 'librechat-data-provider'; +import { Permissions, PermissionTypes, PermissionBits } from 'librechat-data-provider'; import type { TCreatePrompt, TPrompt, TPromptGroup } from 'librechat-data-provider'; import { useGetPrompts, @@ -186,8 +186,8 @@ const PromptForm = () => { group?._id || '', ); - const canEdit = hasPermission(PERMISSION_BITS.EDIT); - const canView = hasPermission(PERMISSION_BITS.VIEW); + const canEdit = hasPermission(PermissionBits.EDIT); + const canView = hasPermission(PermissionBits.VIEW); const methods = useForm({ defaultValues: { diff --git a/client/src/components/Prompts/SharePrompt.tsx b/client/src/components/Prompts/SharePrompt.tsx index 8582e5410..9dda10066 100644 --- a/client/src/components/Prompts/SharePrompt.tsx +++ b/client/src/components/Prompts/SharePrompt.tsx @@ -3,8 +3,9 @@ import { Share2Icon } from 'lucide-react'; import { SystemRoles, Permissions, + ResourceType, PermissionTypes, - PERMISSION_BITS, + PermissionBits, } from 'librechat-data-provider'; import { Button } from '@librechat/client'; import type { TPromptGroup } from 'librechat-data-provider'; @@ -25,7 +26,7 @@ const SharePrompt = React.memo( // The query will be disabled if groupId is empty const groupId = group?._id || ''; const { hasPermission, isLoading: permissionsLoading } = useResourcePermissions( - 'promptGroup', + ResourceType.PROMPTGROUP, groupId, ); @@ -34,7 +35,7 @@ const SharePrompt = React.memo( return null; } - const canShareThisPrompt = hasPermission(PERMISSION_BITS.SHARE); + const canShareThisPrompt = hasPermission(PermissionBits.SHARE); const shouldShowShareButton = (group.author === user?.id || user?.role === SystemRoles.ADMIN || canShareThisPrompt) && @@ -49,7 +50,7 @@ const SharePrompt = React.memo(