mirror of
https://github.com/Unleash/unleash.git
synced 2025-03-18 00:19:49 +01:00
fix: store redirect to localStorage to avoid loss of redirect (#5929)
Stores `redirect` param in localStorage in the Authentication component. Retrieves the `redirect` param from localStorage at the Login screen if it is not there in the url. This will solve losing the redirect information all provider logins Closes # [1-1890](https://linear.app/unleash/issue/1-1890/capture-path-before-logging-in-and-redirect-to-it-if-there-and-custom) --------- Signed-off-by: andreas-unleash <andreas@getunleash.ai>
This commit is contained in:
parent
4ee2acb3aa
commit
a096b2a485
@ -17,6 +17,7 @@ import { useAuthDetails } from 'hooks/api/getters/useAuth/useAuthDetails';
|
|||||||
import { AUTH_PAGE_ID } from 'utils/testIds';
|
import { AUTH_PAGE_ID } from 'utils/testIds';
|
||||||
import { ReactElement, useEffect } from 'react';
|
import { ReactElement, useEffect } from 'react';
|
||||||
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
import { usePlausibleTracker } from 'hooks/usePlausibleTracker';
|
||||||
|
import { setSessionStorageItem } from 'utils/storage';
|
||||||
|
|
||||||
interface IAuthenticationProps {
|
interface IAuthenticationProps {
|
||||||
redirect: string;
|
redirect: string;
|
||||||
@ -32,6 +33,12 @@ const Authentication = ({
|
|||||||
const error = params.get('errorMsg');
|
const error = params.get('errorMsg');
|
||||||
const { trackEvent } = usePlausibleTracker();
|
const { trackEvent } = usePlausibleTracker();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (redirect) {
|
||||||
|
setSessionStorageItem('login-redirect', redirect, 1000 * 60 * 10);
|
||||||
|
}
|
||||||
|
}, [redirect]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (invited) {
|
if (invited) {
|
||||||
trackEvent('invite', {
|
trackEvent('invite', {
|
||||||
|
@ -8,6 +8,7 @@ import Authentication from '../Authentication/Authentication';
|
|||||||
import { useAuthDetails } from 'hooks/api/getters/useAuth/useAuthDetails';
|
import { useAuthDetails } from 'hooks/api/getters/useAuth/useAuthDetails';
|
||||||
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
|
import { useAuthUser } from 'hooks/api/getters/useAuth/useAuthUser';
|
||||||
import { parseRedirectParam } from 'component/user/Login/parseRedirectParam';
|
import { parseRedirectParam } from 'component/user/Login/parseRedirectParam';
|
||||||
|
import { getSessionStorageItem, setSessionStorageItem } from 'utils/storage';
|
||||||
|
|
||||||
const StyledDiv = styled('div')(({ theme }) => ({
|
const StyledDiv = styled('div')(({ theme }) => ({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
@ -26,9 +27,11 @@ const Login = () => {
|
|||||||
const query = useQueryParams();
|
const query = useQueryParams();
|
||||||
const resetPassword = query.get('reset') === 'true';
|
const resetPassword = query.get('reset') === 'true';
|
||||||
const invited = query.get('invited') === 'true';
|
const invited = query.get('invited') === 'true';
|
||||||
const redirect = query.get('redirect') || '/';
|
const redirect =
|
||||||
|
query.get('redirect') || getSessionStorageItem('login-redirect') || '/';
|
||||||
|
|
||||||
if (user) {
|
if (user) {
|
||||||
|
setSessionStorageItem('login-redirect');
|
||||||
return <Navigate to={parseRedirectParam(redirect)} replace />;
|
return <Navigate to={parseRedirectParam(redirect)} replace />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
120
frontend/src/utils/storage.test.ts
Normal file
120
frontend/src/utils/storage.test.ts
Normal file
@ -0,0 +1,120 @@
|
|||||||
|
import {
|
||||||
|
setLocalStorageItem,
|
||||||
|
getLocalStorageItem,
|
||||||
|
setSessionStorageItem,
|
||||||
|
getSessionStorageItem,
|
||||||
|
} from './storage';
|
||||||
|
import { vi } from 'vitest';
|
||||||
|
|
||||||
|
// Mocking the global localStorage
|
||||||
|
const localStorageMock = (() => {
|
||||||
|
let store: Record<string, string> = {};
|
||||||
|
|
||||||
|
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<string, string> = {};
|
||||||
|
|
||||||
|
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<typeof testObject>(
|
||||||
|
'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<typeof testObject>(
|
||||||
|
'testObjectKeySession',
|
||||||
|
);
|
||||||
|
expect(retrievedObject).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
@ -1,28 +1,90 @@
|
|||||||
|
type Expirable<T> = {
|
||||||
|
value: T | undefined;
|
||||||
|
expiry: number | null;
|
||||||
|
};
|
||||||
|
|
||||||
// Get an item from localStorage.
|
// Get an item from localStorage.
|
||||||
// Returns undefined if the browser denies access.
|
// Returns undefined if the browser denies access.
|
||||||
export function getLocalStorageItem<T>(key: string): T | undefined {
|
export function getLocalStorageItem<T>(key: string): T | undefined {
|
||||||
try {
|
try {
|
||||||
return parseStoredItem<T>(window.localStorage.getItem(key));
|
const itemStr = window.localStorage.getItem(key);
|
||||||
|
if (!itemStr) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item: Expirable<T> | undefined = parseStoredItem(itemStr);
|
||||||
|
if (item?.expiry && new Date().getTime() > item.expiry) {
|
||||||
|
window.localStorage.removeItem(key);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return item?.value;
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
|
return undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Store an item in localStorage.
|
// Store an item in localStorage.
|
||||||
// Does nothing if the browser denies access.
|
// Does nothing if the browser denies access.
|
||||||
export function setLocalStorageItem(key: string, value: unknown) {
|
export function setLocalStorageItem<T>(
|
||||||
|
key: string,
|
||||||
|
value: T | undefined = undefined,
|
||||||
|
timeToLive?: number,
|
||||||
|
) {
|
||||||
try {
|
try {
|
||||||
window.localStorage.setItem(
|
const item: Expirable<T> = {
|
||||||
key,
|
value,
|
||||||
JSON.stringify(value, (_key, value) =>
|
expiry:
|
||||||
value instanceof Set ? [...value] : value,
|
timeToLive !== undefined
|
||||||
),
|
? new Date().getTime() + timeToLive
|
||||||
);
|
: null,
|
||||||
|
};
|
||||||
|
window.localStorage.setItem(key, JSON.stringify(item));
|
||||||
} catch (err: unknown) {
|
} catch (err: unknown) {
|
||||||
console.warn(err);
|
console.warn(err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store an item in sessionStorage with optional TTL
|
||||||
|
export function setSessionStorageItem<T>(
|
||||||
|
key: string,
|
||||||
|
value: T | undefined = undefined,
|
||||||
|
timeToLive?: number,
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const item: Expirable<T> = {
|
||||||
|
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<T>(key: string): T | undefined {
|
||||||
|
try {
|
||||||
|
const itemStr = window.sessionStorage.getItem(key);
|
||||||
|
if (!itemStr) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
const item: Expirable<T> | 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Parse an item from localStorage.
|
// Parse an item from localStorage.
|
||||||
// Returns undefined if the item could not be parsed.
|
// Returns undefined if the item could not be parsed.
|
||||||
function parseStoredItem<T>(data: string | null): T | undefined {
|
function parseStoredItem<T>(data: string | null): T | undefined {
|
||||||
|
Loading…
Reference in New Issue
Block a user