From 66275180171594b3de557b5f17b95fc0046529bd Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Tue, 18 Mar 2025 11:28:35 +0100 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=9A(frontend)=20redirect=20to=20401=20?= =?UTF-8?q?page=20when=20401=20error?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- CHANGELOG.md | 1 + .../__tests__/app-impress/doc-routing.spec.ts | 44 ++++++++++++++++++- .../apps/impress/src/core/AppProvider.tsx | 38 +++++++++++++--- 3 files changed, 75 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f4b60a1..3152f8a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,6 +31,7 @@ and this project adheres to and descendants views #695 - 🐛(action) fix notify-argocd workflow #713 - 🚨(helm) fix helmfile lint #736 +- 🚚(frontend) redirect to 401 page when 401 error #759 ## [2.4.0] - 2025-03-06 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 5280a892..95019784 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,12 @@ 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.beforeEach(async ({ page }) => { @@ -50,6 +56,42 @@ test.describe('Doc Routing', () => { 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', () => { diff --git a/src/frontend/apps/impress/src/core/AppProvider.tsx b/src/frontend/apps/impress/src/core/AppProvider.tsx index 6727c48c..48dea15c 100644 --- a/src/frontend/apps/impress/src/core/AppProvider.tsx +++ b/src/frontend/apps/impress/src/core/AppProvider.tsx @@ -1,9 +1,10 @@ import { CunninghamProvider } from '@openfun/cunningham-react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; +import { useRouter } from 'next/router'; import { useEffect } from 'react'; import { useCunninghamTheme } from '@/cunningham'; -import { Auth } from '@/features/auth'; +import { Auth, KEY_AUTH, setAuthUrl } from '@/features/auth'; import { useResponsiveStore } from '@/stores/'; import { ConfigProvider } from './config/'; @@ -15,17 +16,19 @@ import { ConfigProvider } from './config/'; * - global cache duration - we decided 3 minutes * - It can be overridden to each query */ -const queryClient = new QueryClient({ - defaultOptions: { - queries: { - staleTime: 1000 * 60 * 3, - retry: 1, - }, +const defaultOptions = { + queries: { + staleTime: 1000 * 60 * 3, + retry: 1, }, +}; +const queryClient = new QueryClient({ + defaultOptions, }); export function AppProvider({ children }: { children: React.ReactNode }) { const { theme } = useCunninghamTheme(); + const { replace } = useRouter(); const initializeResizeListener = useResponsiveStore( (state) => state.initializeResizeListener, @@ -36,6 +39,27 @@ export function AppProvider({ children }: { children: React.ReactNode }) { return cleanupResizeListener; }, [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 (