diff --git a/src/frontend/apps/desk/src/api/__tests__/fetchApi.test.tsx b/src/frontend/apps/desk/src/api/__tests__/fetchApi.test.tsx index d08d86d..cd20fb8 100644 --- a/src/frontend/apps/desk/src/api/__tests__/fetchApi.test.tsx +++ b/src/frontend/apps/desk/src/api/__tests__/fetchApi.test.tsx @@ -34,25 +34,22 @@ 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({ + authenticated: true, + userData: { id: '123', email: 'test@test.com' }, }); - useAuthStore.setState({ userData: { email: 'test@test.com', id: '1234' } }); - fetchMock.mock('http://some.api.url/api/v1.0/some/url', 401); + fetchMock.mock('http://some.api.url/api/v1.0/logout/', 302); await fetchAPI('some/url'); - expect(useAuthStore.getState().userData).toBeUndefined(); + await Promise.all([fetchMock.flush()]); - expect(mockReplace).toHaveBeenCalledWith( - 'http://some.api.url/api/v1.0/authenticate/', - ); + expect(fetchMock.lastUrl()).toEqual('http://some.api.url/api/v1.0/logout/'); + + const { userData, authenticated } = useAuthStore.getState(); + expect(userData).toBeUndefined(); + expect(authenticated).toBeFalsy(); }); }); diff --git a/src/frontend/apps/desk/src/api/fetchApi.ts b/src/frontend/apps/desk/src/api/fetchApi.ts index c2e2e2b..48b381b 100644 --- a/src/frontend/apps/desk/src/api/fetchApi.ts +++ b/src/frontend/apps/desk/src/api/fetchApi.ts @@ -1,4 +1,4 @@ -import { login, useAuthStore } from '@/core/auth'; +import { useAuthStore } from '@/core/auth'; /** * 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; diff --git a/src/frontend/apps/desk/src/components/DropButton.tsx b/src/frontend/apps/desk/src/components/DropButton.tsx index 2493c9c..f79b78e 100644 --- a/src/frontend/apps/desk/src/components/DropButton.tsx +++ b/src/frontend/apps/desk/src/components/DropButton.tsx @@ -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 { diff --git a/src/frontend/apps/desk/src/core/auth/Auth.tsx b/src/frontend/apps/desk/src/core/auth/Auth.tsx index 865a815..bc314a7 100644 --- a/src/frontend/apps/desk/src/core/auth/Auth.tsx +++ b/src/frontend/apps/desk/src/core/auth/Auth.tsx @@ -10,7 +10,7 @@ export const Auth = ({ children }: PropsWithChildren) => { useEffect(() => { initAuth(); - }, [initAuth]); + }, [initAuth, authenticated]); if (!authenticated) { return ( diff --git a/src/frontend/apps/desk/src/core/auth/api/index.ts b/src/frontend/apps/desk/src/core/auth/api/index.ts index d81520d..45350f2 100644 --- a/src/frontend/apps/desk/src/core/auth/api/index.ts +++ b/src/frontend/apps/desk/src/core/auth/api/index.ts @@ -1,2 +1,3 @@ -export * from './getMe'; export * from './types'; +export * from './getMe'; +export * from './logout'; diff --git a/src/frontend/apps/desk/src/core/auth/api/logout.ts b/src/frontend/apps/desk/src/core/auth/api/logout.ts new file mode 100644 index 0000000..31bbea7 --- /dev/null +++ b/src/frontend/apps/desk/src/core/auth/api/logout.ts @@ -0,0 +1,8 @@ +import { fetchAPI } from '@/api'; + +export const logout = async () => { + await fetchAPI(`logout/`, { + method: 'POST', + redirect: 'manual', + }); +}; diff --git a/src/frontend/apps/desk/src/core/auth/useAuthStore.tsx b/src/frontend/apps/desk/src/core/auth/useAuthStore.tsx index e2a2f3a..6b430e1 100644 --- a/src/frontend/apps/desk/src/core/auth/useAuthStore.tsx +++ b/src/frontend/apps/desk/src/core/auth/useAuthStore.tsx @@ -1,6 +1,6 @@ import { create } from 'zustand'; -import { User, getMe } from './api'; +import { User, getMe, logout } from './api'; export const login = () => { window.location.replace( @@ -30,11 +30,12 @@ export const useAuthStore = create((set) => ({ set({ authenticated: true, userData: data }); }) .catch(() => { - // todo - implement a proper login screen to prevent automatic navigation. login(); }); }, logout: () => { - set(initialState); + void logout().then(() => { + set(initialState); + }); }, })); diff --git a/src/frontend/apps/desk/src/features/header/AccountDropdown.tsx b/src/frontend/apps/desk/src/features/header/AccountDropdown.tsx new file mode 100644 index 0000000..13c77b5 --- /dev/null +++ b/src/frontend/apps/desk/src/features/header/AccountDropdown.tsx @@ -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 ( + + {t('My account')} + + arrow_drop_down + + + } + > + + + ); +}; diff --git a/src/frontend/apps/desk/src/features/header/Header.tsx b/src/frontend/apps/desk/src/features/header/Header.tsx index cf648ac..5f13419 100644 --- a/src/frontend/apps/desk/src/features/header/Header.tsx +++ b/src/frontend/apps/desk/src/features/header/Header.tsx @@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import { Box, Text } from '@/components/'; +import { AccountDropdown } from '@/features/header/AccountDropdown'; import { ApplicationsMenu } from '@/features/header/ApplicationsMenu'; import { LanguagePicker } from '../language/'; @@ -11,7 +12,6 @@ import { LanguagePicker } from '../language/'; import { default as IconApplication } from './assets/icon-application.svg?url'; import { default as IconGouv } from './assets/icon-gouv.svg?url'; import { default as IconMarianne } from './assets/icon-marianne.svg?url'; -import IconMyAccount from './assets/icon-my-account.png'; export const HEADER_HEIGHT = '100px'; @@ -62,35 +62,10 @@ export const Header = () => { - button { - padding: 0; - } - `} - $gap="5rem" - $justify="flex-end" - $direction="row" - > - - - - - - - John Doe - - {t(`Profile - - - + + + + diff --git a/src/frontend/apps/desk/src/features/header/assets/icon-my-account.png b/src/frontend/apps/desk/src/features/header/assets/icon-my-account.png deleted file mode 100644 index 54bd64c..0000000 Binary files a/src/frontend/apps/desk/src/features/header/assets/icon-my-account.png and /dev/null differ diff --git a/src/frontend/apps/desk/src/i18n/translations.json b/src/frontend/apps/desk/src/i18n/translations.json index 961a528..a04cdb4 100644 --- a/src/frontend/apps/desk/src/i18n/translations.json +++ b/src/frontend/apps/desk/src/i18n/translations.json @@ -56,11 +56,13 @@ "Language Icon": "Icône de langue", "Last update at": "Dernière modification le", "List members card": "Carte liste des membres", + "Logout": "Déconnexion", "Marianne Logo": "Logo Marianne", "Member": "Membre", "Member icon": "Icône de membre", "Member {{name}} added to the team": "Membre {{name}} ajouté au groupe", "Members of “{{teamName}}“": "Membres de “{{teamName}}“", + "My account": "Mon compte", "Name the team": "Nommer le groupe", "Names": "Noms", "New name...": "Nouveau nom...", diff --git a/src/frontend/apps/e2e/__tests__/app-desk/header.spec.ts b/src/frontend/apps/e2e/__tests__/app-desk/header.spec.ts index 67d7253..8d94763 100644 --- a/src/frontend/apps/e2e/__tests__/app-desk/header.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-desk/header.spec.ts @@ -34,4 +34,22 @@ test.describe('Header', () => { await expect(header.getByAltText('Language Icon')).toBeVisible(); await expect(header.getByText('My account')).toBeVisible(); }); + + test('checks logout button', async ({ page }) => { + await page + .getByRole('button', { + name: 'My account', + }) + .click(); + + await page + .getByRole('button', { + name: 'Logout', + }) + .click(); + + // FIXME - assert the session has been killed in Keycloak + + await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible(); + }); });