Skip to content

Commit 11e1c51

Browse files
fix: Device logout not redirecting to login page (#38616)
1 parent 54670a6 commit 11e1c51

File tree

20 files changed

+310
-10
lines changed

20 files changed

+310
-10
lines changed
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@rocket.chat/core-services': patch
3+
'@rocket.chat/ddp-client': patch
4+
'@rocket.chat/meteor': patch
5+
---
6+
7+
Fixes device management logout not redirecting to login page.

apps/meteor/client/components/deviceManagement/DeviceManagementTable/DeviceManagementTable.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ const DeviceManagementTable = <T extends DeviceManagementSession | DeviceManagem
5757
return (
5858
<>
5959
{data?.sessions.length === 0 && isSuccess && <GenericNoResults />}
60-
<GenericTable>
60+
<GenericTable aria-label={t('Devices')}>
6161
{data?.sessions && data.sessions.length > 0 && headers && <GenericTableHeader>{headers}</GenericTableHeader>}
6262
<GenericTableBody>
6363
{isPending && <GenericTableLoadingTable headerCells={headers.filter(Boolean).length} />}

apps/meteor/client/views/account/deviceManagement/DeviceManagementAccountTable/DeviceManagementAccountRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ const DeviceManagementAccountRow = ({ _id, deviceName, deviceType = 'browser', d
2424
const handleDeviceLogout = useDeviceLogout(_id, '/v1/sessions/logout.me');
2525

2626
return (
27-
<GenericTableRow key={_id}>
27+
<GenericTableRow key={_id} aria-label={_id}>
2828
<GenericTableCell>
2929
<Box display='flex' alignItems='center'>
3030
<DeviceIcon deviceType={deviceType} />

apps/meteor/client/views/admin/deviceManagement/DeviceManagementAdminTable/DeviceManagementAdminRow.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ const DeviceManagementAdminRow = ({
6666
];
6767

6868
return (
69-
<GenericTableRow key={_id} onKeyDown={handleKeyDown} onClick={handleClick} tabIndex={0} action>
69+
<GenericTableRow key={_id} onKeyDown={handleKeyDown} onClick={handleClick} tabIndex={0} action aria-label={_id}>
7070
<GenericTableCell>
7171
<Box display='flex' alignItems='center'>
7272
<DeviceIcon deviceType={deviceType} />

apps/meteor/client/views/root/hooks/loggedIn/useForceLogout.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { useStream, useSessionDispatch } from '@rocket.chat/ui-contexts';
2+
import { Meteor } from 'meteor/meteor';
23
import { useEffect } from 'react';
34

45
export const useForceLogout = (userId: string) => {
@@ -8,7 +9,13 @@ export const useForceLogout = (userId: string) => {
89
useEffect(() => {
910
setForceLogout(false);
1011

11-
const unsubscribe = getNotifyUserStream(`${userId}/force_logout`, () => {
12+
const unsubscribe = getNotifyUserStream(`${userId}/force_logout`, (sessionId) => {
13+
const currentSessionId = Meteor.connection._lastSessionId;
14+
15+
if (sessionId === currentSessionId) {
16+
window.location.reload();
17+
}
18+
1219
setForceLogout(true);
1320
});
1421

apps/meteor/definition/externals/meteor/meteor.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,7 @@ declare module 'meteor/meteor' {
115115
},
116116
]
117117
): SubscriptionHandle;
118+
_lastSessionId: string;
118119
}
119120

120121
const connection: IMeteorConnection;

apps/meteor/ee/server/api/sessions.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,8 @@ API.v1.addRoute(
136136
return API.v1.notFound('Session not found');
137137
}
138138

139+
await api.broadcast('user.forceLogout', sessionObj.userId, sessionId);
140+
139141
await Promise.all([
140142
Users.unsetOneLoginToken(this.userId, sessionObj.loginToken),
141143
Sessions.logoutByloginTokenAndUserId({ loginToken: sessionObj.loginToken, userId: this.userId }),
@@ -239,7 +241,7 @@ API.v1.addRoute(
239241
return API.v1.notFound('Session not found');
240242
}
241243

242-
await api.broadcast('user.forceLogout', sessionObj.userId);
244+
await api.broadcast('user.forceLogout', sessionObj.userId, sessionId);
243245

244246
await Promise.all([
245247
Users.unsetOneLoginToken(sessionObj.userId, sessionObj.loginToken),

apps/meteor/server/modules/listeners/listeners.module.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,8 +46,8 @@ export class ListenersModule {
4646
});
4747
});
4848

49-
service.onEvent('user.forceLogout', (uid) => {
50-
notifications.notifyUserInThisInstance(uid, 'force_logout');
49+
service.onEvent('user.forceLogout', (uid: string, sessionId?: string) => {
50+
notifications.notifyUserInThisInstance(uid, 'force_logout', sessionId);
5151
});
5252

5353
service.onEvent('notify.ephemeralMessage', (uid, rid, message) => {
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import type { Page } from 'playwright-core';
2+
3+
import { IS_EE } from './config/constants';
4+
import { createAuxContext } from './fixtures/createAuxContext';
5+
import injectInitialData from './fixtures/inject-initial-data';
6+
import { Users } from './fixtures/userStates';
7+
import { Registration } from './page-objects';
8+
import { AccountManageDevices } from './page-objects/account.manage-devices';
9+
import { test, expect } from './utils/test';
10+
11+
test.describe('Account Manage Devices Page', () => {
12+
test.skip(!IS_EE);
13+
test.use({ storageState: Users.user1.state });
14+
15+
let page: Page;
16+
let accountDevices: AccountManageDevices;
17+
let loginPage: Registration;
18+
19+
test.beforeEach(async ({ browser }) => {
20+
({ page } = await createAuxContext(browser, Users.user1));
21+
accountDevices = new AccountManageDevices(page);
22+
loginPage = new Registration(page);
23+
await page.goto('/account/manage-devices');
24+
});
25+
26+
test.afterEach(async () => {
27+
await page.close();
28+
await injectInitialData();
29+
});
30+
31+
test('should show Manage Devices page', async () => {
32+
await expect(accountDevices.devicesPageContent).toBeVisible();
33+
});
34+
35+
test('should logout current device and redirect to login page', async () => {
36+
await accountDevices.table.orderByLastLogin();
37+
38+
const deviceId = await accountDevices.getNthDeviceId(1);
39+
await accountDevices.logoutDeviceById(deviceId);
40+
await expect(loginPage.loginForm).toBeVisible();
41+
});
42+
43+
test('should logout other device successfully', async ({ browser }) => {
44+
const context2 = await browser.newContext({ storageState: { cookies: [], origins: [] } });
45+
const page2 = await context2.newPage();
46+
const loginPage2 = new Registration(page2);
47+
const accountDevices2 = new AccountManageDevices(page2);
48+
49+
await test.step('should login same user in another session', async () => {
50+
await page2.goto('/account/manage-devices');
51+
await loginPage2.username.type('user1');
52+
await loginPage2.inputPassword.type('password');
53+
await loginPage2.btnLogin.click();
54+
55+
await expect(accountDevices2.devicesPageContent).toBeVisible();
56+
});
57+
58+
await test.step('should logout device 1 from session 2', async () => {
59+
await accountDevices2.table.orderByLastLogin();
60+
const device1Id = await accountDevices2.getNthDeviceId(2);
61+
await accountDevices2.logoutDeviceById(device1Id);
62+
await loginPage.loginForm.waitFor();
63+
await expect(loginPage.loginForm).toBeVisible();
64+
await expect(accountDevices2.table.getDeviceRowById(device1Id)).not.toBeVisible();
65+
});
66+
67+
await context2.close();
68+
});
69+
});
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import type { Page } from 'playwright-core';
2+
3+
import { IS_EE } from './config/constants';
4+
import { createAuxContext } from './fixtures/createAuxContext';
5+
import injectInitialData from './fixtures/inject-initial-data';
6+
import { Users } from './fixtures/userStates';
7+
import { Registration } from './page-objects';
8+
import { AdminDeviceManagement } from './page-objects/admin-device-management';
9+
import { test, expect } from './utils/test';
10+
11+
test.describe('Admin Device Management Page', () => {
12+
test.skip(!IS_EE);
13+
test.use({ storageState: Users.admin.state });
14+
15+
let page: Page;
16+
let adminDeviceManagement: AdminDeviceManagement;
17+
let loginPage: Registration;
18+
19+
test.beforeEach(async ({ browser }) => {
20+
({ page } = await createAuxContext(browser, Users.admin));
21+
adminDeviceManagement = new AdminDeviceManagement(page);
22+
loginPage = new Registration(page);
23+
await page.goto('/admin/device-management');
24+
});
25+
26+
test.afterEach(async () => {
27+
await page.close();
28+
await injectInitialData();
29+
});
30+
31+
test('should show Device management page', async () => {
32+
await expect(adminDeviceManagement.adminPageContent).toBeVisible();
33+
});
34+
35+
test('should logout current device and redirect to login page', async () => {
36+
const deviceId = await adminDeviceManagement.getUsersDeviceId('rocketchat.internal.admin.test');
37+
await adminDeviceManagement.logoutDeviceById(deviceId);
38+
await expect(loginPage.loginForm).toBeVisible();
39+
});
40+
41+
test('should logout current device from device info tab and redirect to login page', async () => {
42+
const deviceId = await adminDeviceManagement.getUsersDeviceId('rocketchat.internal.admin.test');
43+
await adminDeviceManagement.searchUserDevice('rocketchat.internal.admin.test');
44+
await adminDeviceManagement.table.getDeviceRowById(deviceId).click();
45+
46+
await expect(adminDeviceManagement.deviceInfo.getDeviceInfoId(deviceId)).toBeVisible();
47+
await adminDeviceManagement.deviceInfo.btnLogoutDevice.click();
48+
await adminDeviceManagement.logoutModal.confirmLogout();
49+
await expect(loginPage.loginForm).toBeVisible();
50+
});
51+
52+
test('should logout other device successfully', async ({ browser }) => {
53+
const user2Page = await browser.newPage({ storageState: Users.user2.state });
54+
const loginPage2 = new Registration(user2Page);
55+
await user2Page.goto('/');
56+
await expect(user2Page.getByRole('main')).toBeVisible();
57+
58+
const user2DeviceId = await adminDeviceManagement.getUsersDeviceId('user2');
59+
60+
await test.step('should logout user2 and redirect to login page', async () => {
61+
await adminDeviceManagement.logoutDeviceById(user2DeviceId);
62+
await loginPage2.loginForm.waitFor();
63+
await expect(loginPage2.loginForm).toBeVisible();
64+
});
65+
66+
await test.step('should no longer show user2 device in admin device management page', async () => {
67+
await adminDeviceManagement.searchUserDevice('user2');
68+
await expect(adminDeviceManagement.table.getDeviceRowById(user2DeviceId)).not.toBeVisible();
69+
});
70+
71+
await user2Page.close();
72+
});
73+
});

0 commit comments

Comments
 (0)