🚚(frontend) redirect to 401 page when 401 error
Users could still be able to edit a document if the session was expired. It could give the feeling that the document was not saved. If during a mutation request (POST, PUT, DELETE), the server returns a 401 error, the user is redirected to the 401 page.
This commit is contained in:
@@ -31,6 +31,7 @@ and this project adheres to
|
|||||||
and descendants views #695
|
and descendants views #695
|
||||||
- 🐛(action) fix notify-argocd workflow #713
|
- 🐛(action) fix notify-argocd workflow #713
|
||||||
- 🚨(helm) fix helmfile lint #736
|
- 🚨(helm) fix helmfile lint #736
|
||||||
|
- 🚚(frontend) redirect to 401 page when 401 error #759
|
||||||
|
|
||||||
|
|
||||||
## [2.4.0] - 2025-03-06
|
## [2.4.0] - 2025-03-06
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { expect, test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
import { expectLoginPage, keyCloakSignIn, mockedDocument } from './common';
|
import {
|
||||||
|
createDoc,
|
||||||
|
expectLoginPage,
|
||||||
|
keyCloakSignIn,
|
||||||
|
mockedDocument,
|
||||||
|
verifyDocName,
|
||||||
|
} from './common';
|
||||||
|
|
||||||
test.describe('Doc Routing', () => {
|
test.describe('Doc Routing', () => {
|
||||||
test.beforeEach(async ({ page }) => {
|
test.beforeEach(async ({ page }) => {
|
||||||
@@ -50,6 +56,42 @@ test.describe('Doc Routing', () => {
|
|||||||
timeout: 15000,
|
timeout: 15000,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('checks 401 on docs/[id] page', async ({ page, browserName }) => {
|
||||||
|
const [docTitle] = await createDoc(page, 'My new doc', browserName, 1);
|
||||||
|
await verifyDocName(page, docTitle);
|
||||||
|
|
||||||
|
const responsePromise = page.route(
|
||||||
|
/.*\/link-configuration\/$|users\/me\/$/,
|
||||||
|
async (route) => {
|
||||||
|
const request = route.request();
|
||||||
|
|
||||||
|
if (
|
||||||
|
request.method().includes('PUT') ||
|
||||||
|
request.method().includes('GET')
|
||||||
|
) {
|
||||||
|
await route.fulfill({
|
||||||
|
status: 401,
|
||||||
|
json: {
|
||||||
|
detail: 'Log in to access the document',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
await route.continue();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
await page.getByRole('button', { name: 'Share' }).click();
|
||||||
|
|
||||||
|
const selectVisibility = page.getByLabel('Visibility', { exact: true });
|
||||||
|
await selectVisibility.click();
|
||||||
|
await page.getByLabel('Connected').click();
|
||||||
|
|
||||||
|
await responsePromise;
|
||||||
|
|
||||||
|
await expect(page.getByText('Log in to access the document')).toBeVisible();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('Doc Routing: Not loggued', () => {
|
test.describe('Doc Routing: Not loggued', () => {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { CunninghamProvider } from '@openfun/cunningham-react';
|
import { CunninghamProvider } from '@openfun/cunningham-react';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
|
import { useRouter } from 'next/router';
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
import { useCunninghamTheme } from '@/cunningham';
|
import { useCunninghamTheme } from '@/cunningham';
|
||||||
import { Auth } from '@/features/auth';
|
import { Auth, KEY_AUTH, setAuthUrl } from '@/features/auth';
|
||||||
import { useResponsiveStore } from '@/stores/';
|
import { useResponsiveStore } from '@/stores/';
|
||||||
|
|
||||||
import { ConfigProvider } from './config/';
|
import { ConfigProvider } from './config/';
|
||||||
@@ -15,17 +16,19 @@ import { ConfigProvider } from './config/';
|
|||||||
* - global cache duration - we decided 3 minutes
|
* - global cache duration - we decided 3 minutes
|
||||||
* - It can be overridden to each query
|
* - It can be overridden to each query
|
||||||
*/
|
*/
|
||||||
const queryClient = new QueryClient({
|
const defaultOptions = {
|
||||||
defaultOptions: {
|
queries: {
|
||||||
queries: {
|
staleTime: 1000 * 60 * 3,
|
||||||
staleTime: 1000 * 60 * 3,
|
retry: 1,
|
||||||
retry: 1,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
|
};
|
||||||
|
const queryClient = new QueryClient({
|
||||||
|
defaultOptions,
|
||||||
});
|
});
|
||||||
|
|
||||||
export function AppProvider({ children }: { children: React.ReactNode }) {
|
export function AppProvider({ children }: { children: React.ReactNode }) {
|
||||||
const { theme } = useCunninghamTheme();
|
const { theme } = useCunninghamTheme();
|
||||||
|
const { replace } = useRouter();
|
||||||
|
|
||||||
const initializeResizeListener = useResponsiveStore(
|
const initializeResizeListener = useResponsiveStore(
|
||||||
(state) => state.initializeResizeListener,
|
(state) => state.initializeResizeListener,
|
||||||
@@ -36,6 +39,27 @@ export function AppProvider({ children }: { children: React.ReactNode }) {
|
|||||||
return cleanupResizeListener;
|
return cleanupResizeListener;
|
||||||
}, [initializeResizeListener]);
|
}, [initializeResizeListener]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
queryClient.setDefaultOptions({
|
||||||
|
...defaultOptions,
|
||||||
|
mutations: {
|
||||||
|
onError: (error) => {
|
||||||
|
if (
|
||||||
|
error instanceof Error &&
|
||||||
|
'status' in error &&
|
||||||
|
error.status === 401
|
||||||
|
) {
|
||||||
|
void queryClient.resetQueries({
|
||||||
|
queryKey: [KEY_AUTH],
|
||||||
|
});
|
||||||
|
setAuthUrl();
|
||||||
|
void replace(`/401`);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}, [replace]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<CunninghamProvider theme={theme}>
|
<CunninghamProvider theme={theme}>
|
||||||
|
|||||||
Reference in New Issue
Block a user