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); + }, +};