feat(ui): support /logout path to perform logout from api redirect (#20040)

This commit is contained in:
Chirag Madlani 2025-03-02 15:52:50 +05:30 committed by GitHub
parent 9331a526ea
commit 30a8d64959
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 101 additions and 22 deletions

View File

@ -349,7 +349,7 @@ public class AuthenticationCodeFlowHandler {
if (session != null) {
LOG.debug("Invalidating the session for logout");
session.invalidate();
httpServletResponse.sendRedirect(serverUrl);
httpServletResponse.sendRedirect(serverUrl + "/logout");
} else {
LOG.error("No session store available for this web context");
}

View File

@ -26,6 +26,7 @@ import AppContainer from '../AppContainer/AppContainer';
import Loader from '../common/Loader/Loader';
import { UnAuthenticatedAppRouter } from './UnAuthenticatedAppRouter';
import { LogoutPage } from '../../pages/LogoutPage/LogoutPage';
import SamlCallback from '../../pages/SamlCallback';
const AppRouter = () => {
@ -86,6 +87,7 @@ const AppRouter = () => {
return (
<Switch>
<Route exact component={PageNotFound} path={ROUTES.NOT_FOUND} />
<Route exact component={LogoutPage} path={ROUTES.LOGOUT} />
<Route
exact
component={AccessNotAllowedPage}

View File

@ -20,6 +20,7 @@ import { useHistory } from 'react-router-dom';
import { ROUTES } from '../../../constants/constants';
import { useApplicationStore } from '../../../hooks/useApplicationStore';
import { logoutUser, renewToken } from '../../../rest/LoginAPI';
import TokenService from '../../../utils/Auth/TokenService/TokenServiceUtil';
import { setOidcToken } from '../../../utils/LocalStorageUtils';
export const GenericAuthenticator = forwardRef(
@ -37,6 +38,7 @@ export const GenericAuthenticator = forwardRef(
const handleLogout = async () => {
await logoutUser();
TokenService.getInstance().clearRefreshInProgress();
history.push(ROUTES.SIGNIN);
setOidcToken('');
setIsAuthenticated(false);

View File

@ -36,6 +36,7 @@ import { showErrorToast } from '../../../utils/ToastUtils';
import { ROUTES } from '../../../constants/constants';
import { useApplicationStore } from '../../../hooks/useApplicationStore';
import { AccessTokenResponse, refreshSAMLToken } from '../../../rest/auth-API';
import TokenService from '../../../utils/Auth/TokenService/TokenServiceUtil';
import {
getOidcToken,
getRefreshToken,
@ -76,24 +77,19 @@ const SamlAuthenticator = forwardRef<AuthenticatorRef, Props>(
}
};
const logout = () => {
const logout = async () => {
const token = getOidcToken();
if (token) {
postSamlLogout()
.then(() => {
setIsAuthenticated(false);
try {
onLogoutSuccess();
} catch (err) {
// TODO: Handle error on logout failure
// eslint-disable-next-line no-console
console.log(err);
}
})
.catch((err) => {
// eslint-disable-next-line no-console
console.log('Error while logging out', err);
});
try {
await postSamlLogout();
setIsAuthenticated(false);
onLogoutSuccess();
TokenService.getInstance().clearRefreshInProgress();
} catch (err) {
// TODO: Handle error on logout failure
// eslint-disable-next-line no-console
console.log(err);
}
}
};

View File

@ -167,6 +167,7 @@ export const AuthProvider = ({
resetWebAnalyticSession();
};
// Handler to perform logout within application
const onLogoutHandler = useCallback(() => {
clearTimeout(timeoutId);
@ -392,6 +393,7 @@ export const AuthProvider = ({
]
);
// Callback to cleanup session related info upon successful logout
const handleSuccessfulLogout = () => {
resetUserDetails();
};

View File

@ -39,6 +39,7 @@ import {
import { resetWebAnalyticSession } from '../../../utils/WebAnalyticsUtils';
import { toLower } from 'lodash';
import TokenService from '../../../utils/Auth/TokenService/TokenServiceUtil';
import { extractDetailsFromToken } from '../../../utils/AuthProvider.util';
import {
getOidcToken,
@ -181,6 +182,8 @@ const BasicAuthProvider = ({
try {
await logoutUser({ token, refreshToken });
setOidcToken('');
setRefreshToken('');
TokenService.getInstance().clearRefreshInProgress();
history.push(ROUTES.SIGNIN);
} catch (error) {
showErrorToast(error as AxiosError);

View File

@ -131,6 +131,7 @@ export const ROUTES = {
NOT_FOUND: '/404',
FORBIDDEN: '/403',
UNAUTHORISED: '/unauthorised',
LOGOUT: '/logout',
MY_DATA: '/my-data',
TOUR: '/tour',
REPORTS: '/reports',

View File

@ -97,6 +97,9 @@ export const useApplicationStore = create<ApplicationStore>()((set, get) => ({
onLoginHandler: () => {
// This is a placeholder function that will be replaced by the actual function
},
/**
* Handler to perform logout within application
*/
onLogoutHandler: () => {
// This is a placeholder function that will be replaced by the actual function
},
@ -110,11 +113,6 @@ export const useApplicationStore = create<ApplicationStore>()((set, get) => ({
updateAxiosInterceptors: () => {
// This is a placeholder function that will be replaced by the actual function
},
trySilentSignIn: (forceLogout?: boolean) => {
if (forceLogout) {
// This is a placeholder function that will be replaced by the actual function
}
},
updateCurrentUser: (user) => {
set({ currentUser: user });
},

View File

@ -0,0 +1,48 @@
/*
* Copyright 2025 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { render } from '@testing-library/react';
import React from 'react';
import { useApplicationStore } from '../../hooks/useApplicationStore';
import { LogoutPage } from './LogoutPage';
// Mock the useApplicationStore hook
jest.mock('../../hooks/useApplicationStore', () => ({
useApplicationStore: jest.fn(),
}));
describe('LogoutPage', () => {
const mockOnLogoutHandler = jest.fn();
beforeEach(() => {
// Reset mock before each test
jest.clearAllMocks();
// Setup mock implementation
(useApplicationStore as unknown as jest.Mock).mockReturnValue({
onLogoutHandler: mockOnLogoutHandler,
});
});
it('should call onLogoutHandler on mount', () => {
render(<LogoutPage />);
expect(mockOnLogoutHandler).toHaveBeenCalledTimes(1);
});
it('should render Loader component with fullScreen prop', () => {
const { container } = render(<LogoutPage />);
const loaderElement = container.firstChild;
expect(loaderElement).toBeInTheDocument();
});
});

View File

@ -0,0 +1,25 @@
/*
* Copyright 2025 Collate.
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { useEffect } from 'react';
import Loader from '../../components/common/Loader/Loader';
import { useApplicationStore } from '../../hooks/useApplicationStore';
export const LogoutPage = () => {
const { onLogoutHandler } = useApplicationStore();
useEffect(() => {
onLogoutHandler();
}, []);
return <Loader fullScreen />;
};

View File

@ -127,6 +127,7 @@ class TokenService {
// Clear the refresh flag (used after refresh is complete)
clearRefreshInProgress() {
localStorage.removeItem(REFRESH_IN_PROGRESS_KEY);
localStorage.removeItem(REFRESHED_KEY);
}
// Check if a refresh is already in progress (used by other tabs)

View File

@ -311,6 +311,7 @@ export const isProtectedRoute = (pathname: string) => {
ROUTES.HOME,
ROUTES.AUTH_CALLBACK,
ROUTES.NOT_FOUND,
ROUTES.LOGOUT,
].indexOf(pathname) === -1
);
};