diff --git a/.changeset/bump-patch-1724077277110.md b/.changeset/bump-patch-1724077277110.md new file mode 100644 index 000000000000..e1eaa7980afb --- /dev/null +++ b/.changeset/bump-patch-1724077277110.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Bump @rocket.chat/meteor version. diff --git a/.changeset/gentle-bugs-think.md b/.changeset/gentle-bugs-think.md new file mode 100644 index 000000000000..fc4738f3043a --- /dev/null +++ b/.changeset/gentle-bugs-think.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Prevent `processRoomAbandonment` callback from erroring out when a room was inactive during a day Business Hours was not configured for. diff --git a/.changeset/orange-clocks-wait.md b/.changeset/orange-clocks-wait.md new file mode 100644 index 000000000000..eacb88108a0f --- /dev/null +++ b/.changeset/orange-clocks-wait.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Security Hotfix (https://docs.rocket.chat/docs/security-fixes-and-updates) diff --git a/.changeset/strong-rings-rush.md b/.changeset/strong-rings-rush.md new file mode 100644 index 000000000000..5125f47dcb3b --- /dev/null +++ b/.changeset/strong-rings-rush.md @@ -0,0 +1,5 @@ +--- +"@rocket.chat/meteor": patch +--- + +Restored tooltips to the unit edit department field selected options diff --git a/.changeset/two-bikes-crash.md b/.changeset/two-bikes-crash.md new file mode 100644 index 000000000000..a120435e4a48 --- /dev/null +++ b/.changeset/two-bikes-crash.md @@ -0,0 +1,7 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixed an issue related to setting Accounts_ForgetUserSessionOnWindowClose, this setting was not working as expected. + +The new meteor 2.16 release introduced a new option to configure the Accounts package and choose between the local storage or session storage. They also changed how Meteor.\_localstorage works internally. Due to these changes in Meteor, our setting to use session storage wasn't working as expected. This PR fixes this issue and configures the Accounts package according to the workspace settings. diff --git a/.changeset/wise-avocados-taste.md b/.changeset/wise-avocados-taste.md new file mode 100644 index 000000000000..c4c9bce010b8 --- /dev/null +++ b/.changeset/wise-avocados-taste.md @@ -0,0 +1,5 @@ +--- +'@rocket.chat/meteor': patch +--- + +Fixes an issue where multi-step modals were closing unexpectedly diff --git a/apps/meteor/app/lib/server/functions/getModifiedHttpHeaders.ts b/apps/meteor/app/lib/server/functions/getModifiedHttpHeaders.ts new file mode 100644 index 000000000000..e62727814de3 --- /dev/null +++ b/apps/meteor/app/lib/server/functions/getModifiedHttpHeaders.ts @@ -0,0 +1,20 @@ +export const getModifiedHttpHeaders = (httpHeaders: Record) => { + const modifiedHttpHeaders = { ...httpHeaders }; + + if ('x-auth-token' in modifiedHttpHeaders) { + modifiedHttpHeaders['x-auth-token'] = '[redacted]'; + } + + if (modifiedHttpHeaders.cookie) { + const cookies = modifiedHttpHeaders.cookie.split('; '); + const modifiedCookies = cookies.map((cookie: string) => { + if (cookie.startsWith('rc_token=')) { + return 'rc_token=[redacted]'; + } + return cookie; + }); + modifiedHttpHeaders.cookie = modifiedCookies.join('; '); + } + + return modifiedHttpHeaders; +}; diff --git a/apps/meteor/app/lib/server/lib/debug.js b/apps/meteor/app/lib/server/lib/debug.js index aaa492e80337..cbf38528579f 100644 --- a/apps/meteor/app/lib/server/lib/debug.js +++ b/apps/meteor/app/lib/server/lib/debug.js @@ -7,6 +7,7 @@ import _ from 'underscore'; import { getMethodArgs } from '../../../../server/lib/logger/logPayloads'; import { metrics } from '../../../metrics/server'; import { settings } from '../../../settings/server'; +import { getModifiedHttpHeaders } from '../functions/getModifiedHttpHeaders'; const logger = new Logger('Meteor'); @@ -41,7 +42,7 @@ const traceConnection = (enable, filter, prefix, name, connection, userId) => { console.log(name, { id: connection.id, clientAddress: connection.clientAddress, - httpHeaders: connection.httpHeaders, + httpHeaders: getModifiedHttpHeaders(connection.httpHeaders), userId, }); } else { diff --git a/apps/meteor/app/livechat/server/api/v1/room.ts b/apps/meteor/app/livechat/server/api/v1/room.ts index b0f45a63ff87..565f8e0bb3f4 100644 --- a/apps/meteor/app/livechat/server/api/v1/room.ts +++ b/apps/meteor/app/livechat/server/api/v1/room.ts @@ -31,63 +31,72 @@ import { findVisitorInfo } from '../lib/visitors'; const isAgentWithInfo = (agentObj: ILivechatAgent | { hiddenInfo: boolean }): agentObj is ILivechatAgent => !('hiddenInfo' in agentObj); -API.v1.addRoute('livechat/room', { - async get() { - // I'll temporary use check for validation, as validateParams doesnt support what's being done here - const extraCheckParams = await onCheckRoomParams({ - token: String, - rid: Match.Maybe(String), - agentId: Match.Maybe(String), - }); - - check(this.queryParams, extraCheckParams as any); - - const { token, rid, agentId, ...extraParams } = this.queryParams; - - const guest = token && (await findGuest(token)); - if (!guest) { - throw new Error('invalid-token'); - } - - if (!rid) { - const room = await LivechatRooms.findOneOpenByVisitorToken(token, {}); - if (room) { - return API.v1.success({ room, newRoom: false }); - } - - let agent: SelectedAgent | undefined; - const agentObj = agentId && (await findAgent(agentId)); - if (agentObj) { - if (isAgentWithInfo(agentObj)) { - const { username = undefined } = agentObj; - agent = { agentId, username }; - } else { - agent = { agentId }; - } +API.v1.addRoute( + 'livechat/room', + { + rateLimiterOptions: { + numRequestsAllowed: 5, + intervalTimeInMS: 60000, + }, + }, + { + async get() { + // I'll temporary use check for validation, as validateParams doesnt support what's being done here + const extraCheckParams = await onCheckRoomParams({ + token: String, + rid: Match.Maybe(String), + agentId: Match.Maybe(String), + }); + + check(this.queryParams, extraCheckParams as any); + + const { token, rid, agentId, ...extraParams } = this.queryParams; + + const guest = token && (await findGuest(token)); + if (!guest) { + throw new Error('invalid-token'); } - const roomInfo = { - source: { - type: isWidget(this.request.headers) ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API, - }, - }; + if (!rid) { + const room = await LivechatRooms.findOneOpenByVisitorToken(token, {}); + if (room) { + return API.v1.success({ room, newRoom: false }); + } - const newRoom = await LivechatTyped.createRoom({ visitor: guest, roomInfo, agent, extraData: extraParams }); + let agent: SelectedAgent | undefined; + const agentObj = agentId && (await findAgent(agentId)); + if (agentObj) { + if (isAgentWithInfo(agentObj)) { + const { username = undefined } = agentObj; + agent = { agentId, username }; + } else { + agent = { agentId }; + } + } - return API.v1.success({ - room: newRoom, - newRoom: true, - }); - } + const roomInfo = { + source: { + type: isWidget(this.request.headers) ? OmnichannelSourceType.WIDGET : OmnichannelSourceType.API, + }, + }; + + const newRoom = await LivechatTyped.createRoom({ visitor: guest, roomInfo, agent, extraData: extraParams }); - const froom = await LivechatRooms.findOneOpenByRoomIdAndVisitorToken(rid, token, {}); - if (!froom) { - throw new Error('invalid-room'); - } + return API.v1.success({ + room: newRoom, + newRoom: true, + }); + } + + const froom = await LivechatRooms.findOneOpenByRoomIdAndVisitorToken(rid, token, {}); + if (!froom) { + throw new Error('invalid-room'); + } - return API.v1.success({ room: froom, newRoom: false }); + return API.v1.success({ room: froom, newRoom: false }); + }, }, -}); +); // Note: use this route if a visitor is closing a room // If a RC user(like eg agent) is closing a room, use the `livechat/room.closeByUser` route diff --git a/apps/meteor/app/livechat/server/api/v1/visitor.ts b/apps/meteor/app/livechat/server/api/v1/visitor.ts index a5b3f2de35b1..ed32f0e2d279 100644 --- a/apps/meteor/app/livechat/server/api/v1/visitor.ts +++ b/apps/meteor/app/livechat/server/api/v1/visitor.ts @@ -9,119 +9,128 @@ import { settings } from '../../../../settings/server'; import { Livechat as LivechatTyped } from '../../lib/LivechatTyped'; import { findGuest, normalizeHttpHeaderData } from '../lib/livechat'; -API.v1.addRoute('livechat/visitor', { - async post() { - check(this.bodyParams, { - visitor: Match.ObjectIncluding({ - token: String, - name: Match.Maybe(String), - email: Match.Maybe(String), - department: Match.Maybe(String), - phone: Match.Maybe(String), - username: Match.Maybe(String), - customFields: Match.Maybe([ - Match.ObjectIncluding({ - key: String, - value: String, - overwrite: Boolean, - }), - ]), - }), - }); - - const { customFields, id, token, name, email, department, phone, username, connectionData } = this.bodyParams.visitor; - - if (!token?.trim()) { - throw new Meteor.Error('error-invalid-token', 'Token cannot be empty', { method: 'livechat/visitor' }); - } - - const guest = { - token, - ...(id && { id }), - ...(name && { name }), - ...(email && { email }), - ...(department && { department }), - ...(username && { username }), - ...(connectionData && { connectionData }), - ...(phone && typeof phone === 'string' && { phone: { number: phone as string } }), - connectionData: normalizeHttpHeaderData(this.request.headers), - }; - - const visitor = await LivechatTyped.registerGuest(guest); - if (!visitor) { - throw new Meteor.Error('error-livechat-visitor-registration', 'Error registering visitor', { - method: 'livechat/visitor', +API.v1.addRoute( + 'livechat/visitor', + { + rateLimiterOptions: { + numRequestsAllowed: 5, + intervalTimeInMS: 60000, + }, + }, + { + async post() { + check(this.bodyParams, { + visitor: Match.ObjectIncluding({ + token: String, + name: Match.Maybe(String), + email: Match.Maybe(String), + department: Match.Maybe(String), + phone: Match.Maybe(String), + username: Match.Maybe(String), + customFields: Match.Maybe([ + Match.ObjectIncluding({ + key: String, + value: String, + overwrite: Boolean, + }), + ]), + }), }); - } - const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); - // If it's updating an existing visitor, it must also update the roomInfo - const rooms = await LivechatRooms.findOpenByVisitorToken(visitor?.token, {}, extraQuery).toArray(); - await Promise.all( - rooms.map( - (room: IRoom) => - visitor && - LivechatTyped.saveRoomInfo(room, { - _id: visitor._id, - name: visitor.name, - phone: visitor.phone?.[0]?.phoneNumber, - livechatData: visitor.livechatData as { [k: string]: string }, - }), - ), - ); - - if (customFields && Array.isArray(customFields) && customFields.length > 0) { - const keys = customFields.map((field) => field.key); - const errors: string[] = []; - - const processedKeys = await Promise.all( - await LivechatCustomField.findByIdsAndScope>(keys, 'visitor', { - projection: { _id: 1 }, - }) - .map(async (field) => { - const customField = customFields.find((f) => f.key === field._id); - if (!customField) { - return; - } - - const { key, value, overwrite } = customField; - // TODO: Change this to Bulk update - if (!(await VisitorsRaw.updateLivechatDataByToken(token, key, value, overwrite))) { - errors.push(key); - } - - return key; - }) - .toArray(), - ); + const { customFields, id, token, name, email, department, phone, username, connectionData } = this.bodyParams.visitor; - if (processedKeys.length !== keys.length) { - LivechatTyped.logger.warn({ - msg: 'Some custom fields were not processed', - visitorId: visitor._id, - missingKeys: keys.filter((key) => !processedKeys.includes(key)), - }); + if (!token?.trim()) { + throw new Meteor.Error('error-invalid-token', 'Token cannot be empty', { method: 'livechat/visitor' }); } - if (errors.length > 0) { - LivechatTyped.logger.error({ - msg: 'Error updating custom fields', - visitorId: visitor._id, - errors, + const guest = { + token, + ...(id && { id }), + ...(name && { name }), + ...(email && { email }), + ...(department && { department }), + ...(username && { username }), + ...(connectionData && { connectionData }), + ...(phone && typeof phone === 'string' && { phone: { number: phone as string } }), + connectionData: normalizeHttpHeaderData(this.request.headers), + }; + + const visitor = await LivechatTyped.registerGuest(guest); + if (!visitor) { + throw new Meteor.Error('error-livechat-visitor-registration', 'Error registering visitor', { + method: 'livechat/visitor', }); - throw new Error('error-updating-custom-fields'); } - return API.v1.success({ visitor: await VisitorsRaw.findOneEnabledById(visitor._id) }); - } + const extraQuery = await callbacks.run('livechat.applyRoomRestrictions', {}); + // If it's updating an existing visitor, it must also update the roomInfo + const rooms = await LivechatRooms.findOpenByVisitorToken(visitor?.token, {}, extraQuery).toArray(); + await Promise.all( + rooms.map( + (room: IRoom) => + visitor && + LivechatTyped.saveRoomInfo(room, { + _id: visitor._id, + name: visitor.name, + phone: visitor.phone?.[0]?.phoneNumber, + livechatData: visitor.livechatData as { [k: string]: string }, + }), + ), + ); - if (!visitor) { - throw new Meteor.Error('error-saving-visitor', 'An error ocurred while saving visitor'); - } + if (customFields && Array.isArray(customFields) && customFields.length > 0) { + const keys = customFields.map((field) => field.key); + const errors: string[] = []; - return API.v1.success({ visitor }); + const processedKeys = await Promise.all( + await LivechatCustomField.findByIdsAndScope>(keys, 'visitor', { + projection: { _id: 1 }, + }) + .map(async (field) => { + const customField = customFields.find((f) => f.key === field._id); + if (!customField) { + return; + } + + const { key, value, overwrite } = customField; + // TODO: Change this to Bulk update + if (!(await VisitorsRaw.updateLivechatDataByToken(token, key, value, overwrite))) { + errors.push(key); + } + + return key; + }) + .toArray(), + ); + + if (processedKeys.length !== keys.length) { + LivechatTyped.logger.warn({ + msg: 'Some custom fields were not processed', + visitorId: visitor._id, + missingKeys: keys.filter((key) => !processedKeys.includes(key)), + }); + } + + if (errors.length > 0) { + LivechatTyped.logger.error({ + msg: 'Error updating custom fields', + visitorId: visitor._id, + errors, + }); + throw new Error('error-updating-custom-fields'); + } + + return API.v1.success({ visitor: await VisitorsRaw.findOneEnabledById(visitor._id) }); + } + + if (!visitor) { + throw new Meteor.Error('error-saving-visitor', 'An error ocurred while saving visitor'); + } + + return API.v1.success({ visitor }); + }, }, -}); +); API.v1.addRoute('livechat/visitor/:token', { async get() { diff --git a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts index 8a5a4c280670..8eb53fbb8fa7 100644 --- a/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts +++ b/apps/meteor/app/livechat/server/hooks/processRoomAbandonment.ts @@ -43,7 +43,8 @@ const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, agentLas officeDays = (await businessHourManager.getBusinessHour())?.workHours.reduce(parseDays, {}); } - if (!officeDays) { + // Empty object we assume invalid config + if (!officeDays || !Object.keys(officeDays).length) { return getSecondsWhenOfficeHoursIsDisabled(room, agentLastMessage); } @@ -55,6 +56,11 @@ const getSecondsSinceLastAgentResponse = async (room: IOmnichannelRoom, agentLas for (let index = 0; index <= daysOfInactivity; index++) { const today = inactivityDay.clone().format('dddd'); const officeDay = officeDays[today]; + // Config doesnt have data for this day, we skip day + if (!officeDay) { + inactivityDay.add(1, 'days'); + continue; + } const startTodaysOfficeHour = moment(`${officeDay.start.day}:${officeDay.start.time}`, 'dddd:HH:mm').add(index, 'days'); const endTodaysOfficeHour = moment(`${officeDay.finish.day}:${officeDay.finish.time}`, 'dddd:HH:mm').add(index, 'days'); if (officeDays[today].open) { diff --git a/apps/meteor/client/components/GenericModal/GenericModal.tsx b/apps/meteor/client/components/GenericModal/GenericModal.tsx index d371e1ff4ef2..5d025e05827d 100644 --- a/apps/meteor/client/components/GenericModal/GenericModal.tsx +++ b/apps/meteor/client/components/GenericModal/GenericModal.tsx @@ -111,7 +111,7 @@ const GenericModal = ({ {tagline && {tagline}} {title ?? t('Are_you_sure')} - + {onClose && } {children} diff --git a/apps/meteor/client/components/GenericModal/GenericModalSkeleton.tsx b/apps/meteor/client/components/GenericModal/GenericModalSkeleton.tsx index d56cbdd26a67..2dcdf3b3578c 100644 --- a/apps/meteor/client/components/GenericModal/GenericModalSkeleton.tsx +++ b/apps/meteor/client/components/GenericModal/GenericModalSkeleton.tsx @@ -1,25 +1,13 @@ import { Skeleton } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import type { ComponentProps } from 'react'; import React from 'react'; import GenericModal from './GenericModal'; -const GenericModalSkeleton = ({ onClose, ...props }: ComponentProps) => { - const t = useTranslation(); - - return ( - } - confirmText={t('Cancel')} - onConfirm={onClose} - > - - - ); -}; +const GenericModalSkeleton = (props: ComponentProps) => ( + }> + + +); export default GenericModalSkeleton; diff --git a/apps/meteor/client/omnichannel/units/UnitEdit.tsx b/apps/meteor/client/omnichannel/units/UnitEdit.tsx index e4bc1c0efb50..e71e8e2a94d0 100644 --- a/apps/meteor/client/omnichannel/units/UnitEdit.tsx +++ b/apps/meteor/client/omnichannel/units/UnitEdit.tsx @@ -228,7 +228,7 @@ const UnitEdit = ({ unitData, unitMonitors, unitDepartments }: UnitEditProps) => value={value} onChange={onChange} onBlur={onBlur} - withTitle={false} + withTitle filter={departmentsFilter} setFilter={setDepartmentsFilter} options={departmentsOptions} diff --git a/apps/meteor/client/startup/accounts.ts b/apps/meteor/client/startup/accounts.ts index 3be110bc0a09..88008a606656 100644 --- a/apps/meteor/client/startup/accounts.ts +++ b/apps/meteor/client/startup/accounts.ts @@ -2,6 +2,7 @@ import { Accounts } from 'meteor/accounts-base'; import { Meteor } from 'meteor/meteor'; import { Tracker } from 'meteor/tracker'; +import { settings } from '../../app/settings/client'; import { mainReady } from '../../app/ui-utils/client'; import { sdk } from '../../app/utils/client/lib/SDKClient'; import { t } from '../../app/utils/lib/i18n'; @@ -24,3 +25,17 @@ Accounts.onEmailVerificationLink((token: string) => { }); }); }); + +Meteor.startup(() => { + Tracker.autorun((computation) => { + const forgetUserSessionOnWindowClose = settings.get('Accounts_ForgetUserSessionOnWindowClose'); + + if (forgetUserSessionOnWindowClose === undefined) { + return; + } + + computation.stop(); + + Accounts.config({ clientStorage: forgetUserSessionOnWindowClose ? 'session' : 'local' }); + }); +}); diff --git a/apps/meteor/client/views/omnichannel/realTimeMonitoring/RealTimeMonitoringPage.js b/apps/meteor/client/views/omnichannel/realTimeMonitoring/RealTimeMonitoringPage.js index 5b4d837d211c..b6e29530b5e7 100644 --- a/apps/meteor/client/views/omnichannel/realTimeMonitoring/RealTimeMonitoringPage.js +++ b/apps/meteor/client/views/omnichannel/realTimeMonitoring/RealTimeMonitoringPage.js @@ -18,11 +18,19 @@ import ChatsOverview from './overviews/ChatsOverview'; import ConversationOverview from './overviews/ConversationOverview'; import ProductivityOverview from './overviews/ProductivityOverview'; +const randomizeKeys = (keys) => { + keys.current = keys.current.map((_key, i) => { + return `${i}_${new Date().getTime()}`; + }); +}; + const dateRange = getDateRange(); const RealTimeMonitoringPage = () => { const t = useTranslation(); + const keys = useRef([...Array(10).keys()]); + const [reloadFrequency, setReloadFrequency] = useState(5); const [departmentId, setDepartment] = useState(''); @@ -43,6 +51,10 @@ const RealTimeMonitoringPage = () => { [departmentParams], ); + useEffect(() => { + randomizeKeys(keys); + }, [allParams]); + const reloadCharts = useMutableCallback(() => { Object.values(reloadRef.current).forEach((reload) => { reload(); @@ -53,6 +65,7 @@ const RealTimeMonitoringPage = () => { const interval = setInterval(reloadCharts, reloadFrequency * 1000); return () => { clearInterval(interval); + randomizeKeys(keys); }; }, [reloadCharts, reloadFrequency]); @@ -90,30 +103,54 @@ const RealTimeMonitoringPage = () => { - + - - + + - + - - + + - + - + - + - + diff --git a/apps/meteor/client/views/room/modals/ReadReceiptsModal/ReadReceiptsModal.tsx b/apps/meteor/client/views/room/modals/ReadReceiptsModal/ReadReceiptsModal.tsx index c4da16264646..ca033c2dcb0d 100644 --- a/apps/meteor/client/views/room/modals/ReadReceiptsModal/ReadReceiptsModal.tsx +++ b/apps/meteor/client/views/room/modals/ReadReceiptsModal/ReadReceiptsModal.tsx @@ -1,11 +1,11 @@ import type { IMessage, ReadReceipt } from '@rocket.chat/core-typings'; -import { Skeleton } from '@rocket.chat/fuselage'; import { useMethod, useToastMessageDispatch, useTranslation } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { useEffect } from 'react'; import GenericModal from '../../../../components/GenericModal'; +import GenericModalSkeleton from '../../../../components/GenericModal/GenericModalSkeleton'; import ReadReceiptRow from './ReadReceiptRow'; type ReadReceiptsModalProps = { @@ -29,11 +29,7 @@ const ReadReceiptsModal = ({ messageId, onClose }: ReadReceiptsModalProps): Reac }, [dispatchToastMessage, t, onClose, readReceiptsResult.isError, readReceiptsResult.error]); if (readReceiptsResult.isLoading || readReceiptsResult.isError) { - return ( - - - - ); + return ; } const readReceipts = readReceiptsResult.data; diff --git a/apps/meteor/client/views/teams/ConvertToChannelModal/ConvertToChannelModal.tsx b/apps/meteor/client/views/teams/ConvertToChannelModal/ConvertToChannelModal.tsx index c29f6c0ec586..3efcdb89690f 100644 --- a/apps/meteor/client/views/teams/ConvertToChannelModal/ConvertToChannelModal.tsx +++ b/apps/meteor/client/views/teams/ConvertToChannelModal/ConvertToChannelModal.tsx @@ -20,7 +20,7 @@ const ConvertToChannelModal = ({ onClose, onCancel, onConfirm, teamId, userId }: }); if (phase === AsyncStatePhase.LOADING) { - return ; + return ; } return ; diff --git a/apps/meteor/client/views/teams/contextualBar/info/DeleteTeam/DeleteTeamModalWithRooms.tsx b/apps/meteor/client/views/teams/contextualBar/info/DeleteTeam/DeleteTeamModalWithRooms.tsx index 5226a3602c0f..1375ec532c91 100644 --- a/apps/meteor/client/views/teams/contextualBar/info/DeleteTeam/DeleteTeamModalWithRooms.tsx +++ b/apps/meteor/client/views/teams/contextualBar/info/DeleteTeam/DeleteTeamModalWithRooms.tsx @@ -1,11 +1,10 @@ import type { IRoom } from '@rocket.chat/core-typings'; -import { Skeleton } from '@rocket.chat/fuselage'; -import { useTranslation, useEndpoint } from '@rocket.chat/ui-contexts'; +import { useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React, { useMemo } from 'react'; -import GenericModal from '../../../../../components/GenericModal'; +import GenericModalSkeleton from '../../../../../components/GenericModal/GenericModalSkeleton'; import DeleteTeamModal from './DeleteTeamModal'; type DeleteTeamModalWithRoomsProps = { @@ -15,17 +14,12 @@ type DeleteTeamModalWithRoomsProps = { }; const DeleteTeamModalWithRooms = ({ teamId, onConfirm, onCancel }: DeleteTeamModalWithRoomsProps): ReactElement => { - const t = useTranslation(); const query = useMemo(() => ({ teamId }), [teamId]); const getTeamsListRooms = useEndpoint('GET', '/v1/teams.listRooms'); const { data, isLoading } = useQuery(['getTeamsListRooms', query], async () => getTeamsListRooms(query)); if (isLoading) { - return ( - } confirmText={t('Cancel')}> - - - ); + return ; } return ; }; diff --git a/apps/meteor/client/views/teams/contextualBar/info/LeaveTeam/LeaveTeamWithData.tsx b/apps/meteor/client/views/teams/contextualBar/info/LeaveTeam/LeaveTeamWithData.tsx index 9bab0acc3d86..58f98705d2bb 100644 --- a/apps/meteor/client/views/teams/contextualBar/info/LeaveTeam/LeaveTeamWithData.tsx +++ b/apps/meteor/client/views/teams/contextualBar/info/LeaveTeam/LeaveTeamWithData.tsx @@ -1,11 +1,10 @@ import type { ITeam } from '@rocket.chat/core-typings'; -import { Skeleton } from '@rocket.chat/fuselage'; -import { useUserId, useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; +import { useUserId, useEndpoint } from '@rocket.chat/ui-contexts'; import { useQuery } from '@tanstack/react-query'; import type { ReactElement } from 'react'; import React from 'react'; -import GenericModal from '../../../../../components/GenericModal'; +import GenericModalSkeleton from '../../../../../components/GenericModal/GenericModalSkeleton'; import LeaveTeamModal from './LeaveTeamModal/LeaveTeamModal'; type LeaveTeamWithDataProps = { @@ -15,7 +14,6 @@ type LeaveTeamWithDataProps = { }; const LeaveTeamWithData = ({ teamId, onCancel, onConfirm }: LeaveTeamWithDataProps): ReactElement => { - const t = useTranslation(); const userId = useUserId(); if (!userId) { @@ -26,11 +24,7 @@ const LeaveTeamWithData = ({ teamId, onCancel, onConfirm }: LeaveTeamWithDataPro const { data, isLoading } = useQuery(['teams.listRoomsOfUser'], () => getRoomsOfUser({ teamId, userId })); if (isLoading) { - return ( - } confirmText={t('Cancel')}> - - - ); + return ; } return ; diff --git a/apps/meteor/client/views/teams/contextualBar/members/RemoveUsersModal/RemoveUsersModal.js b/apps/meteor/client/views/teams/contextualBar/members/RemoveUsersModal/RemoveUsersModal.js index 76a38b680692..f85d5434c1d1 100644 --- a/apps/meteor/client/views/teams/contextualBar/members/RemoveUsersModal/RemoveUsersModal.js +++ b/apps/meteor/client/views/teams/contextualBar/members/RemoveUsersModal/RemoveUsersModal.js @@ -1,8 +1,6 @@ -import { Skeleton } from '@rocket.chat/fuselage'; -import { useTranslation } from '@rocket.chat/ui-contexts'; import React, { useMemo } from 'react'; -import GenericModal from '../../../../../components/GenericModal'; +import GenericModalSkeleton from '../../../../../components/GenericModal/GenericModalSkeleton'; import { useEndpointData } from '../../../../../hooks/useEndpointData'; import { AsyncStatePhase } from '../../../../../lib/asyncState'; import BaseRemoveUsersModal from './BaseRemoveUsersModal'; @@ -10,7 +8,6 @@ import BaseRemoveUsersModal from './BaseRemoveUsersModal'; const initialData = { user: { username: '' } }; const RemoveUsersModal = ({ teamId, userId, onClose, onCancel, onConfirm }) => { - const t = useTranslation(); const { value, phase } = useEndpointData('/v1/teams.listRoomsOfUser', { params: useMemo(() => ({ teamId, userId }), [teamId, userId]) }); const userDataFetch = useEndpointData('/v1/users.info', { params: useMemo(() => ({ userId }), [userId]), initialValue: initialData }); const { @@ -18,11 +15,7 @@ const RemoveUsersModal = ({ teamId, userId, onClose, onCancel, onConfirm }) => { } = userDataFetch?.value; if (phase === AsyncStatePhase.LOADING) { - return ( - } confirmText={t('Cancel')} onConfirm={onClose}> - - - ); + return ; } return ; diff --git a/apps/meteor/definition/externals/meteor/accounts-base.d.ts b/apps/meteor/definition/externals/meteor/accounts-base.d.ts index 3f0b148120e7..31b70f7b7154 100644 --- a/apps/meteor/definition/externals/meteor/accounts-base.d.ts +++ b/apps/meteor/definition/externals/meteor/accounts-base.d.ts @@ -42,6 +42,8 @@ declare module 'meteor/accounts-base' { function _clearAllLoginTokens(userId: string | null): void; + function config(options: { clientStorage: 'session' | 'local' }): void; + class ConfigError extends Error {} class LoginCancelledError extends Error { diff --git a/apps/meteor/tests/e2e/account-forgetSessionOnWindowClose.spec.ts b/apps/meteor/tests/e2e/account-forgetSessionOnWindowClose.spec.ts new file mode 100644 index 000000000000..a19b0e9866da --- /dev/null +++ b/apps/meteor/tests/e2e/account-forgetSessionOnWindowClose.spec.ts @@ -0,0 +1,55 @@ +import { DEFAULT_USER_CREDENTIALS } from './config/constants'; +import { Registration } from './page-objects'; +import { test, expect } from './utils/test'; + +test.describe.serial('Forget session on window close setting', () => { + let poRegistration: Registration; + + test.beforeEach(async ({ page }) => { + poRegistration = new Registration(page); + + await page.goto('/home'); + }); + + test.describe('Setting off', async () => { + test.beforeAll(async ({ api }) => { + await api.post('/settings/Accounts_ForgetUserSessionOnWindowClose', { value: false }); + }); + + test('Login using credentials and reload to stay logged in', async ({ page, context }) => { + await poRegistration.username.type('user1'); + await poRegistration.inputPassword.type(DEFAULT_USER_CREDENTIALS.password); + await poRegistration.btnLogin.click(); + + 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=heading[name="Welcome to Rocket.Chat"]')).toBeVisible(); + }); + }); + + test.describe('Setting on', async () => { + test.beforeAll(async ({ api }) => { + await api.post('/settings/Accounts_ForgetUserSessionOnWindowClose', { value: true }); + }); + + test.afterAll(async ({ api }) => { + await api.post('/settings/Accounts_ForgetUserSessionOnWindowClose', { value: false }); + }); + + test('Login using credentials and reload to get logged out', async ({ page, context }) => { + await poRegistration.username.type('user1'); + await poRegistration.inputPassword.type(DEFAULT_USER_CREDENTIALS.password); + await poRegistration.btnLogin.click(); + + 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(); + }); + }); +}); diff --git a/apps/meteor/tests/e2e/omnichannel/omnichannel-units.spec.ts b/apps/meteor/tests/e2e/omnichannel/omnichannel-units.spec.ts index 9c1b5fdd5948..cd33a56caa04 100644 --- a/apps/meteor/tests/e2e/omnichannel/omnichannel-units.spec.ts +++ b/apps/meteor/tests/e2e/omnichannel/omnichannel-units.spec.ts @@ -185,11 +185,14 @@ test.describe('OC - Manage Units', () => { await poOmnichannelUnits.btnSave.click(); }); - await test.step('expect department to be in the chosen departments list', async () => { + await test.step('expect department to be in the chosen departments list and have title', async () => { await poOmnichannelUnits.search(unit.name); await poOmnichannelUnits.findRowByName(unit.name).click(); await expect(poOmnichannelUnits.contextualBar).toBeVisible(); - await expect(page.getByRole('option', { name: department2.data.name })).toBeVisible(); + await expect(poOmnichannelUnits.selectOptionChip(department2.data.name)).toBeVisible(); + await poOmnichannelUnits.selectOptionChip(department2.data.name).hover(); + + await expect(page.getByRole('tooltip', { name: department2.data.name })).toBeVisible(); await poOmnichannelUnits.btnContextualbarClose.click(); }); diff --git a/apps/meteor/tests/e2e/page-objects/omnichannel-units.ts b/apps/meteor/tests/e2e/page-objects/omnichannel-units.ts index abcd794f8efa..2d0e24073a45 100644 --- a/apps/meteor/tests/e2e/page-objects/omnichannel-units.ts +++ b/apps/meteor/tests/e2e/page-objects/omnichannel-units.ts @@ -43,6 +43,10 @@ export class OmnichannelUnits extends OmnichannelAdministration { return this.page.locator(`[role=option][value="${name}"]`); } + public selectOptionChip(name: string) { + return this.page.getByRole('option', { name }); + } + async selectDepartment({ name, _id }: { name: string; _id: string }) { await this.inputDepartments.click(); await this.inputDepartments.fill(name); diff --git a/apps/meteor/tests/end-to-end/api/miscellaneous.ts b/apps/meteor/tests/end-to-end/api/miscellaneous.ts index 613a874ecd8c..b8341f7c0994 100644 --- a/apps/meteor/tests/end-to-end/api/miscellaneous.ts +++ b/apps/meteor/tests/end-to-end/api/miscellaneous.ts @@ -5,7 +5,7 @@ import type { IInstance } from '@rocket.chat/rest-typings'; import { AssertionError, expect } from 'chai'; import { after, before, describe, it } from 'mocha'; -import { getCredentials, api, request, credentials } from '../../data/api-data'; +import { getCredentials, api, request, credentials, methodCall } from '../../data/api-data'; import { updatePermission, updateSetting } from '../../data/permissions.helper'; import { createRoom, deleteRoom } from '../../data/rooms.helper'; import { createTeam, deleteTeam } from '../../data/teams.helper'; @@ -703,4 +703,139 @@ describe('miscellaneous', () => { .end(done); }); }); + + describe('[/stdout.queue]', () => { + let testUser: TestUser; + let testUsername: string; + let testUserPassword: string; + before(async () => { + testUser = await createUser(); + testUsername = testUser.username; + testUserPassword = password; + await updateSetting('Log_Trace_Methods', true); + await updateSetting('Log_Level', '2'); + + // populate the logs by sending method calls + const populateLogsPromises = []; + populateLogsPromises.push( + request + .post(methodCall('getRoomRoles')) + .set(credentials) + .set('Cookie', `rc_token=${credentials['X-Auth-Token']}`) + .send({ + message: JSON.stringify({ + method: 'getRoomRoles', + params: ['GENERAL'], + id: 'id', + msg: 'method', + }), + }), + ); + + populateLogsPromises.push( + request + .post(methodCall('private-settings:get')) + .set(credentials) + .send({ + message: JSON.stringify({ + method: 'private-settings/get', + params: [ + { + $date: new Date().getTime(), + }, + ], + id: 'id', + msg: 'method', + }), + }), + ); + + populateLogsPromises.push( + request.post(api('login')).send({ + user: { + username: testUsername, + }, + password: testUserPassword, + }), + ); + + await Promise.all(populateLogsPromises); + }); + + after(async () => { + await Promise.all([updateSetting('Log_Trace_Methods', false), updateSetting('Log_Level', '0'), deleteUser(testUser)]); + }); + + it('if log trace enabled, x-auth-token should be redacted', async () => { + await request + .get(api('stdout.queue')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('queue').that.is.an('array'); + + const { queue } = res.body; + let foundRedactedToken = false; + + for (const log of queue) { + if (log.string.includes("'x-auth-token': '[redacted]'")) { + foundRedactedToken = true; + break; + } + } + + expect(foundRedactedToken).to.be.true; + }); + }); + + it('if log trace enabled, rc_token should be redacted', async () => { + await request + .get(api('stdout.queue')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('queue').that.is.an('array'); + + const { queue } = res.body; + let foundRedactedCookie = false; + + for (const log of queue) { + if (log.string.includes('rc_token=[redacted]')) { + foundRedactedCookie = true; + break; + } + } + + expect(foundRedactedCookie).to.be.true; + }); + }); + + it('should not return user token anywhere in the log stream', async () => { + await request + .get(api('stdout.queue')) + .set(credentials) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('queue').that.is.an('array'); + + const { queue } = res.body; + let foundTokenValue = false; + + for (const log of queue) { + if (log.string.includes(credentials['X-Auth-Token'])) { + foundTokenValue = true; + break; + } + } + + expect(foundTokenValue).to.be.false; + }); + }); + }); }); diff --git a/apps/meteor/tests/unit/app/lib/server/functions/getModifiedHttpHeaders.tests.ts b/apps/meteor/tests/unit/app/lib/server/functions/getModifiedHttpHeaders.tests.ts new file mode 100644 index 000000000000..5130bbe59a99 --- /dev/null +++ b/apps/meteor/tests/unit/app/lib/server/functions/getModifiedHttpHeaders.tests.ts @@ -0,0 +1,59 @@ +import { expect } from 'chai'; + +import { getModifiedHttpHeaders } from '../../../../../../app/lib/server/functions/getModifiedHttpHeaders'; + +describe('getModifiedHttpHeaders', () => { + it('should redact x-auth-token if present', () => { + const inputHeaders = { + 'x-auth-token': '12345', + 'some-other-header': 'value', + }; + const result = getModifiedHttpHeaders(inputHeaders); + expect(result['x-auth-token']).to.equal('[redacted]'); + expect(result['some-other-header']).to.equal('value'); + }); + + it('should not modify headers if x-auth-token is not present', () => { + const inputHeaders = { + 'some-other-header': 'value', + }; + const result = getModifiedHttpHeaders(inputHeaders); + expect(result).to.deep.equal(inputHeaders); + }); + + it('should redact rc_token in cookies if present', () => { + const inputHeaders = { + cookie: 'session_id=abc123; rc_token=98765; other_cookie=value', + }; + const expectedCookies = 'session_id=abc123; rc_token=[redacted]; other_cookie=value'; + const result = getModifiedHttpHeaders(inputHeaders); + expect(result.cookie).to.equal(expectedCookies); + }); + + it('should not modify cookies if rc_token is not present', () => { + const inputHeaders = { + cookie: 'session_id=abc123; other_cookie=value', + }; + const result = getModifiedHttpHeaders(inputHeaders); + expect(result.cookie).to.equal(inputHeaders.cookie); + }); + + it('should return headers unchanged if neither x-auth-token nor cookie are present', () => { + const inputHeaders = { + 'some-other-header': 'value', + }; + const result = getModifiedHttpHeaders(inputHeaders); + expect(result).to.deep.equal(inputHeaders); + }); + + it('should handle cases with both x-auth-token and rc_token in cookie', () => { + const inputHeaders = { + 'x-auth-token': '12345', + 'cookie': 'session_id=abc123; rc_token=98765; other_cookie=value', + }; + const expectedCookies = 'session_id=abc123; rc_token=[redacted]; other_cookie=value'; + const result = getModifiedHttpHeaders(inputHeaders); + expect(result['x-auth-token']).to.equal('[redacted]'); + expect(result.cookie).to.equal(expectedCookies); + }); +}); diff --git a/packages/ui-client/src/components/TooltipComponent.tsx b/packages/ui-client/src/components/TooltipComponent.tsx index 137ec913ec78..4a46e5536a53 100644 --- a/packages/ui-client/src/components/TooltipComponent.tsx +++ b/packages/ui-client/src/components/TooltipComponent.tsx @@ -12,7 +12,7 @@ export const TooltipComponent = ({ title, anchor }: TooltipComponentProps): Reac return ( - {title} + {title} ); }; diff --git a/yarn.lock b/yarn.lock index 84ca676c886c..93f2f41c7999 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8969,10 +8969,10 @@ __metadata: "@rocket.chat/icons": "*" "@rocket.chat/prettier-config": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": 5.0.0 - "@rocket.chat/ui-contexts": 9.0.0 + "@rocket.chat/ui-avatar": 5.0.1 + "@rocket.chat/ui-contexts": 9.0.1 "@rocket.chat/ui-kit": 0.36.0 - "@rocket.chat/ui-video-conf": 9.0.0 + "@rocket.chat/ui-video-conf": 9.0.1 "@tanstack/react-query": "*" react: "*" react-dom: "*" @@ -9061,8 +9061,8 @@ __metadata: "@rocket.chat/fuselage-tokens": "*" "@rocket.chat/message-parser": 0.31.29 "@rocket.chat/styled": "*" - "@rocket.chat/ui-client": 9.0.0 - "@rocket.chat/ui-contexts": 9.0.0 + "@rocket.chat/ui-client": 9.0.1 + "@rocket.chat/ui-contexts": 9.0.1 katex: "*" react: "*" languageName: unknown @@ -10282,7 +10282,7 @@ __metadata: typescript: ~5.3.3 peerDependencies: "@rocket.chat/fuselage": "*" - "@rocket.chat/ui-contexts": 9.0.0 + "@rocket.chat/ui-contexts": 9.0.1 react: ~17.0.2 languageName: unknown linkType: soft @@ -10335,7 +10335,7 @@ __metadata: "@rocket.chat/fuselage": "*" "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" - "@rocket.chat/ui-contexts": 9.0.0 + "@rocket.chat/ui-contexts": 9.0.1 react: ~17.0.2 languageName: unknown linkType: soft @@ -10511,8 +10511,8 @@ __metadata: "@rocket.chat/fuselage-hooks": "*" "@rocket.chat/icons": "*" "@rocket.chat/styled": "*" - "@rocket.chat/ui-avatar": 5.0.0 - "@rocket.chat/ui-contexts": 9.0.0 + "@rocket.chat/ui-avatar": 5.0.1 + "@rocket.chat/ui-contexts": 9.0.1 react: ^17.0.2 react-dom: ^17.0.2 languageName: unknown @@ -10602,7 +10602,7 @@ __metadata: peerDependencies: "@rocket.chat/layout": "*" "@rocket.chat/tools": 0.2.2 - "@rocket.chat/ui-contexts": 9.0.0 + "@rocket.chat/ui-contexts": 9.0.1 "@tanstack/react-query": "*" react: "*" react-hook-form: "*"