✨(frontend) add logout button
Rework the header based on latest Johann's design, which introduced a dropdown menu to manage user account. In this menu, you can find a logout button, which ends up the backend session by calling the logout endpoint. Please that automatic redirection when receiving the backend response were disabled. We handle it in our custom hook, which reload the page. Has the session cookie have been cleared, on reloading the page, a new loggin flow is initiated, and the user is redirected to the OIDC provider.
This commit is contained in:
@@ -64,4 +64,20 @@ test.describe('Header', () => {
|
||||
|
||||
await expect(page.getByRole('link', { name: 'Grist' })).toBeVisible();
|
||||
});
|
||||
|
||||
test('checks logout button', async ({ page }) => {
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'My account',
|
||||
})
|
||||
.click();
|
||||
|
||||
await page
|
||||
.getByRole('button', {
|
||||
name: 'Logout',
|
||||
})
|
||||
.click();
|
||||
|
||||
await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -31,25 +31,15 @@ describe('fetchAPI', () => {
|
||||
});
|
||||
|
||||
it('logout if 401 response', async () => {
|
||||
const mockReplace = jest.fn();
|
||||
Object.defineProperty(window, 'location', {
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
value: {
|
||||
replace: mockReplace,
|
||||
},
|
||||
});
|
||||
|
||||
useAuthStore.setState({ userData: { email: 'test@test.com', id: '1234' } });
|
||||
const logoutMock = jest.fn();
|
||||
jest
|
||||
.spyOn(useAuthStore.getState(), 'logout')
|
||||
.mockImplementation(logoutMock);
|
||||
|
||||
fetchMock.mock('http://test.jest/api/some/url', 401);
|
||||
|
||||
await fetchAPI('some/url');
|
||||
|
||||
expect(useAuthStore.getState().userData).toBeUndefined();
|
||||
|
||||
expect(mockReplace).toHaveBeenCalledWith(
|
||||
'http://test.jest/api/authenticate/',
|
||||
);
|
||||
expect(logoutMock).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { baseApiUrl, login, useAuthStore } from '@/core';
|
||||
import { baseApiUrl, useAuthStore } from '@/core';
|
||||
|
||||
/**
|
||||
* Retrieves the CSRF token from the document's cookies.
|
||||
@@ -29,12 +29,8 @@ export const fetchAPI = async (input: string, init?: RequestInit) => {
|
||||
},
|
||||
});
|
||||
|
||||
// todo - handle 401, redirect to login screen
|
||||
// todo - please have a look to this documentation page https://mozilla-django-oidc.readthedocs.io/en/stable/xhr.html
|
||||
if (response.status === 401) {
|
||||
logout();
|
||||
// Fix - force re-logging the user, will be refactored
|
||||
login();
|
||||
}
|
||||
|
||||
return response;
|
||||
|
||||
@@ -23,6 +23,10 @@ const StyledButton = styled(Button)`
|
||||
background: none;
|
||||
outline: none;
|
||||
transition: all 0.2s ease-in-out;
|
||||
font-family: Marianne, Arial, serif;
|
||||
font-weight: 500;
|
||||
font-size: 0.938rem;
|
||||
text-wrap: nowrap;
|
||||
`;
|
||||
|
||||
interface DropButtonProps {
|
||||
|
||||
@@ -4,10 +4,6 @@ import { baseApiUrl } from '@/core/conf';
|
||||
|
||||
import { User, getMe } from './api';
|
||||
|
||||
export const login = () => {
|
||||
window.location.replace(new URL('authenticate/', baseApiUrl()).href);
|
||||
};
|
||||
|
||||
interface AuthStore {
|
||||
authenticated: boolean;
|
||||
initAuth: () => void;
|
||||
@@ -30,11 +26,10 @@ export const useAuthStore = create<AuthStore>((set) => ({
|
||||
set({ authenticated: true, userData: data });
|
||||
})
|
||||
.catch(() => {
|
||||
// todo - implement a proper login screen to prevent automatic navigation.
|
||||
login();
|
||||
window.location.replace(new URL('authenticate/', baseApiUrl()).href);
|
||||
});
|
||||
},
|
||||
logout: () => {
|
||||
set(initialState);
|
||||
window.location.replace(new URL('logout/', baseApiUrl()).href);
|
||||
},
|
||||
}));
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
import { Button } from '@openfun/cunningham-react';
|
||||
import React from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import { Box, DropButton, Text } from '@/components';
|
||||
import { useAuthStore } from '@/core/auth';
|
||||
|
||||
export const AccountDropdown = () => {
|
||||
const { t } = useTranslation();
|
||||
const { logout } = useAuthStore();
|
||||
|
||||
return (
|
||||
<DropButton
|
||||
aria-label={t('My account')}
|
||||
button={
|
||||
<Box $flex $direction="row" $align="center">
|
||||
<Text $theme="primary">{t('My account')}</Text>
|
||||
<Text className="material-icons" $theme="primary">
|
||||
arrow_drop_down
|
||||
</Text>
|
||||
</Box>
|
||||
}
|
||||
>
|
||||
<Button
|
||||
onClick={logout}
|
||||
color="primary-text"
|
||||
icon={<span className="material-icons">logout</span>}
|
||||
aria-label={t('Logout')}
|
||||
>
|
||||
<Text $weight="normal">{t('Logout')}</Text>
|
||||
</Button>
|
||||
</DropButton>
|
||||
);
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import { Box, StyledLink, Text } from '@/components/';
|
||||
|
||||
import { LanguagePicker } from '../language/';
|
||||
|
||||
import { AccountDropdown } from './AccountDropdown';
|
||||
import { LaGaufre } from './LaGaufre';
|
||||
import { default as IconImpress } from './assets/icon-impress.svg?url';
|
||||
|
||||
@@ -59,7 +60,8 @@ export const Header = () => {
|
||||
</Box>
|
||||
</StyledLink>
|
||||
</Box>
|
||||
<Box $align="center" $gap="1rem" $direction="row">
|
||||
<Box $align="center" $gap="1.5rem" $direction="row">
|
||||
<AccountDropdown />
|
||||
<LanguagePicker />
|
||||
<LaGaufre />
|
||||
</Box>
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.8 KiB |
Reference in New Issue
Block a user