🛂 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

@ -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';