From 3d47dff182086e627d1b18559766905e5056d339 Mon Sep 17 00:00:00 2001 From: Rohit <40559587+Rohit3523@users.noreply.github.com> Date: Sat, 25 Jul 2026 20:58:48 +0530 Subject: [PATCH 01/10] fix: mark session as logged out on REST /v1/logout --- .changeset/rest-logout-session-cleanup.md | 5 +++++ apps/meteor/server/api/ApiClass.ts | 5 ++--- 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 .changeset/rest-logout-session-cleanup.md diff --git a/.changeset/rest-logout-session-cleanup.md b/.changeset/rest-logout-session-cleanup.md new file mode 100644 index 0000000000000..f2f4338311629 --- /dev/null +++ b/.changeset/rest-logout-session-cleanup.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Marks the user session as logged out in the Sessions collection when logging out via `POST /v1/logout`. Previously the session cleanup relied on an indirect chain through `watch.users` → `Accounts.onLogout` that could be broken by a race condition, leaving orphaned sessions visible in Device Manager. diff --git a/apps/meteor/server/api/ApiClass.ts b/apps/meteor/server/api/ApiClass.ts index 71dfc86bc0020..abba4ac602d5a 100644 --- a/apps/meteor/server/api/ApiClass.ts +++ b/apps/meteor/server/api/ApiClass.ts @@ -2,7 +2,7 @@ import type { IMethodConnection, IUser } from '@rocket.chat/core-typings'; import type { Route, Router } from '@rocket.chat/http-router'; import { License } from '@rocket.chat/license'; import { Logger } from '@rocket.chat/logger'; -import { Users } from '@rocket.chat/models'; +import { Sessions, Users } from '@rocket.chat/models'; import { Random } from '@rocket.chat/random'; import type { JoinPathPattern, Method } from '@rocket.chat/rest-typings'; import { ajv } from '@rocket.chat/rest-typings'; @@ -1135,8 +1135,7 @@ export class APIClass { const userTokens = await Users.findOneById(this.userId, { projection: { [tokenPath]: 1 } }); if (!userTokens) { From b6e2eeb19c644c04f6dc1d51d3bea44846acc647 Mon Sep 17 00:00:00 2001 From: Rohit <40559587+Rohit3523@users.noreply.github.com> Date: Sat, 25 Jul 2026 21:23:16 +0530 Subject: [PATCH 02/10] using logoutBySessionIdAndUserId --- apps/meteor/server/api/ApiClass.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/server/api/ApiClass.ts b/apps/meteor/server/api/ApiClass.ts index abba4ac602d5a..783a9439a905a 100644 --- a/apps/meteor/server/api/ApiClass.ts +++ b/apps/meteor/server/api/ApiClass.ts @@ -1135,7 +1135,7 @@ export class APIClass { const userTokens = await Users.findOneById(this.userId, { projection: { [tokenPath]: 1 } }); if (!userTokens) { From f3f8ea576f8f1ac7ffc568587c8596777c2850e9 Mon Sep 17 00:00:00 2001 From: Rohit <40559587+Rohit3523@users.noreply.github.com> Date: Mon, 27 Jul 2026 20:52:36 +0530 Subject: [PATCH 03/10] Added test --- apps/meteor/tests/end-to-end/api/users.ts | 73 +++++++++++++++++++++++ 1 file changed, 73 insertions(+) diff --git a/apps/meteor/tests/end-to-end/api/users.ts b/apps/meteor/tests/end-to-end/api/users.ts index 156330b68626f..3f93e9ff35518 100644 --- a/apps/meteor/tests/end-to-end/api/users.ts +++ b/apps/meteor/tests/end-to-end/api/users.ts @@ -5476,6 +5476,79 @@ describe('[Users]', () => { }); }); + describe('[/api/v1/logout]', () => { + let user: TestUser; + + before(async () => { + user = await createUser(); + }); + + after(() => deleteUser(user)); + + it('should logout the current user and invalidate the session', async () => { + const userCredentials = await login(user.username, password); + + await request + .post(api('logout')) + .set(userCredentials) + .expect('Content-Type', 'application/json') + .expect(200); + + const meRes = await request.get(api('me')).set(userCredentials); + expect(meRes.statusCode).to.equal(401); + }); + + it('should set logoutAt on the session document', async () => { + const userCredentials = await login(user.username, password); + const authToken = userCredentials['X-Auth-Token']; + const hashedToken = crypto.createHash('sha256').update(authToken as string).digest('base64'); + + const client = new MongoClient(URL_MONGODB); + try { + await client.connect(); + const db = client.db(); + + const { insertedId } = await db.collection('rocketchat_sessions').insertOne({ + userId: user._id, + loginToken: hashedToken, + type: 'session', + sessionId: Random.id(), + instanceId: 'test', + createdAt: new Date(), + loginAt: new Date(), + lastActivityAt: new Date(), + ip: '127.0.0.1', + host: 'localhost', + year: new Date().getFullYear(), + month: new Date().getMonth() + 1, + day: new Date().getDate(), + searchTerm: '', + roles: [], + }); + + await request.post(api('logout')).set(userCredentials).expect(200); + + const session = await db.collection('rocketchat_sessions').findOne({ _id: insertedId }); + console.log(session) + expect(session).to.not.be.null; + expect(session?.logoutAt).to.not.be.null; + expect(session?.logoutBy).to.equal(user._id); + } finally { + await client.close(); + } + }); + + it('should return 401 when not authenticated', async () => { + await request + .post(api('logout')) + .expect('Content-Type', 'application/json') + .expect(401) + .expect((res: Response) => { + expect(res.body).to.have.property('status', 'error'); + }); + }); + }); + describe('[/users.autocomplete]', () => { after(() => updatePermission('view-outside-room', ['admin', 'owner', 'moderator', 'user'])); From 7deef47e899b804073117e5219026f277153ee42 Mon Sep 17 00:00:00 2001 From: Rohit <40559587+Rohit3523@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:01:14 +0530 Subject: [PATCH 04/10] making coderabbit happy --- apps/meteor/tests/end-to-end/api/users.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/end-to-end/api/users.ts b/apps/meteor/tests/end-to-end/api/users.ts index 3f93e9ff35518..2c4c2243a6bdd 100644 --- a/apps/meteor/tests/end-to-end/api/users.ts +++ b/apps/meteor/tests/end-to-end/api/users.ts @@ -5531,7 +5531,7 @@ describe('[Users]', () => { const session = await db.collection('rocketchat_sessions').findOne({ _id: insertedId }); console.log(session) expect(session).to.not.be.null; - expect(session?.logoutAt).to.not.be.null; + expect(session?.logoutAt).to.be.instanceOf(Date); expect(session?.logoutBy).to.equal(user._id); } finally { await client.close(); From 93bc9f2a95e363d3e5a350bff605835f84ea5f8c Mon Sep 17 00:00:00 2001 From: Rohit <40559587+Rohit3523@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:01:24 +0530 Subject: [PATCH 05/10] remove console --- apps/meteor/tests/end-to-end/api/users.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/apps/meteor/tests/end-to-end/api/users.ts b/apps/meteor/tests/end-to-end/api/users.ts index 2c4c2243a6bdd..879a8c7d1ee6e 100644 --- a/apps/meteor/tests/end-to-end/api/users.ts +++ b/apps/meteor/tests/end-to-end/api/users.ts @@ -5529,7 +5529,6 @@ describe('[Users]', () => { await request.post(api('logout')).set(userCredentials).expect(200); const session = await db.collection('rocketchat_sessions').findOne({ _id: insertedId }); - console.log(session) expect(session).to.not.be.null; expect(session?.logoutAt).to.be.instanceOf(Date); expect(session?.logoutBy).to.equal(user._id); From ef9062c387ec023952f569f16a062fcee197ac3f Mon Sep 17 00:00:00 2001 From: Rohit <40559587+Rohit3523@users.noreply.github.com> Date: Mon, 27 Jul 2026 21:21:10 +0530 Subject: [PATCH 06/10] lint fix --- apps/meteor/tests/end-to-end/api/users.ts | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/users.ts b/apps/meteor/tests/end-to-end/api/users.ts index 879a8c7d1ee6e..637104648735b 100644 --- a/apps/meteor/tests/end-to-end/api/users.ts +++ b/apps/meteor/tests/end-to-end/api/users.ts @@ -5488,11 +5488,7 @@ describe('[Users]', () => { it('should logout the current user and invalidate the session', async () => { const userCredentials = await login(user.username, password); - await request - .post(api('logout')) - .set(userCredentials) - .expect('Content-Type', 'application/json') - .expect(200); + await request.post(api('logout')).set(userCredentials).expect('Content-Type', 'application/json').expect(200); const meRes = await request.get(api('me')).set(userCredentials); expect(meRes.statusCode).to.equal(401); @@ -5501,7 +5497,10 @@ describe('[Users]', () => { it('should set logoutAt on the session document', async () => { const userCredentials = await login(user.username, password); const authToken = userCredentials['X-Auth-Token']; - const hashedToken = crypto.createHash('sha256').update(authToken as string).digest('base64'); + const hashedToken = crypto + .createHash('sha256') + .update(authToken as string) + .digest('base64'); const client = new MongoClient(URL_MONGODB); try { From 08fbfdf4522569fae7814985ef306a9b434452aa Mon Sep 17 00:00:00 2001 From: Rohit <40559587+Rohit3523@users.noreply.github.com> Date: Tue, 28 Jul 2026 02:18:27 +0530 Subject: [PATCH 07/10] remove db thing from test --- apps/meteor/tests/end-to-end/api/users.ts | 66 +++++++++-------------- 1 file changed, 26 insertions(+), 40 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/users.ts b/apps/meteor/tests/end-to-end/api/users.ts index 637104648735b..fda1fe990dd32 100644 --- a/apps/meteor/tests/end-to-end/api/users.ts +++ b/apps/meteor/tests/end-to-end/api/users.ts @@ -5494,46 +5494,32 @@ describe('[Users]', () => { expect(meRes.statusCode).to.equal(401); }); - it('should set logoutAt on the session document', async () => { - const userCredentials = await login(user.username, password); - const authToken = userCredentials['X-Auth-Token']; - const hashedToken = crypto - .createHash('sha256') - .update(authToken as string) - .digest('base64'); - - const client = new MongoClient(URL_MONGODB); - try { - await client.connect(); - const db = client.db(); - - const { insertedId } = await db.collection('rocketchat_sessions').insertOne({ - userId: user._id, - loginToken: hashedToken, - type: 'session', - sessionId: Random.id(), - instanceId: 'test', - createdAt: new Date(), - loginAt: new Date(), - lastActivityAt: new Date(), - ip: '127.0.0.1', - host: 'localhost', - year: new Date().getFullYear(), - month: new Date().getMonth() + 1, - day: new Date().getDate(), - searchTerm: '', - roles: [], - }); - - await request.post(api('logout')).set(userCredentials).expect(200); - - const session = await db.collection('rocketchat_sessions').findOne({ _id: insertedId }); - expect(session).to.not.be.null; - expect(session?.logoutAt).to.be.instanceOf(Date); - expect(session?.logoutBy).to.equal(user._id); - } finally { - await client.close(); - } + it('should remove the session from the list after logout', async () => { + const credentials = await login(user.username, password); + const authToken = credentials['X-Auth-Token']; + + await request.post(api('login')).send({ resume: authToken }).expect(200); + + const sessionsBefore = await request + .get(api('sessions/list')) + .set(credentials) + .query({ count: 25, offset: 0, sort: '{"loginAt":1}' }) + .expect(200); + expect(sessionsBefore.body.sessions).to.have.lengthOf(1); + + await request.post(api('logout')).set(credentials).expect(200); + + const newCredentials = await login(user.username, password); + const newAuthToken = newCredentials['X-Auth-Token']; + + await request.post(api('login')).send({ resume: newAuthToken }).expect(200); + + const sessionsAfter = await request + .get(api('sessions/list')) + .set(newCredentials) + .query({ count: 25, offset: 0, sort: '{"loginAt":1}' }) + .expect(200); + expect(sessionsAfter.body.sessions).to.have.lengthOf(1); }); it('should return 401 when not authenticated', async () => { From 773a2b926ac758aa856bfb9982b2f27e810e7cfa Mon Sep 17 00:00:00 2001 From: Rohit <40559587+Rohit3523@users.noreply.github.com> Date: Tue, 28 Jul 2026 03:23:46 +0530 Subject: [PATCH 08/10] removed query... --- apps/meteor/tests/end-to-end/api/users.ts | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/apps/meteor/tests/end-to-end/api/users.ts b/apps/meteor/tests/end-to-end/api/users.ts index fda1fe990dd32..9911a6eb30476 100644 --- a/apps/meteor/tests/end-to-end/api/users.ts +++ b/apps/meteor/tests/end-to-end/api/users.ts @@ -5500,11 +5500,7 @@ describe('[Users]', () => { await request.post(api('login')).send({ resume: authToken }).expect(200); - const sessionsBefore = await request - .get(api('sessions/list')) - .set(credentials) - .query({ count: 25, offset: 0, sort: '{"loginAt":1}' }) - .expect(200); + const sessionsBefore = await request.get(api('sessions/list')).set(credentials).expect(200); expect(sessionsBefore.body.sessions).to.have.lengthOf(1); await request.post(api('logout')).set(credentials).expect(200); @@ -5514,11 +5510,7 @@ describe('[Users]', () => { await request.post(api('login')).send({ resume: newAuthToken }).expect(200); - const sessionsAfter = await request - .get(api('sessions/list')) - .set(newCredentials) - .query({ count: 25, offset: 0, sort: '{"loginAt":1}' }) - .expect(200); + const sessionsAfter = await request.get(api('sessions/list')).set(newCredentials).expect(200); expect(sessionsAfter.body.sessions).to.have.lengthOf(1); }); From f0ee6c63dbc90c10457b70372ba79940cd287433 Mon Sep 17 00:00:00 2001 From: Rohit <40559587+Rohit3523@users.noreply.github.com> Date: Tue, 28 Jul 2026 04:10:07 +0530 Subject: [PATCH 09/10] added condition for test... --- apps/meteor/tests/end-to-end/api/users.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/meteor/tests/end-to-end/api/users.ts b/apps/meteor/tests/end-to-end/api/users.ts index 9911a6eb30476..04e7f39aa63d7 100644 --- a/apps/meteor/tests/end-to-end/api/users.ts +++ b/apps/meteor/tests/end-to-end/api/users.ts @@ -5494,7 +5494,7 @@ describe('[Users]', () => { expect(meRes.statusCode).to.equal(401); }); - it('should remove the session from the list after logout', async () => { + (IS_EE ? it : it.skip)('should remove the session from the list after logout', async () => { const credentials = await login(user.username, password); const authToken = credentials['X-Auth-Token']; From 923dd74719ee1abd41843a7506a8d0ddd1ff44b5 Mon Sep 17 00:00:00 2001 From: Rohit <40559587+Rohit3523@users.noreply.github.com> Date: Mon, 3 Aug 2026 21:58:41 +0530 Subject: [PATCH 10/10] Added session id check --- apps/meteor/tests/end-to-end/api/users.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/apps/meteor/tests/end-to-end/api/users.ts b/apps/meteor/tests/end-to-end/api/users.ts index 04e7f39aa63d7..74589e3aa97d4 100644 --- a/apps/meteor/tests/end-to-end/api/users.ts +++ b/apps/meteor/tests/end-to-end/api/users.ts @@ -5502,6 +5502,7 @@ describe('[Users]', () => { const sessionsBefore = await request.get(api('sessions/list')).set(credentials).expect(200); expect(sessionsBefore.body.sessions).to.have.lengthOf(1); + const sessionIdBefore = sessionsBefore.body.sessions[0]._id; await request.post(api('logout')).set(credentials).expect(200); @@ -5512,6 +5513,7 @@ describe('[Users]', () => { const sessionsAfter = await request.get(api('sessions/list')).set(newCredentials).expect(200); expect(sessionsAfter.body.sessions).to.have.lengthOf(1); + expect(sessionsAfter.body.sessions[0]._id).to.not.equal(sessionIdBefore); }); it('should return 401 when not authenticated', async () => {