🔧 refactor: Organize Sharing/Agent Components and Improve Type Safety

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

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

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

chore: remove PublicSharingToggle component and update index exports

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

chore: move AgentCategoryDisplay component with tests

chore: remove commented out code

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

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

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

refactor: remove unused canAccessPromptResource middleware and related code

refactor: remove unused prompt access roles from createAccessRoleMethods

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

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

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

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

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

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

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

chore: move SearchPicker to PeoplePicker dir

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

chore: fix typing, import order for agent admin settings

fix: agent admin settings, prevent agent form submission

refactor: rename `ACCESS_ROLE_IDS` to `AccessRoleIds`

refactor: replace PermissionBits with PERMISSION_BITS

refactor: replace PERMISSION_BITS with PermissionBits
This commit is contained in:
Danny Avila 2025-07-28 17:52:36 -04:00
parent ae732b2ebc
commit 81b32e400a
No known key found for this signature in database
GPG key ID: BF31EEB2C5CA0956
96 changed files with 781 additions and 798 deletions

View file

@ -1,16 +1,4 @@
/**
* Permission bit flags
*/
export enum PermissionBits {
/** 0001 - Can view/access the resource */
VIEW = 1,
/** 0010 - Can modify the resource */
EDIT = 2,
/** 0100 - Can delete the resource */
DELETE = 4,
/** 1000 - Can share the resource with others */
SHARE = 8,
}
import { PermissionBits } from 'librechat-data-provider';
/**
* Common role combinations

View file

@ -1,9 +1,10 @@
import mongoose from 'mongoose';
import { AccessRoleIds, ResourceType, PermissionBits } from 'librechat-data-provider';
import { MongoMemoryServer } from 'mongodb-memory-server';
import { createAccessRoleMethods } from './accessRole';
import { PermissionBits, RoleBits } from '~/common';
import accessRoleSchema from '~/schema/accessRole';
import type * as t from '~/types';
import { createAccessRoleMethods } from './accessRole';
import accessRoleSchema from '~/schema/accessRole';
import { RoleBits } from '~/common';
let mongoServer: MongoMemoryServer;
let AccessRole: mongoose.Model<t.IAccessRole>;
@ -32,7 +33,7 @@ describe('AccessRole Model Tests', () => {
accessRoleId: 'test_viewer',
name: 'Test Viewer',
description: 'Test role for viewer permissions',
resourceType: 'agent',
resourceType: ResourceType.AGENT,
permBits: RoleBits.VIEWER,
};
@ -98,7 +99,7 @@ describe('AccessRole Model Tests', () => {
accessRoleId: 'test_editor',
name: 'Test Editor',
description: 'Test role for editor permissions',
resourceType: 'agent',
resourceType: ResourceType.AGENT,
permBits: RoleBits.EDITOR,
},
];
@ -120,17 +121,17 @@ describe('AccessRole Model Tests', () => {
// Create sample roles for testing
await Promise.all([
methods.createRole({
accessRoleId: 'agent_viewer',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
name: 'Agent Viewer',
description: 'Can view agents',
resourceType: 'agent',
resourceType: ResourceType.AGENT,
permBits: RoleBits.VIEWER,
}),
methods.createRole({
accessRoleId: 'agent_editor',
accessRoleId: AccessRoleIds.AGENT_EDITOR,
name: 'Agent Editor',
description: 'Can edit agents',
resourceType: 'agent',
resourceType: ResourceType.AGENT,
permBits: RoleBits.EDITOR,
}),
methods.createRole({
@ -154,7 +155,7 @@ describe('AccessRole Model Tests', () => {
const agentRoles = await methods.findRolesByResourceType('agent');
expect(agentRoles).toHaveLength(2);
expect(agentRoles.map((r) => r.accessRoleId).sort()).toEqual(
['agent_editor', 'agent_viewer'].sort(),
[AccessRoleIds.AGENT_EDITOR, AccessRoleIds.AGENT_VIEWER].sort(),
);
const projectRoles = await methods.findRolesByResourceType('project');
@ -167,11 +168,11 @@ describe('AccessRole Model Tests', () => {
test('should find role by permissions', async () => {
const viewerRole = await methods.findRoleByPermissions('agent', RoleBits.VIEWER);
expect(viewerRole).toBeDefined();
expect(viewerRole?.accessRoleId).toBe('agent_viewer');
expect(viewerRole?.accessRoleId).toBe(AccessRoleIds.AGENT_VIEWER);
const editorRole = await methods.findRoleByPermissions('agent', RoleBits.EDITOR);
expect(editorRole).toBeDefined();
expect(editorRole?.accessRoleId).toBe('agent_editor');
expect(editorRole?.accessRoleId).toBe(AccessRoleIds.AGENT_EDITOR);
});
test('should return null when no role matches the permissions', async () => {
@ -192,19 +193,26 @@ describe('AccessRole Model Tests', () => {
// Verify the result contains the default roles
expect(Object.keys(result).sort()).toEqual(
['agent_editor', 'agent_owner', 'agent_viewer'].sort(),
[
AccessRoleIds.AGENT_EDITOR,
AccessRoleIds.AGENT_OWNER,
AccessRoleIds.AGENT_VIEWER,
AccessRoleIds.PROMPTGROUP_EDITOR,
AccessRoleIds.PROMPTGROUP_OWNER,
AccessRoleIds.PROMPTGROUP_VIEWER,
].sort(),
);
// Verify each role exists in the database
const agentViewerRole = await methods.findRoleByIdentifier('agent_viewer');
const agentViewerRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER);
expect(agentViewerRole).toBeDefined();
expect(agentViewerRole?.permBits).toBe(RoleBits.VIEWER);
const agentEditorRole = await methods.findRoleByIdentifier('agent_editor');
const agentEditorRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_EDITOR);
expect(agentEditorRole).toBeDefined();
expect(agentEditorRole?.permBits).toBe(RoleBits.EDITOR);
const agentOwnerRole = await methods.findRoleByIdentifier('agent_owner');
const agentOwnerRole = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_OWNER);
expect(agentOwnerRole).toBeDefined();
expect(agentOwnerRole?.permBits).toBe(RoleBits.OWNER);
});
@ -212,10 +220,10 @@ describe('AccessRole Model Tests', () => {
test('should not modify existing roles when seeding', async () => {
// Create a modified version of a default role
const customRole = {
accessRoleId: 'agent_viewer',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
name: 'Custom Viewer',
description: 'Custom viewer description',
resourceType: 'agent',
resourceType: ResourceType.AGENT,
permBits: RoleBits.VIEWER,
};
@ -225,7 +233,7 @@ describe('AccessRole Model Tests', () => {
await methods.seedDefaultRoles();
// Verify the custom role was not modified
const role = await methods.findRoleByIdentifier('agent_viewer');
const role = await methods.findRoleByIdentifier(AccessRoleIds.AGENT_VIEWER);
expect(role?.name).toBe(customRole.name);
expect(role?.description).toBe(customRole.description);
});
@ -238,27 +246,27 @@ describe('AccessRole Model Tests', () => {
// Create sample roles with ascending permission levels
await Promise.all([
methods.createRole({
accessRoleId: 'agent_viewer',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
name: 'Agent Viewer',
resourceType: 'agent',
resourceType: ResourceType.AGENT,
permBits: RoleBits.VIEWER, // 1
}),
methods.createRole({
accessRoleId: 'agent_editor',
accessRoleId: AccessRoleIds.AGENT_EDITOR,
name: 'Agent Editor',
resourceType: 'agent',
resourceType: ResourceType.AGENT,
permBits: RoleBits.EDITOR, // 3
}),
methods.createRole({
accessRoleId: 'agent_manager',
name: 'Agent Manager',
resourceType: 'agent',
resourceType: ResourceType.AGENT,
permBits: RoleBits.MANAGER, // 7
}),
methods.createRole({
accessRoleId: 'agent_owner',
accessRoleId: AccessRoleIds.AGENT_OWNER,
name: 'Agent Owner',
resourceType: 'agent',
resourceType: ResourceType.AGENT,
permBits: RoleBits.OWNER, // 15
}),
]);
@ -267,7 +275,7 @@ describe('AccessRole Model Tests', () => {
test('should find exact matching role', async () => {
const role = await methods.getRoleForPermissions('agent', RoleBits.EDITOR);
expect(role).toBeDefined();
expect(role?.accessRoleId).toBe('agent_editor');
expect(role?.accessRoleId).toBe(AccessRoleIds.AGENT_EDITOR);
expect(role?.permBits).toBe(RoleBits.EDITOR);
});
@ -278,7 +286,7 @@ describe('AccessRole Model Tests', () => {
// Should return VIEWER (1) as closest matching role without exceeding the permission bits
const role = await methods.getRoleForPermissions('agent', customPerm);
expect(role).toBeDefined();
expect(role?.accessRoleId).toBe('agent_viewer');
expect(role?.accessRoleId).toBe(AccessRoleIds.AGENT_VIEWER);
});
test('should return null when no compatible role is found', async () => {
@ -301,7 +309,7 @@ describe('AccessRole Model Tests', () => {
// Query for agent roles
const agentRole = await methods.getRoleForPermissions('agent', RoleBits.VIEWER);
expect(agentRole).toBeDefined();
expect(agentRole?.accessRoleId).toBe('agent_viewer');
expect(agentRole?.accessRoleId).toBe(AccessRoleIds.AGENT_VIEWER);
// Query for project roles
const projectRole = await methods.getRoleForPermissions('project', RoleBits.VIEWER);

View file

@ -1,6 +1,7 @@
import { AccessRoleIds, ResourceType, PermissionBits } from 'librechat-data-provider';
import type { Model, Types, DeleteResult } from 'mongoose';
import { RoleBits, PermissionBits } from '~/common';
import type { IAccessRole } from '~/types';
import { RoleBits } from '~/common';
export function createAccessRoleMethods(mongoose: typeof import('mongoose')) {
/**
@ -104,68 +105,45 @@ export function createAccessRoleMethods(mongoose: typeof import('mongoose')) {
const AccessRole = mongoose.models.AccessRole as Model<IAccessRole>;
const defaultRoles = [
{
accessRoleId: 'agent_viewer',
accessRoleId: AccessRoleIds.AGENT_VIEWER,
name: 'com_ui_role_viewer',
description: 'com_ui_role_viewer_desc',
resourceType: 'agent',
resourceType: ResourceType.AGENT,
permBits: RoleBits.VIEWER,
},
{
accessRoleId: 'agent_editor',
accessRoleId: AccessRoleIds.AGENT_EDITOR,
name: 'com_ui_role_editor',
description: 'com_ui_role_editor_desc',
resourceType: 'agent',
resourceType: ResourceType.AGENT,
permBits: RoleBits.EDITOR,
},
{
accessRoleId: 'agent_owner',
accessRoleId: AccessRoleIds.AGENT_OWNER,
name: 'com_ui_role_owner',
description: 'com_ui_role_owner_desc',
resourceType: 'agent',
resourceType: ResourceType.AGENT,
permBits: RoleBits.OWNER,
},
// Prompt access roles
{
accessRoleId: 'prompt_viewer',
accessRoleId: AccessRoleIds.PROMPTGROUP_VIEWER,
name: 'com_ui_role_viewer',
description: 'com_ui_role_viewer_desc',
resourceType: 'prompt',
resourceType: ResourceType.PROMPTGROUP,
permBits: RoleBits.VIEWER,
},
{
accessRoleId: 'prompt_editor',
accessRoleId: AccessRoleIds.PROMPTGROUP_EDITOR,
name: 'com_ui_role_editor',
description: 'com_ui_role_editor_desc',
resourceType: 'prompt',
resourceType: ResourceType.PROMPTGROUP,
permBits: RoleBits.EDITOR,
},
{
accessRoleId: 'prompt_owner',
accessRoleId: AccessRoleIds.PROMPTGROUP_OWNER,
name: 'com_ui_role_owner',
description: 'com_ui_role_owner_desc',
resourceType: 'prompt',
permBits: RoleBits.OWNER,
},
// PromptGroup access roles
{
accessRoleId: 'promptGroup_viewer',
name: 'com_ui_role_viewer',
description: 'com_ui_role_viewer_desc',
resourceType: 'promptGroup',
permBits: RoleBits.VIEWER,
},
{
accessRoleId: 'promptGroup_editor',
name: 'com_ui_role_editor',
description: 'com_ui_role_editor_desc',
resourceType: 'promptGroup',
permBits: RoleBits.EDITOR,
},
{
accessRoleId: 'promptGroup_owner',
name: 'com_ui_role_owner',
description: 'com_ui_role_owner_desc',
resourceType: 'promptGroup',
resourceType: ResourceType.PROMPTGROUP,
permBits: RoleBits.OWNER,
},
];

View file

@ -1,9 +1,9 @@
import mongoose from 'mongoose';
import { ResourceType, PermissionBits } from 'librechat-data-provider';
import { MongoMemoryServer } from 'mongodb-memory-server';
import { createAclEntryMethods } from './aclEntry';
import { PermissionBits } from '~/common';
import aclEntrySchema from '~/schema/aclEntry';
import type * as t from '~/types';
import { createAclEntryMethods } from './aclEntry';
import aclEntrySchema from '~/schema/aclEntry';
let mongoServer: MongoMemoryServer;
let AclEntry: mongoose.Model<t.IAclEntry>;
@ -38,7 +38,7 @@ describe('AclEntry Model Tests', () => {
const entry = await methods.grantPermission(
'user',
userId,
'agent',
ResourceType.AGENT,
resourceId,
PermissionBits.VIEW,
grantedById,
@ -59,7 +59,7 @@ describe('AclEntry Model Tests', () => {
const entry = await methods.grantPermission(
'group',
groupId,
'agent',
ResourceType.AGENT,
resourceId,
PermissionBits.VIEW | PermissionBits.EDIT,
grantedById,
@ -76,7 +76,7 @@ describe('AclEntry Model Tests', () => {
const entry = await methods.grantPermission(
'public',
null,
'agent',
ResourceType.AGENT,
resourceId,
PermissionBits.VIEW,
grantedById,
@ -93,7 +93,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission(
'user',
userId,
'agent',
ResourceType.AGENT,
resourceId,
PermissionBits.VIEW,
grantedById,
@ -122,7 +122,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission(
'user',
userId,
'agent',
ResourceType.AGENT,
resourceId,
PermissionBits.VIEW,
grantedById,
@ -130,7 +130,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission(
'group',
groupId,
'agent',
ResourceType.AGENT,
resourceId,
PermissionBits.EDIT,
grantedById,
@ -138,7 +138,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission(
'public',
null,
'agent',
ResourceType.AGENT,
resourceId,
PermissionBits.VIEW,
grantedById,
@ -155,7 +155,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission(
'user',
userId,
'agent',
ResourceType.AGENT,
resourceId,
PermissionBits.VIEW,
grantedById,
@ -163,7 +163,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission(
'group',
groupId,
'agent',
ResourceType.AGENT,
resourceId,
PermissionBits.EDIT,
grantedById,
@ -173,7 +173,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission(
'public',
null,
'agent',
ResourceType.AGENT,
otherResourceId,
PermissionBits.VIEW,
grantedById,
@ -188,7 +188,7 @@ describe('AclEntry Model Tests', () => {
const entries = await methods.findEntriesByPrincipalsAndResource(
principalsList,
'agent',
ResourceType.AGENT,
resourceId,
);
expect(entries).toHaveLength(2);
@ -200,7 +200,7 @@ describe('AclEntry Model Tests', () => {
/** User has VIEW permission */
const hasViewPermission = await methods.hasPermission(
principalsList,
'agent',
ResourceType.AGENT,
resourceId,
PermissionBits.VIEW,
);
@ -209,7 +209,7 @@ describe('AclEntry Model Tests', () => {
/** User doesn't have EDIT permission */
const hasEditPermission = await methods.hasPermission(
principalsList,
'agent',
ResourceType.AGENT,
resourceId,
PermissionBits.EDIT,
);
@ -222,7 +222,7 @@ describe('AclEntry Model Tests', () => {
/** Group has EDIT permission */
const hasEditPermission = await methods.hasPermission(
principalsList,
'agent',
ResourceType.AGENT,
resourceId,
PermissionBits.EDIT,
);
@ -238,7 +238,7 @@ describe('AclEntry Model Tests', () => {
/** User has VIEW and group has EDIT, together they should have both */
const hasViewPermission = await methods.hasPermission(
principalsList,
'agent',
ResourceType.AGENT,
resourceId,
PermissionBits.VIEW,
);
@ -246,7 +246,7 @@ describe('AclEntry Model Tests', () => {
const hasEditPermission = await methods.hasPermission(
principalsList,
'agent',
ResourceType.AGENT,
resourceId,
PermissionBits.EDIT,
);
@ -255,7 +255,7 @@ describe('AclEntry Model Tests', () => {
/** Neither has DELETE permission */
const hasDeletePermission = await methods.hasPermission(
principalsList,
'agent',
ResourceType.AGENT,
resourceId,
PermissionBits.DELETE,
);
@ -281,7 +281,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission(
'user',
userId,
'agent',
ResourceType.AGENT,
resourceId,
PermissionBits.VIEW,
grantedById,
@ -292,7 +292,7 @@ describe('AclEntry Model Tests', () => {
expect(entriesBefore).toHaveLength(1);
/** Revoke it */
const result = await methods.revokePermission('user', userId, 'agent', resourceId);
const result = await methods.revokePermission('user', userId, ResourceType.AGENT, resourceId);
expect(result.deletedCount).toBe(1);
/** Verify it's gone */
@ -305,7 +305,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission(
'user',
userId,
'agent',
ResourceType.AGENT,
resourceId,
PermissionBits.VIEW,
grantedById,
@ -315,7 +315,7 @@ describe('AclEntry Model Tests', () => {
const updated = await methods.modifyPermissionBits(
'user',
userId,
'agent',
ResourceType.AGENT,
resourceId,
PermissionBits.EDIT,
null,
@ -330,7 +330,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission(
'user',
userId,
'agent',
ResourceType.AGENT,
resourceId,
PermissionBits.VIEW | PermissionBits.EDIT,
grantedById,
@ -340,7 +340,7 @@ describe('AclEntry Model Tests', () => {
const updated = await methods.modifyPermissionBits(
'user',
userId,
'agent',
ResourceType.AGENT,
resourceId,
null,
PermissionBits.EDIT,
@ -355,7 +355,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission(
'user',
userId,
'agent',
ResourceType.AGENT,
resourceId,
PermissionBits.VIEW,
grantedById,
@ -387,7 +387,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission(
'user',
userId,
'agent',
ResourceType.AGENT,
resourceId1,
PermissionBits.VIEW,
grantedById,
@ -397,7 +397,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission(
'user',
userId,
'agent',
ResourceType.AGENT,
resourceId2,
PermissionBits.VIEW | PermissionBits.EDIT,
grantedById,
@ -407,7 +407,7 @@ describe('AclEntry Model Tests', () => {
await methods.grantPermission(
'group',
groupId,
'agent',
ResourceType.AGENT,
resourceId3,
PermissionBits.VIEW,
grantedById,
@ -416,7 +416,7 @@ describe('AclEntry Model Tests', () => {
/** Find resources with VIEW permission for user */
const userViewableResources = await methods.findAccessibleResources(
[{ principalType: 'user', principalId: userId }],
'agent',
ResourceType.AGENT,
PermissionBits.VIEW,
);
@ -431,7 +431,7 @@ describe('AclEntry Model Tests', () => {
{ principalType: 'user', principalId: userId },
{ principalType: 'group', principalId: groupId },
],
'agent',
ResourceType.AGENT,
PermissionBits.VIEW,
);
@ -440,7 +440,7 @@ describe('AclEntry Model Tests', () => {
/** Find resources with EDIT permission for user */
const editableResources = await methods.findAccessibleResources(
[{ principalType: 'user', principalId: userId }],
'agent',
ResourceType.AGENT,
PermissionBits.EDIT,
);
@ -467,7 +467,7 @@ describe('AclEntry Model Tests', () => {
principalType: 'user',
principalId: userId,
principalModel: 'User',
resourceType: 'agent',
resourceType: ResourceType.AGENT,
resourceId: childResourceId,
permBits: PermissionBits.VIEW,
grantedBy: grantedById,
@ -477,7 +477,7 @@ describe('AclEntry Model Tests', () => {
/** Get effective permissions */
const effective = await methods.getEffectivePermissions(
[{ principalType: 'user', principalId: userId }],
'agent',
ResourceType.AGENT,
childResourceId,
);

View file

@ -16,7 +16,7 @@ const accessRoleSchema = new Schema<IAccessRole>(
description: String,
resourceType: {
type: String,
enum: ['agent', 'project', 'file', 'prompt', 'promptGroup'],
enum: ['agent', 'project', 'file', 'promptGroup'],
required: true,
default: 'agent',
},

View file

@ -7,8 +7,8 @@ export type AclEntry = {
principalId?: Types.ObjectId;
/** The model name for the principal ('User' or 'Group') */
principalModel?: 'User' | 'Group';
/** The type of resource ('agent', 'project', 'file', 'prompt', 'promptGroup') */
resourceType: 'agent' | 'project' | 'file' | 'prompt' | 'promptGroup';
/** The type of resource ('agent', 'project', 'file', 'promptGroup') */
resourceType: 'agent' | 'project' | 'file' | 'promptGroup';
/** The ID of the resource */
resourceId: Types.ObjectId;
/** Permission bits for this entry */