mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-22 06:00:56 +02:00

bugfix: Enhance Agent and AgentCategory schemas with new fields for category, support contact, and promotion status refactored and moved agent category methods and schema to data-schema package 🔧 fix: Merge and Rebase Conflicts - Move AgentCategory from api/models to @packages/data-schemas structure - Add schema, types, methods, and model following codebase conventions - Implement auto-seeding of default categories during AppService startup - Update marketplace controller to use new data-schemas methods - Remove old model file and standalone seed script refactor: unify agent marketplace to single endpoint with cursor pagination - Replace multiple marketplace routes with unified /marketplace endpoint - Add query string controls: category, search, limit, cursor, promoted, requiredPermission - Implement cursor-based pagination replacing page-based system - Integrate ACL permissions for proper access control - Fix ObjectId constructor error in Agent model - Update React components to use unified useGetMarketplaceAgentsQuery hook - Enhance type safety and remove deprecated useDynamicAgentQuery - Update tests for new marketplace architecture -Known issues: see more button after category switching + Unit tests feat: add icon property to ProcessedAgentCategory interface - Add useMarketplaceAgentsInfiniteQuery and useGetAgentCategoriesQuery to client/src/data-provider/Agents/ - Replace manual pagination in AgentGrid with infinite query pattern - Update imports to use local data provider instead of librechat-data-provider - Add proper permission handling with PERMISSION_BITS.VIEW/EDIT constants - Improve agent access control by adding requiredPermission validation in backend - Remove manual cursor/state management in favor of infinite query built-ins - Maintain existing search and category filtering functionality refactor: consolidate agent marketplace endpoints into main agents API and improve data management consistency - Remove dedicated marketplace controller and routes, merging functionality into main agents v1 API - Add countPromotedAgents function to Agent model for promoted agents count - Enhance getListAgents handler with marketplace filtering (category, search, promoted status) - Move getAgentCategories from marketplace to v1 controller with same functionality - Update agent mutations to invalidate marketplace queries and handle multiple permission levels - Improve cache management by updating all agent query variants (VIEW/EDIT permissions) - Consolidate agent data access patterns for better maintainability and consistency - Remove duplicate marketplace route definitions and middleware selected view only agents injected in the drop down fix: remove minlength validation for support contact name in agent schema feat: add validation and error messages for agent name in AgentConfig and AgentPanel fix: update agent permission check logic in AgentPanel to simplify condition Fix linting WIP Fix Unit tests WIP ESLint fixes eslint fix refactor: enhance isDuplicateVersion function in Agent model for improved comparison logic - Introduced handling for undefined/null values in array and object comparisons. - Normalized array comparisons to treat undefined/null as empty arrays. - Added deep comparison for objects and improved handling of primitive values. - Enhanced projectIds comparison to ensure consistent MongoDB ObjectId handling. refactor: remove redundant properties from IAgent interface in agent schema chore: update localization for agent detail component and clean up imports ci: update access middleware tests chore: remove unused PermissionTypes import from Role model ci: update AclEntry model tests ci: update button accessibility labels in AgentDetail tests refactor: update exhaustive dep. lint warning 🔧 fix: Fixed agent actions access feat: Add role-level permissions for agent sharing people picker - Add PEOPLE_PICKER permission type with VIEW_USERS and VIEW_GROUPS permissions - Create custom middleware for query-aware permission validation - Implement permission-based type filtering in PeoplePicker component - Hide people picker UI when user lacks permissions, show only public toggle - Support granular access: users-only, groups-only, or mixed search modes refactor: Replace marketplace interface config with permission-based system - Add MARKETPLACE permission type to handle marketplace access control - Update interface configuration to use role-based marketplace settings (admin/user) - Replace direct marketplace boolean config with permission-based checks - Modify frontend components to use marketplace permissions instead of interface config - Update agent query hooks to use marketplace permissions for determining permission levels - Add marketplace configuration structure similar to peoplePicker in YAML config - Backend now sets MARKETPLACE permissions based on interface configuration - When marketplace enabled: users get agents with EDIT permissions in dropdown lists (builder mode) - When marketplace disabled: users get agents with VIEW permissions in dropdown lists (browse mode) 🔧 fix: Redirect to New Chat if No Marketplace Access and Required Agent Name Placeholder (#8213) * Fix: Fix the redirect to new chat page if access to marketplace is denied * Fixed the required agent name placeholder --------- Co-authored-by: Atef Bellaaj <slalom.bellaaj@external.daimlertruck.com> chore: fix tests, remove unnecessary imports refactor: Implement permission checks for file access via agents - Updated `hasAccessToFilesViaAgent` to utilize permission checks for VIEW and EDIT access. - Replaced project-based access validation with permission-based checks. - Enhanced tests to cover new permission logic and ensure proper access control for files associated with agents. - Cleaned up imports and initialized models in test files for consistency. refactor: Enhance test setup and cleanup for file access control - Introduced modelsToCleanup array to track models added during tests for proper cleanup. - Updated afterAll hooks in test files to ensure all collections are cleared and only added models are deleted. - Improved consistency in model initialization across test files. - Added comments for clarity on cleanup processes and test data management. chore: Update Jest configuration and test setup for improved timeout handling - Added a global test timeout of 30 seconds in jest.config.js. - Configured jest.setTimeout in jestSetup.js to allow individual test overrides if needed. - Enhanced test reliability by ensuring consistent timeout settings across all tests. refactor: Implement file access filtering based on agent permissions - Introduced `filterFilesByAgentAccess` function to filter files based on user access through agents. - Updated `getFiles` and `primeFiles` functions to utilize the new filtering logic. - Moved `hasAccessToFilesViaAgent` function from the File model to permission services, adjusting imports accordingly - Enhanced tests to ensure proper access control and filtering behavior for files associated with agents. fix: make support_contact field a nested object rather than a sub-document refactor: Update support_contact field initialization in agent model - Removed handling for empty support_contact object in createAgent function. - Changed default value of support_contact in agent schema to undefined. test: Add comprehensive tests for support_contact field handling and versioning refactor: remove unused avatar upload mutation field and add informational toast for success chore: add missing SidePanelProvider for AgentMarketplace and organize imports fix: resolve agent selection race condition in marketplace HandleStartChat - Set agent in localStorage before newConversation to prevent useSelectorEffects from auto-selecting previous agent fix: resolve agent dropdown showing raw ID instead of agent info from URL - Add proactive agent fetching when agent_id is present in URL parameters - Inject fetched agent into agents cache so dropdowns display proper name/avatar - Use useAgentsMap dependency to ensure proper cache initialization timing - Prevents raw agent IDs from showing in UI when visiting shared agent links Fix: Agents endpoint renamed to "My Agent" for less confusion with the Marketplace agents. chore: fix ESLint issues and Test Mocks ci: update permissions structure in loadDefaultInterface tests - Refactored permissions for MEMORY and added new permissions for MARKETPLACE and PEOPLE_PICKER. - Ensured consistent structure for permissions across different types. feat: support_contact validation to allow empty email strings
352 lines
12 KiB
TypeScript
352 lines
12 KiB
TypeScript
import React, { useState, useEffect } from 'react';
|
|
import { ACCESS_ROLE_IDS, TPrincipal } from 'librechat-data-provider';
|
|
import { Settings, Users, Loader, UserCheck, Trash2, Shield } from 'lucide-react';
|
|
import {
|
|
useGetAccessRolesQuery,
|
|
useGetResourcePermissionsQuery,
|
|
useUpdateResourcePermissionsMutation,
|
|
} from 'librechat-data-provider/react-query';
|
|
import {
|
|
Button,
|
|
OGDialog,
|
|
OGDialogTitle,
|
|
OGDialogClose,
|
|
OGDialogContent,
|
|
OGDialogTrigger,
|
|
useToastContext,
|
|
} from '@librechat/client';
|
|
import SelectedPrincipalsList from './PeoplePicker/SelectedPrincipalsList';
|
|
import PublicSharingToggle from './PublicSharingToggle';
|
|
import { cn, removeFocusOutlines } from '~/utils';
|
|
import { useLocalize } from '~/hooks';
|
|
|
|
export default function ManagePermissionsDialog({
|
|
agentDbId,
|
|
agentName,
|
|
resourceType = 'agent',
|
|
onUpdatePermissions,
|
|
}: {
|
|
agentDbId: string;
|
|
agentName?: string;
|
|
resourceType?: string;
|
|
onUpdatePermissions?: (shares: TPrincipal[], isPublic: boolean, publicRole: string) => void;
|
|
}) {
|
|
const localize = useLocalize();
|
|
const { showToast } = useToastContext();
|
|
|
|
const {
|
|
data: permissionsData,
|
|
isLoading: isLoadingPermissions,
|
|
error: permissionsError,
|
|
} = useGetResourcePermissionsQuery(resourceType, agentDbId, {
|
|
enabled: !!agentDbId,
|
|
});
|
|
const {
|
|
data: accessRoles,
|
|
// isLoading,
|
|
} = useGetAccessRolesQuery(resourceType);
|
|
|
|
const updatePermissionsMutation = useUpdateResourcePermissionsMutation();
|
|
|
|
const [managedShares, setManagedShares] = useState<TPrincipal[]>([]);
|
|
const [managedIsPublic, setManagedIsPublic] = useState(false);
|
|
const [managedPublicRole, setManagedPublicRole] = useState<string>(ACCESS_ROLE_IDS.AGENT_VIEWER);
|
|
const [isModalOpen, setIsModalOpen] = useState(false);
|
|
const [hasChanges, setHasChanges] = useState(false);
|
|
|
|
const currentShares: TPrincipal[] = permissionsData?.principals || [];
|
|
|
|
const isPublic = permissionsData?.public || false;
|
|
const publicRole = permissionsData?.publicAccessRoleId || ACCESS_ROLE_IDS.AGENT_VIEWER;
|
|
|
|
useEffect(() => {
|
|
if (permissionsData) {
|
|
const shares = permissionsData.principals || [];
|
|
const isPublicValue = permissionsData.public || false;
|
|
const publicRoleValue = permissionsData.publicAccessRoleId || ACCESS_ROLE_IDS.AGENT_VIEWER;
|
|
|
|
setManagedShares(shares);
|
|
setManagedIsPublic(isPublicValue);
|
|
setManagedPublicRole(publicRoleValue);
|
|
setHasChanges(false);
|
|
}
|
|
}, [permissionsData, isModalOpen]);
|
|
|
|
if (!agentDbId) {
|
|
return null;
|
|
}
|
|
|
|
if (permissionsError) {
|
|
return <div className="text-sm text-red-600">{localize('com_ui_permissions_failed_load')}</div>;
|
|
}
|
|
|
|
const handleRemoveShare = (idOnTheSource: string) => {
|
|
setManagedShares(managedShares.filter((s) => s.idOnTheSource !== idOnTheSource));
|
|
setHasChanges(true);
|
|
};
|
|
|
|
const handleRoleChange = (idOnTheSource: string, newRole: string) => {
|
|
setManagedShares(
|
|
managedShares.map((s) =>
|
|
s.idOnTheSource === idOnTheSource ? { ...s, accessRoleId: newRole } : s,
|
|
),
|
|
);
|
|
setHasChanges(true);
|
|
};
|
|
|
|
const handleSaveChanges = async () => {
|
|
try {
|
|
const originalSharesMap = new Map(
|
|
currentShares.map((share) => [`${share.type}-${share.idOnTheSource}`, share]),
|
|
);
|
|
const managedSharesMap = new Map(
|
|
managedShares.map((share) => [`${share.type}-${share.idOnTheSource}`, share]),
|
|
);
|
|
|
|
const updated = managedShares.filter((share) => {
|
|
const key = `${share.type}-${share.idOnTheSource}`;
|
|
const original = originalSharesMap.get(key);
|
|
return !original || original.accessRoleId !== share.accessRoleId;
|
|
});
|
|
|
|
const removed = currentShares.filter((share) => {
|
|
const key = `${share.type}-${share.idOnTheSource}`;
|
|
return !managedSharesMap.has(key);
|
|
});
|
|
|
|
await updatePermissionsMutation.mutateAsync({
|
|
resourceType,
|
|
resourceId: agentDbId,
|
|
data: {
|
|
updated,
|
|
removed,
|
|
public: managedIsPublic,
|
|
publicAccessRoleId: managedIsPublic ? managedPublicRole : undefined,
|
|
},
|
|
});
|
|
|
|
if (onUpdatePermissions) {
|
|
onUpdatePermissions(managedShares, managedIsPublic, managedPublicRole);
|
|
}
|
|
|
|
showToast({
|
|
message: localize('com_ui_permissions_updated_success'),
|
|
status: 'success',
|
|
});
|
|
|
|
setIsModalOpen(false);
|
|
} catch (error) {
|
|
console.error('Error updating permissions:', error);
|
|
showToast({
|
|
message: localize('com_ui_permissions_failed_update'),
|
|
status: 'error',
|
|
});
|
|
}
|
|
};
|
|
|
|
const handleCancel = () => {
|
|
setManagedShares(currentShares);
|
|
setManagedIsPublic(isPublic);
|
|
setManagedPublicRole(publicRole);
|
|
setIsModalOpen(false);
|
|
};
|
|
|
|
const handleRevokeAll = () => {
|
|
setManagedShares([]);
|
|
setManagedIsPublic(false);
|
|
setHasChanges(true);
|
|
};
|
|
const handlePublicToggle = (isPublic: boolean) => {
|
|
setManagedIsPublic(isPublic);
|
|
setHasChanges(true);
|
|
if (!isPublic) {
|
|
setManagedPublicRole(ACCESS_ROLE_IDS.AGENT_VIEWER);
|
|
}
|
|
};
|
|
const handlePublicRoleChange = (role: string) => {
|
|
setManagedPublicRole(role);
|
|
setHasChanges(true);
|
|
};
|
|
const totalShares = managedShares.length + (managedIsPublic ? 1 : 0);
|
|
const originalTotalShares = currentShares.length + (isPublic ? 1 : 0);
|
|
|
|
/** Check if there's at least one owner (user, group, or public with owner role) */
|
|
const hasAtLeastOneOwner =
|
|
managedShares.some((share) => share.accessRoleId === ACCESS_ROLE_IDS.AGENT_OWNER) ||
|
|
(managedIsPublic && managedPublicRole === ACCESS_ROLE_IDS.AGENT_OWNER);
|
|
|
|
let peopleLabel = localize('com_ui_people');
|
|
if (managedShares.length === 1) {
|
|
peopleLabel = localize('com_ui_person');
|
|
}
|
|
|
|
let buttonAriaLabel = localize('com_ui_manage_permissions_for') + ' agent';
|
|
if (agentName != null && agentName !== '') {
|
|
buttonAriaLabel = localize('com_ui_manage_permissions_for') + ` "${agentName}"`;
|
|
}
|
|
|
|
let dialogTitle = localize('com_ui_manage_permissions_for') + ' Agent';
|
|
if (agentName != null && agentName !== '') {
|
|
dialogTitle = localize('com_ui_manage_permissions_for') + ` "${agentName}"`;
|
|
}
|
|
|
|
let publicSuffix = '';
|
|
if (managedIsPublic) {
|
|
publicSuffix = localize('com_ui_and_public');
|
|
}
|
|
|
|
return (
|
|
<OGDialog open={isModalOpen} onOpenChange={setIsModalOpen}>
|
|
<OGDialogTrigger asChild>
|
|
<button
|
|
className={cn(
|
|
'btn btn-neutral border-token-border-light relative h-9 rounded-lg font-medium',
|
|
removeFocusOutlines,
|
|
)}
|
|
aria-label={buttonAriaLabel}
|
|
type="button"
|
|
>
|
|
<div className="flex items-center justify-center gap-2 text-blue-500">
|
|
<Settings className="icon-md h-4 w-4" />
|
|
<span className="hidden sm:inline">{localize('com_ui_manage')}</span>
|
|
{originalTotalShares > 0 && `(${originalTotalShares})`}
|
|
</div>
|
|
</button>
|
|
</OGDialogTrigger>
|
|
|
|
<OGDialogContent className="max-h-[90vh] w-11/12 overflow-y-auto md:max-w-3xl">
|
|
<OGDialogTitle>
|
|
<div className="flex items-center gap-2">
|
|
<Shield className="h-5 w-5 text-blue-500" />
|
|
{dialogTitle}
|
|
</div>
|
|
</OGDialogTitle>
|
|
|
|
<div className="space-y-6 p-2">
|
|
<div className="rounded-lg bg-surface-tertiary p-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h3 className="text-sm font-medium text-text-primary">
|
|
{localize('com_ui_current_access')}
|
|
</h3>
|
|
<p className="text-xs text-text-secondary">
|
|
{(() => {
|
|
if (totalShares === 0) {
|
|
return localize('com_ui_no_users_groups_access');
|
|
}
|
|
return localize('com_ui_shared_with_count', {
|
|
0: managedShares.length,
|
|
1: peopleLabel,
|
|
2: publicSuffix,
|
|
});
|
|
})()}
|
|
</p>
|
|
</div>
|
|
{(managedShares.length > 0 || managedIsPublic) && (
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={handleRevokeAll}
|
|
className="text-red-600 hover:text-red-700"
|
|
>
|
|
<Trash2 className="mr-2 h-4 w-4" />
|
|
{localize('com_ui_revoke_all')}
|
|
</Button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{(() => {
|
|
if (isLoadingPermissions) {
|
|
return (
|
|
<div className="flex items-center justify-center p-8">
|
|
<Loader className="h-6 w-6 animate-spin" />
|
|
<span className="ml-2 text-sm text-text-secondary">
|
|
{localize('com_ui_loading_permissions')}
|
|
</span>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (managedShares.length > 0) {
|
|
return (
|
|
<div>
|
|
<h3 className="mb-3 flex items-center gap-2 text-sm font-medium text-text-primary">
|
|
<UserCheck className="h-4 w-4" />
|
|
{localize('com_ui_user_group_permissions')} ({managedShares.length})
|
|
</h3>
|
|
<SelectedPrincipalsList
|
|
principles={managedShares}
|
|
onRemoveHandler={handleRemoveShare}
|
|
availableRoles={accessRoles || []}
|
|
onRoleChange={(id, newRole) => handleRoleChange(id, newRole)}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="rounded-lg border-2 border-dashed border-border-light p-8 text-center">
|
|
<Users className="mx-auto h-8 w-8 text-text-secondary" />
|
|
<p className="mt-2 text-sm text-text-secondary">
|
|
{localize('com_ui_no_individual_access')}
|
|
</p>
|
|
</div>
|
|
);
|
|
})()}
|
|
|
|
<div>
|
|
<h3 className="mb-3 text-sm font-medium text-text-primary">
|
|
{localize('com_ui_public_access')}
|
|
</h3>
|
|
<PublicSharingToggle
|
|
isPublic={managedIsPublic}
|
|
publicRole={managedPublicRole}
|
|
onPublicToggle={handlePublicToggle}
|
|
onPublicRoleChange={handlePublicRoleChange}
|
|
/>
|
|
</div>
|
|
|
|
<div className="flex justify-end gap-3 border-t pt-4">
|
|
<OGDialogClose asChild>
|
|
<Button variant="outline" onClick={handleCancel}>
|
|
{localize('com_ui_cancel')}
|
|
</Button>
|
|
</OGDialogClose>
|
|
<Button
|
|
onClick={handleSaveChanges}
|
|
disabled={
|
|
updatePermissionsMutation.isLoading ||
|
|
!hasChanges ||
|
|
isLoadingPermissions ||
|
|
!hasAtLeastOneOwner
|
|
}
|
|
className="min-w-[120px]"
|
|
>
|
|
{updatePermissionsMutation.isLoading ? (
|
|
<div className="flex items-center gap-2">
|
|
<Loader className="h-4 w-4 animate-spin" />
|
|
{localize('com_ui_saving')}
|
|
</div>
|
|
) : (
|
|
localize('com_ui_save_changes')
|
|
)}
|
|
</Button>
|
|
</div>
|
|
|
|
{hasChanges && (
|
|
<div className="text-xs text-orange-600 dark:text-orange-400">
|
|
* {localize('com_ui_unsaved_changes')}
|
|
</div>
|
|
)}
|
|
|
|
{!hasAtLeastOneOwner && hasChanges && (
|
|
<div className="text-xs text-red-600 dark:text-red-400">
|
|
* {localize('com_ui_at_least_one_owner_required')}
|
|
</div>
|
|
)}
|
|
</div>
|
|
</OGDialogContent>
|
|
</OGDialog>
|
|
);
|
|
}
|