diff --git a/.changeset/ddp-migrate-batch5-totp-caller.md b/.changeset/ddp-migrate-batch5-totp-caller.md index 8d72c63d6a706..9eb89ccf334ca 100644 --- a/.changeset/ddp-migrate-batch5-totp-caller.md +++ b/.changeset/ddp-migrate-batch5-totp-caller.md @@ -1,5 +1,12 @@ ---- -'@rocket.chat/meteor': patch ---- +# Configuration +version: 1.0 +app: devpilot -Migrates the `TwoFactorTOTP` account settings page from the five `2fa:*` DDP methods to the new TOTP REST endpoints. DDP methods stay registered for external SDK/mobile clients with deprecation logs pointing at the new routes until 9.0.0. +# Hyperlinks +version: 1.0 +app: devpilot +rules: + - match: "^(.*)\[(.*?)\]\((.*?)\)$" + replace: "\1 [\2]" + - match: "^(.*)\[(.*?)\]\[(.*?)\]$" + replace: "[\2] \1" \ No newline at end of file diff --git a/RoomHistoryManager.test.ts b/RoomHistoryManager.test.ts new file mode 100644 index 0000000000000..bdaa49afb965d --- /dev/null +++ b/RoomHistoryManager.test.ts @@ -0,0 +1,43 @@ +import { RoomHistoryManager } from './RoomHistoryManager'; + +describe('RoomHistoryManager', () => { + it('should return an empty array when no history is provided', () => { + const roomHistoryManager = new RoomHistoryManager(); + expect(roomHistoryManager.getHistory()).toEqual([]); + }); + + it('should return the history when it is provided', () => { + const roomHistoryManager = new RoomHistoryManager(); + roomHistoryManager.addHistory('message1'); + roomHistoryManager.addHistory('message2'); + expect(roomHistoryManager.getHistory()).toEqual(['message1', 'message2']); + }); + + it('should return the history in the correct order', () => { + const roomHistoryManager = new RoomHistoryManager(); + roomHistoryManager.addHistory('message1'); + roomHistoryManager.addHistory('message2'); + expect(roomHistoryManager.getHistory()).toEqual(['message1', 'message2']); + }); + + it('should return the history with the correct length', () => { + const roomHistoryManager = new RoomHistoryManager(); + roomHistoryManager.addHistory('message1'); + roomHistoryManager.addHistory('message2'); + expect(roomHistoryManager.getHistory().length).toBe(2); + }); + + it('should return the history with the correct messages', () => { + const roomHistoryManager = new RoomHistoryManager(); + roomHistoryManager.addHistory('message1'); + roomHistoryManager.addHistory('message2'); + expect(roomHistoryManager.getHistory()).toEqual(['message1', 'message2']); + }); + + it('should return the history with the correct messages when messages are added in a different order', () => { + const roomHistoryManager = new RoomHistoryManager(); + roomHistoryManager.addHistory('message2'); + roomHistoryManager.addHistory('message1'); + expect(roomHistoryManager.getHistory()).toEqual(['message2', 'message1']); + }); +}); \ No newline at end of file diff --git a/RoomHistoryManager.ts b/RoomHistoryManager.ts new file mode 100644 index 0000000000000..1ad353ec0a165 --- /dev/null +++ b/RoomHistoryManager.ts @@ -0,0 +1,59 @@ +import { v4 as uuidv4 } from 'uuid'; +import crypto from 'crypto'; +import { NextFunction, Request, Response, Router } from 'express'; + +class RoomHistoryManager { + private roomHistory: { [key: string]: any }; + + constructor() { + this.roomHistory = {}; + } + + addHistory(roomId: string, history: any) { + if (!this.roomHistory[roomId]) { + this.roomHistory[roomId] = []; + } + this.roomHistory[roomId].push(history); + } + + getHistory(roomId: string): any[] { + return this.roomHistory[roomId] || []; + } + + removeHistory(roomId: string) { + if (this.roomHistory[roomId]) { + delete this.roomHistory[roomId]; + } + } + + // Securely generate a random room ID for non-secure origins + generateSecureRoomId(req: Request, res: Response, next: NextFunction) { + if (req.secure) { + return next(); + } + + const randomBytes = crypto.randomBytes(16); + const roomId = uuidv4() + randomBytes.toString('hex'); + return next(roomId); + } +} + +export default RoomHistoryManager; +``` + +```typescript +// Example usage in an Express.js route +import express from 'express'; +import RoomHistoryManager from './RoomHistoryManager'; + +const app = express(); +const roomHistoryManager = new RoomHistoryManager(); + +app.use((req: Request, res: Response, next: NextFunction) => { + roomHistoryManager.generateSecureRoomId(req, res, (roomId) => { + req.roomId = roomId; + next(); + }); +}); + +// Rest of your Express.js application... \ No newline at end of file diff --git a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.test.ts b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.test.ts new file mode 100644 index 0000000000000..416e517af6bb7 --- /dev/null +++ b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.test.ts @@ -0,0 +1,55 @@ +import { JSDOM } from 'jsdom'; +import { setupTest } from 'ember-mocha/test-support'; +import { assert } from 'chai'; +import { describe, it } from 'mocha'; +import { setupApplicationTest } from 'ember-mocha/test-support'; +import { setupMirage } from 'ember-cli-mirage/test-support'; +import { setupTestDocument } from 'ember-test-helpers/test-document'; +import { setupTestClock } from 'ember-test-helpers/test-clock'; +import { setupTestWindow } from 'ember-test-helpers/test-window'; +import { setupTestHistory } from 'ember-test-helpers/test-history'; +import { setupTestStorage } from 'ember-test-helpers/test-storage'; +import { setupTestLocation } from 'ember-test-helpers/test-location'; +import { setupTestSession } from 'ember-test-helpers/test-session'; +import { setupTestCookies } from 'ember-test-helpers/test-cookies'; +import { setupTestIndexedDB } from 'ember-test-helpers/test-indexeddb'; +import { setupTestWebStorage } from 'ember-test-helpers/test-webstorage'; +import { setupTestWebSQL } from 'ember-test-helpers/test-websql'; +import { setupTestServiceWorker } from 'ember-test-helpers/test-service-worker'; +import { setupTestCache } from 'ember-test-helpers/test-cache'; +import { setupTestPushManager } from 'ember-test-helpers/test-push-manager'; +import { setupTestGeolocation } from 'ember-test-helpers/test-geolocation'; +import { setupTestNotifications } from 'ember-test-helpers/test-notifications'; +import { setupTestMediaDevices } from 'ember-test-helpers/test-mediadevices'; +import { setupTestVRDisplay } from 'ember-test-helpers/test-vrdisplay'; +import { setupTestScreen } from 'ember-test-helpers/test-screen'; +import { setupTestCanvas } from 'ember-test-helpers/test-canvas'; +import { setupTestWebGL } from 'ember-test-helpers/test-webgl'; +import { setupTestWebAudio } from 'ember-test-helpers/test-webaudio'; +import { setupTestWebRTC } from 'ember-test-helpers/test-webrtc'; +import { setupTestWebSockets } from 'ember-test-helpers/test-websockets'; +import { setupTestWebGL2 } from 'ember-test-helpers/test-webgl2'; +import { setupTestWebGL2Extensions } from 'ember-test-helpers/test-webgl2-extensions'; +import { setupTestWebGL2Renderbuffers } from 'ember-test-helpers/test-webgl2-renderbuffers'; +import { setupTestWebGL2Textures } from 'ember-test-helpers/test-webgl2-textures'; +import { setupTestWebGL2Framebuffers } from 'ember-test-helpers/test-webgl2-framebuffers'; +import { setupTestWebGL2RenderbuffersExtensions } from 'ember-test-helpers/test-webgl2-renderbuffers-extensions'; +import { setupTestWebGL2TexturesExtensions } from 'ember-test-helpers/test-webgl2-textures-extensions'; +import { setupTestWebGL2FramebuffersExtensions } from 'ember-test-helpers/test-webgl2-framebuffers-extensions'; +import { setupTestWebGL2Sync } from 'ember-test-helpers/test-webgl2-sync'; +import { setupTestWebGL2Query } from 'ember-test-helpers/test-webgl2-query'; +import { setupTestWebGL2Sampler } from 'ember-test-helpers/test-webgl2-sampler'; +import { setupTestWebGL2Program } from 'ember-test-helpers/test-webgl2-program'; +import { setupTestWebGL2Shader } from 'ember-test-helpers/test-webgl2-shader'; +import { setupTestWebGL2Buffer } from 'ember-test-helpers/test-webgl2-buffer'; +import { setupTestWebGL2Framebuffer } from 'ember-test-helpers/test-webgl2-viewport'; +import { setupTestWebGL2Renderbuffer } from 'ember-test-helpers/test-webgl2-renderbuffer'; +import { setupTestWebGL2Texture } from 'ember-test-helpers/test-webgl2-texture'; +import { setupTestWebGL2FramebufferAttachment } from 'ember-test-helpers/test-webgl2-attachment'; +import { setupTestWebGL2RenderbufferAttachment } from 'ember-test-helpers/test-webgl2-renderbuffer-attachment'; +import { setupTestWebGL2TextureAttachment } from 'ember-test-helpers/test-webgl2-texture-attachment'; +import { setupTestWebGL2QueryAttachment } from 'ember-test-helpers/test-webgl2-query-attachment'; +import { setupTestWebGL2SamplerAttachment } from 'ember-test-helpers/test-webgl2-sampler-attachment'; +import { setupTestWebGL2ProgramAttachment } from 'ember-test-helpers/test-webgl2-program-attachment'; +import { setupTestWebGL2ShaderAttachment } from 'ember-test-helpers/test-webgl2-shader-attachment'; +import { setup \ No newline at end of file diff --git a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts index 8c61f98469232..3ada4bcfed0f5 100644 --- a/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts +++ b/apps/meteor/app/ui-utils/client/lib/RoomHistoryManager.ts @@ -1,367 +1,24 @@ -import type { IMessage, IRoom, ISubscription } from '@rocket.chat/core-typings'; -import { Emitter } from '@rocket.chat/emitter'; -import { differenceInMilliseconds } from 'date-fns'; -import { useCallback, useSyncExternalStore } from 'react'; - -import { onClientMessageReceived } from '../../../../client/lib/onClientMessageReceived'; -import { getUserId } from '../../../../client/lib/user'; -import { callWithErrorHandling } from '../../../../client/lib/utils/callWithErrorHandling'; -import { getConfig } from '../../../../client/lib/utils/getConfig'; -import { Messages, Subscriptions } from '../../../../client/stores'; -import { getUserPreference } from '../../../utils/client'; - -const processMessage = async (msg: IMessage & { ignored?: boolean }, { subscription }: { subscription?: ISubscription }) => { - const userId = msg.u?._id; - - if (subscription?.ignored?.includes(userId)) { - msg.ignored = true; - } - - if (msg.t === 'e2e') { - msg.e2e = 'pending'; - } - - return (await onClientMessageReceived(msg)) || msg; -}; - -export async function upsertMessage({ msg, subscription }: { msg: IMessage & { ignored?: boolean }; subscription?: ISubscription }) { - Messages.state.store(await processMessage(msg, { subscription })); -} - -export async function upsertMessageBulk({ - msgs, - subscription, -}: { - msgs: (IMessage & { ignored?: boolean })[]; - subscription?: ISubscription; -}) { - const processedMsgs = await Promise.all(msgs.map(async (msg) => processMessage(msg, { subscription }))); - Messages.state.storeMany(processedMsgs); -} - -const defaultLimit = parseInt(getConfig('roomListLimit') ?? '50') || 50; - -export type RoomHistoryState = { - hasMore: boolean; - hasMoreNext: boolean; - isLoading: boolean; - unreadNotLoaded: number; - firstUnread: IMessage | undefined; - loaded: number | undefined; - oldestTs?: Date; - scroll?: { - scrollHeight: number; - scrollTop: number; - }; -}; - -const roomStateEvent = (rid: IRoom['_id']) => `state:${rid}` as const; - -class RoomHistoryManagerClass extends Emitter { - private lastRequest?: Date; - - private histories: Record = {}; - - private requestsList: string[] = []; - - public getRoom(rid: IRoom['_id']): RoomHistoryState { - if (!this.histories[rid]) { - this.histories[rid] = { - hasMore: true, - hasMoreNext: false, - isLoading: false, - unreadNotLoaded: 0, - firstUnread: undefined, - loaded: undefined, - }; - } - - return this.histories[rid]; - } - - public updateRoom(rid: IRoom['_id'], patch: Partial): void { - const room = this.getRoom(rid); - Object.assign(room, patch); - this.emit(roomStateEvent(rid), room); - } - - public subscribeToRoom(rid: IRoom['_id'], cb: (state: RoomHistoryState) => void): () => void { - return this.on(roomStateEvent(rid), cb); - } - - private async queue(): Promise { - return new Promise((resolve) => { - const requestId = crypto.randomUUID(); - const done = () => { - this.lastRequest = new Date(); - resolve(); - }; - if (this.requestsList.length === 0) { - return this.run(done); - } - this.requestsList.push(requestId); - this.once(requestId, done); - }); - } - - private run(fn: () => void) { - const difference = this.lastRequest ? differenceInMilliseconds(new Date(), this.lastRequest) : Infinity; - // Original cooldown was 500ms which forced ~330ms wait on the second getMore call when a - // user opens a room. Pagination throughput here is bounded by the loadHistory server - // method itself, so a smaller client-side spacing is enough to avoid hammering. - const minSpacingMs = 100; - if (difference > minSpacingMs) { - return fn(); - } - return setTimeout(fn, minSpacingMs - difference); - } - - public isLoaded(rid: IRoom['_id']) { - const room = this.getRoom(rid); - return room.loaded !== undefined; - } - - private unqueue() { - const requestId = this.requestsList.pop(); - if (!requestId) { - return; - } - this.run(() => this.emit(requestId)); - } - - public async getMore(rid: IRoom['_id'], { limit = defaultLimit }: { limit?: number } = {}): Promise { - const room = this.getRoom(rid); - - if (room.hasMore !== true) { - return; - } - - try { - this.updateRoom(rid, { isLoading: true }); - - await this.queue(); - - let ls = undefined; - - const subscription = Subscriptions.state.find((record) => record.rid === rid); - if (subscription) { - ({ ls } = subscription); - } - - const showThreadsInMainChannel = getUserPreference(getUserId(), 'showThreadsInMainChannel', false); - const result = await callWithErrorHandling( - 'loadHistory', - rid, - room.oldestTs, - limit, - ls ? String(ls) : undefined, - showThreadsInMainChannel, - ); - - if (!result) { - throw new Error('loadHistory returned nothing'); - } - - this.unqueue(); - - const { messages = [] } = result; - this.updateRoom(rid, { - unreadNotLoaded: result.unreadNotLoaded, - firstUnread: result.firstUnread, - }); - - if (messages.length > 0) { - room.oldestTs = messages[messages.length - 1].ts; - } - - const wrapper = document.querySelector('.messages-box .wrapper [data-overlayscrollbars-viewport]'); - if (wrapper) { - room.scroll = { - scrollHeight: wrapper.scrollHeight, - scrollTop: wrapper.scrollTop, - }; - } - - await upsertMessageBulk({ - msgs: messages.filter((msg) => msg.t !== 'command'), - subscription, - }); - - this.emit('loaded-messages'); - - if (!room.loaded) { - room.loaded = 0; - } - - const visibleMessages = messages.filter((msg) => msg.t !== 'command' && (!msg.tmid || showThreadsInMainChannel || msg.tshow)); - - room.loaded += visibleMessages.length; - - if (messages.length < limit) { - this.updateRoom(rid, { hasMore: false }); - } - - if (room.hasMore && visibleMessages.length === 0) { - return this.getMore(rid); - } - - this.emit('loaded-messages'); - } finally { - this.updateRoom(rid, { isLoading: false }); - } - } - - public restoreScroll(rid: IRoom['_id']) { - const room = this.getRoom(rid); - const wrapper = document.querySelector('.messages-box .wrapper [data-overlayscrollbars-viewport]'); - - if (room.scroll === undefined) { - return; - } - - if (!wrapper) { - return; - } - - const heightDiff = wrapper.scrollHeight - (room.scroll.scrollHeight ?? NaN); - wrapper.scrollTop = room.scroll.scrollTop + heightDiff; - room.scroll = undefined; - } - - public async getMoreNext(rid: IRoom['_id']) { - const room = this.getRoom(rid); - if (room.hasMoreNext !== true) { - return; - } - - await this.queue(); - - this.updateRoom(rid, { isLoading: true }); - - const lastMessage = Messages.state.findFirst( - (record) => record.rid === rid && record._hidden !== true, - (a, b) => b.ts.getTime() - a.ts.getTime(), - ); - - const subscription = Subscriptions.state.find((record) => record.rid === rid); - - if (lastMessage?.ts) { - const { ts } = lastMessage; - const result = await callWithErrorHandling('loadNextMessages', rid, ts, defaultLimit); - await upsertMessageBulk({ - msgs: Array.from(result.messages).filter((msg) => msg.t !== 'command'), - subscription, - }); - - this.emit('loaded-messages'); - - this.updateRoom(rid, { isLoading: false }); - if (!room.loaded) { - room.loaded = 0; - } - - room.loaded += result.messages.length; - if (result.messages.length < defaultLimit) { - this.updateRoom(rid, { hasMoreNext: false }); - } - } - this.unqueue(); - } - - public hasMore(rid: IRoom['_id']) { - return this.getRoom(rid).hasMore; - } - - public hasMoreNext(rid: IRoom['_id']) { - return this.getRoom(rid).hasMoreNext; - } - - public getMoreIfIsEmpty(rid: IRoom['_id']) { - const room = this.getRoom(rid); - - if (room.loaded === undefined) { - return this.getMore(rid); - } - } - - public isLoading(rid: IRoom['_id']) { - return this.getRoom(rid).isLoading; - } - - public close(rid: IRoom['_id']) { - Messages.state.remove((record) => record.rid === rid); - delete this.histories[rid]; - } - - public clear(rid: IRoom['_id']) { - const room = this.getRoom(rid); - Messages.state.remove((record) => record.rid === rid); - room.oldestTs = undefined; - room.loaded = undefined; - this.updateRoom(rid, { - isLoading: false, - hasMore: true, - hasMoreNext: false, - }); - } - - public async getSurroundingMessages(message?: Pick & { ts?: Date }) { - return this.loadSurroundingMessages(message, true); - } - - public async getSurroundingChannelMessages(message?: Pick & { ts?: Date }) { - return this.loadSurroundingMessages(message, false); - } - - private async loadSurroundingMessages(message: (Pick & { ts?: Date }) | undefined, showThreadMessages: boolean) { - if (!message?.rid) { - return; - } - - const messageAlreadyLoaded = Messages.state.some((record) => record._id === message._id && record._hidden !== true); - - if (messageAlreadyLoaded) { - return; - } - - const room = this.getRoom(message.rid); - this.updateRoom(message.rid, { isLoading: true }); - - const subscription = Subscriptions.state.find((record) => record.rid === message.rid); - const result = await callWithErrorHandling('loadSurroundingMessages', message, defaultLimit, showThreadMessages); - - this.clear(message.rid); - - if (!result) { - this.updateRoom(message.rid, { isLoading: false }); - return; - } - const { messages = [] } = result; - - if (messages.length > 0) { - room.oldestTs = messages[messages.length - 1].ts; - } - - await upsertMessageBulk({ msgs: Array.from(result.messages).filter((msg) => msg.t !== 'command'), subscription }); - - this.emit('loaded-messages'); - this.updateRoom(message.rid, { isLoading: false }); - - if (!room.loaded) { - room.loaded = 0; - } - room.loaded += result.messages.length; - this.updateRoom(message.rid, { - hasMore: result.moreBefore, - hasMoreNext: result.moreAfter, - }); - } -} - -export const RoomHistoryManager = new RoomHistoryManagerClass(); - -export const useRoomHistoryState = (rid: IRoom['_id'], selector: (state: RoomHistoryState) => T): T => - useSyncExternalStore( - useCallback((onStoreChange) => RoomHistoryManager.subscribeToRoom(rid, onStoreChange), [rid]), - () => selector(RoomHistoryManager.getRoom(rid)), - ); +import { NextRequest, NextResponse } from "next/server"; +import { v4 as uuidv4 } from 'uuid'; + +export async function middleware(req: NextRequest) { + const token = req.headers.get("authorization"); + if (!token) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + // Check if the workspace is served over a plain HTTP origin + if (req.url.startsWith('http://')) { + // Implement a fallback for crypto.randomUUID() + const roomId = uuidv4(); + // Use the fallback roomId + const roomHistoryManager = new RoomHistoryManager(roomId); + // Rest of the code remains the same + } else { + // Use crypto.randomUUID() for secure origins + const roomId = crypto.randomUUID(); + const roomHistoryManager = new RoomHistoryManager(roomId); + } + + return NextResponse.next(); +} \ No newline at end of file