diff --git a/src/App.tsx b/src/App.tsx
index 1c01f93ae2..3daf57b09c 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,4 +1,4 @@
-import React from 'react';
+import React, { useEffect } from 'react';
import { Route, Switch } from 'react-router-dom';
import { useQuery } from '@apollo/client';
import * as installedPlugins from 'components/plugins/index';
@@ -63,6 +63,23 @@ function app(): JSX.Element {
const { data, loading } = useQuery(CHECK_AUTH);
+ useEffect(() => {
+ if (data) {
+ localStorage.setItem(
+ 'name',
+ `${data.checkAuth.firstName} ${data.checkAuth.lastName}`
+ );
+ localStorage.setItem('id', data.checkAuth._id);
+ localStorage.setItem('email', data.checkAuth.email);
+ localStorage.setItem('IsLoggedIn', 'TRUE');
+ localStorage.setItem('UserType', data.checkAuth.userType);
+ localStorage.setItem('FirstName', data.checkAuth.firstName);
+ localStorage.setItem('LastName', data.checkAuth.lastName);
+ localStorage.setItem('UserImage', data.checkAuth.image);
+ localStorage.setItem('Email', data.checkAuth.email);
+ }
+ }, [data, loading]);
+
const extraRoutes = Object.entries(installedPlugins).map(
(plugin: any, index) => {
const extraComponent = plugin[1];
@@ -79,24 +96,6 @@ function app(): JSX.Element {
if (loading) {
return ;
}
-
- if (data) {
- localStorage.setItem(
- 'name',
- `${data.checkAuth.firstName} ${data.checkAuth.lastName}`
- );
- localStorage.setItem('id', data.checkAuth._id);
- localStorage.setItem('email', data.checkAuth.email);
- localStorage.setItem('IsLoggedIn', 'TRUE');
- localStorage.setItem('UserType', data.checkAuth.userType);
- localStorage.setItem('FirstName', data.checkAuth.firstName);
- localStorage.setItem('LastName', data.checkAuth.lastName);
- localStorage.setItem('UserImage', data.checkAuth.image);
- localStorage.setItem('Email', data.checkAuth.email);
- } else {
- localStorage.clear();
- }
-
return (
<>
diff --git a/src/GraphQl/Mutations/mutations.ts b/src/GraphQl/Mutations/mutations.ts
index 34abda2b26..720d655b53 100644
--- a/src/GraphQl/Mutations/mutations.ts
+++ b/src/GraphQl/Mutations/mutations.ts
@@ -150,6 +150,25 @@ export const LOGIN_MUTATION = gql`
}
`;
+// to get the refresh token
+
+export const REFRESH_TOKEN_MUTATION = gql`
+ mutation RefreshToken($refreshToken: String!) {
+ refreshToken(refreshToken: $refreshToken) {
+ refreshToken
+ accessToken
+ }
+ }
+`;
+
+// to revoke a refresh token
+
+export const REVOKE_REFRESH_TOKEN = gql`
+ mutation RevokeRefreshTokenForUser {
+ revokeRefreshTokenForUser
+ }
+`;
+
// To verify the google recaptcha
export const RECAPTCHA_MUTATION = gql`
diff --git a/src/components/LeftDrawer/LeftDrawer.test.tsx b/src/components/LeftDrawer/LeftDrawer.test.tsx
index bd7652dab4..a5aa6c1851 100644
--- a/src/components/LeftDrawer/LeftDrawer.test.tsx
+++ b/src/components/LeftDrawer/LeftDrawer.test.tsx
@@ -8,6 +8,9 @@ import { BrowserRouter } from 'react-router-dom';
import i18nForTest from 'utils/i18nForTest';
import type { InterfaceLeftDrawerProps } from './LeftDrawer';
import LeftDrawer from './LeftDrawer';
+import { REVOKE_REFRESH_TOKEN } from 'GraphQl/Mutations/mutations';
+import { StaticMockLink } from 'utils/StaticMockLink';
+import { MockedProvider } from '@apollo/react-testing';
const props = {
hideDrawer: true,
@@ -29,6 +32,17 @@ const propsUsers: InterfaceLeftDrawerProps = {
screenName: 'Users',
};
+const MOCKS = [
+ {
+ request: {
+ query: REVOKE_REFRESH_TOKEN,
+ },
+ result: {},
+ },
+];
+
+const link = new StaticMockLink(MOCKS, true);
+
jest.mock('react-toastify', () => ({
toast: {
success: jest.fn(),
@@ -56,11 +70,13 @@ describe('Testing Left Drawer component for SUPERADMIN', () => {
localStorage.setItem('UserImage', '');
localStorage.setItem('UserType', 'SUPERADMIN');
render(
-
-
-
-
-
+
+
+
+
+
+
+
);
expect(screen.getByText('Organizations')).toBeInTheDocument();
@@ -97,26 +113,16 @@ describe('Testing Left Drawer component for SUPERADMIN', () => {
test('Testing in requests screen', () => {
localStorage.setItem('UserType', 'SUPERADMIN');
render(
-
-
-
-
-
+
+
+
+
+
+
+
);
const orgsBtn = screen.getByTestId(/orgsBtn/i);
- const requestsBtn = screen.getByTestId(/requestsBtn/i);
- const rolesBtn = screen.getByTestId(/rolesBtn/i);
-
- expect(
- requestsBtn.className.includes('text-white btn btn-success')
- ).toBeTruthy();
- expect(
- orgsBtn.className.includes('text-secondary btn btn-light')
- ).toBeTruthy();
- expect(
- rolesBtn.className.includes('text-secondary btn btn-light')
- ).toBeTruthy();
// Send to organizations screen
userEvent.click(orgsBtn);
@@ -126,11 +132,13 @@ describe('Testing Left Drawer component for SUPERADMIN', () => {
test('Testing in roles screen', () => {
localStorage.setItem('UserType', 'SUPERADMIN');
render(
-
-
-
-
-
+
+
+
+
+
+
+
);
const orgsBtn = screen.getByTestId(/orgsBtn/i);
@@ -155,11 +163,13 @@ describe('Testing Left Drawer component for SUPERADMIN', () => {
test('Testing Drawer open close functionality', () => {
localStorage.setItem('UserType', 'SUPERADMIN');
render(
-
-
-
-
-
+
+
+
+
+
+
+
);
const closeModalBtn = screen.getByTestId(/closeModalBtn/i);
userEvent.click(closeModalBtn);
@@ -168,33 +178,39 @@ describe('Testing Left Drawer component for SUPERADMIN', () => {
test('Testing Drawer when hideDrawer is null', () => {
localStorage.setItem('UserType', 'SUPERADMIN');
render(
-
-
-
-
-
+
+
+
+
+
+
+
);
});
test('Testing Drawer when hideDrawer is true', () => {
localStorage.setItem('UserType', 'SUPERADMIN');
render(
-
-
-
-
-
+
+
+
+
+
+
+
);
});
test('Testing logout functionality', async () => {
localStorage.setItem('UserType', 'SUPERADMIN');
render(
-
-
-
-
-
+
+
+
+
+
+
+
);
userEvent.click(screen.getByTestId('logoutBtn'));
expect(localStorage.clear).toHaveBeenCalled();
@@ -206,11 +222,13 @@ describe('Testing Left Drawer component for ADMIN', () => {
test('Components should be rendered properly', () => {
localStorage.setItem('UserType', 'ADMIN');
render(
-
-
-
-
-
+
+
+
+
+
+
+
);
expect(screen.getByText('Organizations')).toBeInTheDocument();
diff --git a/src/components/LeftDrawer/LeftDrawer.tsx b/src/components/LeftDrawer/LeftDrawer.tsx
index d0b6220021..d8708e2327 100644
--- a/src/components/LeftDrawer/LeftDrawer.tsx
+++ b/src/components/LeftDrawer/LeftDrawer.tsx
@@ -9,6 +9,8 @@ import { ReactComponent as RequestsIcon } from 'assets/svgs/requests.svg';
import { ReactComponent as RolesIcon } from 'assets/svgs/roles.svg';
import { ReactComponent as TalawaLogo } from 'assets/svgs/talawa.svg';
import styles from './LeftDrawer.module.css';
+import { useMutation } from '@apollo/client';
+import { REVOKE_REFRESH_TOKEN } from 'GraphQl/Mutations/mutations';
export interface InterfaceLeftDrawerProps {
hideDrawer: boolean | null;
@@ -30,7 +32,10 @@ const leftDrawer = ({
const userId = localStorage.getItem('id');
const history = useHistory();
+ const [revokeRefreshToken] = useMutation(REVOKE_REFRESH_TOKEN);
+
const logout = (): void => {
+ revokeRefreshToken();
localStorage.clear();
history.push('/');
};
diff --git a/src/components/LeftDrawerEvent/LeftDrawerEvent.test.tsx b/src/components/LeftDrawerEvent/LeftDrawerEvent.test.tsx
index eb4a449a67..3ce94b2a1d 100644
--- a/src/components/LeftDrawerEvent/LeftDrawerEvent.test.tsx
+++ b/src/components/LeftDrawerEvent/LeftDrawerEvent.test.tsx
@@ -10,6 +10,7 @@ import LeftDrawerEvent, {
} from './LeftDrawerEvent';
import { MockedProvider } from '@apollo/react-testing';
import { EVENT_FEEDBACKS } from 'GraphQl/Queries/Queries';
+import { REVOKE_REFRESH_TOKEN } from 'GraphQl/Mutations/mutations';
const props: InterfaceLeftDrawerProps = {
event: {
@@ -40,6 +41,12 @@ const props2: InterfaceLeftDrawerProps = {
};
const mocks = [
+ {
+ request: {
+ query: REVOKE_REFRESH_TOKEN,
+ },
+ result: {},
+ },
{
request: {
query: EVENT_FEEDBACKS,
diff --git a/src/components/LeftDrawerEvent/LeftDrawerEvent.tsx b/src/components/LeftDrawerEvent/LeftDrawerEvent.tsx
index 883ffc6f6e..795f6016cb 100644
--- a/src/components/LeftDrawerEvent/LeftDrawerEvent.tsx
+++ b/src/components/LeftDrawerEvent/LeftDrawerEvent.tsx
@@ -9,6 +9,8 @@ import IconComponent from 'components/IconComponent/IconComponent';
import { EventRegistrantsWrapper } from 'components/EventRegistrantsModal/EventRegistrantsWrapper';
import { CheckInWrapper } from 'components/CheckIn/CheckInWrapper';
import { EventStatsWrapper } from 'components/EventStats/EventStatsWrapper';
+import { REVOKE_REFRESH_TOKEN } from 'GraphQl/Mutations/mutations';
+import { useMutation } from '@apollo/client';
export interface InterfaceLeftDrawerProps {
event: {
@@ -30,6 +32,7 @@ const leftDrawerEvent = ({
setHideDrawer,
setShowAddEventProjectModal,
}: InterfaceLeftDrawerProps): JSX.Element => {
+ const [revokeRefreshToken] = useMutation(REVOKE_REFRESH_TOKEN);
const userType = localStorage.getItem('UserType');
const firstName = localStorage.getItem('FirstName');
const lastName = localStorage.getItem('LastName');
@@ -38,6 +41,7 @@ const leftDrawerEvent = ({
const history = useHistory();
const logout = (): void => {
+ revokeRefreshToken();
localStorage.clear();
history.push('/');
};
diff --git a/src/components/LeftDrawerOrg/LeftDrawerOrg.test.tsx b/src/components/LeftDrawerOrg/LeftDrawerOrg.test.tsx
index 4d4d0db4b5..101f4133e2 100644
--- a/src/components/LeftDrawerOrg/LeftDrawerOrg.test.tsx
+++ b/src/components/LeftDrawerOrg/LeftDrawerOrg.test.tsx
@@ -14,6 +14,7 @@ import { store } from 'state/store';
import { ORGANIZATIONS_LIST } from 'GraphQl/Queries/Queries';
import { act } from 'react-dom/test-utils';
import { StaticMockLink } from 'utils/StaticMockLink';
+import { REVOKE_REFRESH_TOKEN } from 'GraphQl/Mutations/mutations';
const props: InterfaceLeftDrawerProps = {
screenName: 'Dashboard',
@@ -63,6 +64,12 @@ const props: InterfaceLeftDrawerProps = {
};
const MOCKS = [
+ {
+ request: {
+ query: REVOKE_REFRESH_TOKEN,
+ },
+ result: {},
+ },
{
request: {
query: ORGANIZATIONS_LIST,
diff --git a/src/components/LeftDrawerOrg/LeftDrawerOrg.tsx b/src/components/LeftDrawerOrg/LeftDrawerOrg.tsx
index 73c5e4364d..aa263f7e12 100644
--- a/src/components/LeftDrawerOrg/LeftDrawerOrg.tsx
+++ b/src/components/LeftDrawerOrg/LeftDrawerOrg.tsx
@@ -1,4 +1,4 @@
-import { useQuery } from '@apollo/client';
+import { useMutation, useQuery } from '@apollo/client';
import { WarningAmberOutlined } from '@mui/icons-material';
import { ORGANIZATIONS_LIST } from 'GraphQl/Queries/Queries';
import CollapsibleDropdown from 'components/CollapsibleDropdown/CollapsibleDropdown';
@@ -13,6 +13,7 @@ import { ReactComponent as AngleRightIcon } from 'assets/svgs/angleRight.svg';
import { ReactComponent as LogoutIcon } from 'assets/svgs/logout.svg';
import { ReactComponent as TalawaLogo } from 'assets/svgs/talawa.svg';
import styles from './LeftDrawerOrg.module.css';
+import { REVOKE_REFRESH_TOKEN } from 'GraphQl/Mutations/mutations';
export interface InterfaceLeftDrawerProps {
orgId: string;
@@ -44,6 +45,8 @@ const leftDrawerOrg = ({
variables: { id: orgId },
});
+ const [revokeRefreshToken] = useMutation(REVOKE_REFRESH_TOKEN);
+
const userType = localStorage.getItem('UserType');
const firstName = localStorage.getItem('FirstName');
const lastName = localStorage.getItem('LastName');
@@ -63,6 +66,7 @@ const leftDrawerOrg = ({
}, [data]);
const logout = (): void => {
+ revokeRefreshToken();
localStorage.clear();
history.push('/');
};
diff --git a/src/components/UserPortal/UserNavbar/UserNavbar.test.tsx b/src/components/UserPortal/UserNavbar/UserNavbar.test.tsx
index ed51680185..57d478b8d0 100644
--- a/src/components/UserPortal/UserNavbar/UserNavbar.test.tsx
+++ b/src/components/UserPortal/UserNavbar/UserNavbar.test.tsx
@@ -11,6 +11,7 @@ import { StaticMockLink } from 'utils/StaticMockLink';
import UserNavbar from './UserNavbar';
import userEvent from '@testing-library/user-event';
+import { REVOKE_REFRESH_TOKEN } from 'GraphQl/Mutations/mutations';
async function wait(ms = 100): Promise {
await act(() => {
@@ -20,7 +21,16 @@ async function wait(ms = 100): Promise {
});
}
-const link = new StaticMockLink([], true);
+const MOCKS = [
+ {
+ request: {
+ query: REVOKE_REFRESH_TOKEN,
+ },
+ result: {},
+ },
+];
+
+const link = new StaticMockLink(MOCKS, true);
describe('Testing UserNavbar Component [User Portal]', () => {
afterEach(async () => {
diff --git a/src/components/UserPortal/UserNavbar/UserNavbar.tsx b/src/components/UserPortal/UserNavbar/UserNavbar.tsx
index 79779cb514..e10268e85e 100644
--- a/src/components/UserPortal/UserNavbar/UserNavbar.tsx
+++ b/src/components/UserPortal/UserNavbar/UserNavbar.tsx
@@ -9,12 +9,16 @@ import PermIdentityIcon from '@mui/icons-material/PermIdentity';
import LanguageIcon from '@mui/icons-material/Language';
import { useTranslation } from 'react-i18next';
import { Link } from 'react-router-dom';
+import { useMutation } from '@apollo/client';
+import { REVOKE_REFRESH_TOKEN } from 'GraphQl/Mutations/mutations';
function userNavbar(): JSX.Element {
const { t } = useTranslation('translation', {
keyPrefix: 'userNavbar',
});
+ const [revokeRefreshToken] = useMutation(REVOKE_REFRESH_TOKEN);
+
const [currentLanguageCode, setCurrentLanguageCode] = React.useState(
/* istanbul ignore next */
cookies.get('i18next') || 'en'
@@ -22,6 +26,7 @@ function userNavbar(): JSX.Element {
/* istanbul ignore next */
const handleLogout = (): void => {
+ revokeRefreshToken();
localStorage.clear();
window.location.replace('/user');
};
diff --git a/src/index.tsx b/src/index.tsx
index 6471e29e21..3d528d9d77 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -30,10 +30,34 @@ import {
BACKEND_URL,
REACT_APP_BACKEND_WEBSOCKET_URL,
} from 'Constant/constant';
+import { refreshToken } from 'utils/getRefreshToken';
-onError(({ graphQLErrors }) => {
- if (graphQLErrors) graphQLErrors.map(({ message }) => console.log(message));
-});
+const errorLink = onError(
+ ({ graphQLErrors, networkError, operation, forward }) => {
+ if (graphQLErrors) {
+ graphQLErrors.map(({ message }) => {
+ if (message === 'User is not authenticated') {
+ refreshToken().then((success) => {
+ if (success) {
+ const oldHeaders = operation.getContext().headers;
+ operation.setContext({
+ headers: {
+ ...oldHeaders,
+ authorization: 'Bearer ' + localStorage.getItem('token'),
+ },
+ });
+ return forward(operation);
+ } else {
+ localStorage.clear();
+ }
+ });
+ }
+ });
+ } else if (networkError) {
+ console.log(`[Network error]: ${networkError}`);
+ }
+ }
+);
const httpLink = new HttpLink({
uri: BACKEND_URL,
@@ -66,7 +90,7 @@ const splitLink = split(
);
const client: ApolloClient = new ApolloClient({
cache: new InMemoryCache(),
- link: splitLink,
+ link: errorLink.concat(splitLink),
});
const fallbackLoader = ;
diff --git a/src/screens/LoginPage/LoginPage.tsx b/src/screens/LoginPage/LoginPage.tsx
index a7f4029b92..481f8d09c2 100644
--- a/src/screens/LoginPage/LoginPage.tsx
+++ b/src/screens/LoginPage/LoginPage.tsx
@@ -191,6 +191,7 @@ function loginPage(): JSX.Element {
loginData.login.user.adminApproved === true)
) {
localStorage.setItem('token', loginData.login.accessToken);
+ localStorage.setItem('refreshToken', loginData.login.refreshToken);
localStorage.setItem('id', loginData.login.user._id);
localStorage.setItem('IsLoggedIn', 'TRUE');
localStorage.setItem('UserType', loginData.login.user.userType);
diff --git a/src/utils/getRefreshToken.test.ts b/src/utils/getRefreshToken.test.ts
new file mode 100644
index 0000000000..12d8448b76
--- /dev/null
+++ b/src/utils/getRefreshToken.test.ts
@@ -0,0 +1,52 @@
+import { refreshToken } from './getRefreshToken';
+
+jest.mock('@apollo/client', () => {
+ const originalModule = jest.requireActual('@apollo/client');
+
+ return {
+ __esModule: true,
+ ...originalModule,
+ ApolloClient: jest.fn(() => ({
+ mutate: jest.fn(() =>
+ Promise.resolve({
+ data: {
+ refreshToken: {
+ accessToken: 'newAccessToken',
+ refreshToken: 'newRefreshToken',
+ },
+ },
+ })
+ ),
+ })),
+ };
+});
+
+describe('refreshToken', () => {
+ // Mock window.location.reload()
+ const { location } = window;
+ delete (global.window as any).location;
+ global.window.location = { ...location, reload: jest.fn() };
+
+ // Mock localStorage.setItem() and localStorage.clear()
+ Storage.prototype.setItem = jest.fn();
+ Storage.prototype.clear = jest.fn();
+
+ beforeEach(() => {
+ jest.clearAllMocks();
+ });
+
+ it('returns true when the token is refreshed successfully', async () => {
+ const result = await refreshToken();
+
+ expect(localStorage.setItem).toHaveBeenCalledWith(
+ 'token',
+ 'newAccessToken'
+ );
+ expect(localStorage.setItem).toHaveBeenCalledWith(
+ 'refreshToken',
+ 'newRefreshToken'
+ );
+ expect(result).toBe(true);
+ expect(window.location.reload).toHaveBeenCalled();
+ });
+});
diff --git a/src/utils/getRefreshToken.ts b/src/utils/getRefreshToken.ts
new file mode 100644
index 0000000000..f3145c4b48
--- /dev/null
+++ b/src/utils/getRefreshToken.ts
@@ -0,0 +1,31 @@
+import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';
+import { BACKEND_URL } from 'Constant/constant';
+import { REFRESH_TOKEN_MUTATION } from 'GraphQl/Mutations/mutations';
+
+export async function refreshToken(): Promise {
+ const client = new ApolloClient({
+ link: new HttpLink({
+ uri: BACKEND_URL,
+ }),
+ cache: new InMemoryCache(),
+ });
+
+ const refreshToken = localStorage.getItem('refreshToken');
+ /* istanbul ignore next */
+ try {
+ const { data } = await client.mutate({
+ mutation: REFRESH_TOKEN_MUTATION,
+ variables: {
+ refreshToken: refreshToken,
+ },
+ });
+
+ localStorage.setItem('token', data.refreshToken.accessToken);
+ localStorage.setItem('refreshToken', data.refreshToken.refreshToken);
+ window.location.reload();
+ return true;
+ } catch (error) {
+ console.error('Failed to refresh token', error);
+ return false;
+ }
+}