🛂(frontend) access public docs without being logged

We can now access public docs without being logged.
This commit is contained in:
Anthony LC
2024-09-06 14:23:22 +02:00
committed by Anthony LC
parent 2a7e3116bd
commit 459cb5e2e2
13 changed files with 140 additions and 82 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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 (
<Button
onClick={login}
color="primary-text"
icon={<span className="material-icons">login</span>}
aria-label={t('Login')}
>
{t('Login')}
</Button>
);
}
return (
<Button
onClick={logout}
color="primary-text"
icon={<span className="material-icons">logout</span>}
aria-label={t('Logout')}
>
{t('Logout')}
</Button>
);
};

View File

@@ -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<boolean>(
!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 (
<Box $height="100vh" $width="100vw" $align="center" $justify="center">
<Loader />

View File

@@ -1,3 +1,4 @@
export * from './AccountDropdown';
export * from './api/types';
export * from './Auth';
export * from './useAuthStore';
export * from './api/types';

View File

@@ -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<AuthStore>((set) => ({
initiated: initialState.initiated,
authenticated: initialState.authenticated,
userData: initialState.userData,
@@ -34,20 +38,21 @@ export const useAuthStore = create<AuthStore>((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/`);
},
}));

View File

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

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

View File

@@ -1,10 +1,8 @@
import { Head, Html, Main, NextScript } from 'next/document';
import '@/i18n/initI18n';
export default function RootLayout() {
return (
<Html lang="en">
<Html>
<Head />
<body suppressHydrationWarning={process.env.NODE_ENV === 'development'}>
<Main />