mirror of
https://github.com/strapi/strapi.git
synced 2025-06-27 00:41:25 +00:00
enhancement: admin session presists across tabs (#23188)
* enhancement: admin session presists across tabs * test: clear cookies in login tests * test: remove tests from EE
This commit is contained in:
parent
66fd5e4469
commit
3aeb285c7b
@ -71,7 +71,7 @@ interface AuthProviderProps {
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
TOKEN: 'jwtToken',
|
||||
USER: 'userInfo',
|
||||
STATUS: 'isLoggedIn',
|
||||
};
|
||||
|
||||
const AuthProvider = ({
|
||||
@ -149,7 +149,7 @@ const AuthProvider = ({
|
||||
* This will log a user out of all tabs if they log out in one tab.
|
||||
*/
|
||||
const handleUserStorageChange = (event: StorageEvent) => {
|
||||
if (event.key === STORAGE_KEYS.USER && event.newValue === null) {
|
||||
if (event.key === STORAGE_KEYS.STATUS && event.newValue === null) {
|
||||
clearStateAndLogout();
|
||||
}
|
||||
};
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
|
||||
import { PermissionMap } from './types/permissions';
|
||||
import { getCookieValue, setCookie, deleteCookie } from './utils/cookies';
|
||||
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
|
||||
@ -21,15 +22,14 @@ interface AppState {
|
||||
|
||||
const STORAGE_KEYS = {
|
||||
TOKEN: 'jwtToken',
|
||||
USER: 'userInfo',
|
||||
STATUS: 'isLoggedIn',
|
||||
};
|
||||
|
||||
const THEME_LOCAL_STORAGE_KEY = 'STRAPI_THEME';
|
||||
const LANGUAGE_LOCAL_STORAGE_KEY = 'strapi-admin-language';
|
||||
|
||||
export const getStoredToken = (): string | null => {
|
||||
const token =
|
||||
localStorage.getItem(STORAGE_KEYS.TOKEN) ?? sessionStorage.getItem(STORAGE_KEYS.TOKEN);
|
||||
const token = localStorage.getItem(STORAGE_KEYS.TOKEN) ?? getCookieValue(STORAGE_KEYS.TOKEN);
|
||||
|
||||
if (typeof token === 'string') {
|
||||
return JSON.parse(token);
|
||||
@ -75,19 +75,18 @@ const adminSlice = createSlice({
|
||||
const { token, persist } = action.payload;
|
||||
|
||||
if (!persist) {
|
||||
window.sessionStorage.setItem(STORAGE_KEYS.TOKEN, JSON.stringify(token));
|
||||
setCookie(STORAGE_KEYS.TOKEN, JSON.stringify(token));
|
||||
} else {
|
||||
window.localStorage.setItem(STORAGE_KEYS.TOKEN, JSON.stringify(token));
|
||||
}
|
||||
|
||||
window.localStorage.setItem(STORAGE_KEYS.STATUS, 'true');
|
||||
state.token = token;
|
||||
},
|
||||
logout(state) {
|
||||
state.token = null;
|
||||
deleteCookie(STORAGE_KEYS.TOKEN);
|
||||
window.localStorage.removeItem(STORAGE_KEYS.TOKEN);
|
||||
window.localStorage.removeItem(STORAGE_KEYS.USER);
|
||||
window.sessionStorage.removeItem(STORAGE_KEYS.TOKEN);
|
||||
window.sessionStorage.removeItem(STORAGE_KEYS.USER);
|
||||
window.localStorage.removeItem(STORAGE_KEYS.STATUS);
|
||||
},
|
||||
},
|
||||
});
|
||||
|
43
packages/core/admin/admin/src/utils/cookies.ts
Normal file
43
packages/core/admin/admin/src/utils/cookies.ts
Normal file
@ -0,0 +1,43 @@
|
||||
/**
|
||||
* Retrieves the value of a specified cookie.
|
||||
*
|
||||
* @param name - The name of the cookie to retrieve.
|
||||
* @returns The decoded cookie value if found, otherwise null.
|
||||
*/
|
||||
export const getCookieValue = (name: string): string | null => {
|
||||
let result = null;
|
||||
const cookieArray = document.cookie.split(';');
|
||||
cookieArray.forEach((cookie) => {
|
||||
const [key, value] = cookie.split('=').map((item) => item.trim());
|
||||
if (key === name) {
|
||||
result = decodeURIComponent(value);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
/**
|
||||
* Sets a cookie with the given name, value, and optional expiration time.
|
||||
*
|
||||
* @param name - The name of the cookie.
|
||||
* @param value - The value of the cookie.
|
||||
* @param days - (Optional) Number of days until the cookie expires. If omitted, the cookie is a session cookie.
|
||||
*/
|
||||
export const setCookie = (name: string, value: string, days?: number): void => {
|
||||
let expires = '';
|
||||
if (days) {
|
||||
const date = new Date();
|
||||
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000);
|
||||
expires = `; Expires=${date.toUTCString()}`;
|
||||
}
|
||||
document.cookie = `${name}=${encodeURIComponent(value)}; Path=/${expires}`;
|
||||
};
|
||||
|
||||
/**
|
||||
* Deletes a cookie by setting its expiration date to a past date.
|
||||
*
|
||||
* @param name - The name of the cookie to delete.
|
||||
*/
|
||||
export const deleteCookie = (name: string): void => {
|
||||
document.cookie = `${name}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
|
||||
};
|
@ -1,6 +1,8 @@
|
||||
import pipe from 'lodash/fp/pipe';
|
||||
import qs from 'qs';
|
||||
|
||||
import { getCookieValue } from './cookies';
|
||||
|
||||
import type { errors } from '@strapi/utils';
|
||||
|
||||
export type ApiError =
|
||||
@ -72,7 +74,7 @@ const isFetchError = (error: unknown): error is FetchError => {
|
||||
|
||||
const getToken = () =>
|
||||
JSON.parse(
|
||||
localStorage.getItem(STORAGE_KEYS.TOKEN) ?? sessionStorage.getItem(STORAGE_KEYS.TOKEN) ?? '""'
|
||||
localStorage.getItem(STORAGE_KEYS.TOKEN) ?? getCookieValue(STORAGE_KEYS.TOKEN) ?? '""'
|
||||
);
|
||||
|
||||
type FetchClient = {
|
||||
|
42
packages/core/admin/admin/src/utils/tests/cookies.test.ts
Normal file
42
packages/core/admin/admin/src/utils/tests/cookies.test.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import { getCookieValue, setCookie, deleteCookie } from '../cookies';
|
||||
|
||||
describe('Cookie Utilities', () => {
|
||||
beforeEach(() => {
|
||||
// Clear all cookies before each test
|
||||
document.cookie.split(';').forEach((cookie) => {
|
||||
const [name] = cookie.split('=');
|
||||
document.cookie = `${name}=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;`;
|
||||
});
|
||||
});
|
||||
|
||||
describe('setCookie', () => {
|
||||
it('should set a cookie with the correct value', () => {
|
||||
setCookie('testCookie', 'testValue', 1);
|
||||
expect(document.cookie).toContain('testCookie=testValue');
|
||||
});
|
||||
|
||||
it('should set a session cookie when no expiration is given', () => {
|
||||
setCookie('sessionCookie', 'sessionValue');
|
||||
expect(document.cookie).toContain('sessionCookie=sessionValue');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getCookieValue', () => {
|
||||
it('should return the correct cookie value', () => {
|
||||
document.cookie = 'user=JohnDoe; Path=/;';
|
||||
expect(getCookieValue('user')).toBe('JohnDoe');
|
||||
});
|
||||
|
||||
it('should return null for a non-existing cookie', () => {
|
||||
expect(getCookieValue('nonExistent')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteCookie', () => {
|
||||
it('should delete a cookie', () => {
|
||||
document.cookie = 'deleteMe=value; Path=/;';
|
||||
deleteCookie('deleteMe');
|
||||
expect(getCookieValue('deleteMe')).toBeNull();
|
||||
});
|
||||
});
|
||||
});
|
@ -6,7 +6,7 @@ import { useNavigate, useMatch } from 'react-router-dom';
|
||||
import { Page } from '../../../../admin/src/components/PageHelpers';
|
||||
import { useTypedDispatch } from '../../../../admin/src/core/store/hooks';
|
||||
import { login } from '../../../../admin/src/reducer';
|
||||
import { getCookieValue, deleteCookie } from '../utils/cookies';
|
||||
import { getCookieValue, deleteCookie } from '../../../../admin/src/utils/cookies';
|
||||
|
||||
const AuthResponse = () => {
|
||||
const match = useMatch('/auth/login/:authResponse');
|
||||
|
@ -1,16 +0,0 @@
|
||||
export const getCookieValue = (name: string) => {
|
||||
let result = null;
|
||||
const cookieArray = document.cookie.split(';');
|
||||
cookieArray.forEach((cookie) => {
|
||||
const [key, value] = cookie.split('=').map((item) => item.trim());
|
||||
if (key.trim() === name) {
|
||||
result = decodeURIComponent(value);
|
||||
}
|
||||
});
|
||||
return result;
|
||||
};
|
||||
|
||||
export const deleteCookie = (name: string) => {
|
||||
// Set the cookie to expire in the past
|
||||
document.cookie = name + '=; Path=/; Expires=Thu, 01 Jan 1970 00:00:01 GMT;';
|
||||
};
|
@ -1,35 +0,0 @@
|
||||
import { getCookieValue, deleteCookie } from '../src/utils/cookies';
|
||||
describe('Cookie utils', () => {
|
||||
beforeEach(() => {
|
||||
document.cookie = 'cookie1=value1;';
|
||||
document.cookie = 'cookie2=value2;';
|
||||
document.cookie = 'cookie3=value3;';
|
||||
});
|
||||
|
||||
describe('getCookieValue', () => {
|
||||
it('should return the value of the specified cookie', () => {
|
||||
expect(getCookieValue('cookie1')).toBe('value1');
|
||||
expect(getCookieValue('cookie2')).toBe('value2');
|
||||
expect(getCookieValue('cookie3')).toBe('value3');
|
||||
});
|
||||
|
||||
it('should return null if the specified cookie does not exist', () => {
|
||||
expect(getCookieValue('cookie4')).toBeNull();
|
||||
expect(getCookieValue('cookie5')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteCookie', () => {
|
||||
it('should delete the specified cookie', () => {
|
||||
deleteCookie('cookie2');
|
||||
expect(document.cookie).toBe('cookie1=value1; cookie3=value3');
|
||||
});
|
||||
|
||||
it('should not delete any cookies if the specified cookie does not exist', () => {
|
||||
deleteCookie('cookie4');
|
||||
expect(document.cookie.split(';')).toEqual(
|
||||
expect.arrayContaining('cookie1=value1; cookie2=value2; cookie3=value3'.split(';'))
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
@ -5,8 +5,9 @@ import { ADMIN_EMAIL_ADDRESS, ADMIN_PASSWORD, TITLE_HOME, TITLE_LOGIN } from '..
|
||||
import { login } from '../../utils/login';
|
||||
|
||||
test.describe('Login', () => {
|
||||
test.beforeEach(async ({ page }) => {
|
||||
test.beforeEach(async ({ page, context }) => {
|
||||
await resetDatabaseAndImportDataFromPath('with-admin.tar');
|
||||
await context.clearCookies();
|
||||
await page.goto('/admin');
|
||||
});
|
||||
|
||||
@ -20,7 +21,7 @@ test.describe('Login', () => {
|
||||
await expect(page).toHaveTitle(TITLE_HOME);
|
||||
|
||||
await page.close();
|
||||
|
||||
await context.clearCookies();
|
||||
page = await context.newPage();
|
||||
await page.goto('/admin');
|
||||
await expect(page).toHaveTitle(TITLE_LOGIN);
|
||||
@ -30,7 +31,7 @@ test.describe('Login', () => {
|
||||
await expect(page).toHaveTitle(TITLE_HOME);
|
||||
|
||||
await page.close();
|
||||
|
||||
await context.clearCookies();
|
||||
page = await context.newPage();
|
||||
await page.goto('/admin');
|
||||
await expect(page).toHaveTitle(TITLE_HOME);
|
||||
|
Loading…
x
Reference in New Issue
Block a user