🛂 feat: OpenID Logout Redirect to end_session_endpoint (#5626)

* WIP: end session endpoint

* refactor: move useGetBannerQuery outside of package

* refactor: add queriesEnabled and move useGetEndpointsConfigQuery to data-provider (local)

* refactor: move useGetEndpointsQuery import to data-provider

* refactor: relocate useGetEndpointsQuery import to improve module organization

* refactor: move `useGetStartupConfig` from package to `~/data-provider`

* refactor: move useGetUserBalance to data-provider and update imports

* refactor: update query enabled conditions to include config check

* refactor: remove unused useConfigOverride import from useAppStartup

* refactor: integrate queriesEnabled state into file and search queries and move useGetSearchEnabledQuery to data-provider (local)

* refactor: move useGetUserQuery to data-provider and update imports

* refactor: enhance loginUser mutation with success and error handling as pass in options to hook

* refactor: update enabled condition in queries to handle undefined config

* refactor: enhance authentication mutations with queriesEnabled state management

* refactor: improve conditional rendering for error messages and feature flags in Login component

* refactor: remove unused queriesEnabled state from AuthContextProvider

* refactor: implement queriesEnabled state management in LoginLayout with timeout handling

* refactor: add conditional check for end session endpoint in OpenID strategy

* ci: fix tests after changes

* refactor: remove endSessionEndpoint from user schema and update logoutController to use OpenID issuer's end_session_endpoint

* refactor: update logoutController to use end_session_endpoint from issuer metadata
This commit is contained in:
Danny Avila 2025-02-03 10:53:04 -05:00 committed by GitHub
parent d93f5c9061
commit 45dd2b262f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
73 changed files with 385 additions and 270 deletions

View file

@ -1,5 +1,7 @@
const cookies = require('cookie');
const { Issuer } = require('openid-client');
const { logoutUser } = require('~/server/services/AuthService');
const { isEnabled } = require('~/server/utils');
const { logger } = require('~/config');
const logoutController = async (req, res) => {
@ -8,7 +10,23 @@ const logoutController = async (req, res) => {
const logout = await logoutUser(req, refreshToken);
const { status, message } = logout;
res.clearCookie('refreshToken');
return res.status(status).send({ message });
const response = { message };
if (
req.user.openidId != null &&
isEnabled(process.env.OPENID_USE_END_SESSION_ENDPOINT) &&
process.env.OPENID_ISSUER
) {
const issuer = await Issuer.discover(process.env.OPENID_ISSUER);
const redirect = issuer.metadata.end_session_endpoint;
if (!redirect) {
logger.warn(
'[logoutController] end_session_endpoint not found in OpenID issuer metadata. Please verify that the issuer is correct.',
);
} else {
response.redirect = redirect;
}
}
return res.status(status).send(response);
} catch (err) {
logger.error('[logoutController]', err);
return res.status(500).json({ message: err.message });

View file

@ -129,7 +129,8 @@ async function setupOpenId() {
redirect_uris: [process.env.DOMAIN_SERVER + process.env.OPENID_CALLBACK_URL],
};
if (isEnabled(process.env.OPENID_SET_FIRST_SUPPORTED_ALGORITHM)) {
clientMetadata.id_token_signed_response_alg = issuer.id_token_signing_alg_values_supported?.[0] || 'RS256';
clientMetadata.id_token_signed_response_alg =
issuer.id_token_signing_alg_values_supported?.[0] || 'RS256';
}
const client = new issuer.Client(clientMetadata);
const requiredRole = process.env.OPENID_REQUIRED_ROLE;

View file

@ -412,7 +412,7 @@ export type TAuthConfig = {
};
export type IconProps = Pick<t.TMessage, 'isCreatedByUser' | 'model'> &
Pick<t.TConversation, 'chatGptLabel' | 'modelLabel' | 'jailbreak'> & {
Pick<t.TConversation, 'chatGptLabel' | 'modelLabel'> & {
size?: number;
button?: boolean;
iconURL?: string;

View file

@ -13,8 +13,8 @@ function Login() {
return (
<>
{error && <ErrorMessage>{localize(getLoginError(error))}</ErrorMessage>}
{startupConfig?.emailLoginEnabled && (
{error != null && <ErrorMessage>{localize(getLoginError(error))}</ErrorMessage>}
{startupConfig?.emailLoginEnabled === true && (
<LoginForm
onSubmit={login}
startupConfig={startupConfig}
@ -22,7 +22,7 @@ function Login() {
setError={setError}
/>
)}
{startupConfig?.registrationEnabled && (
{startupConfig?.registrationEnabled === true && (
<p className="my-4 text-center text-sm font-light text-gray-700 dark:text-white">
{' '}
{localize('com_auth_no_account')}{' '}

View file

@ -1,9 +1,8 @@
import { useForm } from 'react-hook-form';
import React, { useState, useEffect } from 'react';
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import type { TLoginUser, TStartupConfig } from 'librechat-data-provider';
import type { TAuthContext } from '~/common';
import { useResendVerificationEmail } from '~/data-provider';
import { useResendVerificationEmail, useGetStartupConfig } from '~/data-provider';
import { useLocalize } from '~/hooks';
type TLoginFormProps = {

View file

@ -1,9 +1,11 @@
import reactRouter from 'react-router-dom';
import userEvent from '@testing-library/user-event';
import { getByTestId, render, waitFor } from 'test/layout-test-utils';
import * as mockDataProvider from 'librechat-data-provider/react-query';
import type { TStartupConfig } from 'librechat-data-provider';
import * as authDataProvider from '~/data-provider/Auth/mutations';
import * as endpointQueries from '~/data-provider/Endpoints/queries';
import * as miscDataProvider from '~/data-provider/Misc/queries';
import * as authMutations from '~/data-provider/Auth/mutations';
import * as authQueries from '~/data-provider/Auth/queries';
import AuthLayout from '~/components/Auth/AuthLayout';
import Login from '~/components/Auth/Login';
@ -62,23 +64,23 @@ const setup = ({
},
} = {}) => {
const mockUseLoginUser = jest
.spyOn(authDataProvider, 'useLoginUserMutation')
.spyOn(authMutations, 'useLoginUserMutation')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useLoginUserReturnValue);
const mockUseGetUserQuery = jest
.spyOn(mockDataProvider, 'useGetUserQuery')
.spyOn(authQueries, 'useGetUserQuery')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useGetUserQueryReturnValue);
const mockUseGetStartupConfig = jest
.spyOn(mockDataProvider, 'useGetStartupConfig')
.spyOn(endpointQueries, 'useGetStartupConfig')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useGetStartupConfigReturnValue);
const mockUseRefreshTokenMutation = jest
.spyOn(mockDataProvider, 'useRefreshTokenMutation')
.spyOn(authMutations, 'useRefreshTokenMutation')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useRefreshTokenMutationReturnValue);
const mockUseGetBannerQuery = jest
.spyOn(mockDataProvider, 'useGetBannerQuery')
.spyOn(miscDataProvider, 'useGetBannerQuery')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useGetBannerQueryReturnValue);
const mockUseOutletContext = jest.spyOn(reactRouter, 'useOutletContext').mockReturnValue({

View file

@ -1,8 +1,10 @@
import { render, getByTestId } from 'test/layout-test-utils';
import userEvent from '@testing-library/user-event';
import * as mockDataProvider from 'librechat-data-provider/react-query';
import type { TStartupConfig } from 'librechat-data-provider';
import * as authDataProvider from '~/data-provider/Auth/mutations';
import * as endpointQueries from '~/data-provider/Endpoints/queries';
import * as miscDataProvider from '~/data-provider/Misc/queries';
import * as authMutations from '~/data-provider/Auth/mutations';
import * as authQueries from '~/data-provider/Auth/queries';
import Login from '../LoginForm';
jest.mock('librechat-data-provider/react-query');
@ -67,23 +69,23 @@ const setup = ({
},
} = {}) => {
const mockUseLoginUser = jest
.spyOn(authDataProvider, 'useLoginUserMutation')
.spyOn(authMutations, 'useLoginUserMutation')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useLoginUserReturnValue);
const mockUseGetUserQuery = jest
.spyOn(mockDataProvider, 'useGetUserQuery')
.spyOn(authQueries, 'useGetUserQuery')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useGetUserQueryReturnValue);
const mockUseGetStartupConfig = jest
.spyOn(mockDataProvider, 'useGetStartupConfig')
.spyOn(endpointQueries, 'useGetStartupConfig')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useGetStartupConfigReturnValue);
const mockUseRefreshTokenMutation = jest
.spyOn(mockDataProvider, 'useRefreshTokenMutation')
.spyOn(authMutations, 'useRefreshTokenMutation')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useRefreshTokenMutationReturnValue);
const mockUseGetBannerQuery = jest
.spyOn(mockDataProvider, 'useGetBannerQuery')
.spyOn(miscDataProvider, 'useGetBannerQuery')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useGetBannerQueryReturnValue);
return {

View file

@ -3,6 +3,10 @@ import userEvent from '@testing-library/user-event';
import { render, waitFor, screen } from 'test/layout-test-utils';
import * as mockDataProvider from 'librechat-data-provider/react-query';
import type { TStartupConfig } from 'librechat-data-provider';
import * as miscDataProvider from '~/data-provider/Misc/queries';
import * as endpointQueries from '~/data-provider/Endpoints/queries';
import * as authMutations from '~/data-provider/Auth/mutations';
import * as authQueries from '~/data-provider/Auth/queries';
import Registration from '~/components/Auth/Registration';
import AuthLayout from '~/components/Auth/AuthLayout';
@ -62,22 +66,22 @@ const setup = ({
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useRegisterUserMutationReturnValue);
const mockUseGetUserQuery = jest
.spyOn(mockDataProvider, 'useGetUserQuery')
.spyOn(authQueries, 'useGetUserQuery')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useGetUserQueryReturnValue);
const mockUseGetStartupConfig = jest
.spyOn(mockDataProvider, 'useGetStartupConfig')
.spyOn(endpointQueries, 'useGetStartupConfig')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useGetStartupConfigReturnValue);
const mockUseRefreshTokenMutation = jest
.spyOn(mockDataProvider, 'useRefreshTokenMutation')
.spyOn(authMutations, 'useRefreshTokenMutation')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useRefreshTokenMutationReturnValue);
const mockUseOutletContext = jest.spyOn(reactRouter, 'useOutletContext').mockReturnValue({
startupConfig: useGetStartupConfigReturnValue.data,
});
const mockUseGetBannerQuery = jest
.spyOn(mockDataProvider, 'useGetBannerQuery')
.spyOn(miscDataProvider, 'useGetBannerQuery')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useGetBannerQueryReturnValue);
const renderResult = render(

View file

@ -1,8 +1,8 @@
import { useEffect, useRef } from 'react';
import { XIcon } from 'lucide-react';
import { useRecoilState } from 'recoil';
import { useGetBannerQuery } from 'librechat-data-provider/react-query';
import { useGetBannerQuery } from '~/data-provider';
import store from '~/store';
import { useEffect, useRef } from 'react';
export const Banner = ({ onHeightChange }: { onHeightChange?: (height: number) => void }) => {
const { data: banner } = useGetBannerQuery();

View file

@ -2,7 +2,7 @@ import React, { useEffect } from 'react';
import ReactMarkdown from 'react-markdown';
import TagManager from 'react-gtm-module';
import { Constants } from 'librechat-data-provider';
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import { useGetStartupConfig } from '~/data-provider';
import { useLocalize } from '~/hooks';
export default function Footer({ className }: { className?: string }) {

View file

@ -1,9 +1,9 @@
import { useMemo } from 'react';
import { useOutletContext } from 'react-router-dom';
import { getConfigDefaults, PermissionTypes, Permissions } from 'librechat-data-provider';
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import type { ContextType } from '~/common';
import { EndpointsMenu, ModelSpecsMenu, PresetsMenu, HeaderNewChat } from './Menus';
import { useGetStartupConfig } from '~/data-provider';
import ExportAndShareMenu from './ExportAndShareMenu';
import { useMediaQuery, useHasAccess } from '~/hooks';
import HeaderOptions from './Input/HeaderOptions';

View file

@ -1,8 +1,8 @@
import { useMemo } from 'react';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { TConversation, TEndpointOption, TPreset } from 'librechat-data-provider';
import type { SetterOrUpdater } from 'recoil';
import useGetSender from '~/hooks/Conversations/useGetSender';
import { useGetEndpointsQuery } from '~/data-provider';
import { EndpointIcon } from '~/components/Endpoints';
import { getPresetTitle } from '~/utils';

View file

@ -2,8 +2,8 @@ import * as Ariakit from '@ariakit/react';
import React, { useRef, useState, useMemo } from 'react';
import { FileSearch, ImageUpIcon, TerminalSquareIcon } from 'lucide-react';
import { EToolResources, EModelEndpoint } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import { FileUpload, TooltipAnchor, DropdownPopup } from '~/components/ui';
import { useGetEndpointsQuery } from '~/data-provider';
import { AttachmentIcon } from '~/components/svg';
import { useLocalize } from '~/hooks';
import { cn } from '~/utils';

View file

@ -1,8 +1,8 @@
import React, { useMemo } from 'react';
import { EModelEndpoint, EToolResources } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import { FileSearch, ImageUpIcon, TerminalSquareIcon } from 'lucide-react';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { useGetEndpointsQuery } from '~/data-provider';
import useLocalize from '~/hooks/useLocalize';
import { OGDialog } from '~/components/ui';

View file

@ -2,13 +2,13 @@ import { useRecoilState } from 'recoil';
import { Settings2 } from 'lucide-react';
import { Root, Anchor } from '@radix-ui/react-popover';
import { useState, useEffect, useMemo } from 'react';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import { tConvoUpdateSchema, EModelEndpoint, isParamEndpoint } from 'librechat-data-provider';
import type { TPreset, TInterfaceConfig } from 'librechat-data-provider';
import { EndpointSettings, SaveAsPresetDialog, AlternativeSettings } from '~/components/Endpoints';
import { PluginStoreDialog, TooltipAnchor } from '~/components';
import { ModelSelect } from '~/components/Input/ModelSelect';
import { useSetIndexOptions, useLocalize } from '~/hooks';
import { useGetEndpointsQuery } from '~/data-provider';
import OptionsPopover from './OptionsPopover';
import PopoverButtons from './PopoverButtons';
import { useChatContext } from '~/Providers';

View file

@ -1,10 +1,13 @@
import { useMemo } from 'react';
import { EModelEndpoint, Constants } from 'librechat-data-provider';
import { useGetEndpointsQuery, useGetStartupConfig } from 'librechat-data-provider/react-query';
import type * as t from 'librechat-data-provider';
import type { ReactNode } from 'react';
import { useChatContext, useAgentsMapContext, useAssistantsMapContext } from '~/Providers';
import { useGetAssistantDocsQuery } from '~/data-provider';
import {
useGetAssistantDocsQuery,
useGetEndpointsQuery,
useGetStartupConfig,
} from '~/data-provider';
import ConvoIcon from '~/components/Endpoints/ConvoIcon';
import { getIconEndpoint, getEntity, cn } from '~/utils';
import { useLocalize, useSubmitMessage } from '~/hooks';

View file

@ -2,12 +2,12 @@ import { useState } from 'react';
import { Settings } from 'lucide-react';
import { useRecoilValue } from 'recoil';
import { EModelEndpoint } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { TConversation } from 'librechat-data-provider';
import type { FC } from 'react';
import { cn, getConvoSwitchLogic, getEndpointField, getIconKey } from '~/utils';
import { useLocalize, useUserKey, useDefaultConvo } from '~/hooks';
import { SetKeyDialog } from '~/components/Input/SetKeyDialog';
import { useGetEndpointsQuery } from '~/data-provider';
import { useChatContext } from '~/Providers';
import { icons } from './Icons';
import store from '~/store';

View file

@ -6,7 +6,7 @@ import {
PermissionTypes,
Permissions,
} from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import { useGetEndpointsQuery } from '~/data-provider';
import MenuSeparator from '../UI/MenuSeparator';
import { getEndpointField } from '~/utils';
import { useHasAccess } from '~/hooks';

View file

@ -1,9 +1,9 @@
import { useCallback, useRef } from 'react';
import { alternateName } from 'librechat-data-provider';
import { Content, Portal, Root } from '@radix-ui/react-popover';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { FC, KeyboardEvent } from 'react';
import { useChatContext, useAgentsMapContext, useAssistantsMapContext } from '~/Providers';
import { useGetEndpointsQuery } from '~/data-provider';
import { mapEndpoints, getEntity } from '~/utils';
import EndpointItems from './Endpoints/MenuItems';
import useLocalize from '~/hooks/useLocalize';

View file

@ -1,13 +1,13 @@
import { useRecoilValue } from 'recoil';
import { useMemo, useCallback, useRef } from 'react';
import { Content, Portal, Root } from '@radix-ui/react-popover';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import { EModelEndpoint, isAssistantsEndpoint } from 'librechat-data-provider';
import type { TModelSpec, TConversation, TEndpointsConfig } from 'librechat-data-provider';
import type { KeyboardEvent } from 'react';
import { useChatContext, useAssistantsMapContext } from '~/Providers';
import { useDefaultConvo, useNewConvo, useLocalize } from '~/hooks';
import { getConvoSwitchLogic, getModelSpecIconURL } from '~/utils';
import { useGetEndpointsQuery } from '~/data-provider';
import MenuButton from './MenuButton';
import ModelSpecs from './ModelSpecs';
import store from '~/store';

View file

@ -2,7 +2,6 @@ import { useRecoilState } from 'recoil';
import { useCallback, useEffect, useMemo } from 'react';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, isAgentsEndpoint } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { TModelsConfig, TEndpointsConfig } from 'librechat-data-provider';
import {
cn,
@ -16,6 +15,7 @@ import { useSetIndexOptions, useLocalize, useDebouncedInput } from '~/hooks';
import PopoverButtons from '~/components/Chat/Input/PopoverButtons';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { EndpointSettings } from '~/components/Endpoints';
import { useGetEndpointsQuery } from '~/data-provider';
import { useChatContext } from '~/Providers';
import store from '~/store';

View file

@ -1,7 +1,6 @@
import { useRecoilValue } from 'recoil';
import { Close } from '@radix-ui/react-popover';
import { Flipper, Flipped } from 'react-flip-toolkit';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { FC } from 'react';
import type { TPreset } from 'librechat-data-provider';
import { getPresetTitle, getEndpointField, getIconKey } from '~/utils';
@ -9,6 +8,7 @@ import FileUpload from '~/components/Chat/Input/Files/FileUpload';
import { PinIcon, EditIcon, TrashIcon } from '~/components/svg';
import { Dialog, DialogTrigger, Label } from '~/components/ui';
import DialogTemplate from '~/components/ui/DialogTemplate';
import { useGetEndpointsQuery } from '~/data-provider';
import { MenuSeparator, MenuItem } from '../UI';
import { icons } from '../Endpoints/Icons';
import { useLocalize } from '~/hooks';

View file

@ -1,9 +1,9 @@
import React, { useMemo, memo } from 'react';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { Assistant, Agent } from 'librechat-data-provider';
import type { TMessageIcon } from '~/common';
import { getEndpointField, getIconEndpoint, logger } from '~/utils';
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
import { useGetEndpointsQuery } from '~/data-provider';
import Icon from '~/components/Endpoints/Icon';
const MessageIcon = memo(

View file

@ -1,10 +1,9 @@
import { useRecoilValue } from 'recoil';
import { useEffect, useMemo } from 'react';
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import { FileSources, LocalStorageKeys, getConfigDefaults } from 'librechat-data-provider';
import type { ExtendedFile } from '~/common';
import { useDeleteFilesMutation, useGetStartupConfig } from '~/data-provider';
import DragDropWrapper from '~/components/Chat/Input/Files/DragDropWrapper';
import { useDeleteFilesMutation } from '~/data-provider';
import Artifacts from '~/components/Artifacts/Artifacts';
import { SidePanel } from '~/components/SidePanel';
import { useSetFilesToDelete } from '~/hooks';

View file

@ -3,12 +3,12 @@ import { useRecoilValue } from 'recoil';
import { Check, X } from 'lucide-react';
import { useParams } from 'react-router-dom';
import { Constants } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { MouseEvent, FocusEvent, KeyboardEvent } from 'react';
import type { TConversation } from 'librechat-data-provider';
import { useNavigateToConvo, useMediaQuery, useLocalize } from '~/hooks';
import { useUpdateConversationMutation } from '~/data-provider';
import EndpointIcon from '~/components/Endpoints/EndpointIcon';
import { useGetEndpointsQuery } from '~/data-provider';
import { NotificationSeverity } from '~/common';
import { useToastContext } from '~/Providers';
import { ConvoOptions } from './ConvoOptions';

View file

@ -1,12 +1,11 @@
import { useState, useId, useRef, memo } from 'react';
import * as Menu from '@ariakit/react/menu';
import { Ellipsis, Share2, Copy, Archive, Pen, Trash } from 'lucide-react';
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import type { MouseEvent } from 'react';
import type * as t from '~/common';
import { useDuplicateConversationMutation, useGetStartupConfig } from '~/data-provider';
import { useLocalize, useArchiveHandler, useNavigateToConvo } from '~/hooks';
import { useToastContext, useChatContext } from '~/Providers';
import { useDuplicateConversationMutation } from '~/data-provider';
import { DropdownPopup } from '~/components/ui';
import DeleteButton from './DeleteButton';
import ShareButton from './ShareButton';

View file

@ -1,9 +1,10 @@
import { useRecoilValue } from 'recoil';
import { SettingsViews, TConversation } from 'librechat-data-provider';
import { useGetModelsQuery, useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
import type { TSettingsProps } from '~/common';
import { getSettings } from './Settings';
import { useGetEndpointsQuery } from '~/data-provider';
import { cn, getEndpointField } from '~/utils';
import { getSettings } from './Settings';
import store from '~/store';
export default function Settings({

View file

@ -1,11 +1,11 @@
import React, { useState } from 'react';
import { useForm, FormProvider } from 'react-hook-form';
import { EModelEndpoint, alternateName, isAssistantsEndpoint } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { TDialogProps } from '~/common';
import OGDialogTemplate from '~/components/ui/OGDialogTemplate';
import { RevokeKeysButton } from '~/components/Nav';
import { useGetEndpointsQuery } from '~/data-provider';
import { OGDialog, Dropdown } from '~/components/ui';
import { RevokeKeysButton } from '~/components/Nav';
import { useUserKey, useLocalize } from '~/hooks';
import { useToastContext } from '~/Providers';
import CustomConfig from './CustomEndpoint';
@ -118,7 +118,7 @@ const SetKeyDialog = ({
if (isOpenAIBase && key === 'baseURL') {
return false;
}
if (key === 'baseURL' && !userProvideURL) {
if (key === 'baseURL' && !(userProvideURL ?? false)) {
return false;
}
return data[key] === '';
@ -205,7 +205,7 @@ const SetKeyDialog = ({
leftButtons={
<RevokeKeysButton
endpoint={endpoint}
disabled={!expiryTime}
disabled={!(expiryTime ?? '')}
setDialogOpen={onOpenChange}
/>
}

View file

@ -1,8 +1,8 @@
import { Disclosure, DisclosureButton, DisclosurePanel } from '@headlessui/react';
import { useCallback, memo, ReactNode } from 'react';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { TResPlugin, TInput } from 'librechat-data-provider';
import { ChevronDownIcon, LucideProps } from 'lucide-react';
import type { TResPlugin, TInput } from 'librechat-data-provider';
import { useGetEndpointsQuery } from '~/data-provider';
import { useShareContext } from '~/Providers';
import { cn, formatJSON } from '~/utils';
import { Spinner } from '~/components';
@ -47,7 +47,7 @@ const Plugin: React.FC<PluginProps> = ({ plugin }) => {
if (pluginKey === 'n/a' || pluginKey === 'self reflection') {
return pluginKey;
}
return plugins?.[pluginKey] ?? 'self reflection';
return plugins[pluginKey] ?? 'self reflection';
},
[plugins],
);
@ -105,7 +105,7 @@ const Plugin: React.FC<PluginProps> = ({ plugin }) => {
<DisclosurePanel className="mt-3 flex max-w-full flex-col gap-3">
<CodeBlock
lang={latestPlugin ? `REQUEST TO ${latestPlugin?.toUpperCase()}` : 'REQUEST'}
lang={latestPlugin ? `REQUEST TO ${latestPlugin.toUpperCase()}` : 'REQUEST'}
codeChildren={formatInputs(plugin.inputs ?? [])}
plugin={true}
classProp="max-h-[450px]"
@ -113,7 +113,7 @@ const Plugin: React.FC<PluginProps> = ({ plugin }) => {
{plugin.outputs && plugin.outputs.length > 0 && (
<CodeBlock
lang={
latestPlugin ? `RESPONSE FROM ${latestPlugin?.toUpperCase()}` : 'RESPONSE'
latestPlugin ? `RESPONSE FROM ${latestPlugin.toUpperCase()}` : 'RESPONSE'
}
codeChildren={formatJSON(plugin.outputs ?? '')}
plugin={true}

View file

@ -2,8 +2,8 @@ import { useRecoilState } from 'recoil';
import * as Select from '@ariakit/react/select';
import { Fragment, useState, memo } from 'react';
import { FileText, LogOut } from 'lucide-react';
import { useGetUserBalance, useGetStartupConfig } from 'librechat-data-provider/react-query';
import { LinkIcon, GearIcon, DropdownMenuSeparator } from '~/components';
import { useGetStartupConfig, useGetUserBalance } from '~/data-provider';
import FilesView from '~/components/Chat/Input/Files/FilesView';
import { useAuthContext } from '~/hooks/AuthContext';
import useAvatar from '~/hooks/Messages/useAvatar';

View file

@ -3,11 +3,11 @@ import { useRecoilValue } from 'recoil';
import { useNavigate } from 'react-router-dom';
import { useQueryClient } from '@tanstack/react-query';
import { QueryKeys, Constants } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { TConversation, TMessage } from 'librechat-data-provider';
import { getEndpointField, getIconEndpoint, getIconKey } from '~/utils';
import { icons } from '~/components/Chat/Menus/Endpoints/Icons';
import ConvoIconURL from '~/components/Endpoints/ConvoIconURL';
import { useGetEndpointsQuery } from '~/data-provider';
import { useLocalize, useNewConvo } from '~/hooks';
import { NewChatIcon } from '~/components/svg';
import { cn } from '~/utils';

View file

@ -2,6 +2,8 @@ import { render, screen, fireEvent } from 'test/layout-test-utils';
import PluginStoreDialog from '../PluginStoreDialog';
import userEvent from '@testing-library/user-event';
import * as mockDataProvider from 'librechat-data-provider/react-query';
import * as authMutations from '~/data-provider/Auth/mutations';
import * as authQueries from '~/data-provider/Auth/queries';
jest.mock('librechat-data-provider/react-query');
@ -143,11 +145,11 @@ const setup = ({
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useUpdateUserPluginsMutationReturnValue);
const mockUseGetUserQuery = jest
.spyOn(mockDataProvider, 'useGetUserQuery')
.spyOn(authQueries, 'useGetUserQuery')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useGetUserQueryReturnValue);
const mockUseRefreshTokenMutation = jest
.spyOn(mockDataProvider, 'useRefreshTokenMutation')
.spyOn(authMutations, 'useRefreshTokenMutation')
//@ts-ignore - we don't need all parameters of the QueryObserverSuccessResult
.mockReturnValue(useRefreshTokenMutationReturnValue);
const mockSetIsOpen = jest.fn();

View file

@ -1,9 +1,9 @@
import { useNavigate } from 'react-router-dom';
import { PermissionTypes, Permissions } from 'librechat-data-provider';
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import type { TPromptGroup, TStartupConfig } from 'librechat-data-provider';
import DashGroupItem from '~/components/Prompts/Groups/DashGroupItem';
import ChatGroupItem from '~/components/Prompts/Groups/ChatGroupItem';
import { useGetStartupConfig } from '~/data-provider';
import { useLocalize, useHasAccess } from '~/hooks';
import { Button, Skeleton } from '~/components/ui';

View file

@ -2,7 +2,6 @@ import React, { useEffect, useMemo } from 'react';
import { Share2Icon } from 'lucide-react';
import { useForm, Controller } from 'react-hook-form';
import { Permissions } from 'librechat-data-provider';
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import type {
TPromptGroup,
TStartupConfig,
@ -15,7 +14,7 @@ import {
OGDialogTrigger,
OGDialogClose,
} from '~/components/ui';
import { useUpdatePromptGroup } from '~/data-provider';
import { useUpdatePromptGroup, useGetStartupConfig } from '~/data-provider';
import { Button, Switch } from '~/components/ui';
import { useToastContext } from '~/Providers';
import { useLocalize } from '~/hooks';

View file

@ -1,7 +1,8 @@
import { memo } from 'react';
import { useParams } from 'react-router-dom';
import { useGetSharedMessages, useGetStartupConfig } from 'librechat-data-provider/react-query';
import { useGetSharedMessages } from 'librechat-data-provider/react-query';
import { useLocalize, useDocumentTitle } from '~/hooks';
import { useGetStartupConfig } from '~/data-provider';
import { ShareContext } from '~/Providers';
import { Spinner } from '~/components/svg';
import MessagesView from './MessagesView';

View file

@ -1,9 +1,8 @@
import { useState, useEffect, useMemo } from 'react';
import { EModelEndpoint } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { ActionsEndpoint } from '~/common';
import type { Action, TConfig, TEndpointsConfig } from 'librechat-data-provider';
import { useGetActionsQuery } from '~/data-provider';
import { useGetActionsQuery, useGetEndpointsQuery } from '~/data-provider';
import { useChatContext } from '~/Providers';
import ActionsPanel from './ActionsPanel';
import AgentPanel from './AgentPanel';

View file

@ -1,14 +1,13 @@
import { Plus, EarthIcon } from 'lucide-react';
import { useCallback, useEffect, useRef } from 'react';
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import { AgentCapabilities, defaultAgentFormValues } from 'librechat-data-provider';
import type { UseMutationResult, QueryObserverResult } from '@tanstack/react-query';
import type { Agent, AgentCreateParams } from 'librechat-data-provider';
import type { UseFormReset } from 'react-hook-form';
import type { TAgentCapabilities, AgentForm, TAgentOption } from '~/common';
import { cn, createDropdownSetter, createProviderOption, processAgentOption } from '~/utils';
import { useListAgentsQuery, useGetStartupConfig } from '~/data-provider';
import SelectDropDown from '~/components/ui/SelectDropDown';
import { useListAgentsQuery } from '~/data-provider';
import { useLocalize } from '~/hooks';
const keys = new Set(Object.keys(defaultAgentFormValues));

View file

@ -2,12 +2,12 @@ import React, { useMemo, useEffect } from 'react';
import { ChevronLeft, RotateCcw } from 'lucide-react';
import { getSettingsKeys } from 'librechat-data-provider';
import { useFormContext, useWatch, Controller } from 'react-hook-form';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type * as t from 'librechat-data-provider';
import type { AgentForm, AgentModelPanelProps, StringOption } from '~/common';
import { componentMapping } from '~/components/SidePanel/Parameters/components';
import { agentSettings } from '~/components/SidePanel/Parameters/settings';
import { getEndpointField, cn, cardStyle } from '~/utils';
import { useGetEndpointsQuery } from '~/data-provider';
import { SelectDropDown } from '~/components/ui';
import { useLocalize } from '~/hooks';
import { Panel } from '~/common';

View file

@ -2,7 +2,6 @@ import React, { useEffect, useMemo } from 'react';
import { Share2Icon } from 'lucide-react';
import { useForm, Controller } from 'react-hook-form';
import { Permissions } from 'librechat-data-provider';
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import type { TStartupConfig, AgentUpdateParams } from 'librechat-data-provider';
import {
Button,
@ -13,7 +12,7 @@ import {
OGDialogContent,
OGDialogTrigger,
} from '~/components/ui';
import { useUpdateAgentMutation } from '~/data-provider';
import { useUpdateAgentMutation, useGetStartupConfig } from '~/data-provider';
import { cn, removeFocusOutlines } from '~/utils';
import { useToastContext } from '~/Providers';
import { useLocalize } from '~/hooks';

View file

@ -1,9 +1,12 @@
import { useState, useEffect, useMemo } from 'react';
import { defaultAssistantsVersion } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { Action, TEndpointsConfig, AssistantsEndpoint } from 'librechat-data-provider';
import type { ActionsEndpoint } from '~/common';
import { useGetActionsQuery, useGetAssistantDocsQuery } from '~/data-provider';
import {
useGetActionsQuery,
useGetEndpointsQuery,
useGetAssistantDocsQuery,
} from '~/data-provider';
import AssistantPanel from './AssistantPanel';
import { useChatContext } from '~/Providers';
import ActionsPanel from './ActionsPanel';

View file

@ -1,9 +1,9 @@
import React, { useMemo, useState, useEffect, useCallback } from 'react';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import { getSettingsKeys, tConvoUpdateSchema } from 'librechat-data-provider';
import type { TPreset } from 'librechat-data-provider';
import { SaveAsPresetDialog } from '~/components/Endpoints';
import { useSetIndexOptions, useLocalize } from '~/hooks';
import { useGetEndpointsQuery } from '~/data-provider';
import { getEndpointField, logger } from '~/utils';
import { componentMapping } from './components';
import { useChatContext } from '~/Providers';

View file

@ -1,14 +1,11 @@
import throttle from 'lodash/throttle';
import { getConfigDefaults } from 'librechat-data-provider';
import { useUserKeyQuery } from 'librechat-data-provider/react-query';
import { useState, useRef, useCallback, useEffect, useMemo, memo } from 'react';
import {
useGetEndpointsQuery,
useGetStartupConfig,
useUserKeyQuery,
} from 'librechat-data-provider/react-query';
import type { TEndpointsConfig, TInterfaceConfig } from 'librechat-data-provider';
import type { ImperativePanelHandle } from 'react-resizable-panels';
import { ResizableHandleAlt, ResizablePanel, ResizablePanelGroup } from '~/components/ui/Resizable';
import { useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider';
import { useMediaQuery, useLocalStorage, useLocalize } from '~/hooks';
import useSideNavLinks from '~/hooks/Nav/useSideNavLinks';
import NavToggle from '~/components/Nav/NavToggle';

View file

@ -1 +1,2 @@
export * from './queries';
export * from './mutations';

View file

@ -1,6 +1,6 @@
import { useResetRecoilState } from 'recoil';
import { useResetRecoilState, useSetRecoilState } from 'recoil';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { MutationKeys, dataService } from 'librechat-data-provider';
import { MutationKeys, dataService, request } from 'librechat-data-provider';
import type { UseMutationResult } from '@tanstack/react-query';
import type * as t from 'librechat-data-provider';
import useClearStates from '~/hooks/Config/useClearStates';
@ -9,15 +9,17 @@ import store from '~/store';
/* login/logout */
export const useLogoutUserMutation = (
options?: t.LogoutOptions,
): UseMutationResult<unknown, unknown, undefined, unknown> => {
): UseMutationResult<t.TLogoutResponse, unknown, undefined, unknown> => {
const queryClient = useQueryClient();
const clearStates = useClearStates();
const resetDefaultPreset = useResetRecoilState(store.defaultPreset);
const setQueriesEnabled = useSetRecoilState<boolean>(store.queriesEnabled);
return useMutation([MutationKeys.logoutUser], {
mutationFn: () => dataService.logout(),
...(options || {}),
onSuccess: (...args) => {
setQueriesEnabled(false);
resetDefaultPreset();
clearStates();
queryClient.removeQueries();
@ -26,20 +28,39 @@ export const useLogoutUserMutation = (
});
};
export const useLoginUserMutation = (): UseMutationResult<
t.TLoginResponse,
unknown,
t.TLoginUser,
unknown
> => {
export const useLoginUserMutation = (
options?: t.MutationOptions<t.TLoginResponse, t.TLoginUser, unknown, unknown>,
): UseMutationResult<t.TLoginResponse, unknown, t.TLoginUser, unknown> => {
const queryClient = useQueryClient();
const clearStates = useClearStates();
const resetDefaultPreset = useResetRecoilState(store.defaultPreset);
return useMutation((payload: t.TLoginUser) => dataService.login(payload), {
onMutate: () => {
const setQueriesEnabled = useSetRecoilState<boolean>(store.queriesEnabled);
return useMutation([MutationKeys.loginUser], {
mutationFn: (payload: t.TLoginUser) => dataService.login(payload),
...(options || {}),
onMutate: (vars) => {
resetDefaultPreset();
clearStates();
queryClient.removeQueries();
options?.onMutate?.(vars);
},
onSuccess: (...args) => {
setQueriesEnabled(true);
options?.onSuccess?.(...args);
},
});
};
export const useRefreshTokenMutation = (
options?: t.MutationOptions<t.TRefreshTokenResponse | undefined, undefined, unknown, unknown>,
): UseMutationResult<t.TRefreshTokenResponse | undefined, unknown, undefined, unknown> => {
const queryClient = useQueryClient();
return useMutation([MutationKeys.refreshToken], {
mutationFn: () => request.refreshToken(),
...(options || {}),
onMutate: (vars) => {
queryClient.removeQueries();
options?.onMutate?.(vars);
},
});
};

View file

@ -0,0 +1,20 @@
import { useRecoilValue } from 'recoil';
import { QueryKeys, dataService } from 'librechat-data-provider';
import { useQuery } from '@tanstack/react-query';
import type { QueryObserverResult, UseQueryOptions } from '@tanstack/react-query';
import type t from 'librechat-data-provider';
import store from '~/store';
export const useGetUserQuery = (
config?: UseQueryOptions<t.TUser>,
): QueryObserverResult<t.TUser> => {
const queriesEnabled = useRecoilValue<boolean>(store.queriesEnabled);
return useQuery<t.TUser>([QueryKeys.user], () => dataService.getUser(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
retry: false,
...config,
enabled: (config?.enabled ?? true) === true && queriesEnabled,
});
};

View file

@ -0,0 +1 @@
export * from './queries';

View file

@ -0,0 +1,41 @@
import { useRecoilValue } from 'recoil';
import { QueryKeys, dataService } from 'librechat-data-provider';
import { useQuery } from '@tanstack/react-query';
import type { QueryObserverResult, UseQueryOptions } from '@tanstack/react-query';
import type t from 'librechat-data-provider';
import store from '~/store';
export const useGetEndpointsQuery = <TData = t.TEndpointsConfig>(
config?: UseQueryOptions<t.TEndpointsConfig, unknown, TData>,
): QueryObserverResult<TData> => {
const queriesEnabled = useRecoilValue<boolean>(store.queriesEnabled);
return useQuery<t.TEndpointsConfig, unknown, TData>(
[QueryKeys.endpoints],
() => dataService.getAIEndpoints(),
{
staleTime: Infinity,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
enabled: (config?.enabled ?? true) === true && queriesEnabled,
},
);
};
export const useGetStartupConfig = (
config?: UseQueryOptions<t.TStartupConfig>,
): QueryObserverResult<t.TStartupConfig> => {
const queriesEnabled = useRecoilValue<boolean>(store.queriesEnabled);
return useQuery<t.TStartupConfig>(
[QueryKeys.startupConfig],
() => dataService.getStartupConfig(),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
enabled: (config?.enabled ?? true) === true && queriesEnabled,
},
);
};

View file

@ -1,17 +1,21 @@
import { useRecoilValue } from 'recoil';
import { QueryKeys, dataService } from 'librechat-data-provider';
import { useQuery, useQueryClient } from '@tanstack/react-query';
import type { QueryObserverResult, UseQueryOptions } from '@tanstack/react-query';
import type t from 'librechat-data-provider';
import { addFileToCache } from '~/utils';
import store from '~/store';
export const useGetFiles = <TData = t.TFile[] | boolean>(
config?: UseQueryOptions<t.TFile[], unknown, TData>,
): QueryObserverResult<TData, unknown> => {
const queriesEnabled = useRecoilValue<boolean>(store.queriesEnabled);
return useQuery<t.TFile[], unknown, TData>([QueryKeys.files], () => dataService.getFiles(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
enabled: (config?.enabled ?? true) === true && queriesEnabled,
});
};

View file

@ -0,0 +1 @@
export * from './queries';

View file

@ -0,0 +1,45 @@
import { useRecoilValue } from 'recoil';
import { QueryKeys, dataService } from 'librechat-data-provider';
import { useQuery } from '@tanstack/react-query';
import type { QueryObserverResult, UseQueryOptions } from '@tanstack/react-query';
import type t from 'librechat-data-provider';
import store from '~/store';
export const useGetBannerQuery = (
config?: UseQueryOptions<t.TBannerResponse>,
): QueryObserverResult<t.TBannerResponse> => {
const queriesEnabled = useRecoilValue<boolean>(store.queriesEnabled);
return useQuery<t.TBannerResponse>([QueryKeys.banner], () => dataService.getBanner(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
enabled: (config?.enabled ?? true) === true && queriesEnabled,
});
};
export const useGetUserBalance = (
config?: UseQueryOptions<string>,
): QueryObserverResult<string> => {
const queriesEnabled = useRecoilValue<boolean>(store.queriesEnabled);
return useQuery<string>([QueryKeys.balance], () => dataService.getUserBalance(), {
refetchOnWindowFocus: true,
refetchOnReconnect: true,
refetchOnMount: true,
...config,
enabled: (config?.enabled ?? true) === true && queriesEnabled,
});
};
export const useGetSearchEnabledQuery = (
config?: UseQueryOptions<boolean>,
): QueryObserverResult<boolean> => {
const queriesEnabled = useRecoilValue<boolean>(store.queriesEnabled);
return useQuery<boolean>([QueryKeys.searchEnabled], () => dataService.getSearchEnabled(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
enabled: (config?.enabled ?? true) === true && queriesEnabled,
});
};

View file

@ -1,7 +1,9 @@
export * from './Auth';
export * from './Agents';
export * from './Endpoints';
export * from './Files';
export * from './Messages';
export * from './Misc';
export * from './Tools';
export * from './connection';
export * from './mutations';

View file

@ -5,6 +5,7 @@ import {
defaultOrderQuery,
defaultAssistantsVersion,
} from 'librechat-data-provider';
import { useRecoilValue } from 'recoil';
import { useQuery, useInfiniteQuery, useQueryClient } from '@tanstack/react-query';
import type {
UseInfiniteQueryOptions,
@ -28,6 +29,7 @@ import type {
SharedLinksResponse,
} from 'librechat-data-provider';
import { findPageForConversation } from '~/utils';
import store from '~/store';
export const useGetPresetsQuery = (
config?: UseQueryOptions<TPreset[]>,
@ -115,6 +117,7 @@ export const useConversationsInfiniteQuery = (
params?: ConversationListParams,
config?: UseInfiniteQueryOptions<ConversationListResponse, unknown>,
) => {
const queriesEnabled = useRecoilValue<boolean>(store.queriesEnabled);
return useInfiniteQuery<ConversationListResponse, unknown>(
params?.isArchived === true ? [QueryKeys.archivedConversations] : [QueryKeys.allConversations],
({ pageParam = '' }) =>
@ -135,6 +138,7 @@ export const useConversationsInfiniteQuery = (
refetchOnReconnect: false,
refetchOnMount: false,
...config,
enabled: (config?.enabled ?? true) === true && queriesEnabled,
},
);
};
@ -156,7 +160,7 @@ export const useSharedLinksQuery = (
sortBy,
sortDirection,
}),
getNextPageParam: (lastPage) => lastPage?.nextCursor ?? undefined,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
keepPreviousData: true,
staleTime: 5 * 60 * 1000, // 5 minutes
cacheTime: 30 * 60 * 1000, // 30 minutes

View file

@ -7,12 +7,17 @@ import {
useCallback,
createContext,
} from 'react';
import { useRecoilState } from 'recoil';
import { useNavigate } from 'react-router-dom';
import { useRecoilState, useRecoilValue } from 'recoil';
import { setTokenHeader, SystemRoles } from 'librechat-data-provider';
import { useGetUserQuery, useRefreshTokenMutation } from 'librechat-data-provider/react-query';
import type { TLoginResponse, TLoginUser } from 'librechat-data-provider';
import { useLoginUserMutation, useLogoutUserMutation, useGetRole } from '~/data-provider';
import type * as t from 'librechat-data-provider';
import {
useGetRole,
useGetUserQuery,
useLoginUserMutation,
useLogoutUserMutation,
useRefreshTokenMutation,
} from '~/data-provider';
import { TAuthConfig, TUserContext, TAuthContext, TResError } from '~/common';
import useTimeout from './useTimeout';
import store from '~/store';
@ -42,14 +47,20 @@ const AuthContextProvider = ({
const setUserContext = useCallback(
(userContext: TUserContext) => {
const { token, isAuthenticated, user, redirect } = userContext;
if (user) {
setUser(user);
}
setUser(user);
setToken(token);
//@ts-ignore - ok for token to be undefined initially
setTokenHeader(token);
setIsAuthenticated(isAuthenticated);
if (redirect != null && redirect) {
if (redirect == null) {
return;
}
if (redirect.startsWith('http://') || redirect.startsWith('https://')) {
// For external links, use window.location
window.location.href = redirect;
// Or if you want to open in a new tab:
// window.open(redirect, '_blank');
} else {
navigate(redirect, { replace: true });
}
},
@ -57,14 +68,25 @@ const AuthContextProvider = ({
);
const doSetError = useTimeout({ callback: (error) => setError(error as string | undefined) });
const loginUser = useLoginUserMutation();
const loginUser = useLoginUserMutation({
onSuccess: (data: t.TLoginResponse) => {
const { user, token } = data;
setError(undefined);
setUserContext({ token, isAuthenticated: true, user, redirect: '/c/new' });
},
onError: (error: TResError | unknown) => {
const resError = error as TResError;
doSetError(resError.message);
navigate('/login', { replace: true });
},
});
const logoutUser = useLogoutUserMutation({
onSuccess: () => {
onSuccess: (data) => {
setUserContext({
token: undefined,
isAuthenticated: false,
user: undefined,
redirect: '/login',
redirect: data.redirect ?? '/login',
});
},
onError: (error) => {
@ -77,24 +99,13 @@ const AuthContextProvider = ({
});
},
});
const refreshToken = useRefreshTokenMutation();
const logout = useCallback(() => logoutUser.mutate(undefined), [logoutUser]);
const userQuery = useGetUserQuery({ enabled: !!(token ?? '') });
const refreshToken = useRefreshTokenMutation();
const login = (data: TLoginUser) => {
loginUser.mutate(data, {
onSuccess: (data: TLoginResponse) => {
const { user, token } = data;
setError(undefined);
setUserContext({ token, isAuthenticated: true, user, redirect: '/c/new' });
},
onError: (error: TResError | unknown) => {
const resError = error as TResError;
doSetError(resError.message);
navigate('/login', { replace: true });
},
});
const login = (data: t.TLoginUser) => {
loginUser.mutate(data);
};
const silentRefresh = useCallback(() => {
@ -103,7 +114,7 @@ const AuthContextProvider = ({
return;
}
refreshToken.mutate(undefined, {
onSuccess: (data: TLoginResponse | undefined) => {
onSuccess: (data: t.TRefreshTokenResponse | undefined) => {
const { user, token = '' } = data ?? {};
if (token) {
setUserContext({ token, isAuthenticated: true, user });

View file

@ -5,7 +5,6 @@ import { LocalStorageKeys } from 'librechat-data-provider';
import { useAvailablePluginsQuery } from 'librechat-data-provider/react-query';
import type { TStartupConfig, TPlugin, TUser } from 'librechat-data-provider';
import { mapPlugins, selectPlugins, processPlugins } from '~/utils';
import useConfigOverride from './useConfigOverride';
import store from '~/store';
const pluginStore: TPlugin = {
@ -25,7 +24,6 @@ export default function useAppStartup({
startupConfig?: TStartupConfig;
user?: TUser;
}) {
useConfigOverride();
const setAvailableTools = useSetRecoilState(store.availableTools);
const [defaultPreset, setDefaultPreset] = useRecoilState(store.defaultPreset);
const { data: allPlugins } = useAvailablePluginsQuery({

View file

@ -1,11 +1,12 @@
import { useGetEndpointsQuery, useGetModelsQuery } from 'librechat-data-provider/react-query';
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
import type {
TConversation,
TPreset,
TEndpointsConfig,
TModelsConfig,
TConversation,
TPreset,
} from 'librechat-data-provider';
import { getDefaultEndpoint, buildDefaultConvo } from '~/utils';
import { useGetEndpointsQuery } from '~/data-provider';
type TDefaultConvo = { conversation: Partial<TConversation>; preset?: Partial<TPreset> | null };

View file

@ -1,17 +1,19 @@
import { useRecoilValue } from 'recoil';
import { useCallback, useRef, useEffect } from 'react';
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
import { LocalStorageKeys, isAssistantsEndpoint } from 'librechat-data-provider';
import { useGetModelsQuery, useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type {
TPreset,
TModelsConfig,
TConversation,
TEndpointsConfig,
EModelEndpoint,
} from 'librechat-data-provider';
import type { SetterOrUpdater } from 'recoil';
import type { AssistantListItem } from '~/common';
import { getEndpointField, buildDefaultConvo, getDefaultEndpoint } from '~/utils';
import useAssistantListMap from '~/hooks/Assistants/useAssistantListMap';
import { useGetEndpointsQuery } from '~/data-provider';
import { mainTextareaId } from '~/common';
import store from '~/store';
@ -32,7 +34,7 @@ const useGenerateConvo = ({
const rootConvo = useRecoilValue(store.conversationByKeySelector(rootIndex));
useEffect(() => {
if (rootConvo?.conversationId && setConversation) {
if (rootConvo?.conversationId != null && setConversation) {
setConversation((prevState) => {
if (!prevState) {
return prevState;
@ -85,11 +87,11 @@ const useGenerateConvo = ({
}
const isAssistantEndpoint = isAssistantsEndpoint(defaultEndpoint);
const assistants: AssistantListItem[] = assistantsListMap[defaultEndpoint] ?? [];
const assistants: AssistantListItem[] = assistantsListMap[defaultEndpoint ?? ''] ?? [];
if (
conversation.assistant_id &&
!assistantsListMap[defaultEndpoint]?.[conversation.assistant_id]
!assistantsListMap[defaultEndpoint ?? '']?.[conversation.assistant_id]
) {
conversation.assistant_id = undefined;
}
@ -101,7 +103,7 @@ const useGenerateConvo = ({
}
if (
conversation.assistant_id &&
conversation.assistant_id != null &&
isAssistantEndpoint &&
conversation.conversationId === 'new'
) {
@ -109,19 +111,19 @@ const useGenerateConvo = ({
conversation.model = assistant?.model;
}
if (conversation.assistant_id && !isAssistantEndpoint) {
if (conversation.assistant_id != null && !isAssistantEndpoint) {
conversation.assistant_id = undefined;
}
const models = modelsConfig?.[defaultEndpoint] ?? [];
const models = modelsConfig?.[defaultEndpoint ?? ''] ?? [];
conversation = buildDefaultConvo({
conversation,
lastConversationSetup: preset as TConversation,
endpoint: defaultEndpoint,
endpoint: defaultEndpoint ?? ('' as EModelEndpoint),
models,
});
if (preset?.title) {
if (preset?.title != null && preset.title !== '') {
conversation.title = preset.title;
}

View file

@ -1,7 +1,7 @@
import { useCallback } from 'react';
import { getResponseSender } from 'librechat-data-provider';
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import type { TEndpointOption, TEndpointsConfig } from 'librechat-data-provider';
import { useGetEndpointsQuery } from '~/data-provider';
export default function useGetSender() {
const { data: endpointsConfig = {} as TEndpointsConfig } = useGetEndpointsQuery();

View file

@ -1,10 +1,9 @@
import { useEffect, useState, useCallback } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useNavigate, useLocation } from 'react-router-dom';
import { useGetSearchEnabledQuery } from 'librechat-data-provider/react-query';
import type { UseInfiniteQueryResult } from '@tanstack/react-query';
import type { ConversationListResponse } from 'librechat-data-provider';
import { useSearchInfiniteQuery } from '~/data-provider';
import { useSearchInfiniteQuery, useGetSearchEnabledQuery } from '~/data-provider';
import useNewConvo from '~/hooks/useNewConvo';
import store from '~/store';

View file

@ -1,9 +1,5 @@
import { useMemo } from 'react';
import {
useGetModelsQuery,
useGetStartupConfig,
useGetEndpointsQuery,
} from 'librechat-data-provider/react-query';
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
import {
alternateName,
EModelEndpoint,
@ -13,8 +9,13 @@ import {
} from 'librechat-data-provider';
import type { TAssistantsMap, TEndpointsConfig } from 'librechat-data-provider';
import type { MentionOption } from '~/common';
import {
useGetPresetsQuery,
useGetEndpointsQuery,
useListAgentsQuery,
useGetStartupConfig,
} from '~/data-provider';
import useAssistantListMap from '~/hooks/Assistants/useAssistantListMap';
import { useGetPresetsQuery, useListAgentsQuery } from '~/data-provider';
import { mapEndpoints, getPresetTitle } from '~/utils';
import { EndpointIcon } from '~/components/Endpoints';

View file

@ -1,5 +1,5 @@
import { useGetEndpointsQuery } from 'librechat-data-provider/react-query';
import { useChatContext } from '~/Providers/ChatContext';
import { useGetEndpointsQuery } from '~/data-provider';
import { getEndpointField } from '~/utils';
import useUserKey from './useUserKey';

View file

@ -1,10 +1,7 @@
import { useMemo, useCallback } from 'react';
import { EModelEndpoint } from 'librechat-data-provider';
import {
useUserKeyQuery,
useGetEndpointsQuery,
useUpdateUserKeysMutation,
} from 'librechat-data-provider/react-query';
import { useUserKeyQuery, useUpdateUserKeysMutation } from 'librechat-data-provider/react-query';
import { useGetEndpointsQuery } from '~/data-provider';
const useUserKey = (endpoint: string) => {
const { data: endpointsConfig } = useGetEndpointsQuery();
@ -24,7 +21,7 @@ const useUserKey = (endpoint: string) => {
const getExpiry = useCallback(() => {
if (checkUserKey.data) {
return checkUserKey.data?.expiresAt || 'never';
return checkUserKey.data.expiresAt || 'never';
}
}, [checkUserKey.data]);

View file

@ -1,23 +1,22 @@
import type { EventSubmission, TMessage, TPayload, TSubmission } from 'librechat-data-provider';
import { useEffect, useState } from 'react';
import { v4 } from 'uuid';
import { SSE } from 'sse.js';
import { useSetRecoilState } from 'recoil';
import {
request,
/* @ts-ignore */
createPayload,
isAgentsEndpoint,
isAssistantsEndpoint,
removeNullishValues,
request,
isAssistantsEndpoint,
} from 'librechat-data-provider';
import { useGetStartupConfig, useGetUserBalance } from 'librechat-data-provider/react-query';
import { useEffect, useState } from 'react';
import { useSetRecoilState } from 'recoil';
import { SSE } from 'sse.js';
import { v4 } from 'uuid';
import type { TResData } from '~/common';
import { useGenTitleMutation } from '~/data-provider';
import { useAuthContext } from '~/hooks/AuthContext';
import store from '~/store';
import type { EventSubmission, TMessage, TPayload, TSubmission } from 'librechat-data-provider';
import type { EventHandlerParams } from './useEventHandlers';
import type { TResData } from '~/common';
import { useGenTitleMutation, useGetStartupConfig, useGetUserBalance } from '~/data-provider';
import { useAuthContext } from '~/hooks/AuthContext';
import useEventHandlers from './useEventHandlers';
import store from '~/store';
type ChatHelpers = Pick<
EventHandlerParams,

View file

@ -1,9 +1,5 @@
import { useCallback, useRef } from 'react';
import {
useGetModelsQuery,
useGetStartupConfig,
useGetEndpointsQuery,
} from 'librechat-data-provider/react-query';
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
import { useNavigate } from 'react-router-dom';
import {
Constants,
@ -30,8 +26,8 @@ import {
getModelSpecIconURL,
updateLastSelectedModel,
} from '~/utils';
import { useDeleteFilesMutation, useGetEndpointsQuery, useGetStartupConfig } from '~/data-provider';
import useAssistantListMap from './Assistants/useAssistantListMap';
import { useDeleteFilesMutation } from '~/data-provider';
import { usePauseGlobalAudio } from './Audio';
import { mainTextareaId } from '~/common';
import store from '~/store';

View file

@ -1,14 +1,15 @@
import { useEffect } from 'react';
import { useParams } from 'react-router-dom';
import { Constants, EModelEndpoint } from 'librechat-data-provider';
import {
useGetModelsQuery,
useGetStartupConfig,
useGetEndpointsQuery,
} from 'librechat-data-provider/react-query';
import { useGetModelsQuery } from 'librechat-data-provider/react-query';
import type { TPreset } from 'librechat-data-provider';
import {
useGetConvoIdQuery,
useHealthCheck,
useGetEndpointsQuery,
useGetStartupConfig,
} from '~/data-provider';
import { useNewConvo, useAppStartup, useAssistantListMap } from '~/hooks';
import { useGetConvoIdQuery, useHealthCheck } from '~/data-provider';
import { getDefaultModelSpec, getModelSpecIconURL } from '~/utils';
import { ToolCallsMapProvider } from '~/Providers';
import ChatView from '~/components/Chat/ChatView';

View file

@ -1,7 +1,23 @@
import { useEffect } from 'react';
import { useRecoilState } from 'recoil';
import { useAuthContext } from '~/hooks/AuthContext';
import StartupLayout from './Startup';
import store from '~/store';
export default function LoginLayout() {
const { isAuthenticated } = useAuthContext();
const [queriesEnabled, setQueriesEnabled] = useRecoilState<boolean>(store.queriesEnabled);
useEffect(() => {
if (queriesEnabled) {
return;
}
const timeout: NodeJS.Timeout = setTimeout(() => {
setQueriesEnabled(true);
}, 500);
return () => {
clearTimeout(timeout);
};
}, [queriesEnabled, setQueriesEnabled]);
return <StartupLayout isAuthenticated={isAuthenticated} />;
}

View file

@ -1,7 +1,7 @@
import { useEffect, useState } from 'react';
import { Outlet, useNavigate, useLocation } from 'react-router-dom';
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import type { TStartupConfig } from 'librechat-data-provider';
import { useGetStartupConfig } from '~/data-provider';
import AuthLayout from '~/components/Auth/AuthLayout';
import { useLocalize } from '~/hooks';

View file

@ -1,6 +1,5 @@
import React, { useState, useEffect } from 'react';
import { Outlet, useNavigate } from 'react-router-dom';
import { useGetStartupConfig } from 'librechat-data-provider/react-query';
import type { ContextType } from '~/common';
import {
AgentsMapContext,
@ -11,7 +10,7 @@ import {
} from '~/Providers';
import { useAuthContext, useAssistantsMap, useAgentsMap, useFileMap, useSearch } from '~/hooks';
import TermsAndConditionsModal from '~/components/ui/TermsAndConditionsModal';
import { useUserTermsQuery } from '~/data-provider';
import { useUserTermsQuery, useGetStartupConfig } from '~/data-provider';
import { Nav, MobileNav } from '~/components/Nav';
import { Banner } from '~/components/Banners';

View file

@ -9,4 +9,9 @@ const messageAttachmentsMap = atom<Record<string, TAttachment[] | undefined>>({
default: {},
});
export default { hideBannerHint, messageAttachmentsMap };
const queriesEnabled = atom<boolean>({
key: 'queriesEnabled',
default: true,
});
export default { hideBannerHint, messageAttachmentsMap, queriesEnabled };

View file

@ -133,11 +133,11 @@ export const updateTokenCount = (text: string) => {
return request.post(endpoints.tokenizer(), { arg: text });
};
export const login = (payload: t.TLoginUser) => {
export const login = (payload: t.TLoginUser): Promise<t.TLoginResponse> => {
return request.post(endpoints.login(), payload);
};
export const logout = () => {
export const logout = (): Promise<m.TLogoutResponse> => {
return request.post(endpoints.logout());
};

View file

@ -53,7 +53,9 @@ export enum MutationKeys {
fileDelete = 'fileDelete',
updatePreset = 'updatePreset',
deletePreset = 'deletePreset',
loginUser = 'loginUser',
logoutUser = 'logoutUser',
refreshToken = 'refreshToken',
avatarUpload = 'avatarUpload',
speechToText = 'speechToText',
textToSpeech = 'textToSpeech',

View file

@ -5,12 +5,10 @@ import type {
QueryObserverResult,
} from '@tanstack/react-query';
import { initialModelsConfig } from '../config';
import type { TStartupConfig } from '../config';
import { defaultOrderQuery } from '../types/assistants';
import * as dataService from '../data-service';
import * as m from '../types/mutations';
import { QueryKeys } from '../keys';
import request from '../request';
import * as s from '../schemas';
import * as t from '../types';
@ -31,18 +29,6 @@ export const useAbortRequestWithMessage = (): UseMutationResult<
);
};
export const useGetUserQuery = (
config?: UseQueryOptions<t.TUser>,
): QueryObserverResult<t.TUser> => {
return useQuery<t.TUser>([QueryKeys.user], () => dataService.getUser(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
retry: false,
...config,
});
};
export const useGetMessagesByConvoId = <TData = s.TMessage[]>(
id: string,
config?: UseQueryOptions<s.TMessage[], unknown, TData>,
@ -98,17 +84,6 @@ export const useGetSharedLinkQuery = (
);
};
export const useGetUserBalance = (
config?: UseQueryOptions<string>,
): QueryObserverResult<string> => {
return useQuery<string>([QueryKeys.balance], () => dataService.getUserBalance(), {
refetchOnWindowFocus: true,
refetchOnReconnect: true,
refetchOnMount: true,
...config,
});
};
export const useGetConversationByIdQuery = (
id: string,
config?: UseQueryOptions<s.TConversation>,
@ -226,33 +201,6 @@ export const useRevokeAllUserKeysMutation = (): UseMutationResult<unknown> => {
});
};
export const useGetSearchEnabledQuery = (
config?: UseQueryOptions<boolean>,
): QueryObserverResult<boolean> => {
return useQuery<boolean>([QueryKeys.searchEnabled], () => dataService.getSearchEnabled(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
});
};
export const useGetEndpointsQuery = <TData = t.TEndpointsConfig>(
config?: UseQueryOptions<t.TEndpointsConfig, unknown, TData>,
): QueryObserverResult<TData> => {
return useQuery<t.TEndpointsConfig, unknown, TData>(
[QueryKeys.endpoints],
() => dataService.getAIEndpoints(),
{
staleTime: Infinity,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
},
);
};
export const useGetModelsQuery = (
config?: UseQueryOptions<t.TModelsConfig>,
): QueryObserverResult<t.TModelsConfig> => {
@ -343,20 +291,6 @@ export const useRegisterUserMutation = (
);
};
export const useRefreshTokenMutation = (): UseMutationResult<
t.TRefreshTokenResponse | undefined,
unknown,
unknown,
unknown
> => {
const queryClient = useQueryClient();
return useMutation(() => request.refreshToken(), {
onMutate: () => {
queryClient.removeQueries();
},
});
};
export const useUserKeyQuery = (
name: string,
config?: UseQueryOptions<t.TCheckUserKeyResponse>,
@ -428,17 +362,6 @@ export const useUpdateUserPluginsMutation = (
});
};
export const useGetStartupConfig = (
config?: UseQueryOptions<TStartupConfig>,
): QueryObserverResult<TStartupConfig> => {
return useQuery<TStartupConfig>([QueryKeys.startupConfig], () => dataService.getStartupConfig(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
});
};
export const useGetCustomConfigSpeechQuery = (
config?: UseQueryOptions<t.TCustomConfigSpeechResponse>,
): QueryObserverResult<t.TCustomConfigSpeechResponse> => {
@ -453,14 +376,3 @@ export const useGetCustomConfigSpeechQuery = (
},
);
};
export const useGetBannerQuery = (
config?: UseQueryOptions<t.TBannerResponse>,
): QueryObserverResult<t.TBannerResponse> => {
return useQuery<t.TBannerResponse>([QueryKeys.banner], () => dataService.getBanner(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
});
};

View file

@ -91,6 +91,10 @@ axios.interceptors.response.use(
return Promise.reject(error);
}
if (originalRequest.url?.includes('/api/auth/logout') === true) {
return Promise.reject(error);
}
if (error.response.status === 401 && !originalRequest._retry) {
console.warn('401 error, refreshing token');
originalRequest._retry = true;

View file

@ -49,8 +49,6 @@ export type UpdatePresetOptions = MutationOptions<types.TPreset, types.TPreset>;
export type DeletePresetOptions = MutationOptions<PresetDeleteResponse, types.TPreset | undefined>;
export type LogoutOptions = MutationOptions<unknown, undefined>;
/* Assistant mutations */
export type AssistantAvatarVariables = {
@ -331,3 +329,10 @@ export type EditArtifactOptions = MutationOptions<
unknown,
Error
>;
export type TLogoutResponse = {
message: string;
redirect?: string;
};
export type LogoutOptions = MutationOptions<TLogoutResponse, undefined>;