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

View file

@ -2,14 +2,13 @@ import { useState, useRef, useEffect, useMemo, memo, useCallback } from 'react';
import { AutoSizer, List } from 'react-virtualized';
import { Spinner, useCombobox } from '@librechat/client';
import { useSetRecoilState, useRecoilValue } from 'recoil';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import type { TPromptGroup } from 'librechat-data-provider';
import type { PromptOption } from '~/common';
import { removeCharIfLast, detectVariables } from '~/utils';
import VariableDialog from '~/components/Prompts/Groups/VariableDialog';
import { usePromptGroupsContext } from '~/Providers';
import { useLocalize, useHasAccess } from '~/hooks';
import MentionItem from './MentionItem';
import { useLocalize } from '~/hooks';
import store from '~/store';
const commandChar = '/';
@ -54,12 +53,7 @@ function PromptsCommand({
submitPrompt: (textPrompt: string) => void;
}) {
const localize = useLocalize();
const hasAccess = useHasAccess({
permissionType: PermissionTypes.PROMPTS,
permission: Permissions.USE,
});
const { allPromptGroups } = usePromptGroupsContext();
const { allPromptGroups, hasAccess } = usePromptGroupsContext();
const { data, isLoading } = allPromptGroups;
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 type { MenuItemProps } from '@librechat/client';
import type { ReactNode } from 'react';
import { usePromptGroupsContext } from '~/Providers';
import { useCategories } from '~/hooks';
import { cn } from '~/utils';
@ -22,8 +23,9 @@ const CategorySelector: React.FC<CategorySelectorProps> = ({
}) => {
const { t } = useTranslation();
const formContext = useFormContext();
const { categories, emptyCategory } = useCategories();
const [isOpen, setIsOpen] = useState(false);
const { hasAccess } = usePromptGroupsContext();
const { categories, emptyCategory } = useCategories({ hasAccess });
const control = formContext?.control;
const watch = formContext?.watch;

View file

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

View file

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

View file

@ -167,6 +167,7 @@ const PromptForm = () => {
const params = useParams();
const localize = useLocalize();
const { showToast } = useToastContext();
const { hasAccess } = usePromptGroupsContext();
const alwaysMakeProd = useRecoilValue(store.alwaysMakeProd);
const promptId = params.promptId || '';
@ -179,10 +180,12 @@ const PromptForm = () => {
const [showSidePanel, setShowSidePanel] = useState(false);
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(
{ groupId: promptId },
{ enabled: !!promptId },
{ enabled: hasAccess && !!promptId },
);
const { hasPermission, isLoading: permissionsLoading } = useResourcePermissions(

View file

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

View file

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