mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-09-21 21:50:49 +02:00
🗨️ refactor: Only Allow Prompt Queries with Access (#9688)
This commit is contained in:
parent
02bfe32905
commit
208be7c06c
8 changed files with 64 additions and 38 deletions
|
@ -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 (
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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>;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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),
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue