mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-01-26 12:16:13 +01:00
🛂 feat: Role as Permission Principal Type
WIP: Role as Permission Principal Type WIP: add user role check optimization to user principal check, update type comparisons WIP: cover edge cases for string vs ObjectId handling in permission granting and checking chore: Update people picker access middleware to use PrincipalType constants feat: Enhance people picker access control to include roles permissions chore: add missing default role schema values for people picker perms, cleanup typing feat: Enhance PeoplePicker component with role-specific UI and localization updates chore: Add missing `VIEW_ROLES` permission to role schema
This commit is contained in:
parent
28d63dab71
commit
39346d6b8e
49 changed files with 2879 additions and 258 deletions
|
|
@ -1,16 +1,17 @@
|
|||
import React, { useState, useMemo } from 'react';
|
||||
import { PrincipalType } from 'librechat-data-provider';
|
||||
import type { TPrincipal, PrincipalSearchParams } from 'librechat-data-provider';
|
||||
import { useSearchPrincipalsQuery } from 'librechat-data-provider/react-query';
|
||||
import { useLocalize, usePeoplePickerPermissions } from '~/hooks';
|
||||
import PeoplePickerSearchItem from './PeoplePickerSearchItem';
|
||||
import SelectedPrincipalsList from './SelectedPrincipalsList';
|
||||
import { SearchPicker } from './SearchPicker';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
interface PeoplePickerProps {
|
||||
onSelectionChange: (principals: TPrincipal[]) => void;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
typeFilter?: 'user' | 'group' | null;
|
||||
typeFilter?: PrincipalType.USER | PrincipalType.GROUP | PrincipalType.ROLE | null;
|
||||
}
|
||||
|
||||
export default function PeoplePicker({
|
||||
|
|
@ -20,6 +21,7 @@ export default function PeoplePicker({
|
|||
typeFilter = null,
|
||||
}: PeoplePickerProps) {
|
||||
const localize = useLocalize();
|
||||
const { canViewUsers, canViewGroups, canViewRoles } = usePeoplePickerPermissions();
|
||||
const [searchQuery, setSearchQuery] = useState('');
|
||||
const [selectedShares, setSelectedShares] = useState<TPrincipal[]>([]);
|
||||
|
||||
|
|
@ -54,6 +56,28 @@ export default function PeoplePicker({
|
|||
console.error('Principal search error:', error);
|
||||
}
|
||||
|
||||
/** Get appropriate label based on permissions */
|
||||
const getSearchLabel = () => {
|
||||
const permissions = [canViewUsers, canViewGroups, canViewRoles];
|
||||
const permissionCount = permissions.filter(Boolean).length;
|
||||
|
||||
if (permissionCount === 3) {
|
||||
return localize('com_ui_search_users_groups_roles');
|
||||
} else if (permissionCount === 2) {
|
||||
if (canViewUsers && canViewGroups) {
|
||||
return localize('com_ui_search_users_groups');
|
||||
}
|
||||
} else if (canViewUsers) {
|
||||
return localize('com_ui_search_users');
|
||||
} else if (canViewGroups) {
|
||||
return localize('com_ui_search_groups');
|
||||
} else if (canViewRoles) {
|
||||
return localize('com_ui_search_roles');
|
||||
}
|
||||
|
||||
return localize('com_ui_search_users_groups');
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={`space-y-3 ${className}`}>
|
||||
<div className="relative">
|
||||
|
|
@ -83,7 +107,7 @@ export default function PeoplePicker({
|
|||
});
|
||||
setSearchQuery('');
|
||||
}}
|
||||
label={localize('com_ui_search_users_groups')}
|
||||
label={getSearchLabel()}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
import React, { forwardRef } from 'react';
|
||||
import { PrincipalType } from 'librechat-data-provider';
|
||||
import type { TPrincipal } from 'librechat-data-provider';
|
||||
import { cn } from '~/utils';
|
||||
import PrincipalAvatar from '~/components/Sharing/PrincipalAvatar';
|
||||
import { useLocalize } from '~/hooks';
|
||||
import PrincipalAvatar from '../PrincipalAvatar';
|
||||
import { cn } from '~/utils';
|
||||
|
||||
interface PeoplePickerSearchItemProps extends React.HTMLAttributes<HTMLDivElement> {
|
||||
principal: TPrincipal;
|
||||
|
|
@ -16,10 +17,37 @@ const PeoplePickerSearchItem = forwardRef<HTMLDivElement, PeoplePickerSearchItem
|
|||
const localize = useLocalize();
|
||||
const { name, email, type } = principal;
|
||||
|
||||
// Display name with fallback
|
||||
const displayName = name || localize('com_ui_unknown');
|
||||
const subtitle = email || `${type} (${principal.source || 'local'})`;
|
||||
|
||||
/** Get badge styling based on type */
|
||||
const getBadgeConfig = () => {
|
||||
switch (type) {
|
||||
case PrincipalType.USER:
|
||||
return {
|
||||
className: 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300',
|
||||
label: localize('com_ui_user'),
|
||||
};
|
||||
case PrincipalType.GROUP:
|
||||
return {
|
||||
className: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
|
||||
label: localize('com_ui_group'),
|
||||
};
|
||||
case PrincipalType.ROLE:
|
||||
return {
|
||||
className: 'bg-purple-100 text-purple-800 dark:bg-purple-900 dark:text-purple-300',
|
||||
label: localize('com_ui_role'),
|
||||
};
|
||||
default:
|
||||
return {
|
||||
className: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-300',
|
||||
label: type,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const badgeConfig = getBadgeConfig();
|
||||
|
||||
return (
|
||||
<div
|
||||
{...props}
|
||||
|
|
@ -41,12 +69,10 @@ const PeoplePickerSearchItem = forwardRef<HTMLDivElement, PeoplePickerSearchItem
|
|||
<span
|
||||
className={cn(
|
||||
'inline-flex items-center rounded-full px-2 py-1 text-xs font-medium',
|
||||
type === 'user'
|
||||
? 'bg-blue-100 text-blue-800 dark:bg-blue-900 dark:text-blue-300'
|
||||
: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-300',
|
||||
badgeConfig.className,
|
||||
)}
|
||||
>
|
||||
{type === 'user' ? localize('com_ui_user') : localize('com_ui_group')}
|
||||
{badgeConfig.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import * as Menu from '@ariakit/react/menu';
|
|||
import { Button, DropdownPopup } from '@librechat/client';
|
||||
import { Users, X, ExternalLink, ChevronDown } from 'lucide-react';
|
||||
import type { TPrincipal, TAccessRole, AccessRoleIds } from 'librechat-data-provider';
|
||||
import PrincipalAvatar from '~/components/Sharing/PrincipalAvatar';
|
||||
import { getRoleLocalizationKeys } from '~/utils';
|
||||
import PrincipalAvatar from '../PrincipalAvatar';
|
||||
import { useLocalize } from '~/hooks';
|
||||
|
||||
interface SelectedPrincipalsListProps {
|
||||
|
|
@ -36,7 +36,7 @@ export default function SelectedPrincipalsList({
|
|||
<div className={`space-y-3 ${className}`}>
|
||||
<div className="rounded-lg border border-dashed border-border py-8 text-center text-muted-foreground">
|
||||
<Users className="mx-auto mb-2 h-8 w-8 opacity-50" />
|
||||
<p className="mt-1 text-xs">{localize('com_ui_search_above_to_add')}</p>
|
||||
<p className="mt-1 text-xs">{localize('com_ui_search_above_to_add_all')}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue