diff --git a/apps/backoffice/src/app/features/user-management/UserManagementPage.tsx b/apps/backoffice/src/app/features/user-management/UserManagementPage.tsx index d6d89c47d..a79dc9ed0 100644 --- a/apps/backoffice/src/app/features/user-management/UserManagementPage.tsx +++ b/apps/backoffice/src/app/features/user-management/UserManagementPage.tsx @@ -138,8 +138,12 @@ export default function UserManagementPage() { style.textContent = UM_OVERRIDES; document.head.appendChild(style); - const token = localStorage.getItem('ema-backoffice-auth-token') ?? ''; - const refreshToken = localStorage.getItem('ema-backoffice-refresh-token') ?? undefined; + const readCookie = (name: string) => { + const m = document.cookie.match(new RegExp('(?:^|;\\s*)' + name + '=([^;]*)')); + return m ? decodeURIComponent(m[1]) : undefined; + }; + const token = readCookie('ema-backoffice-auth-token') ?? ''; + const refreshToken = readCookie('ema-backoffice-refresh-token'); const session: UserManagementSessionOptions = { initialSession: token diff --git a/apps/backoffice/src/app/store/index.ts b/apps/backoffice/src/app/store/index.ts index bb3ca733e..8f9908a95 100644 --- a/apps/backoffice/src/app/store/index.ts +++ b/apps/backoffice/src/app/store/index.ts @@ -11,7 +11,7 @@ import { import type { AuthUser, CurrentProfile } from '@ema-platform/auth'; import { preferencesReducer } from './preferences.slice'; -configureAuthStorage('ema-backoffice'); +configureAuthStorage('ema-backoffice', true); const preloadedAuth = (() => { const token = authStorage.getToken(); diff --git a/libs/api/src/lib/session/index.ts b/libs/api/src/lib/session/index.ts index 7f7a25284..7a3c1326d 100644 --- a/libs/api/src/lib/session/index.ts +++ b/libs/api/src/lib/session/index.ts @@ -11,13 +11,22 @@ const TOKEN_STORAGE_KEYS = [ 'auth-token', ] as const; +function getCookie(name: string): string | undefined { + const match = document.cookie.match( + new RegExp('(?:^|;\\s*)' + name + '=([^;]*)'), + ); + return match ? decodeURIComponent(match[1]) : undefined; +} + export function resolveTokenFromStorage(): string | undefined { + // cookie first (backoffice stores tokens there), then localStorage (portal / legacy) for (const key of TOKEN_STORAGE_KEYS) { + const cookie = getCookie(key); + if (cookie) return cookie; const stored = localStorage.getItem(key); if (stored) return stored; } - const match = document.cookie.match(/(?:^|;\s*)auth-token=([^;]*)/); - return match ? decodeURIComponent(match[1]) : undefined; + return undefined; } export function resolveSessionContext(state?: { auth?: { token?: string } }): { diff --git a/libs/auth/src/lib/utils/auth-storage.ts b/libs/auth/src/lib/utils/auth-storage.ts index 22fc1757f..06da91355 100644 --- a/libs/auth/src/lib/utils/auth-storage.ts +++ b/libs/auth/src/lib/utils/auth-storage.ts @@ -1,7 +1,37 @@ -let _prefix = 'ema-auth'; +const MAX_AGE = 60 * 60 * 24 * 7; // ponytail: cookie lifetime knob; JWT expiry + refresh govern real auth -export function configureAuthStorage(prefix: string) { +interface Backend { + get(k: string): string | null; + set(k: string, v: string): void; + remove(k: string): void; +} + +const localBackend: Backend = { + get: (k) => localStorage.getItem(k), + set: (k, v) => localStorage.setItem(k, v), + remove: (k) => localStorage.removeItem(k), +}; + +const cookieBackend: Backend = { + get: (k) => { + const m = document.cookie.match(new RegExp('(?:^|;\\s*)' + k + '=([^;]*)')); + return m ? decodeURIComponent(m[1]) : null; + }, + // ponytail: 4KB/cookie cap — auth-user/current-profile JSON must stay under it + set: (k, v) => { + document.cookie = `${k}=${encodeURIComponent(v)}; path=/; Secure; SameSite=Strict; Max-Age=${MAX_AGE}`; + }, + remove: (k) => { + document.cookie = `${k}=; path=/; Secure; SameSite=Strict; Max-Age=0`; + }, +}; + +let _prefix = 'ema-auth'; +let backend: Backend = localBackend; + +export function configureAuthStorage(prefix: string, useCookies = false) { _prefix = prefix; + backend = useCookies ? cookieBackend : localBackend; } function key(k: string) { @@ -9,32 +39,32 @@ function key(k: string) { } export const authStorage = { - getToken: () => localStorage.getItem(key('auth-token')) ?? undefined, - setToken: (token: string) => localStorage.setItem(key('auth-token'), token), - getRefreshToken: () => localStorage.getItem(key('refresh-token')) ?? undefined, - setRefreshToken: (t: string) => localStorage.setItem(key('refresh-token'), t), + getToken: () => backend.get(key('auth-token')) ?? undefined, + setToken: (token: string) => backend.set(key('auth-token'), token), + getRefreshToken: () => backend.get(key('refresh-token')) ?? undefined, + setRefreshToken: (t: string) => backend.set(key('refresh-token'), t), getUser: (): T | null => { try { - return JSON.parse(localStorage.getItem(key('auth-user')) ?? 'null') as T | null; + return JSON.parse(backend.get(key('auth-user')) ?? 'null') as T | null; } catch { return null; } }, - setUser: (u: T) => localStorage.setItem(key('auth-user'), JSON.stringify(u)), - getProfileId: () => localStorage.getItem(key('profile-id')) ?? undefined, - setProfileId: (id: string) => localStorage.setItem(key('profile-id'), id), + setUser: (u: T) => backend.set(key('auth-user'), JSON.stringify(u)), + getProfileId: () => backend.get(key('profile-id')) ?? undefined, + setProfileId: (id: string) => backend.set(key('profile-id'), id), getProfile: (): T | null => { try { - return JSON.parse(localStorage.getItem(key('current-profile')) ?? 'null') as T | null; + return JSON.parse(backend.get(key('current-profile')) ?? 'null') as T | null; } catch { return null; } }, - setProfile: (p: T) => localStorage.setItem(key('current-profile'), JSON.stringify(p)), - removeProfile: () => localStorage.removeItem(key('current-profile')), + setProfile: (p: T) => backend.set(key('current-profile'), JSON.stringify(p)), + removeProfile: () => backend.remove(key('current-profile')), clear: () => { [key('auth-token'), key('refresh-token'), key('auth-user'), key('profile-id'), key('current-profile')].forEach((k) => - localStorage.removeItem(k), + backend.remove(k), ); document.cookie = 'auth-token=; path=/; expires=Thu, 01 Jan 1970 00:00:00 UTC; SameSite=Lax'; diff --git a/package-lock.json b/package-lock.json index 49e6d330a..9049ab3fb 100644 --- a/package-lock.json +++ b/package-lock.json @@ -43,7 +43,7 @@ "@nx/vite": "^22.5.4", "@nx/vitest": "^22.5.4", "@types/node": "^22.0.0", - "@types/react": "^19.0.0", + "@types/react": "^19.2.17", "@types/react-dom": "^19.0.0", "@typescript-eslint/eslint-plugin": "^8.60.0", "@typescript-eslint/parser": "^8.60.0", @@ -3867,18 +3867,6 @@ } } }, - "node_modules/@mui/x-date-pickers/node_modules/@types/react": { - "version": "18.3.31", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.31.tgz", - "integrity": "sha512-vfEqpXTvwT91yhmwdfouStN2hSKwTvyRs8qpLfADyrq/kxDw0hZM7Wk9Ug1FELj8hIby+S/+kQCSRFF32nv2Qw==", - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@types/prop-types": "*", - "csstype": "^3.2.2" - } - }, "node_modules/@mui/x-date-pickers/node_modules/react-is": { "version": "19.2.7", "resolved": "https://registry.npmjs.org/react-is/-/react-is-19.2.7.tgz", @@ -9190,9 +9178,9 @@ "optional": true }, "node_modules/@types/react": { - "version": "19.2.15", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz", - "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==", + "version": "19.2.17", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.17.tgz", + "integrity": "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==", "license": "MIT", "dependencies": { "csstype": "^3.2.2" @@ -11934,7 +11922,7 @@ "version": "0.1.13", "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "iconv-lite": "^0.6.2" @@ -13725,7 +13713,7 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" @@ -17892,7 +17880,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/sax": { diff --git a/package.json b/package.json index 7d291d660..8a7e31be1 100644 --- a/package.json +++ b/package.json @@ -48,7 +48,7 @@ "@nx/vite": "^22.5.4", "@nx/vitest": "^22.5.4", "@types/node": "^22.0.0", - "@types/react": "^19.0.0", + "@types/react": "^19.2.17", "@types/react-dom": "^19.0.0", "@typescript-eslint/eslint-plugin": "^8.60.0", "@typescript-eslint/parser": "^8.60.0",