diff --git a/api/server/routes/roles.js b/api/server/routes/roles.js index 66f77a7474..f197417774 100644 --- a/api/server/routes/roles.js +++ b/api/server/routes/roles.js @@ -1,12 +1,13 @@ const express = require('express'); const { + SystemRoles, + roleDefaults, + PermissionTypes, + agentPermissionsSchema, promptPermissionsSchema, memoryPermissionsSchema, - agentPermissionsSchema, + marketplacePermissionsSchema, peoplePickerPermissionsSchema, - PermissionTypes, - roleDefaults, - SystemRoles, } = require('librechat-data-provider'); const { checkAdmin, requireJwtAuth } = require('~/server/middleware'); const { updateRoleByName, getRoleByName } = require('~/models/Role'); @@ -39,6 +40,11 @@ const permissionConfigs = { permissionType: PermissionTypes.PEOPLE_PICKER, errorMessage: 'Invalid people picker permissions.', }, + marketplace: { + schema: marketplacePermissionsSchema, + permissionType: PermissionTypes.MARKETPLACE, + errorMessage: 'Invalid marketplace permissions.', + }, }; /** @@ -136,4 +142,10 @@ router.put('/:roleName/memories', checkAdmin, createPermissionUpdateHandler('mem */ router.put('/:roleName/people-picker', checkAdmin, createPermissionUpdateHandler('people-picker')); +/** + * PUT /api/roles/:roleName/marketplace + * Update marketplace permissions for a specific role + */ +router.put('/:roleName/marketplace', checkAdmin, createPermissionUpdateHandler('marketplace')); + module.exports = router; diff --git a/api/server/services/start/interface.js b/api/server/services/start/interface.js index 7eca6beea3..3e892138a3 100644 --- a/api/server/services/start/interface.js +++ b/api/server/services/start/interface.js @@ -59,12 +59,7 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol roles: interfaceConfig?.peoplePicker?.roles ?? defaults.peoplePicker?.roles, }, marketplace: { - admin: { - use: interfaceConfig?.marketplace?.admin?.use ?? defaults.marketplace?.admin.use, - }, - user: { - use: interfaceConfig?.marketplace?.user?.use ?? defaults.marketplace?.user.use, - }, + use: interfaceConfig?.marketplace?.use ?? defaults.marketplace?.use, }, }); @@ -89,7 +84,7 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol roleName === SystemRoles.USER ? false : loadedInterface.peoplePicker?.roles, }, [PermissionTypes.MARKETPLACE]: { - [Permissions.USE]: loadedInterface.marketplace.user?.use, + [Permissions.USE]: roleName === SystemRoles.USER ? false : loadedInterface.marketplace?.use, }, [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: loadedInterface.fileSearch }, [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: loadedInterface.fileCitations }, @@ -112,7 +107,7 @@ async function loadDefaultInterface(config, configDefaults, roleName = SystemRol [Permissions.VIEW_ROLES]: loadedInterface.peoplePicker?.roles, }, [PermissionTypes.MARKETPLACE]: { - [Permissions.USE]: loadedInterface.marketplace.admin?.use, + [Permissions.USE]: loadedInterface.marketplace?.use, }, [PermissionTypes.FILE_SEARCH]: { [Permissions.USE]: loadedInterface.fileSearch }, [PermissionTypes.FILE_CITATIONS]: { [Permissions.USE]: loadedInterface.fileCitations }, diff --git a/client/src/components/Agents/Marketplace.tsx b/client/src/components/Agents/Marketplace.tsx index a755cbb6ae..9cf3541e65 100644 --- a/client/src/components/Agents/Marketplace.tsx +++ b/client/src/components/Agents/Marketplace.tsx @@ -9,6 +9,7 @@ import type t from 'librechat-data-provider'; import type { ContextType } from '~/common'; import { useGetEndpointsQuery, useGetAgentCategoriesQuery } from '~/data-provider'; import { useDocumentTitle, useHasAccess, useLocalize } from '~/hooks'; +import MarketplaceAdminSettings from './MarketplaceAdminSettings'; import { SidePanelProvider, useChatContext } from '~/Providers'; import { MarketplaceProvider } from './MarketplaceContext'; import { SidePanelGroup } from '~/components/SidePanel'; @@ -301,8 +302,12 @@ const AgentMarketplace: React.FC = ({ className = '' }) = {/* Scrollable container */}
+ {/* Admin Settings */} +
+ +
{/* Simplified header for agents marketplace - only show nav controls when needed */} {!isSmallScreen && (
diff --git a/client/src/components/Agents/MarketplaceAdminSettings.tsx b/client/src/components/Agents/MarketplaceAdminSettings.tsx new file mode 100644 index 0000000000..10e42f1153 --- /dev/null +++ b/client/src/components/Agents/MarketplaceAdminSettings.tsx @@ -0,0 +1,211 @@ +import { useMemo, useEffect, useState } from 'react'; +import * as Ariakit from '@ariakit/react'; +import { ShieldEllipsis } from 'lucide-react'; +import { useForm, Controller } from 'react-hook-form'; +import { Permissions, SystemRoles, roleDefaults, PermissionTypes } from 'librechat-data-provider'; +import { + Button, + Switch, + OGDialog, + DropdownPopup, + OGDialogTitle, + OGDialogContent, + OGDialogTrigger, + useToastContext, +} from '@librechat/client'; +import type { Control, UseFormSetValue, UseFormGetValues } from 'react-hook-form'; +import { useUpdateMarketplacePermissionsMutation } from '~/data-provider'; +import { useLocalize, useAuthContext } from '~/hooks'; + +type FormValues = { + [Permissions.USE]: boolean; +}; + +type LabelControllerProps = { + label: string; + marketplacePerm: Permissions.USE; + control: Control; + setValue: UseFormSetValue; + getValues: UseFormGetValues; +}; + +const LabelController: React.FC = ({ + control, + marketplacePerm, + label, + getValues, + setValue, +}) => ( +
+ + ( + + )} + /> +
+); + +const MarketplaceAdminSettings = () => { + const localize = useLocalize(); + const { showToast } = useToastContext(); + const { user, roles } = useAuthContext(); + const { mutate, isLoading } = useUpdateMarketplacePermissionsMutation({ + onSuccess: () => { + showToast({ status: 'success', message: localize('com_ui_saved') }); + }, + onError: () => { + showToast({ status: 'error', message: localize('com_ui_error_save_admin_settings') }); + }, + }); + + const [isRoleMenuOpen, setIsRoleMenuOpen] = useState(false); + const [selectedRole, setSelectedRole] = useState(SystemRoles.USER); + + const defaultValues = useMemo(() => { + const rolePerms = roles?.[selectedRole]?.permissions; + if (rolePerms) { + return rolePerms[PermissionTypes.MARKETPLACE]; + } + return roleDefaults[selectedRole].permissions[PermissionTypes.MARKETPLACE]; + }, [roles, selectedRole]); + + const { + reset, + control, + setValue, + getValues, + handleSubmit, + formState: { isSubmitting }, + } = useForm({ + mode: 'onChange', + defaultValues, + }); + + useEffect(() => { + const value = roles?.[selectedRole]?.permissions?.[PermissionTypes.MARKETPLACE]; + if (value) { + reset(value); + } else { + reset(roleDefaults[selectedRole].permissions[PermissionTypes.MARKETPLACE]); + } + }, [roles, selectedRole, reset]); + + if (user?.role !== SystemRoles.ADMIN) { + return null; + } + + const labelControllerData: { + marketplacePerm: Permissions.USE; + label: string; + }[] = [ + { + marketplacePerm: Permissions.USE, + label: localize('com_ui_marketplace_allow_use'), + }, + ]; + + const onSubmit = (data: FormValues) => { + mutate({ roleName: selectedRole, updates: data }); + }; + + const roleDropdownItems = [ + { + label: SystemRoles.USER, + onClick: () => { + setSelectedRole(SystemRoles.USER); + }, + }, + { + label: SystemRoles.ADMIN, + onClick: () => { + setSelectedRole(SystemRoles.ADMIN); + }, + }, + ]; + + return ( + + + + + + {`${localize('com_ui_admin_settings')} - ${localize( + 'com_ui_marketplace', + )}`} +
+ {/* Role selection dropdown */} +
+ {localize('com_ui_role_select')}: + + {selectedRole} + + } + items={roleDropdownItems} + itemClassName="items-center justify-center" + sameWidth={true} + /> +
+ {/* Permissions form */} +
+
+ {labelControllerData.map(({ marketplacePerm, label }) => ( +
+ +
+ ))} +
+
+ +
+
+
+
+
+ ); +}; + +export default MarketplaceAdminSettings; diff --git a/client/src/data-provider/roles.ts b/client/src/data-provider/roles.ts index 9bc4d6ad2f..a60047cecb 100644 --- a/client/src/data-provider/roles.ts +++ b/client/src/data-provider/roles.ts @@ -4,6 +4,7 @@ import { dataService, promptPermissionsSchema, memoryPermissionsSchema, + marketplacePermissionsSchema, peoplePickerPermissionsSchema, } from 'librechat-data-provider'; import type { @@ -169,3 +170,39 @@ export const useUpdatePeoplePickerPermissionsMutation = ( }, ); }; + +export const useUpdateMarketplacePermissionsMutation = ( + options?: t.UpdateMarketplacePermOptions, +): UseMutationResult< + t.UpdatePermResponse, + t.TError | undefined, + t.UpdateMarketplacePermVars, + unknown +> => { + const queryClient = useQueryClient(); + const { onMutate, onSuccess, onError } = options ?? {}; + return useMutation( + (variables) => { + marketplacePermissionsSchema.partial().parse(variables.updates); + return dataService.updateMarketplacePermissions(variables); + }, + { + onSuccess: (data, variables, context) => { + queryClient.invalidateQueries([QueryKeys.roles, variables.roleName]); + if (onSuccess) { + onSuccess(data, variables, context); + } + }, + onError: (...args) => { + const error = args[0]; + if (error != null) { + console.error('Failed to update marketplace permissions:', error); + } + if (onError) { + onError(...args); + } + }, + onMutate, + }, + ); +}; diff --git a/client/src/locales/en/translation.json b/client/src/locales/en/translation.json index 4ef7300668..79a0e720cb 100644 --- a/client/src/locales/en/translation.json +++ b/client/src/locales/en/translation.json @@ -592,6 +592,10 @@ "com_ui_people_picker_allow_view_users": "Allow viewing users", "com_ui_people_picker_allow_view_groups": "Allow viewing groups", "com_ui_people_picker_allow_view_roles": "Allow viewing roles", + "com_ui_marketplace": "Marketplace", + "com_ui_marketplace_allow_use": "Allow using Marketplace", + "com_ui_marketplace_admin_settings": "Marketplace Admin Settings", + "com_ui_marketplace_admin_settings_description": "Configure which roles can access the Agent Marketplace.", "com_ui_all": "all", "com_ui_all_proper": "All", "com_ui_analyzing": "Analyzing", diff --git a/packages/data-provider/src/api-endpoints.ts b/packages/data-provider/src/api-endpoints.ts index 60894fd1ba..866c35469a 100644 --- a/packages/data-provider/src/api-endpoints.ts +++ b/packages/data-provider/src/api-endpoints.ts @@ -278,6 +278,9 @@ export const updateAgentPermissions = (roleName: string) => `${getRole(roleName) export const updatePeoplePickerPermissions = (roleName: string) => `${getRole(roleName)}/people-picker`; +export const updateMarketplacePermissions = (roleName: string) => + `${getRole(roleName)}/marketplace`; + /* Conversation Tags */ export const conversationTags = (tag?: string) => `/api/tags${tag != null && tag ? `/${encodeURIComponent(tag)}` : ''}`; diff --git a/packages/data-provider/src/config.ts b/packages/data-provider/src/config.ts index 95ec80e777..bb333d736a 100644 --- a/packages/data-provider/src/config.ts +++ b/packages/data-provider/src/config.ts @@ -541,16 +541,7 @@ export const interfaceSchema = z .optional(), marketplace: z .object({ - admin: z - .object({ - use: z.boolean().optional(), - }) - .optional(), - user: z - .object({ - use: z.boolean().optional(), - }) - .optional(), + use: z.boolean().optional(), }) .optional(), fileSearch: z.boolean().optional(), @@ -576,12 +567,7 @@ export const interfaceSchema = z roles: true, }, marketplace: { - admin: { - use: false, - }, - user: { - use: false, - }, + use: false, }, fileSearch: true, fileCitations: true, diff --git a/packages/data-provider/src/data-service.ts b/packages/data-provider/src/data-service.ts index 23ff0180bb..8170748560 100644 --- a/packages/data-provider/src/data-service.ts +++ b/packages/data-provider/src/data-service.ts @@ -800,6 +800,12 @@ export function updatePeoplePickerPermissions( ); } +export function updateMarketplacePermissions( + variables: m.UpdateMarketplacePermVars, +): Promise { + return request.put(endpoints.updateMarketplacePermissions(variables.roleName), variables.updates); +} + /* Tags */ export function getConversationTags(): Promise { return request.get(endpoints.conversationTags()); diff --git a/packages/data-provider/src/types/mutations.ts b/packages/data-provider/src/types/mutations.ts index 7a8ff00ffe..810fa15e49 100644 --- a/packages/data-provider/src/types/mutations.ts +++ b/packages/data-provider/src/types/mutations.ts @@ -305,6 +305,15 @@ export type UpdatePeoplePickerPermOptions = MutationOptions< types.TError | null | undefined >; +export type UpdateMarketplacePermVars = UpdatePermVars; + +export type UpdateMarketplacePermOptions = MutationOptions< + UpdatePermResponse, + UpdateMarketplacePermVars, + unknown, + types.TError | null | undefined +>; + export type UpdateConversationTagOptions = MutationOptions< types.TConversationTag, types.TConversationTagRequest