diff --git a/frontend/src/component/user/Authentication/Authentication.tsx b/frontend/src/component/user/Authentication/Authentication.tsx
index 10642d1a2178..84fcb2cf078e 100644
--- a/frontend/src/component/user/Authentication/Authentication.tsx
+++ b/frontend/src/component/user/Authentication/Authentication.tsx
@@ -17,6 +17,7 @@ import { useAuthDetails } from 'hooks/api/getters/useAuth/useAuthDetails';
import { AUTH_PAGE_ID } from 'utils/testIds';
import { ReactElement, useEffect } from 'react';
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
+import { setSessionStorageItem } from 'utils/storage';
interface IAuthenticationProps {
redirect: string;
@@ -32,6 +33,12 @@ const Authentication = ({
const error = params.get('errorMsg');
const { trackEvent } = usePlausibleTracker();
+ useEffect(() => {
+ if (redirect) {
+ setSessionStorageItem('login-redirect', redirect, 1000 * 60 * 10);
+ }
+ }, [redirect]);
+
useEffect(() => {
if (invited) {
trackEvent('invite', {
diff --git a/frontend/src/component/user/Login/Login.tsx b/frontend/src/component/user/Login/Login.tsx
index e09ea3fc7423..a5e86f34354c 100644
--- a/frontend/src/component/user/Login/Login.tsx
+++ b/frontend/src/component/user/Login/Login.tsx
@@ -8,6 +8,7 @@ import Authentication from '../Authentication/Authentication';
import { useAuthDetails } from 'hooks/api/getters/useAuth/useAuthDetails';
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
import { parseRedirectParam } from 'component/user/Login/parseRedirectParam';
+import { getSessionStorageItem, setSessionStorageItem } from 'utils/storage';
const StyledDiv = styled('div')(({ theme }) => ({
display: 'flex',
@@ -26,9 +27,11 @@ const Login = () => {
const query = useQueryParams();
const resetPassword = query.get('reset') === 'true';
const invited = query.get('invited') === 'true';
- const redirect = query.get('redirect') || '/';
+ const redirect =
+ query.get('redirect') || getSessionStorageItem('login-redirect') || '/';
if (user) {
+ setSessionStorageItem('login-redirect');
return ;
}
diff --git a/frontend/src/utils/storage.test.ts b/frontend/src/utils/storage.test.ts
new file mode 100644
index 000000000000..5d98caccf940
--- /dev/null
+++ b/frontend/src/utils/storage.test.ts
@@ -0,0 +1,120 @@
+import {
+ setLocalStorageItem,
+ getLocalStorageItem,
+ setSessionStorageItem,
+ getSessionStorageItem,
+} from './storage';
+import { vi } from 'vitest';
+
+// Mocking the global localStorage
+const localStorageMock = (() => {
+ let store: Record = {};
+
+ return {
+ getItem(key: string) {
+ return store[key] || null;
+ },
+ setItem(key: string, value: string) {
+ store[key] = value.toString();
+ },
+ removeItem(key: string) {
+ delete store[key];
+ },
+ clear() {
+ store = {};
+ },
+ };
+})();
+
+const sessionStorageMock = (() => {
+ let store: Record = {};
+
+ return {
+ getItem(key: string) {
+ return store[key] || null;
+ },
+ setItem(key: string, value: string) {
+ store[key] = value.toString();
+ },
+ removeItem(key: string) {
+ delete store[key];
+ },
+ clear() {
+ store = {};
+ },
+ };
+})();
+
+Object.defineProperty(window, 'localStorage', {
+ value: localStorageMock,
+});
+
+Object.defineProperty(window, 'sessionStorage', {
+ value: sessionStorageMock,
+});
+
+// Test suite
+describe('localStorage with TTL', () => {
+ beforeEach(() => {
+ localStorage.clear();
+ vi.useFakeTimers();
+ });
+
+ test('item should be retrievable before TTL expires', () => {
+ setLocalStorageItem('testKey', 'testValue', 600000);
+ expect(getLocalStorageItem('testKey')).toBe('testValue');
+ });
+
+ test('item should not be retrievable after TTL expires', () => {
+ setLocalStorageItem('testKey', 'testValue', 500000);
+
+ vi.advanceTimersByTime(600000);
+
+ expect(getLocalStorageItem('testKey')).toBeUndefined();
+ });
+ test('object should be retrievable before TTL expires', () => {
+ const testObject = { name: 'Test', number: 123 };
+ setLocalStorageItem('testObjectKey', testObject, 600000);
+
+ const retrievedObject = getLocalStorageItem<{
+ name: string;
+ number: number;
+ }>('testObjectKey');
+ expect(retrievedObject).toEqual(testObject);
+ });
+
+ test('object should not be retrievable after TTL expires', () => {
+ const testObject = { name: 'Test', number: 123 };
+ setLocalStorageItem('testObjectKey', testObject, 500000);
+
+ vi.advanceTimersByTime(600000);
+
+ const retrievedObject = getLocalStorageItem<{
+ name: string;
+ number: number;
+ }>('testObjectKey');
+ expect(retrievedObject).toBeUndefined();
+ });
+
+ test('object should be retrievable before TTL expires in sessionStorage', () => {
+ const testObject = { name: 'TestSession', number: 456 };
+ setSessionStorageItem('testObjectKeySession', testObject, 500000);
+
+ const retrievedObject = getSessionStorageItem(
+ 'testObjectKeySession',
+ );
+ expect(retrievedObject).toEqual(testObject);
+ });
+
+ test('object should not be retrievable after TTL expires in sessionStorage', () => {
+ const testObject = { name: 'TestSession', number: 456 };
+ setSessionStorageItem('testObjectKeySession', testObject, 500000); // 10 minutes TTL
+
+ vi.advanceTimersByTime(600000);
+
+ const retrievedObject = getSessionStorageItem(
+ 'testObjectKeySession',
+ );
+ expect(retrievedObject).toBeUndefined();
+ });
+});
diff --git a/frontend/src/utils/storage.ts b/frontend/src/utils/storage.ts
index 5cad745927a7..113f3a65f544 100644
--- a/frontend/src/utils/storage.ts
+++ b/frontend/src/utils/storage.ts
@@ -1,25 +1,87 @@
+type Expirable = {
+ value: T | undefined;
+ expiry: number | null;
+};
+
// Get an item from localStorage.
// Returns undefined if the browser denies access.
export function getLocalStorageItem(key: string): T | undefined {
try {
- return parseStoredItem(window.localStorage.getItem(key));
+ const itemStr = window.localStorage.getItem(key);
+ if (!itemStr) {
+ return undefined;
+ }
+
+ const item: Expirable | undefined = parseStoredItem(itemStr);
+ if (item?.expiry && new Date().getTime() > item.expiry) {
+ window.localStorage.removeItem(key);
+ return undefined;
+ }
+ return item?.value;
} catch (err: unknown) {
console.warn(err);
+ return undefined;
}
}
// Store an item in localStorage.
// Does nothing if the browser denies access.
-export function setLocalStorageItem(key: string, value: unknown) {
+export function setLocalStorageItem(
+ key: string,
+ value: T | undefined = undefined,
+ timeToLive?: number,
+) {
+ try {
+ const item: Expirable = {
+ value,
+ expiry:
+ timeToLive !== undefined
+ ? new Date().getTime() + timeToLive
+ : null,
+ };
+ window.localStorage.setItem(key, JSON.stringify(item));
+ } catch (err: unknown) {
+ console.warn(err);
+ }
+}
+
+// Store an item in sessionStorage with optional TTL
+export function setSessionStorageItem(
+ key: string,
+ value: T | undefined = undefined,
+ timeToLive?: number,
+) {
try {
- window.localStorage.setItem(
- key,
- JSON.stringify(value, (_key, value) =>
- value instanceof Set ? [...value] : value,
- ),
- );
+ const item: Expirable = {
+ value,
+ expiry:
+ timeToLive !== undefined
+ ? new Date().getTime() + timeToLive
+ : null,
+ };
+ window.sessionStorage.setItem(key, JSON.stringify(item));
+ } catch (err: unknown) {
+ console.warn(err);
+ }
+}
+
+// Get an item from sessionStorage, checking for TTL
+export function getSessionStorageItem(key: string): T | undefined {
+ try {
+ const itemStr = window.sessionStorage.getItem(key);
+ if (!itemStr) {
+ return undefined;
+ }
+
+ const item: Expirable | undefined = parseStoredItem(itemStr);
+ if (item?.expiry && new Date().getTime() > item.expiry) {
+ window.sessionStorage.removeItem(key);
+ return undefined;
+ }
+ return item?.value;
} catch (err: unknown) {
console.warn(err);
+ return undefined;
}
}