mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-03-05 07:40:19 +01:00
📍 feat: Preserve Deep Link Destinations Through the Auth Redirect Flow (#10275)
* added support for url query param persistance * refactor: authentication redirect handling - Introduced utility functions for managing login redirects, including `persistRedirectToSession`, `buildLoginRedirectUrl`, and `getPostLoginRedirect`. - Updated `Login` and `AuthContextProvider` components to utilize these utilities for improved redirect logic. - Refactored `useAuthRedirect` to streamline navigation to the login page while preserving intended destinations. - Cleaned up the `StartupLayout` to remove unnecessary redirect handling, ensuring a more straightforward navigation flow. - Added a new `redirect.ts` file to encapsulate redirect-related logic, enhancing code organization and maintainability. * fix: enhance safe redirect validation logic - Updated the `isSafeRedirect` function to improve validation of redirect URLs. - Ensured that only safe relative paths are accepted, specifically excluding paths that lead to the login page. - Refactored the logic to streamline the checks for valid redirect targets. * test: add unit tests for redirect utility functions - Introduced comprehensive tests for `isSafeRedirect`, `buildLoginRedirectUrl`, `getPostLoginRedirect`, and `persistRedirectToSession` functions. - Validated various scenarios including safe and unsafe redirects, URL encoding, and session storage behavior. - Enhanced test coverage to ensure robust handling of redirect logic and prevent potential security issues. * chore: streamline authentication and redirect handling - Removed unused `useLocation` import from `AuthContextProvider` and replaced its usage with `window.location` for better clarity. - Updated `StartupLayout` to check for pending redirects before navigating to the new chat page, ensuring users are directed appropriately based on their session state. - Enhanced unit tests for `useAuthRedirect` to verify correct handling of redirect parameters, including encoding of the current path and query parameters. * test: add unit tests for StartupLayout redirect behavior - Introduced a new test suite for the StartupLayout component to validate redirect logic based on authentication status and session storage. - Implemented tests to ensure correct navigation to the new conversation page when authenticated without pending redirects, and to prevent navigation when a redirect URL parameter or session storage redirect is present. - Enhanced coverage for scenarios where users are not authenticated, ensuring robust handling of redirect conditions. --------- Co-authored-by: Vamsi Konakanchi <vamsi.k@trackmind.com> Co-authored-by: Danny Avila <danny@librechat.ai>
This commit is contained in:
parent
a0f9782e60
commit
e978a934fc
9 changed files with 529 additions and 44 deletions
|
|
@ -3,7 +3,6 @@ import {
|
|||
useMemo,
|
||||
useState,
|
||||
useEffect,
|
||||
ReactNode,
|
||||
useContext,
|
||||
useCallback,
|
||||
createContext,
|
||||
|
|
@ -12,6 +11,7 @@ import { debounce } from 'lodash';
|
|||
import { useRecoilState } from 'recoil';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { setTokenHeader, SystemRoles } from 'librechat-data-provider';
|
||||
import type { ReactNode } from 'react';
|
||||
import type * as t from 'librechat-data-provider';
|
||||
import {
|
||||
useGetRole,
|
||||
|
|
@ -20,6 +20,7 @@ import {
|
|||
useLogoutUserMutation,
|
||||
useRefreshTokenMutation,
|
||||
} from '~/data-provider';
|
||||
import { isSafeRedirect, buildLoginRedirectUrl, getPostLoginRedirect } from '~/utils';
|
||||
import { TAuthConfig, TUserContext, TAuthContext, TResError } from '~/common';
|
||||
import useTimeout from './useTimeout';
|
||||
import store from '~/store';
|
||||
|
|
@ -58,20 +59,22 @@ const AuthContextProvider = ({
|
|||
setTokenHeader(token);
|
||||
setIsAuthenticated(isAuthenticated);
|
||||
|
||||
// Use a custom redirect if set
|
||||
const finalRedirect = logoutRedirectRef.current || redirect;
|
||||
// Clear the stored redirect
|
||||
const searchParams = new URLSearchParams(window.location.search);
|
||||
const postLoginRedirect = getPostLoginRedirect(searchParams);
|
||||
|
||||
const logoutRedirect = logoutRedirectRef.current;
|
||||
logoutRedirectRef.current = undefined;
|
||||
|
||||
const finalRedirect =
|
||||
logoutRedirect ??
|
||||
postLoginRedirect ??
|
||||
(redirect && isSafeRedirect(redirect) ? redirect : null);
|
||||
|
||||
if (finalRedirect == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (finalRedirect.startsWith('http://') || finalRedirect.startsWith('https://')) {
|
||||
window.location.href = finalRedirect;
|
||||
} else {
|
||||
navigate(finalRedirect, { replace: true });
|
||||
}
|
||||
navigate(finalRedirect, { replace: true });
|
||||
}, 50),
|
||||
[navigate, setUser],
|
||||
);
|
||||
|
|
@ -81,7 +84,6 @@ const AuthContextProvider = ({
|
|||
onSuccess: (data: t.TLoginResponse) => {
|
||||
const { user, token, twoFAPending, tempToken } = data;
|
||||
if (twoFAPending) {
|
||||
// Redirect to the two-factor authentication route.
|
||||
navigate(`/login/2fa?tempToken=${tempToken}`, { replace: true });
|
||||
return;
|
||||
}
|
||||
|
|
@ -91,7 +93,9 @@ const AuthContextProvider = ({
|
|||
onError: (error: TResError | unknown) => {
|
||||
const resError = error as TResError;
|
||||
doSetError(resError.message);
|
||||
navigate('/login', { replace: true });
|
||||
const redirectTo = new URLSearchParams(window.location.search).get('redirect_to');
|
||||
const loginPath = redirectTo ? `/login?redirect_to=${redirectTo}` : '/login';
|
||||
navigate(loginPath, { replace: true });
|
||||
},
|
||||
});
|
||||
const logoutUser = useLogoutUserMutation({
|
||||
|
|
@ -141,30 +145,30 @@ const AuthContextProvider = ({
|
|||
const { user, token = '' } = data ?? {};
|
||||
if (token) {
|
||||
setUserContext({ token, isAuthenticated: true, user });
|
||||
} else {
|
||||
console.log('Token is not present. User is not authenticated.');
|
||||
if (authConfig?.test === true) {
|
||||
return;
|
||||
}
|
||||
navigate('/login');
|
||||
return;
|
||||
}
|
||||
console.log('Token is not present. User is not authenticated.');
|
||||
if (authConfig?.test === true) {
|
||||
return;
|
||||
}
|
||||
navigate(buildLoginRedirectUrl());
|
||||
},
|
||||
onError: (error) => {
|
||||
console.log('refreshToken mutation error:', error);
|
||||
if (authConfig?.test === true) {
|
||||
return;
|
||||
}
|
||||
navigate('/login');
|
||||
navigate(buildLoginRedirectUrl());
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
}, [authConfig?.test, refreshToken, setUserContext, navigate]);
|
||||
|
||||
useEffect(() => {
|
||||
if (userQuery.data) {
|
||||
setUser(userQuery.data);
|
||||
} else if (userQuery.isError) {
|
||||
doSetError((userQuery.error as Error).message);
|
||||
navigate('/login', { replace: true });
|
||||
navigate(buildLoginRedirectUrl(), { replace: true });
|
||||
}
|
||||
if (error != null && error && isAuthenticated) {
|
||||
doSetError(undefined);
|
||||
|
|
@ -186,24 +190,22 @@ const AuthContextProvider = ({
|
|||
]);
|
||||
|
||||
useEffect(() => {
|
||||
const handleTokenUpdate = (event) => {
|
||||
const handleTokenUpdate = (event: CustomEvent<string>) => {
|
||||
console.log('tokenUpdated event received event');
|
||||
const newToken = event.detail;
|
||||
setUserContext({
|
||||
token: newToken,
|
||||
token: event.detail,
|
||||
isAuthenticated: true,
|
||||
user: user,
|
||||
});
|
||||
};
|
||||
|
||||
window.addEventListener('tokenUpdated', handleTokenUpdate);
|
||||
window.addEventListener('tokenUpdated', handleTokenUpdate as EventListener);
|
||||
|
||||
return () => {
|
||||
window.removeEventListener('tokenUpdated', handleTokenUpdate);
|
||||
window.removeEventListener('tokenUpdated', handleTokenUpdate as EventListener);
|
||||
};
|
||||
}, [setUserContext, user]);
|
||||
|
||||
// Make the provider update only when it should
|
||||
const memoedValue = useMemo(
|
||||
() => ({
|
||||
user,
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue