Skip to content
Open
17 changes: 1 addition & 16 deletions apps/meteor/app/ui-master/server/scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,22 +33,7 @@ window.addEventListener('Custom_Script_On_Logout', function() {
${settings.get('Custom_Script_On_Logout')}
})

${
settings.get('Accounts_ForgetUserSessionOnWindowClose')
? `
window.addEventListener('load', function() {
if (window.localStorage) {
Object.keys(window.localStorage).forEach(function(key) {
window.sessionStorage.setItem(key, window.localStorage.getItem(key));
});
window.localStorage.clear();
Meteor._localStorage = window.sessionStorage;
Accounts.config({ clientStorage: 'session' });
}
});
`
: ''
}`;
${settings.get('Accounts_ForgetUserSessionOnWindowClose') ? `window.Accounts_ForgetUserSessionOnWindowClose = true;` : ''}`;

settings.watchMultiple(
['Custom_Script_Logged_Out', 'Custom_Script_Logged_In', 'Custom_Script_On_Logout', 'Accounts_ForgetUserSessionOnWindowClose'],
Expand Down
10 changes: 9 additions & 1 deletion apps/meteor/client/lib/sdk/ddpSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import EJSON from 'ejson';
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';

import { createMeteorBackedSdk } from './meteorBackedSdk';
import { createMeteorBackedSdk, createMeteorBackedStorage } from './meteorBackedSdk';
import { isSdkTransportEnabled } from './sdkTransportEnabled';
import { getRootUrl } from '../meteorRuntimeConfig';
import { STORAGE_KEYS, getStoredItem, removeStoredItem } from './storage';
Expand Down Expand Up @@ -53,6 +53,14 @@ export const getDdpSdk = (): DDPSDK => {
if (!instance) {
if (sdkTransportEnabled) {
instance = DDPSDK.create(computeDdpUrl());
// TODO: This is a temporary fix to ensure Accounts/Meteor and Update Session On Window Close work together.
try {
instance.storage = createMeteorBackedStorage();
} catch (error) {
// DDPSDK.create may return a sealed/frozen instance under strict mode; failing
// to attach the storage hook must not abort SDK bootstrap.
console.warn('[ddpSdk] failed to attach storage hook to SDK instance', error);
}
applyEjsonEncoding(instance);
void startConnect(instance);
} else {
Expand Down
52 changes: 52 additions & 0 deletions apps/meteor/client/lib/sdk/meteorBackedSdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';

import { parseDDP } from './ddpProtocol';
import { setStorageBackend } from './storage';

/**
* Meteor-backed pass-through DDPSDK used when the SDK transport is OFF.
Expand Down Expand Up @@ -271,12 +272,63 @@ const createMeteorBackedAccount = () => {
} as unknown as DDPSDK['account'];
};

export const createMeteorBackedStorage = () => {
let appliedBackend: 'local' | 'session' | undefined;

return {
changeStorageBackend: () => {
const backend: 'local' | 'session' = window[FORGET_SESSION_SETTING_ID] ? 'session' : 'local';

if (appliedBackend === backend) {
return;
}

if (!setStorageBackend(backend)) {
return;
}

(Meteor._localStorage as unknown as Storage) = backend === 'session' ? window.sessionStorage : window.localStorage;

try {
Accounts.config({ clientStorage: backend });
} catch (error) {
// Accounts.config throws when invoked twice with a conflicting value. Meteor's
// own boot may have already set clientStorage; the _localStorage reassign above
// is what actually switches the backend Meteor reads from, so swallow.
console.warn('[storage] Accounts.config(clientStorage) refused at runtime', error);
}

appliedBackend = backend;
},
};
};

export const FORGET_SESSION_SETTING_ID = 'Accounts_ForgetUserSessionOnWindowClose';

declare global {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface Window {
[FORGET_SESSION_SETTING_ID]?: boolean;
}
}

declare module '@rocket.chat/ddp-client' {
// eslint-disable-next-line @typescript-eslint/naming-convention
interface DDPSDK {
storage: {
changeStorageBackend: () => void;
};
}
}

export const createMeteorBackedSdk = (): DDPSDK => {
const connection = createMeteorBackedConnection();
const client = createMeteorBackedClient();
const account = createMeteorBackedAccount();
const storage = createMeteorBackedStorage();

return {
storage,
connection,
client,
account,
Expand Down
81 changes: 77 additions & 4 deletions apps/meteor/client/lib/sdk/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,83 @@ export const STORAGE_KEYS = {

export type StorageKey = (typeof STORAGE_KEYS)[keyof typeof STORAGE_KEYS];

const getStorage = (): Storage | undefined => (typeof window !== 'undefined' ? window.localStorage : undefined);
type StorageBackend = 'local' | 'session';

export const getStoredItem = (key: string): string | null => getStorage()?.getItem(key) ?? null;
const getStorageForBackend = (backend: StorageBackend): Storage | undefined => {
if (typeof window === 'undefined') {
return undefined;
}

export const setStoredItem = (key: string, value: string): void => getStorage()?.setItem(key, value);
try {
return backend === 'session' ? window.sessionStorage : window.localStorage;
} catch {
return undefined;
}
};
Comment on lines +19 to +29
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's what I was expecting from this fix.


export const removeStoredItem = (key: string): void => getStorage()?.removeItem(key);
const getStorage = (): Storage | undefined => {
return getStorageForBackend(storageBackend);
};

export const getStoredItem = (key: StorageKey): string | null => getStorage()?.getItem(key) ?? null;

export const setStoredItem = (key: StorageKey, value: string): void => getStorage()?.setItem(key, value);

export const removeStoredItem = (key: StorageKey): void => getStorage()?.removeItem(key);

let storageBackend: StorageBackend = 'local';

export const setStorageBackend = (backend: StorageBackend): boolean => {
if (backend === storageBackend) {
return true;
}

if (!moveLoginKeys(backend)) {
return false;
}

storageBackend = backend;
return true;
};
Comment on lines +43 to +54
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is all this is to fix the regression or some improvement you've seen? I think it should go to 8.6.0.


const moveLoginKeys = (backend: StorageBackend): boolean => {
const keys = [
STORAGE_KEYS.USER_ID,
STORAGE_KEYS.LOGIN_TOKEN,
STORAGE_KEYS.LOGIN_TOKEN_EXPIRES,
STORAGE_KEYS.E2EE_PUBLIC_KEY,
STORAGE_KEYS.E2EE_PRIVATE_KEY,
STORAGE_KEYS.E2EE_RANDOM_PASSWORD,
];

const sourceStorage = getStorageForBackend(backend === 'session' ? 'local' : 'session');
const targetStorage = getStorageForBackend(backend);

if (!sourceStorage || !targetStorage) {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
console.warn('Unable to switch storage backend because source or target storage is unavailable');
return false;
}

for (const key of keys) {
let value: string | null;
try {
value = sourceStorage.getItem(key);
} catch {
continue;
}

if (value === null) {
continue;
}

try {
targetStorage.setItem(key, value);
sourceStorage.removeItem(key);
} catch {
continue;
}
}
sourceStorage.clear();
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.

return true;
};
27 changes: 27 additions & 0 deletions apps/meteor/client/meteor/startup/accounts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ import { sdk } from '../../../app/utils/client/lib/SDKClient';
import { t } from '../../../app/utils/lib/i18n';
import { PublicSettingsCachedStore, SubscriptionsCachedStore } from '../../cachedStores';
import { getDdpSdk } from '../../lib/sdk/ddpSdk';
import { FORGET_SESSION_SETTING_ID } from '../../lib/sdk/meteorBackedSdk';
import { settings } from '../../lib/settings';
import { dispatchToastMessage } from '../../lib/toast';
import { userIdStore } from '../../lib/user';
import { useUserDataSyncReady } from '../../lib/userData';
Expand Down Expand Up @@ -46,6 +48,31 @@ const whenMainReady = (): Promise<void> => {
});
};

let configuredStorageBackend: 'local' | 'session' = 'local';

const applyForgetSessionOnWindowClose = (): void => {
const forgetSession = Boolean(settings.peek<boolean>(FORGET_SESSION_SETTING_ID) ?? window[FORGET_SESSION_SETTING_ID]);

const storageBackend = forgetSession ? 'session' : 'local';

if (configuredStorageBackend === storageBackend) {
return;
}

window[FORGET_SESSION_SETTING_ID] = forgetSession;
try {
getDdpSdk().storage?.changeStorageBackend();
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
} catch (error) {
console.warn('[accounts] changeStorageBackend failed', error);
return;
}

configuredStorageBackend = storageBackend;
};

applyForgetSessionOnWindowClose();
settings.observe(FORGET_SESSION_SETTING_ID, applyForgetSessionOnWindowClose);

getDdpSdk().account.onEmailVerificationLink(async (token: string) => {
try {
await sdk.call('verifyEmail', token);
Expand Down
3 changes: 2 additions & 1 deletion apps/meteor/client/views/room/E2EESetup/RoomE2EESetup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { useTranslation } from 'react-i18next';

import RoomE2EENotAllowed from './RoomE2EENotAllowed';
import { e2e } from '../../../lib/e2ee';
import { getStoredItem, STORAGE_KEYS } from '../../../lib/sdk/storage';
import RoomBody from '../body/RoomBody';
import { useRoom } from '../contexts/RoomContext';
import { useE2EERoomState } from '../hooks/useE2EERoomState';
Expand All @@ -15,7 +16,7 @@ const RoomE2EESetup = () => {
const e2eRoomState = useE2EERoomState(room._id);

const { t } = useTranslation();
const randomPassword = localStorage.getItem('e2e.randomPassword');
const randomPassword = getStoredItem(STORAGE_KEYS.E2EE_RANDOM_PASSWORD);

const onSavePassword = useCallback(() => {
if (!randomPassword) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { DEFAULT_USER_CREDENTIALS } from './config/constants';
import { Login } from './page-objects';
import { ADMIN_CREDENTIALS, DEFAULT_USER_CREDENTIALS } from './config/constants';
import { AccountSecurity, HomeChannel, Login } from './page-objects';
import { test, expect } from './utils/test';

test.describe.serial('Forget session on window close setting', () => {
Expand Down Expand Up @@ -28,8 +28,7 @@ test.describe.serial('Forget session on window close setting', () => {
});
});

// TODO: Fix this test
test.describe.skip('Setting on', async () => {
test.describe('Setting on', async () => {
test.beforeAll(async ({ api }) => {
await api.post('/settings/Accounts_ForgetUserSessionOnWindowClose', { value: true });
});
Expand All @@ -38,15 +37,64 @@ test.describe.serial('Forget session on window close setting', () => {
await api.post('/settings/Accounts_ForgetUserSessionOnWindowClose', { value: false });
});

test('Login using credentials and reload to get logged out', async ({ page, context }) => {
test('Login using credentials and reload to stay logged in', async ({ page }) => {
await poLogin.login('user1', DEFAULT_USER_CREDENTIALS.password);

await expect(page.locator('role=heading[name="Welcome to Rocket.Chat"]')).toBeVisible();

await page.reload();

await expect(page.locator('role=heading[name="Welcome to Rocket.Chat"]')).toBeVisible();
});

test('Login using credentials in a new tab after first tab logged in', async ({ page, context }) => {
await poLogin.login('user1', DEFAULT_USER_CREDENTIALS.password);

await expect(page.locator('role=heading[name="Welcome to Rocket.Chat"]')).toBeVisible();

const newPage = await context.newPage();
await newPage.goto('/home');

await expect(newPage.locator('role=button[name="Login"]')).toBeVisible();
const newPoLogin = new Login(newPage);
await newPoLogin.login('user1', DEFAULT_USER_CREDENTIALS.password);

await expect(newPage.locator('role=heading[name="Welcome to Rocket.Chat"]')).toBeVisible();
});

test.describe('E2EE save password flow', () => {
test.beforeAll(async ({ api }) => {
await api.post('/settings/E2E_Enable', { value: true });
await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: false });
});

test.afterAll(async ({ api }) => {
await api.post('/settings/E2E_Allow_Unencrypted_Messages', { value: true });
await api.post('/settings/E2E_Enable', { value: false });
});

test('E2EE save password button opens modal in SAVE_PASSWORD state', async ({ page }) => {
const poHomeChannel = new HomeChannel(page);
const poAccountSecurity = new AccountSecurity(page);

await poLogin.login(ADMIN_CREDENTIALS.username, ADMIN_CREDENTIALS.password);
await expect(page.locator('role=heading[name="Welcome to Rocket.Chat"]')).toBeVisible();

await poAccountSecurity.goto();
await poAccountSecurity.resetE2EEPassword();

await page.locator('role=button[name="Login"]').waitFor();

await poLogin.login(ADMIN_CREDENTIALS.username, ADMIN_CREDENTIALS.password);
await expect(page.locator('role=heading[name="Welcome to Rocket.Chat"]')).toBeVisible();
await expect(poHomeChannel.bannerSaveEncryptionPassword).toBeVisible();
await poHomeChannel.bannerSaveEncryptionPassword.click();

const randomPassword = await page.evaluate(() => window.sessionStorage.getItem('e2e.randomPassword') ?? '');
expect(randomPassword).toBeTruthy();

await expect(poHomeChannel.dialogSaveE2EEPassword).toBeVisible();
await expect(poHomeChannel.dialogSaveE2EEPassword).toContainText(randomPassword);
});
});
});
});
Loading