From 7dc7320dac90b9f626719cc17112a65d7afeddb1 Mon Sep 17 00:00:00 2001 From: Anthony LC Date: Thu, 9 Apr 2026 16:49:41 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=9A=B8(frontend)=20redirect=20on=20curren?= =?UTF-8?q?t=20url=20tab=20after=20401?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When multiple tabs were opened and a 401 error occurred, the user was redirected to the login page, then after login, the user was redirected to the page where the last 401 error occurred. We improved this behavior by saving the url per tab, and after login, the user is redirected to the last url of the current tab. --- CHANGELOG.md | 1 + .../__tests__/app-impress/doc-routing.spec.ts | 61 ++++++++++++++----- .../apps/impress/src/features/auth/conf.ts | 2 +- .../apps/impress/src/features/auth/utils.ts | 17 +++--- .../apps/impress/src/utils/storages.ts | 27 ++++++++ 5 files changed, 85 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a09c47046..060a0af06 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ and this project adheres to ### Fixed +- 🚸(frontend) redirect on current url tab after 401 #2197 - 🐛(frontend) abort check media status unmount #2194 - ✨(backend) order pinned documents by last updated at #2028 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 b3941aaa8..2702b22e8 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,13 +1,6 @@ -import crypto from 'crypto'; - import { expect, test } from '@playwright/test'; -import { - createDoc, - getCurrentConfig, - mockedDocument, - verifyDocName, -} from './utils-common'; +import { createDoc, getCurrentConfig, verifyDocName } from './utils-common'; import { writeInEditor } from './utils-editor'; import { SignIn, expectLoginPage } from './utils-signin'; import { createRootSubPage } from './utils-sub-pages'; @@ -119,13 +112,53 @@ test.describe('Doc Routing: Not logged', () => { page, browserName, }) => { - const uuid = crypto.randomUUID(); - await mockedDocument(page, { link_reach: 'public', id: uuid }); - await page.goto(`/docs/${uuid}/`); - await expect(page.locator('h2').getByText('Mocked document')).toBeVisible(); - await page.getByRole('button', { name: 'Login' }).click(); + await page.goto('/'); + await SignIn(page, browserName); + + const [docTitle1] = await createDoc(page, 'doc-login-1', browserName, 1); + await verifyDocName(page, docTitle1); + + const page2 = await page.context().newPage(); + await page2.goto('/'); + const [docTitle2] = await createDoc(page2, 'doc-login-2', browserName, 1); + await verifyDocName(page2, docTitle2); + + // Remove cookies `docs_sessionid` to simulate the user being logged out + await page2.context().clearCookies(); + await page2.reload(); + + // Tab 2 - 401 triggered, user should be redirected to login page + await expect( + page2 + .getByRole('main', { name: 'Main content' }) + .getByRole('button', { name: 'Login' }), + ).toBeVisible({ + timeout: 10000, + }); + + // Tab 1 - 401 triggered, user should be redirected to login page + await page.reload(); + await expect( + page + .getByRole('main', { name: 'Main content' }) + .getByRole('button', { name: 'Login' }), + ).toBeVisible({ + timeout: 10000, + }); + + // Reconnected + await page + .getByRole('main', { name: 'Main content' }) + .getByRole('button', { name: 'Login' }) + .click(); await SignIn(page, browserName, false); - await expect(page.locator('h2').getByText('Mocked document')).toBeVisible(); + + // Tab 1 - Should be on its doc + await verifyDocName(page, docTitle1); + + // Tab 2 - Should be on its doc + await page2.reload(); + await verifyDocName(page2, docTitle2); }); // eslint-disable-next-line playwright/expect-expect diff --git a/src/frontend/apps/impress/src/features/auth/conf.ts b/src/frontend/apps/impress/src/features/auth/conf.ts index 5feae77ce..c5b7033cc 100644 --- a/src/frontend/apps/impress/src/features/auth/conf.ts +++ b/src/frontend/apps/impress/src/features/auth/conf.ts @@ -3,5 +3,5 @@ import { baseApiUrl } from '@/api'; export const HOME_URL = '/home/'; export const LOGIN_URL = `${baseApiUrl()}authenticate/`; export const LOGOUT_URL = `${baseApiUrl()}logout/`; -export const PATH_AUTH_LOCAL_STORAGE = 'docs-path-auth'; +export const PATH_AUTH_SESSION_STORAGE = 'docs-path-auth'; export const SILENT_LOGIN_RETRY = 'silent-login-retry'; diff --git a/src/frontend/apps/impress/src/features/auth/utils.ts b/src/frontend/apps/impress/src/features/auth/utils.ts index cbb05c291..2750f1c88 100644 --- a/src/frontend/apps/impress/src/features/auth/utils.ts +++ b/src/frontend/apps/impress/src/features/auth/utils.ts @@ -1,35 +1,36 @@ import { terminateCrispSession } from '@/services/Crisp'; -import { safeLocalStorage } from '@/utils/storages'; +import { safeLocalStorage, safeSessionStorage } from '@/utils/storages'; import { HOME_URL, LOGIN_URL, LOGOUT_URL, - PATH_AUTH_LOCAL_STORAGE, + PATH_AUTH_SESSION_STORAGE, SILENT_LOGIN_RETRY, } from './conf'; /** - * Get the stored auth URL from local storage + * Get the stored auth URL from session storage (per-tab) */ export const getAuthUrl = () => { - const path_auth = safeLocalStorage.getItem(PATH_AUTH_LOCAL_STORAGE); + const path_auth = safeSessionStorage.getItem(PATH_AUTH_SESSION_STORAGE); if (path_auth) { - safeLocalStorage.removeItem(PATH_AUTH_LOCAL_STORAGE); + safeSessionStorage.removeItem(PATH_AUTH_SESSION_STORAGE); return path_auth; } }; /** - * Store the current path in local storage if it's not the homepage or root - * so we can redirect the user to this path after login + * Store the current path in session storage (per-tab) if it's not the + * homepage or root, so we can redirect the user to this path after login. + * Using sessionStorage ensures each tab independently tracks its own URL. */ export const setAuthUrl = () => { if ( window.location.pathname !== '/' && window.location.pathname !== `${HOME_URL}/` ) { - safeLocalStorage.setItem(PATH_AUTH_LOCAL_STORAGE, window.location.href); + safeSessionStorage.setItem(PATH_AUTH_SESSION_STORAGE, window.location.href); } }; diff --git a/src/frontend/apps/impress/src/utils/storages.ts b/src/frontend/apps/impress/src/utils/storages.ts index b7f152c91..49ccc9966 100644 --- a/src/frontend/apps/impress/src/utils/storages.ts +++ b/src/frontend/apps/impress/src/utils/storages.ts @@ -50,3 +50,30 @@ export const safeLocalStorage: SyncStorage = { localStorage.removeItem(key); }, }; + +/** + * @namespace safeSessionStorage + * @description A utility for safely interacting with sessionStorage. + * sessionStorage is scoped to the current browser tab, making it suitable + * for per-tab state that should not be shared across tabs. + */ +export const safeSessionStorage: SyncStorage = { + getItem: (key: string): string | null => { + if (typeof window === 'undefined') { + return null; + } + return sessionStorage.getItem(key); + }, + setItem: (key: string, value: string): void => { + if (typeof window === 'undefined') { + return; + } + sessionStorage.setItem(key, value); + }, + removeItem: (key: string): void => { + if (typeof window === 'undefined') { + return; + } + sessionStorage.removeItem(key); + }, +};