(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:
Anthony LC
2024-05-14 13:08:21 +02:00
committed by Anthony LC
parent d221ebe4f9
commit 004a4edfe7
8 changed files with 65 additions and 28 deletions

View File

@@ -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();
});
});

View File

@@ -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();
});
});

View File

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

View File

@@ -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 {

View File

@@ -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);
},
}));

View File

@@ -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>
);
};

View File

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