diff --git a/CHANGELOG.md b/CHANGELOG.md index 5faa6dfb..6d88eab1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,11 @@ and this project adheres to ## [Unreleased] +## Added + +- 🛂(frontend) access public docs without being logged #235 + + ## [1.3.0] - 2024-09-05 ## Added diff --git a/src/frontend/apps/e2e/__tests__/app-impress/doc-routing.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/doc-routing.spec.ts index 1d602a9a..e7d2f51d 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/doc-routing.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/doc-routing.spec.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; -import { keyCloakSignIn } from './common'; +import { createDoc, keyCloakSignIn } from './common'; test.describe('Doc Routing', () => { test.beforeEach(async ({ page }) => { @@ -47,4 +47,43 @@ test.describe('Doc Routing: Not loggued', () => { await keyCloakSignIn(page, browserName); await expect(page).toHaveURL(/\/docs\/mocked-document-id\/$/); }); + + test('The homepage redirects to login.', async ({ page }) => { + await page.goto('/'); + await expect( + page.getByRole('button', { + name: 'Sign In', + }), + ).toBeVisible(); + }); + + test('A public doc is accessible even when not authentified.', async ({ + page, + browserName, + }) => { + await page.goto('/'); + await keyCloakSignIn(page, browserName); + + const [docTitle] = await createDoc( + page, + 'My new doc', + browserName, + 1, + true, + ); + + const urlDoc = page.url(); + + await page + .getByRole('button', { + name: 'Logout', + }) + .click(); + + await expect(page.getByRole('button', { name: 'Sign in' })).toBeVisible(); + + await page.goto(urlDoc); + + await expect(page.locator('h2').getByText(docTitle)).toBeVisible(); + }); }); diff --git a/src/frontend/apps/e2e/__tests__/app-impress/header.spec.ts b/src/frontend/apps/e2e/__tests__/app-impress/header.spec.ts index 6dc2a4c5..e2508a69 100644 --- a/src/frontend/apps/e2e/__tests__/app-impress/header.spec.ts +++ b/src/frontend/apps/e2e/__tests__/app-impress/header.spec.ts @@ -22,6 +22,12 @@ test.describe('Header', () => { /Marianne/i, ); + await expect( + header.getByRole('button', { + name: 'Logout', + }), + ).toBeVisible(); + await expect(header.getByAltText('Language Icon')).toBeVisible(); await expect( @@ -68,12 +74,6 @@ test.describe('Header: Log out', () => { await page.goto('/'); await keyCloakSignIn(page, browserName); - await page - .getByRole('button', { - name: 'My account', - }) - .click(); - await page .getByRole('button', { name: 'Logout', diff --git a/src/frontend/apps/impress/src/api/__tests__/fetchApi.test.tsx b/src/frontend/apps/impress/src/api/__tests__/fetchApi.test.tsx index 67c57869..05dc0b13 100644 --- a/src/frontend/apps/impress/src/api/__tests__/fetchApi.test.tsx +++ b/src/frontend/apps/impress/src/api/__tests__/fetchApi.test.tsx @@ -1,7 +1,6 @@ import fetchMock from 'fetch-mock'; import { fetchAPI } from '@/api'; -import { useAuthStore } from '@/core/auth'; describe('fetchAPI', () => { beforeEach(() => { @@ -30,19 +29,6 @@ describe('fetchAPI', () => { }); }); - it('logout if 401 response', async () => { - const logoutMock = jest.fn(); - jest - .spyOn(useAuthStore.getState(), 'logout') - .mockImplementation(logoutMock); - - fetchMock.mock('http://test.jest/api/v1.0/some/url', 401); - - await fetchAPI('some/url'); - - expect(logoutMock).toHaveBeenCalled(); - }); - it('check the versionning', () => { fetchMock.mock('http://test.jest/api/v2.0/some/url', 200); diff --git a/src/frontend/apps/impress/src/api/fetchApi.ts b/src/frontend/apps/impress/src/api/fetchApi.ts index 03fad1f6..208f5e81 100644 --- a/src/frontend/apps/impress/src/api/fetchApi.ts +++ b/src/frontend/apps/impress/src/api/fetchApi.ts @@ -1,4 +1,4 @@ -import { baseApiUrl, useAuthStore } from '@/core'; +import { baseApiUrl } from '@/core'; import { getCSRFToken } from './utils'; @@ -30,10 +30,5 @@ export const fetchAPI = async ( headers, }); - if (response.status === 401) { - const { logout } = useAuthStore.getState(); - logout(); - } - return response; }; diff --git a/src/frontend/apps/impress/src/core/AppProvider.tsx b/src/frontend/apps/impress/src/core/AppProvider.tsx index 663cea2d..a578c9b8 100644 --- a/src/frontend/apps/impress/src/core/AppProvider.tsx +++ b/src/frontend/apps/impress/src/core/AppProvider.tsx @@ -4,7 +4,7 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { useCunninghamTheme } from '@/cunningham'; import '@/i18n/initI18n'; -import { Auth } from './auth/Auth'; +import { Auth } from './auth/'; /** * QueryClient: diff --git a/src/frontend/apps/impress/src/core/auth/AccountDropdown.tsx b/src/frontend/apps/impress/src/core/auth/AccountDropdown.tsx new file mode 100644 index 00000000..43344d4b --- /dev/null +++ b/src/frontend/apps/impress/src/core/auth/AccountDropdown.tsx @@ -0,0 +1,34 @@ +import { Button } from '@openfun/cunningham-react'; +import React from 'react'; +import { useTranslation } from 'react-i18next'; + +import { useAuthStore } from '@/core/auth'; + +export const AccountDropdown = () => { + const { t } = useTranslation(); + const { logout, authenticated, login } = useAuthStore(); + + if (!authenticated) { + return ( + + ); + } + + return ( + + ); +}; diff --git a/src/frontend/apps/impress/src/core/auth/Auth.tsx b/src/frontend/apps/impress/src/core/auth/Auth.tsx index 865a815f..0e5d0131 100644 --- a/src/frontend/apps/impress/src/core/auth/Auth.tsx +++ b/src/frontend/apps/impress/src/core/auth/Auth.tsx @@ -1,18 +1,47 @@ import { Loader } from '@openfun/cunningham-react'; -import { PropsWithChildren, useEffect } from 'react'; +import { useRouter } from 'next/router'; +import { PropsWithChildren, useEffect, useState } from 'react'; import { Box } from '@/components'; import { useAuthStore } from './useAuthStore'; +/** + * TODO: Remove this restriction when we will have a homepage design for non-authenticated users. + * + * We define the paths that are not allowed without authentication. + * Actually, only the home page and the docs page are not allowed without authentication. + * When we will have a homepage design for non-authenticated users, we will remove this restriction to have + * the full website accessible without authentication. + */ +const regexpUrlsAuth = [/\/docs\/$/g, /^\/$/g]; + export const Auth = ({ children }: PropsWithChildren) => { - const { authenticated, initAuth } = useAuthStore(); + const { initAuth, initiated, authenticated, login } = useAuthStore(); + const { asPath } = useRouter(); + + const [pathAllowed, setPathAllowed] = useState( + !regexpUrlsAuth.some((regexp) => !!asPath.match(regexp)), + ); useEffect(() => { initAuth(); }, [initAuth]); - if (!authenticated) { + useEffect(() => { + setPathAllowed(!regexpUrlsAuth.some((regexp) => !!asPath.match(regexp))); + }, [asPath]); + + // We force to login except on allowed paths + useEffect(() => { + if (!initiated || authenticated || pathAllowed) { + return; + } + + login(); + }, [authenticated, pathAllowed, login, initiated]); + + if ((!initiated && pathAllowed) || (!authenticated && !pathAllowed)) { return ( diff --git a/src/frontend/apps/impress/src/core/auth/index.ts b/src/frontend/apps/impress/src/core/auth/index.ts index b04f47a6..abae4bc5 100644 --- a/src/frontend/apps/impress/src/core/auth/index.ts +++ b/src/frontend/apps/impress/src/core/auth/index.ts @@ -1,3 +1,4 @@ +export * from './AccountDropdown'; +export * from './api/types'; export * from './Auth'; export * from './useAuthStore'; -export * from './api/types'; diff --git a/src/frontend/apps/impress/src/core/auth/useAuthStore.tsx b/src/frontend/apps/impress/src/core/auth/useAuthStore.tsx index 8db59f96..64eeeb36 100644 --- a/src/frontend/apps/impress/src/core/auth/useAuthStore.tsx +++ b/src/frontend/apps/impress/src/core/auth/useAuthStore.tsx @@ -6,18 +6,22 @@ import { User, getMe } from './api'; import { PATH_AUTH_LOCAL_STORAGE } from './conf'; interface AuthStore { + initiated: boolean; authenticated: boolean; initAuth: () => void; logout: () => void; + login: () => void; userData?: User; } const initialState = { + initiated: false, authenticated: false, userData: undefined, }; export const useAuthStore = create((set) => ({ + initiated: initialState.initiated, authenticated: initialState.authenticated, userData: initialState.userData, @@ -34,20 +38,21 @@ export const useAuthStore = create((set) => ({ set({ authenticated: true, userData: data }); }) - .catch(() => { - // If we try to access a specific page and we are not authenticated - // we store the path in the local storage to redirect to it after login - if (window.location.pathname !== '/') { - localStorage.setItem( - PATH_AUTH_LOCAL_STORAGE, - window.location.pathname, - ); - } - - window.location.replace(new URL('authenticate/', baseApiUrl()).href); + .catch(() => {}) + .finally(() => { + set({ initiated: true }); }); }, + login: () => { + // If we try to access a specific page and we are not authenticated + // we store the path in the local storage to redirect to it after login + if (window.location.pathname !== '/') { + localStorage.setItem(PATH_AUTH_LOCAL_STORAGE, window.location.pathname); + } + + window.location.replace(`${baseApiUrl()}authenticate/`); + }, logout: () => { - window.location.replace(new URL('logout/', baseApiUrl()).href); + window.location.replace(`${baseApiUrl()}logout/`); }, })); diff --git a/src/frontend/apps/impress/src/features/header/AccountDropdown.tsx b/src/frontend/apps/impress/src/features/header/AccountDropdown.tsx deleted file mode 100644 index 13c77b5c..00000000 --- a/src/frontend/apps/impress/src/features/header/AccountDropdown.tsx +++ /dev/null @@ -1,34 +0,0 @@ -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/impress/src/features/header/Header.tsx b/src/frontend/apps/impress/src/features/header/Header.tsx index 8f7dbf31..f5802b3d 100644 --- a/src/frontend/apps/impress/src/features/header/Header.tsx +++ b/src/frontend/apps/impress/src/features/header/Header.tsx @@ -4,11 +4,11 @@ import { useTranslation } from 'react-i18next'; import styled from 'styled-components'; import { Box, StyledLink, Text } from '@/components/'; +import { AccountDropdown } from '@/core/auth'; import { useCunninghamTheme } from '@/cunningham'; import { LanguagePicker } from '../language/'; -import { AccountDropdown } from './AccountDropdown'; import { LaGaufre } from './LaGaufre'; import { default as IconDocs } from './assets/icon-docs.svg?url'; diff --git a/src/frontend/apps/impress/src/pages/_document.tsx b/src/frontend/apps/impress/src/pages/_document.tsx index 282e0d3f..9f81db4d 100644 --- a/src/frontend/apps/impress/src/pages/_document.tsx +++ b/src/frontend/apps/impress/src/pages/_document.tsx @@ -1,10 +1,8 @@ import { Head, Html, Main, NextScript } from 'next/document'; -import '@/i18n/initI18n'; - export default function RootLayout() { return ( - +