🗨️ refactor: Only Allow Prompt Queries with Access (#9688)

This commit is contained in:
Danny Avila 2025-09-18 10:00:33 -04:00 committed by GitHub
parent 02bfe32905
commit 208be7c06c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 64 additions and 38 deletions

View file

@ -1,9 +1,10 @@
import React, { createContext, useContext, ReactNode, useMemo } from 'react'; import React, { createContext, useContext, ReactNode, useMemo } from 'react';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import type { TPromptGroup } from 'librechat-data-provider'; import type { TPromptGroup } from 'librechat-data-provider';
import type { PromptOption } from '~/common'; import type { PromptOption } from '~/common';
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon'; import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
import { usePromptGroupsNav, useHasAccess } from '~/hooks';
import { useGetAllPromptGroups } from '~/data-provider'; import { useGetAllPromptGroups } from '~/data-provider';
import { usePromptGroupsNav } from '~/hooks';
import { mapPromptGroups } from '~/utils'; import { mapPromptGroups } from '~/utils';
type AllPromptGroupsData = type AllPromptGroupsData =
@ -19,14 +20,21 @@ type PromptGroupsContextType =
data: AllPromptGroupsData; data: AllPromptGroupsData;
isLoading: boolean; isLoading: boolean;
}; };
hasAccess: boolean;
}) })
| null; | null;
const PromptGroupsContext = createContext<PromptGroupsContextType>(null); const PromptGroupsContext = createContext<PromptGroupsContextType>(null);
export const PromptGroupsProvider = ({ children }: { children: ReactNode }) => { export const PromptGroupsProvider = ({ children }: { children: ReactNode }) => {
const promptGroupsNav = usePromptGroupsNav(); const hasAccess = useHasAccess({
permissionType: PermissionTypes.PROMPTS,
permission: Permissions.USE,
});
const promptGroupsNav = usePromptGroupsNav(hasAccess);
const { data: allGroupsData, isLoading: isLoadingAll } = useGetAllPromptGroups(undefined, { const { data: allGroupsData, isLoading: isLoadingAll } = useGetAllPromptGroups(undefined, {
enabled: hasAccess,
select: (data) => { select: (data) => {
const mappedArray: PromptOption[] = data.map((group) => ({ const mappedArray: PromptOption[] = data.map((group) => ({
id: group._id ?? '', id: group._id ?? '',
@ -55,11 +63,12 @@ export const PromptGroupsProvider = ({ children }: { children: ReactNode }) => {
() => ({ () => ({
...promptGroupsNav, ...promptGroupsNav,
allPromptGroups: { allPromptGroups: {
data: allGroupsData, data: hasAccess ? allGroupsData : undefined,
isLoading: isLoadingAll, isLoading: hasAccess ? isLoadingAll : false,
}, },
hasAccess,
}), }),
[promptGroupsNav, allGroupsData, isLoadingAll], [promptGroupsNav, allGroupsData, isLoadingAll, hasAccess],
); );
return ( return (

View file

@ -2,14 +2,13 @@ import { useState, useRef, useEffect, useMemo, memo, useCallback } from 'react';
import { AutoSizer, List } from 'react-virtualized'; import { AutoSizer, List } from 'react-virtualized';
import { Spinner, useCombobox } from '@librechat/client'; import { Spinner, useCombobox } from '@librechat/client';
import { useSetRecoilState, useRecoilValue } from 'recoil'; import { useSetRecoilState, useRecoilValue } from 'recoil';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import type { TPromptGroup } from 'librechat-data-provider'; import type { TPromptGroup } from 'librechat-data-provider';
import type { PromptOption } from '~/common'; import type { PromptOption } from '~/common';
import { removeCharIfLast, detectVariables } from '~/utils'; import { removeCharIfLast, detectVariables } from '~/utils';
import VariableDialog from '~/components/Prompts/Groups/VariableDialog'; import VariableDialog from '~/components/Prompts/Groups/VariableDialog';
import { usePromptGroupsContext } from '~/Providers'; import { usePromptGroupsContext } from '~/Providers';
import { useLocalize, useHasAccess } from '~/hooks';
import MentionItem from './MentionItem'; import MentionItem from './MentionItem';
import { useLocalize } from '~/hooks';
import store from '~/store'; import store from '~/store';
const commandChar = '/'; const commandChar = '/';
@ -54,12 +53,7 @@ function PromptsCommand({
submitPrompt: (textPrompt: string) => void; submitPrompt: (textPrompt: string) => void;
}) { }) {
const localize = useLocalize(); const localize = useLocalize();
const hasAccess = useHasAccess({ const { allPromptGroups, hasAccess } = usePromptGroupsContext();
permissionType: PermissionTypes.PROMPTS,
permission: Permissions.USE,
});
const { allPromptGroups } = usePromptGroupsContext();
const { data, isLoading } = allPromptGroups; const { data, isLoading } = allPromptGroups;
const [activeIndex, setActiveIndex] = useState(0); const [activeIndex, setActiveIndex] = useState(0);

View file

@ -6,6 +6,7 @@ import { LocalStorageKeys } from 'librechat-data-provider';
import { useFormContext, Controller } from 'react-hook-form'; import { useFormContext, Controller } from 'react-hook-form';
import type { MenuItemProps } from '@librechat/client'; import type { MenuItemProps } from '@librechat/client';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import { usePromptGroupsContext } from '~/Providers';
import { useCategories } from '~/hooks'; import { useCategories } from '~/hooks';
import { cn } from '~/utils'; import { cn } from '~/utils';
@ -22,8 +23,9 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const formContext = useFormContext(); const formContext = useFormContext();
const { categories, emptyCategory } = useCategories();
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const { hasAccess } = usePromptGroupsContext();
const { categories, emptyCategory } = useCategories({ hasAccess });
const control = formContext?.control; const control = formContext?.control;
const watch = formContext?.watch; const watch = formContext?.watch;

View file

@ -7,6 +7,7 @@ import CategorySelector from '~/components/Prompts/Groups/CategorySelector';
import VariablesDropdown from '~/components/Prompts/VariablesDropdown'; import VariablesDropdown from '~/components/Prompts/VariablesDropdown';
import PromptVariables from '~/components/Prompts/PromptVariables'; import PromptVariables from '~/components/Prompts/PromptVariables';
import Description from '~/components/Prompts/Description'; import Description from '~/components/Prompts/Description';
import { usePromptGroupsContext } from '~/Providers';
import { useLocalize, useHasAccess } from '~/hooks'; import { useLocalize, useHasAccess } from '~/hooks';
import Command from '~/components/Prompts/Command'; import Command from '~/components/Prompts/Command';
import { useCreatePrompt } from '~/data-provider'; import { useCreatePrompt } from '~/data-provider';
@ -37,10 +38,12 @@ const CreatePromptForm = ({
}) => { }) => {
const localize = useLocalize(); const localize = useLocalize();
const navigate = useNavigate(); const navigate = useNavigate();
const hasAccess = useHasAccess({ const { hasAccess: hasUseAccess } = usePromptGroupsContext();
const hasCreateAccess = useHasAccess({
permissionType: PermissionTypes.PROMPTS, permissionType: PermissionTypes.PROMPTS,
permission: Permissions.CREATE, permission: Permissions.CREATE,
}); });
const hasAccess = hasUseAccess && hasCreateAccess;
useEffect(() => { useEffect(() => {
let timeoutId: ReturnType<typeof setTimeout>; let timeoutId: ReturnType<typeof setTimeout>;

View file

@ -11,8 +11,8 @@ import store from '~/store';
export default function FilterPrompts({ className = '' }: { className?: string }) { export default function FilterPrompts({ className = '' }: { className?: string }) {
const localize = useLocalize(); const localize = useLocalize();
const { name, setName } = usePromptGroupsContext(); const { name, setName, hasAccess } = usePromptGroupsContext();
const { categories } = useCategories('h-4 w-4'); const { categories } = useCategories({ className: 'h-4 w-4', hasAccess });
const [displayName, setDisplayName] = useState(name || ''); const [displayName, setDisplayName] = useState(name || '');
const [isSearching, setIsSearching] = useState(false); const [isSearching, setIsSearching] = useState(false);
const [categoryFilter, setCategory] = useRecoilState(store.promptsCategory); const [categoryFilter, setCategory] = useRecoilState(store.promptsCategory);

View file

@ -167,6 +167,7 @@ const PromptForm = () => {
const params = useParams(); const params = useParams();
const localize = useLocalize(); const localize = useLocalize();
const { showToast } = useToastContext(); const { showToast } = useToastContext();
const { hasAccess } = usePromptGroupsContext();
const alwaysMakeProd = useRecoilValue(store.alwaysMakeProd); const alwaysMakeProd = useRecoilValue(store.alwaysMakeProd);
const promptId = params.promptId || ''; const promptId = params.promptId || '';
@ -179,10 +180,12 @@ const PromptForm = () => {
const [showSidePanel, setShowSidePanel] = useState(false); const [showSidePanel, setShowSidePanel] = useState(false);
const sidePanelWidth = '320px'; const sidePanelWidth = '320px';
const { data: group, isLoading: isLoadingGroup } = useGetPromptGroup(promptId); const { data: group, isLoading: isLoadingGroup } = useGetPromptGroup(promptId, {
enabled: hasAccess && !!promptId,
});
const { data: prompts = [], isLoading: isLoadingPrompts } = useGetPrompts( const { data: prompts = [], isLoading: isLoadingPrompts } = useGetPrompts(
{ groupId: promptId }, { groupId: promptId },
{ enabled: !!promptId }, { enabled: hasAccess && !!promptId },
); );
const { hasPermission, isLoading: permissionsLoading } = useResourcePermissions( const { hasPermission, isLoading: permissionsLoading } = useResourcePermissions(

View file

@ -1,6 +1,6 @@
import { useGetCategories } from '~/data-provider';
import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon'; import CategoryIcon from '~/components/Prompts/Groups/CategoryIcon';
import { useLocalize, TranslationKeys } from '~/hooks'; import { useLocalize, TranslationKeys } from '~/hooks';
import { useGetCategories } from '~/data-provider';
const loadingCategories: { label: TranslationKeys; value: string }[] = [ const loadingCategories: { label: TranslationKeys; value: string }[] = [
{ {
@ -14,9 +14,17 @@ const emptyCategory: { label: TranslationKeys; value: string } = {
value: '', value: '',
}; };
const useCategories = (className = '') => { const useCategories = ({
className = '',
hasAccess = true,
}: {
className?: string;
hasAccess?: boolean;
}) => {
const localize = useLocalize(); const localize = useLocalize();
const { data: categories = loadingCategories } = useGetCategories({ const { data: categories = loadingCategories } = useGetCategories({
enabled: hasAccess,
select: (data) => select: (data) =>
data.map((category) => ({ data.map((category) => ({
label: localize(category.label as TranslationKeys), label: localize(category.label as TranslationKeys),

View file

@ -3,7 +3,7 @@ import { useRecoilState } from 'recoil';
import { usePromptGroupsInfiniteQuery } from '~/data-provider'; import { usePromptGroupsInfiniteQuery } from '~/data-provider';
import store from '~/store'; import store from '~/store';
export default function usePromptGroupsNav() { export default function usePromptGroupsNav(hasAccess = true) {
const [pageSize] = useRecoilState(store.promptsPageSize); const [pageSize] = useRecoilState(store.promptsPageSize);
const [category] = useRecoilState(store.promptsCategory); const [category] = useRecoilState(store.promptsCategory);
const [name, setName] = useRecoilState(store.promptsName); const [name, setName] = useRecoilState(store.promptsName);
@ -14,21 +14,26 @@ export default function usePromptGroupsNav() {
const prevFiltersRef = useRef({ name, category }); const prevFiltersRef = useRef({ name, category });
const groupsQuery = usePromptGroupsInfiniteQuery({ const groupsQuery = usePromptGroupsInfiniteQuery(
{
name, name,
pageSize, pageSize,
category, category,
}); },
{
enabled: hasAccess,
},
);
// Get the current page data // Get the current page data
const currentPageData = useMemo(() => { const currentPageData = useMemo(() => {
if (!groupsQuery.data?.pages || groupsQuery.data.pages.length === 0) { if (!hasAccess || !groupsQuery.data?.pages || groupsQuery.data.pages.length === 0) {
return null; return null;
} }
// Ensure we don't go out of bounds // Ensure we don't go out of bounds
const pageIndex = Math.min(currentPageIndex, groupsQuery.data.pages.length - 1); const pageIndex = Math.min(currentPageIndex, groupsQuery.data.pages.length - 1);
return groupsQuery.data.pages[pageIndex]; return groupsQuery.data.pages[pageIndex];
}, [groupsQuery.data?.pages, currentPageIndex]); }, [hasAccess, groupsQuery.data?.pages, currentPageIndex]);
// Get prompt groups for current page // Get prompt groups for current page
const promptGroups = useMemo(() => { const promptGroups = useMemo(() => {
@ -54,7 +59,7 @@ export default function usePromptGroupsNav() {
// Navigate to next page // Navigate to next page
const nextPage = useCallback(async () => { const nextPage = useCallback(async () => {
if (!hasNextPage) return; if (!hasAccess || !hasNextPage) return;
const nextPageIndex = currentPageIndex + 1; const nextPageIndex = currentPageIndex + 1;
@ -72,16 +77,18 @@ export default function usePromptGroupsNav() {
} }
setCurrentPageIndex(nextPageIndex); setCurrentPageIndex(nextPageIndex);
}, [currentPageIndex, hasNextPage, groupsQuery]); }, [hasAccess, currentPageIndex, hasNextPage, groupsQuery]);
// Navigate to previous page // Navigate to previous page
const prevPage = useCallback(() => { const prevPage = useCallback(() => {
if (!hasPreviousPage) return; if (!hasAccess || !hasPreviousPage) return;
setCurrentPageIndex(currentPageIndex - 1); setCurrentPageIndex(currentPageIndex - 1);
}, [currentPageIndex, hasPreviousPage]); }, [hasAccess, currentPageIndex, hasPreviousPage]);
// Reset when filters change // Reset when filters change
useEffect(() => { useEffect(() => {
if (!hasAccess) return;
const filtersChanged = const filtersChanged =
prevFiltersRef.current.name !== name || prevFiltersRef.current.category !== category; prevFiltersRef.current.name !== name || prevFiltersRef.current.category !== category;
@ -90,18 +97,18 @@ export default function usePromptGroupsNav() {
cursorHistoryRef.current = [null]; cursorHistoryRef.current = [null];
prevFiltersRef.current = { name, category }; prevFiltersRef.current = { name, category };
} }
}, [name, category]); }, [hasAccess, name, category]);
return { return {
promptGroups, promptGroups: hasAccess ? promptGroups : [],
groupsQuery, groupsQuery,
currentPage, currentPage,
totalPages, totalPages,
hasNextPage, hasNextPage: hasAccess && hasNextPage,
hasPreviousPage, hasPreviousPage: hasAccess && hasPreviousPage,
nextPage, nextPage,
prevPage, prevPage,
isFetching: groupsQuery.isFetching, isFetching: hasAccess ? groupsQuery.isFetching : false,
name, name,
setName, setName,
}; };