From df3e405a2b886d8598c6e6fda23a5350b28c6c1c Mon Sep 17 00:00:00 2001 From: yash-rajpal <58601732+yash-rajpal@users.noreply.github.com> Date: Wed, 12 May 2021 17:59:03 +0530 Subject: [PATCH 01/17] [FIX] Adding retentionEnabledDefault check before showing warning message (#20692) Co-authored-by: Tasso Evangelista --- client/hooks/useFormattedRelativeTime.ts | 14 ++++++++++++ .../InfoPanel/RetentionPolicyCallout.tsx | 12 ++++++---- .../contextualBar/Info/RoomInfo/RoomInfo.js | 3 ++- package-lock.json | 22 ++++++++++++------- 4 files changed, 38 insertions(+), 13 deletions(-) create mode 100644 client/hooks/useFormattedRelativeTime.ts diff --git a/client/hooks/useFormattedRelativeTime.ts b/client/hooks/useFormattedRelativeTime.ts new file mode 100644 index 000000000000..b2003c3825b7 --- /dev/null +++ b/client/hooks/useFormattedRelativeTime.ts @@ -0,0 +1,14 @@ +import moment from 'moment'; +import { useMemo } from 'react'; + +export const useFormattedRelativeTime = (timeMs: number): string => + useMemo(() => { + moment.relativeTimeThreshold('s', 60); + moment.relativeTimeThreshold('ss', 0); + moment.relativeTimeThreshold('m', 60); + moment.relativeTimeThreshold('h', 24); + moment.relativeTimeThreshold('d', 31); + moment.relativeTimeThreshold('M', 12); + + return moment.duration(timeMs).humanize(); + }, [timeMs]); diff --git a/client/views/InfoPanel/RetentionPolicyCallout.tsx b/client/views/InfoPanel/RetentionPolicyCallout.tsx index e97c3bcd4fc5..182e79ddc627 100644 --- a/client/views/InfoPanel/RetentionPolicyCallout.tsx +++ b/client/views/InfoPanel/RetentionPolicyCallout.tsx @@ -2,6 +2,7 @@ import { Callout } from '@rocket.chat/fuselage'; import React, { FC } from 'react'; import { useTranslation } from '../../contexts/TranslationContext'; +import { useFormattedRelativeTime } from '../../hooks/useFormattedRelativeTime'; type RetentionPolicyCalloutProps = { filesOnlyDefault: boolean; @@ -15,19 +16,22 @@ const RetentionPolicyCallout: FC = ({ maxAgeDefault, }) => { const t = useTranslation(); + + const time = useFormattedRelativeTime(maxAgeDefault * 1000 * 60 * 60 * 24); + return ( {filesOnlyDefault && excludePinnedDefault && ( -

{t('RetentionPolicy_RoomWarning_FilesOnly', { time: maxAgeDefault })}

+

{t('RetentionPolicy_RoomWarning_FilesOnly', { time })}

)} {filesOnlyDefault && !excludePinnedDefault && ( -

{t('RetentionPolicy_RoomWarning_UnpinnedFilesOnly', { time: maxAgeDefault })}

+

{t('RetentionPolicy_RoomWarning_UnpinnedFilesOnly', { time })}

)} {!filesOnlyDefault && excludePinnedDefault && ( -

{t('RetentionPolicy_RoomWarning', { time: maxAgeDefault })}

+

{t('RetentionPolicy_RoomWarning', { time })}

)} {!filesOnlyDefault && !excludePinnedDefault && ( -

{t('RetentionPolicy_RoomWarning_Unpinned', { time: maxAgeDefault })}

+

{t('RetentionPolicy_RoomWarning_Unpinned', { time })}

)}
); diff --git a/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.js b/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.js index 5d7b3fdd4282..ba40fc2881fb 100644 --- a/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.js +++ b/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.js @@ -37,6 +37,7 @@ function RoomInfo({ filesOnlyDefault, excludePinnedDefault, maxAgeDefault, + retentionEnabledDefault, } = retentionPolicy; const memoizedActions = useMemo( @@ -195,7 +196,7 @@ function RoomInfo({ )} - {retentionPolicyEnabled && ( + {retentionPolicyEnabled && retentionEnabledDefault && ( Date: Wed, 12 May 2021 18:01:25 +0530 Subject: [PATCH 02/17] [FIX] Adding Custom Fields to show on user info check (#20955) --- .../room/contextualBar/UserInfo/UserInfo.js | 13 +++++++------ .../UserInfo/UserInfoWithData.js | 19 +++++++++++++++++-- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/client/views/room/contextualBar/UserInfo/UserInfo.js b/client/views/room/contextualBar/UserInfo/UserInfo.js index 46c79b34a578..1ce43e11aeeb 100644 --- a/client/views/room/contextualBar/UserInfo/UserInfo.js +++ b/client/views/room/contextualBar/UserInfo/UserInfo.js @@ -129,13 +129,14 @@ function UserInfo({ )} - {customFields && - Object.entries(customFields).map(([label, value]) => ( - - {t(label)} - {value} + {customFields.map((customField) => + Object.values(customField)[0] ? ( + + {t(Object.keys(customField)[0])} + {Object.values(customField)[0]} - ))} + ) : null, + )} {t('Created_at')} diff --git a/client/views/room/contextualBar/UserInfo/UserInfoWithData.js b/client/views/room/contextualBar/UserInfo/UserInfoWithData.js index 119be5aaa273..8ba29be43859 100644 --- a/client/views/room/contextualBar/UserInfo/UserInfoWithData.js +++ b/client/views/room/contextualBar/UserInfo/UserInfoWithData.js @@ -40,8 +40,23 @@ function UserInfoWithData({ ]), ); + const customFieldsToShowSetting = useSetting('Accounts_CustomFieldsToShowInUserInfo'); + const user = useMemo(() => { const { user } = value || { user: {} }; + + const customFieldsToShowObj = JSON.parse(customFieldsToShowSetting); + + const customFieldsToShow = customFieldsToShowObj + ? Object.values(customFieldsToShowObj).map((value) => { + const role = Object.values(value); + const roleNameToShow = Object.keys(value); + const customField = {}; + customField[roleNameToShow] = user?.customFields[role]; + return customField; + }) + : []; + const { _id, name, @@ -63,7 +78,7 @@ function UserInfoWithData({ getRoles(roles).map((role, index) => {role}), bio, phone: user.phone, - customFields: user.customFields, + customFields: customFieldsToShow, verified: getUserEmailVerified(user), email: getUserEmailAddress(user), utcOffset, @@ -72,7 +87,7 @@ function UserInfoWithData({ customStatus: statusText, nickname, }; - }, [value, showRealNames, getRoles]); + }, [value, customFieldsToShowSetting, showRealNames, getRoles]); return ( <> From aa3d161ce8f76d02560483c711e80a3b389b396e Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Wed, 12 May 2021 09:35:29 -0300 Subject: [PATCH 03/17] [FIXf] Parent Room Tag Overlapping (#22009) --- client/components/Header/HeaderLink.tsx | 1 + client/components/Header/HeaderTag.tsx | 4 ++-- client/views/room/Header/RoomHeader.tsx | 4 +++- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/client/components/Header/HeaderLink.tsx b/client/components/Header/HeaderLink.tsx index 59f55e370b56..1ed9e7723c75 100644 --- a/client/components/Header/HeaderLink.tsx +++ b/client/components/Header/HeaderLink.tsx @@ -7,6 +7,7 @@ const HeaderLink: FC> = (props) => ( > = ({ children, ...props }) => ( - + - + {children} diff --git a/client/views/room/Header/RoomHeader.tsx b/client/views/room/Header/RoomHeader.tsx index 94d30aa99a6f..0e9f94ba63ad 100644 --- a/client/views/room/Header/RoomHeader.tsx +++ b/client/views/room/Header/RoomHeader.tsx @@ -43,7 +43,9 @@ const RoomHeader: FC = ({ room, topic = '', slots = {} }) => ( - {topic && } + {topic && ( + + )} From e55a7a784b506d7e27caa0ae204fa17020eab6b7 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Wed, 12 May 2021 09:37:16 -0300 Subject: [PATCH 04/17] [FIX] Missing proper permissions on Teams Channels (#21946) --- .../contextualBar/channels/RoomActions.js | 29 +++++++++++++------ .../channels/TeamsChannelItem.js | 24 ++++++++++----- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/client/views/teams/contextualBar/channels/RoomActions.js b/client/views/teams/contextualBar/channels/RoomActions.js index 2a5d14581aec..7b177d6fcf8c 100644 --- a/client/views/teams/contextualBar/channels/RoomActions.js +++ b/client/views/teams/contextualBar/channels/RoomActions.js @@ -3,6 +3,7 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import React, { useMemo } from 'react'; import { roomTypes } from '../../../../../app/utils/client'; +import { usePermission } from '../../../../contexts/AuthorizationContext'; import { useSetModal } from '../../../../contexts/ModalContext'; import { useToastMessageDispatch } from '../../../../contexts/ToastMessagesContext'; import { useTranslation } from '../../../../contexts/TranslationContext'; @@ -23,8 +24,15 @@ const useReactModal = (Component, props) => { const RoomActions = ({ room, reload }) => { const t = useTranslation(); + const rid = room._id; + const type = room.t; + const dispatchToastMessage = useToastMessageDispatch(); + const canDeleteTeamChannel = usePermission(type === 'c' ? 'delete-c' : 'delete-p', rid); + const canEditTeamChannel = usePermission('edit-team-channel'); + const canRemoveTeamChannel = usePermission('remove-team-channel'); + const updateRoomEndpoint = useEndpointActionExperimental('POST', 'teams.updateRoom'); const removeRoomEndpoint = useEndpointActionExperimental( 'POST', @@ -83,7 +91,7 @@ const RoomActions = ({ room, reload }) => { const AutoJoinAction = async () => { try { await updateRoomEndpoint({ - roomId: room._id, + roomId: rid, isDefault: !room.teamDefault, }); } catch (error) { @@ -94,38 +102,41 @@ const RoomActions = ({ room, reload }) => { }; return [ - { + canEditTeamChannel && { label: { label: t('Team_Auto-join'), - icon: room.t === 'c' ? 'hash' : 'hashtag-lock', + icon: type === 'c' ? 'hash' : 'hashtag-lock', }, action: AutoJoinAction, }, - { + canRemoveTeamChannel && { label: { label: t('Team_Remove_from_team'), icon: 'cross', }, action: RemoveFromTeamAction, }, - { + canDeleteTeamChannel && { label: { label: t('Delete'), icon: 'trash', }, action: DeleteChannelAction, }, - ]; + ].filter(Boolean); }, [ DeleteChannelAction, RemoveFromTeamAction, - room._id, - room.t, + rid, + type, room.teamDefault, t, updateRoomEndpoint, reload, dispatchToastMessage, + canDeleteTeamChannel, + canRemoveTeamChannel, + canEditTeamChannel, ]); return ( @@ -144,7 +155,7 @@ const RoomActions = ({ room, reload }) => { ) } - options={menuOptions} + options={(canEditTeamChannel || canRemoveTeamChannel || canDeleteTeamChannel) && menuOptions} /> ); }; diff --git a/client/views/teams/contextualBar/channels/TeamsChannelItem.js b/client/views/teams/contextualBar/channels/TeamsChannelItem.js index fb9dd2ff39b2..51e177c16735 100644 --- a/client/views/teams/contextualBar/channels/TeamsChannelItem.js +++ b/client/views/teams/contextualBar/channels/TeamsChannelItem.js @@ -4,14 +4,22 @@ import React, { useState } from 'react'; import { roomTypes } from '../../../../../app/utils/client'; import RoomAvatar from '../../../../components/avatar/RoomAvatar'; +import { usePermission } from '../../../../contexts/AuthorizationContext'; import { useTranslation } from '../../../../contexts/TranslationContext'; import { usePreventProgation } from '../../../../hooks/usePreventProgation'; import RoomActions from './RoomActions'; const TeamsChannelItem = ({ room, onClickView, reload }) => { const t = useTranslation(); + const rid = room._id; + const type = room.t; + const [showButton, setShowButton] = useState(); + const canRemoveTeamChannel = usePermission('remove-team-channel'); + const canEditTeamChannel = usePermission('edit-team-channel'); + const canDeleteTeamChannel = usePermission(type === 'c' ? 'delete-c' : 'delete-p', rid); + const isReduceMotionEnabled = usePrefersReducedMotion(); const handleMenuEvent = { [isReduceMotionEnabled ? 'onMouseEnter' : 'onTransitionEnd']: setShowButton, @@ -39,13 +47,15 @@ const TeamsChannelItem = ({ room, onClickView, reload }) => { )} - - {showButton ? ( - - ) : ( - - )} - + {(canRemoveTeamChannel || canEditTeamChannel || canDeleteTeamChannel) && ( + + {showButton ? ( + + ) : ( + + )} + + )} ); }; From f3f3d909c49b3585fa1db6a6b9a9343830f1efa2 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Wed, 12 May 2021 09:49:28 -0300 Subject: [PATCH 05/17] [IMPROVE] Replace method to API Endpoint on Prune Messages (#21836) --- app/api/server/v1/rooms.js | 4 +-- app/ui-clean-history/client/index.js | 2 +- .../client/lib/{startup.ts => tabBar.ts} | 2 +- client/components/DeleteWarningModal.tsx | 20 +++++------ .../PruneMessages/DialogPruneMessages.js | 14 -------- .../PruneMessages/PruneMessagesWithData.js | 34 ++++++++++--------- 6 files changed, 31 insertions(+), 45 deletions(-) rename app/ui-clean-history/client/lib/{startup.ts => tabBar.ts} (92%) delete mode 100644 client/views/room/contextualBar/PruneMessages/DialogPruneMessages.js diff --git a/app/api/server/v1/rooms.js b/app/api/server/v1/rooms.js index 0f7d7f6ad4a8..13933c40189e 100644 --- a/app/api/server/v1/rooms.js +++ b/app/api/server/v1/rooms.js @@ -194,7 +194,7 @@ API.v1.addRoute('rooms.cleanHistory', { authRequired: true }, { const inclusive = this.bodyParams.inclusive || false; - Meteor.runAsUser(this.userId, () => Meteor.call('cleanRoomHistory', { + const count = Meteor.runAsUser(this.userId, () => Meteor.call('cleanRoomHistory', { roomId: findResult._id, latest, oldest, @@ -207,7 +207,7 @@ API.v1.addRoute('rooms.cleanHistory', { authRequired: true }, { fromUsers: this.bodyParams.users, })); - return API.v1.success(); + return API.v1.success({ count }); }, }); diff --git a/app/ui-clean-history/client/index.js b/app/ui-clean-history/client/index.js index 6d726d28bbb1..682594792f81 100644 --- a/app/ui-clean-history/client/index.js +++ b/app/ui-clean-history/client/index.js @@ -1 +1 @@ -import './lib/startup'; +import './lib/tabBar'; diff --git a/app/ui-clean-history/client/lib/startup.ts b/app/ui-clean-history/client/lib/tabBar.ts similarity index 92% rename from app/ui-clean-history/client/lib/startup.ts rename to app/ui-clean-history/client/lib/tabBar.ts index 96b9b8343b88..8f743602958a 100644 --- a/app/ui-clean-history/client/lib/startup.ts +++ b/app/ui-clean-history/client/lib/tabBar.ts @@ -9,7 +9,7 @@ const template = lazy(() => import('../../../../client/views/room/contextualBar/ addAction('clean-history', ({ room }) => { const hasPermission = usePermission('clean-channel-history', room._id); return useMemo(() => (hasPermission ? { - groups: ['channel', 'group', 'direct'], + groups: ['channel', 'group', 'team', 'direct'], id: 'clean-history', full: true, title: 'Prune_Messages', diff --git a/client/components/DeleteWarningModal.tsx b/client/components/DeleteWarningModal.tsx index 28392c5000d8..93a7545c5fa7 100644 --- a/client/components/DeleteWarningModal.tsx +++ b/client/components/DeleteWarningModal.tsx @@ -1,4 +1,4 @@ -import { Box, Button, ButtonGroup, Icon, Modal } from '@rocket.chat/fuselage'; +import { Button, ButtonGroup, Icon, Modal } from '@rocket.chat/fuselage'; import React, { FC } from 'react'; import { useTranslation } from '../contexts/TranslationContext'; @@ -31,16 +31,14 @@ const DeleteWarningModal: FC = ({ {children} - - - - - - + + + + ); diff --git a/client/views/room/contextualBar/PruneMessages/DialogPruneMessages.js b/client/views/room/contextualBar/PruneMessages/DialogPruneMessages.js deleted file mode 100644 index 9e63dc34b9aa..000000000000 --- a/client/views/room/contextualBar/PruneMessages/DialogPruneMessages.js +++ /dev/null @@ -1,14 +0,0 @@ -import { Box } from '@rocket.chat/fuselage'; -import React from 'react'; - -import DeleteWarningModal from '../../../../components/DeleteWarningModal'; - -const DialogPruneMessages = ({ children, ...props }) => ( - - - {children} - - -); - -export default DialogPruneMessages; diff --git a/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.js b/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.js index c9d3defe3a28..681aff678fc6 100644 --- a/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.js +++ b/client/views/room/contextualBar/PruneMessages/PruneMessagesWithData.js @@ -2,13 +2,13 @@ import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import moment from 'moment'; import React, { useCallback, useEffect, useState } from 'react'; +import GenericModal from '../../../../components/GenericModal'; import { useSetModal } from '../../../../contexts/ModalContext'; -import { useMethod } from '../../../../contexts/ServerContext'; +import { useEndpoint } from '../../../../contexts/ServerContext'; import { useToastMessageDispatch } from '../../../../contexts/ToastMessagesContext'; import { useTranslation } from '../../../../contexts/TranslationContext'; import { useUserRoom } from '../../../../contexts/UserContext'; import { useForm } from '../../../../hooks/useForm'; -import DialogPruneMessages from './DialogPruneMessages'; import PruneMessages from './PruneMessages'; const getTimeZoneOffset = function () { @@ -43,13 +43,13 @@ const PruneMessagesWithData = ({ rid, tabBar }) => { const onClickClose = useMutableCallback(() => tabBar && tabBar.close()); const closeModal = useCallback(() => setModal(null), [setModal]); const dispatchToastMessage = useToastMessageDispatch(); - const pruneMessages = useMethod('cleanRoomHistory'); + const pruneMessages = useEndpoint('POST', 'rooms.cleanHistory'); const [fromDate, setFromDate] = useState(new Date('0001-01-01T00:00:00Z')); const [toDate, setToDate] = useState(new Date('9999-12-31T23:59:59Z')); const [callOutText, setCallOutText] = useState(); const [validateText, setValidateText] = useState(); - const [count, setCount] = useState(0); + const [counter, setCounter] = useState(0); const { values, handlers, reset } = useForm(initialValues); const { @@ -90,32 +90,32 @@ const PruneMessagesWithData = ({ rid, tabBar }) => { const handlePrune = useMutableCallback(async () => { const limit = 2000; - let result; try { - if (count === limit) { + if (counter === limit) { return; } - result = await pruneMessages({ + const { count } = await pruneMessages({ roomId: rid, latest: toDate, oldest: fromDate, inclusive, limit, excludePinned: pinned, - ignoreDiscussion: discussion, filesOnly: attached, - fromUsers: users, + ignoreDiscussion: discussion, ignoreThreads: threads, + fromUsers: users, }); - setCount(result); - if (result < 1) { + setCounter(count); + + if (count < 1) { throw new Error(t('No_messages_found_to_prune')); } - dispatchToastMessage({ type: 'success', message: `${result} ${t('messages_pruned')}` }); + dispatchToastMessage({ type: 'success', message: `${count} ${t('messages_pruned')}` }); closeModal(); reset(); } catch (error) { @@ -126,13 +126,15 @@ const PruneMessagesWithData = ({ rid, tabBar }) => { const handleModal = () => { setModal( - {t('Prune_Modal')} - , + , ); }; From 931bfe312027ee00e80855af8633dcb04246637e Mon Sep 17 00:00:00 2001 From: Thassio Victor Date: Wed, 12 May 2021 09:54:08 -0300 Subject: [PATCH 06/17] [FIX] Unable to update app manually (#21215) Co-authored-by: Douglas Gubert --- app/apps/server/communication/rest.js | 14 ++++- client/views/admin/apps/AppInstallPage.js | 62 +++++++++++++++---- client/views/admin/apps/AppUpdateModal.tsx | 48 ++++++++++++++ .../apps/lib/getManifestFromZippedApp.ts | 48 ++++++++++++++ .../apps/lib/getPermissionsFromZippedApp.js | 54 ---------------- packages/rocketchat-i18n/i18n/en.i18n.json | 2 + 6 files changed, 159 insertions(+), 69 deletions(-) create mode 100644 client/views/admin/apps/AppUpdateModal.tsx create mode 100644 client/views/admin/apps/lib/getManifestFromZippedApp.ts delete mode 100644 client/views/admin/apps/lib/getPermissionsFromZippedApp.js diff --git a/app/apps/server/communication/rest.js b/app/apps/server/communication/rest.js index 06c5520b58cb..288dfc688348 100644 --- a/app/apps/server/communication/rest.js +++ b/app/apps/server/communication/rest.js @@ -425,6 +425,7 @@ export class AppsRestApi { }, post() { let buff; + let permissionsGranted; if (this.bodyParams.url) { if (settings.get('Apps_Framework_Development_Mode') !== true) { @@ -470,14 +471,23 @@ export class AppsRestApi { return API.v1.failure({ error: 'Direct updating of an App is disabled.' }); } - buff = multipartFormDataHandler(this.request)?.app; + const formData = multipartFormDataHandler(this.request); + buff = formData?.app; + permissionsGranted = (() => { + try { + const permissions = JSON.parse(formData?.permissions || ''); + return permissions.length ? permissions : undefined; + } catch { + return undefined; + } + })(); } if (!buff) { return API.v1.failure({ error: 'Failed to get a file to install for the App. ' }); } - const aff = Promise.await(manager.update(buff, this.bodyParams.permissionsGranted)); + const aff = Promise.await(manager.update(buff, permissionsGranted)); const info = aff.getAppInfo(); if (aff.hasStorageError()) { diff --git a/client/views/admin/apps/AppInstallPage.js b/client/views/admin/apps/AppInstallPage.js index 346830d85fdf..ebf8a71b02aa 100644 --- a/client/views/admin/apps/AppInstallPage.js +++ b/client/views/admin/apps/AppInstallPage.js @@ -9,6 +9,7 @@ import { } from '@rocket.chat/fuselage'; import React, { useCallback, useEffect, useState } from 'react'; +import { Apps } from '../../../../app/apps/client/orchestrator'; import Page from '../../../components/Page'; import { useSetModal } from '../../../contexts/ModalContext'; import { useRoute, useQueryStringParameter } from '../../../contexts/RouterContext'; @@ -17,8 +18,9 @@ import { useTranslation } from '../../../contexts/TranslationContext'; import { useFileInput } from '../../../hooks/useFileInput'; import { useForm } from '../../../hooks/useForm'; import AppPermissionsReviewModal from './AppPermissionsReviewModal'; +import AppUpdateModal from './AppUpdateModal'; import { handleInstallError } from './helpers'; -import { getPermissionsFromZippedApp } from './lib/getPermissionsFromZippedApp'; +import { getManifestFromZippedApp } from './lib/getManifestFromZippedApp'; const placeholderUrl = 'https://rocket.chat/apps/package.zip'; @@ -36,6 +38,7 @@ function AppInstallPage() { const endpointAddress = appId ? `/apps/${appId}` : '/apps'; const downloadApp = useEndpoint('POST', endpointAddress); const uploadApp = useUpload(endpointAddress); + const uploadUpdateApp = useUpload(`${endpointAddress}/update`); const { values, handlers } = useForm({ file: {}, @@ -54,12 +57,19 @@ function AppInstallPage() { const [handleUploadButtonClick] = useFileInput(handleFile, 'app'); - const sendFile = async (permissionsGranted, appFile) => { + const sendFile = async (permissionsGranted, appFile, appId) => { + let app; const fileData = new FormData(); fileData.append('app', appFile, appFile.name); fileData.append('permissions', JSON.stringify(permissionsGranted)); - const { app } = await uploadApp(fileData); - appsRoute.push({ context: 'details', id: app.id }); + + if (appId) { + await uploadUpdateApp(fileData); + } else { + app = await uploadApp(fileData); + } + + appsRoute.push({ context: 'details', id: appId || app.app.id }); setModal(null); }; @@ -68,32 +78,58 @@ function AppInstallPage() { setModal(null); }, [setInstalling, setModal]); + const isAppInstalled = async (appId) => { + try { + const app = await Apps.getApp(appId); + return !!app || false; + } catch (e) { + return false; + } + }; + + const handleAppPermissionsReview = async (permissions, appFile, appId) => { + if (!permissions || permissions.length === 0) { + await sendFile(permissions, appFile, appId); + } else { + setModal( + sendFile(permissions, appFile, appId)} + />, + ); + } + }; + const install = async () => { setInstalling(true); try { - let permissions; + let manifest; let appFile; if (url) { const { buff } = await downloadApp({ url, downloadOnly: true }); const fileData = Uint8Array.from(buff.data); - permissions = await getPermissionsFromZippedApp(fileData); + manifest = await getManifestFromZippedApp(fileData); appFile = new File([fileData], 'app.zip', { type: 'application/zip' }); } else { appFile = file; - permissions = await getPermissionsFromZippedApp(appFile); + manifest = await getManifestFromZippedApp(appFile); } - if (!permissions || permissions.length === 0) { - await sendFile(permissions, appFile); - } else { + const { permissions, id } = manifest; + + const isInstalled = await isAppInstalled(id); + + if (isInstalled) { setModal( - sendFile(permissions, appFile)} + confirm={() => handleAppPermissionsReview(permissions, appFile, id)} />, ); + } else { + await handleAppPermissionsReview(permissions, appFile); } } catch (error) { handleInstallError(error); diff --git a/client/views/admin/apps/AppUpdateModal.tsx b/client/views/admin/apps/AppUpdateModal.tsx new file mode 100644 index 000000000000..88ebcadc3e34 --- /dev/null +++ b/client/views/admin/apps/AppUpdateModal.tsx @@ -0,0 +1,48 @@ +import { Button, ButtonGroup, Icon, Modal } from '@rocket.chat/fuselage'; +import React, { FC } from 'react'; + +import { useTranslation } from '../../../contexts/TranslationContext'; + +type AppUpdateModalProps = { + confirm: () => void; + cancel: () => void; +}; + +const AppUpdateModal: FC = ({ confirm, cancel, ...props }) => { + const t = useTranslation(); + + const handleCloseButtonClick = (): void => { + cancel(); + }; + + const handleCancelButtonClick = (): void => { + cancel(); + }; + + const handleConfirmButtonClick = (): void => { + confirm(); + }; + + return ( + + + + {t('Apps_Manual_Update_Modal_Title')} + + + {t('Apps_Manual_Update_Modal_Body')} + + + + + + + + ); +}; + +export default AppUpdateModal; diff --git a/client/views/admin/apps/lib/getManifestFromZippedApp.ts b/client/views/admin/apps/lib/getManifestFromZippedApp.ts new file mode 100644 index 000000000000..d0da8edcf720 --- /dev/null +++ b/client/views/admin/apps/lib/getManifestFromZippedApp.ts @@ -0,0 +1,48 @@ +import { unzipSync, strFromU8 } from 'fflate'; + +type Uint8ArrayObject = { [fileName: string]: Uint8Array }; +type AppManifestSchema = { [key: string]: string }; + +async function fileToUint8Array(file: File): Promise { + return new Promise((resolve, reject) => { + const fileReader = new FileReader(); + fileReader.onload = (e): void => + resolve(new Uint8Array((e.target as any).result as Uint8Array)); + fileReader.onerror = (e): void => reject(e); + fileReader.readAsArrayBuffer(file); + }); +} + +function unzipAppBuffer(zippedAppBuffer: Uint8Array): Uint8ArrayObject { + return unzipSync(zippedAppBuffer); +} + +function getAppManifest(unzippedAppBuffer: Uint8ArrayObject): AppManifestSchema { + if (!unzippedAppBuffer['app.json']) { + throw new Error('No app.json file found in the zip'); + } + + try { + return JSON.parse(strFromU8(unzippedAppBuffer['app.json'])); + } catch (e) { + throw new Error(`Failed to parse app.json: ${e.message}`); + } +} + +async function unzipZippedApp(zippedApp: File | Uint8Array): Promise { + try { + if (zippedApp instanceof File) { + zippedApp = await fileToUint8Array(zippedApp); + } + + return unzipAppBuffer(zippedApp); + } catch (e) { + console.error(e); + throw e; + } +} + +export async function getManifestFromZippedApp(zippedApp: File): Promise { + const unzippedBuffer = await unzipZippedApp(zippedApp); + return getAppManifest(unzippedBuffer); +} diff --git a/client/views/admin/apps/lib/getPermissionsFromZippedApp.js b/client/views/admin/apps/lib/getPermissionsFromZippedApp.js deleted file mode 100644 index 58cecae6848b..000000000000 --- a/client/views/admin/apps/lib/getPermissionsFromZippedApp.js +++ /dev/null @@ -1,54 +0,0 @@ -import { unzipSync, strFromU8 } from 'fflate'; - -async function fileToUint8Array(file) { - return new Promise((resolve, reject) => { - const fileReader = new FileReader(); - fileReader.onload = (e) => resolve(new Uint8Array(e.target.result)); - fileReader.onerror = (e) => reject(e); - fileReader.readAsArrayBuffer(file); - }); -} - -function unzipAppBuffer(zippedAppBuffer) { - return unzipSync(zippedAppBuffer); -} - -function getAppManifest(unzippedAppBuffer) { - if (!unzippedAppBuffer['app.json']) { - throw new Error('No app.json file found in the zip'); - } - - try { - return JSON.parse(strFromU8(unzippedAppBuffer['app.json'])); - } catch (e) { - throw new Error('Failed to parse app.json', e); - } -} - -function getPermissionsFromManifest(manifest) { - if (!manifest.permissions) { - return undefined; - } - - if (!Array.isArray(manifest.permissions)) { - throw new Error('The "permissions" property from app.json is invalid'); - } - - return manifest.permissions; -} - -export async function getPermissionsFromZippedApp(zippedApp) { - try { - if (zippedApp instanceof File) { - zippedApp = await fileToUint8Array(zippedApp); - } - - const unzippedBuffer = unzipAppBuffer(zippedApp); - const manifest = getAppManifest(unzippedBuffer); - const permissions = getPermissionsFromManifest(manifest); - return permissions; - } catch (e) { - console.error(e); - throw e; - } -} diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index 7748b8af6da7..b4d179a68e2e 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -500,6 +500,8 @@ "Apps_Permissions_scheduler": "Register and maintain scheduled jobs", "Apps_Permissions_ui_interact": "Interact with the UI", "Apps_Settings": "App's Settings", + "Apps_Manual_Update_Modal_Title": "This app is already installed", + "Apps_Manual_Update_Modal_Body": "Do you want to update it?", "Apps_User_Already_Exists": "The username \"__username__\" is already being used. Rename or remove the user using it to install this App", "Apps_WhatIsIt": "Apps: What Are They?", "Apps_WhatIsIt_paragraph1": "A new icon in the administration area! What does this mean and what are Apps?", From e96a0c2a02e17a52dfaac9601f680016dbb62d67 Mon Sep 17 00:00:00 2001 From: yash-rajpal <58601732+yash-rajpal@users.noreply.github.com> Date: Wed, 12 May 2021 20:50:36 +0530 Subject: [PATCH 07/17] [FIX] Adding permission 'add-team-channel' for Team Channels Contextual bar (#21591) --- client/views/teams/contextualBar/channels/TeamsChannels.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/client/views/teams/contextualBar/channels/TeamsChannels.js b/client/views/teams/contextualBar/channels/TeamsChannels.js index 6394d941f690..b7aa7c14d971 100644 --- a/client/views/teams/contextualBar/channels/TeamsChannels.js +++ b/client/views/teams/contextualBar/channels/TeamsChannels.js @@ -6,6 +6,7 @@ import { import React, { useCallback, useMemo, useState } from 'react'; import { roomTypes } from '../../../../../app/utils/client'; +import { usePermission } from '../../../../contexts/AuthorizationContext'; import { useSetModal } from '../../../../contexts/ModalContext'; import { useRecordList } from '../../../../hooks/lists/useRecordList'; import { AsyncStatePhase } from '../../../../lib/asyncState'; @@ -49,6 +50,7 @@ const TeamsChannels = ({ teamId }) => { setText(event.currentTarget.value); }, []); + const canAddExistingTeam = usePermission('add-team-channel'); const addExisting = useReactModal(AddExistingModal, { teamId, reload }); const createNew = useReactModal(CreateChannelWithData, { teamId, reload }); @@ -80,8 +82,8 @@ const TeamsChannels = ({ teamId }) => { channels={items} total={total} onClickClose={onClickClose} - onClickAddExisting={addExisting} - onClickCreateNew={createNew} + onClickAddExisting={canAddExistingTeam && addExisting} + onClickCreateNew={canAddExistingTeam && createNew} onClickView={viewRoom} loadMoreItems={loadMoreItems} reload={reload} From b15f722e0a1cce4936437cf64905f99da740c920 Mon Sep 17 00:00:00 2001 From: savish28 <32800267+savish28@users.noreply.github.com> Date: Wed, 12 May 2021 21:13:00 +0530 Subject: [PATCH 08/17] [FIX] Dismiss button for save your encryption password dialog Issue#13557 (#19872) Co-authored-by: Tasso Evangelista --- app/e2e/client/rocketchat.e2e.js | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/app/e2e/client/rocketchat.e2e.js b/app/e2e/client/rocketchat.e2e.js index a1d19ee45947..772cb1cf17c5 100644 --- a/app/e2e/client/rocketchat.e2e.js +++ b/app/e2e/client/rocketchat.e2e.js @@ -294,8 +294,6 @@ class E2E extends Emitter { async requestPassword() { return new Promise((resolve) => { - let showAlert; - const showModal = () => { modal.open({ title: TAPi18n.__('Enter_E2E_password_to_decode_your_key'), @@ -314,11 +312,11 @@ class E2E extends Emitter { } }, () => { failedToDecodeKey = false; - showAlert(); + this.closeAlert(); }); }; - showAlert = () => { + const showAlert = () => { this.openAlert({ title: TAPi18n.__('Enter_your_E2E_password'), html: TAPi18n.__('Click_here_to_enter_your_encryption_password'), From 8ad4a8dbab5b0432094c9d8c368800dd23095213 Mon Sep 17 00:00:00 2001 From: Tiago Evangelista Pinto Date: Wed, 12 May 2021 12:48:27 -0300 Subject: [PATCH 09/17] [FIX] Close stream properly at Omnichannel room when move to queue (#22015) Co-authored-by: Guilherme Gazzo --- app/ui-cached-collection/client/models/CachedCollection.js | 3 ++- .../room/Header/Omnichannel/QuickActions/QuickActions.tsx | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/ui-cached-collection/client/models/CachedCollection.js b/app/ui-cached-collection/client/models/CachedCollection.js index 107024926b70..1ef9d99624c7 100644 --- a/app/ui-cached-collection/client/models/CachedCollection.js +++ b/app/ui-cached-collection/client/models/CachedCollection.js @@ -282,7 +282,8 @@ export class CachedCollection extends Emitter { }); } if (room) { - RoomManager.close(room.t + room.name); + room.name && RoomManager.close(room.t + room.name); + !room.name && RoomManager.close(room.t + room._id); } this.collection.remove(record._id); } else { diff --git a/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx b/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx index 6b5a373fd20e..810c1b78858e 100644 --- a/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx +++ b/client/views/room/Header/Omnichannel/QuickActions/QuickActions.tsx @@ -13,6 +13,7 @@ import React, { } from 'react'; import toastr from 'toastr'; +import { RoomManager } from '../../../../../../app/ui-utils/client'; import { handleError } from '../../../../../../app/utils/client'; import { IRoom } from '../../../../../../definition/IRoom'; import PlaceChatOnHoldModal from '../../../../../../ee/app/livechat-enterprise/client/components/modals/PlaceChatOnHoldModal'; @@ -92,8 +93,7 @@ const QuickActions: FC = ({ room, className }) => { try { await requestTranscript(rid, email, subject); closeModal(); - Session.set('openedRoom', null); - FlowRouter.go('/home'); + RoomManager.close(`l${rid}`); toastr.success(t('Livechat_transcript_has_been_requested')); } catch (error) { handleError(error); From 7482170671a4494aa519f12eca393e998f3e74b5 Mon Sep 17 00:00:00 2001 From: gabriellsh <40830821+gabriellsh@users.noreply.github.com> Date: Wed, 12 May 2021 14:34:29 -0300 Subject: [PATCH 10/17] [FIX] Wrong icon on "Move to team" option in the channel info actions (#21944) Co-authored-by: dougfabris --- client/views/room/contextualBar/Info/RoomInfo/RoomInfo.js | 2 +- package-lock.json | 6 +++--- package.json | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.js b/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.js index ba40fc2881fb..ff4595a837fa 100644 --- a/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.js +++ b/client/views/room/contextualBar/Info/RoomInfo/RoomInfo.js @@ -66,7 +66,7 @@ function RoomInfo({ ...(onClickMoveToTeam && { move: { label: t('Teams_move_channel_to_team'), - icon: 'team', + icon: 'team-arrow-right', action: onClickMoveToTeam, }, }), diff --git a/package-lock.json b/package-lock.json index b16ac9f8065c..255ed9c9f1cd 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6323,9 +6323,9 @@ } }, "@rocket.chat/fuselage": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@rocket.chat/fuselage/-/fuselage-0.24.0.tgz", - "integrity": "sha512-VcAJGVyhKicq/N62e3m/MnoMIdlwRaAJDPDsQhRzCLZNYCYz0RCobTdQ7AHLBFkJh3UTizXVGYAYGP2IVJaA+g==", + "version": "0.6.3-dev.248", + "resolved": "https://registry.npmjs.org/@rocket.chat/fuselage/-/fuselage-0.6.3-dev.248.tgz", + "integrity": "sha512-moFwib95YoZkCrnfBnLpVr7/PJ0qgJhVCxseyRMQ3TF4utOdoawiiEKhgpWE7WMK0lzXkOhEDgjc/1iykPImNw==", "requires": { "@rocket.chat/css-in-js": "^0.24.0", "@rocket.chat/fuselage-tokens": "^0.24.0", diff --git a/package.json b/package.json index 2951d669c8de..3ba714ce2c4e 100644 --- a/package.json +++ b/package.json @@ -143,7 +143,7 @@ "@rocket.chat/apps-engine": "1.25.0", "@rocket.chat/css-in-js": "^0.24.0", "@rocket.chat/emitter": "^0.24.0", - "@rocket.chat/fuselage": "^0.24.0", + "@rocket.chat/fuselage": "^0.6.3-dev.248", "@rocket.chat/fuselage-hooks": "^0.24.0", "@rocket.chat/fuselage-polyfills": "^0.24.0", "@rocket.chat/fuselage-tokens": "^0.24.0", From 309b6c6d83af4f959ab0aa2b9cd6e2bd937786a8 Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Wed, 12 May 2021 18:10:12 -0300 Subject: [PATCH 11/17] regression: UserInfoTab Broken (#22019) --- .../room/contextualBar/UserInfo/UserInfo.js | 16 +++++++++++++++- .../contextualBar/UserInfo/UserInfoWithData.js | 18 ++---------------- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/client/views/room/contextualBar/UserInfo/UserInfo.js b/client/views/room/contextualBar/UserInfo/UserInfo.js index 1ce43e11aeeb..a28516342d77 100644 --- a/client/views/room/contextualBar/UserInfo/UserInfo.js +++ b/client/views/room/contextualBar/UserInfo/UserInfo.js @@ -5,6 +5,7 @@ import MarkdownText from '../../../../components/MarkdownText'; import UTCClock from '../../../../components/UTCClock'; import UserCard from '../../../../components/UserCard'; import VerticalBar from '../../../../components/VerticalBar'; +import { useSetting } from '../../../../contexts/SettingsContext'; import { useTranslation } from '../../../../contexts/TranslationContext'; import { useTimeAgo } from '../../../../hooks/useTimeAgo'; import InfoPanel from '../../../InfoPanel'; @@ -34,6 +35,19 @@ function UserInfo({ const timeAgo = useTimeAgo(); + const customFieldsToShowSetting = useSetting('Accounts_CustomFieldsToShowInUserInfo'); + const customFieldsToShowObj = JSON.parse(customFieldsToShowSetting); + + const customFieldsToShow = customFieldsToShowObj + ? Object.values(customFieldsToShowObj).map((value) => { + const role = Object.values(value); + const roleNameToShow = Object.keys(value); + const customField = {}; + customField[roleNameToShow] = customFields[role]; + return customField; + }) + : []; + return ( @@ -129,7 +143,7 @@ function UserInfo({ )} - {customFields.map((customField) => + {customFieldsToShow.map((customField) => Object.values(customField)[0] ? ( {t(Object.keys(customField)[0])} diff --git a/client/views/room/contextualBar/UserInfo/UserInfoWithData.js b/client/views/room/contextualBar/UserInfo/UserInfoWithData.js index 8ba29be43859..b723f6b5fad7 100644 --- a/client/views/room/contextualBar/UserInfo/UserInfoWithData.js +++ b/client/views/room/contextualBar/UserInfo/UserInfoWithData.js @@ -40,23 +40,9 @@ function UserInfoWithData({ ]), ); - const customFieldsToShowSetting = useSetting('Accounts_CustomFieldsToShowInUserInfo'); - const user = useMemo(() => { const { user } = value || { user: {} }; - const customFieldsToShowObj = JSON.parse(customFieldsToShowSetting); - - const customFieldsToShow = customFieldsToShowObj - ? Object.values(customFieldsToShowObj).map((value) => { - const role = Object.values(value); - const roleNameToShow = Object.keys(value); - const customField = {}; - customField[roleNameToShow] = user?.customFields[role]; - return customField; - }) - : []; - const { _id, name, @@ -78,7 +64,7 @@ function UserInfoWithData({ getRoles(roles).map((role, index) => {role}), bio, phone: user.phone, - customFields: customFieldsToShow, + customFields: user.customFields, verified: getUserEmailVerified(user), email: getUserEmailAddress(user), utcOffset, @@ -87,7 +73,7 @@ function UserInfoWithData({ customStatus: statusText, nickname, }; - }, [value, customFieldsToShowSetting, showRealNames, getRoles]); + }, [value, showRealNames, getRoles]); return ( <> From c2e99d347d2f74175ffef6e20ea8a3b8eca1d65a Mon Sep 17 00:00:00 2001 From: Diego Sampaio Date: Wed, 12 May 2021 20:23:08 -0300 Subject: [PATCH 12/17] Regression: Add impersonate permission to app role (#22006) --- app/authorization/server/startup.js | 2 +- server/startup/migrations/index.js | 1 + server/startup/migrations/v224.js | 11 +++++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 server/startup/migrations/v224.js diff --git a/app/authorization/server/startup.js b/app/authorization/server/startup.js index 3de0782d59e6..67db2ac1e2b0 100644 --- a/app/authorization/server/startup.js +++ b/app/authorization/server/startup.js @@ -125,7 +125,7 @@ Meteor.startup(function() { { _id: 'send-omnichannel-chat-transcript', roles: ['livechat-manager', 'admin'] }, { _id: 'mail-messages', roles: ['admin'] }, { _id: 'toggle-room-e2e-encryption', roles: ['owner'] }, - { _id: 'message-impersonate', roles: ['bot'] }, + { _id: 'message-impersonate', roles: ['bot', 'app'] }, { _id: 'create-team', roles: ['admin', 'user'] }, { _id: 'delete-team', roles: ['admin', 'owner'] }, { _id: 'edit-team', roles: ['admin', 'owner'] }, diff --git a/server/startup/migrations/index.js b/server/startup/migrations/index.js index e078a9a2844b..765ae09697e1 100644 --- a/server/startup/migrations/index.js +++ b/server/startup/migrations/index.js @@ -220,4 +220,5 @@ import './v220'; import './v221'; import './v222'; import './v223'; +import './v224'; import './xrun'; diff --git a/server/startup/migrations/v224.js b/server/startup/migrations/v224.js new file mode 100644 index 000000000000..6053475f61f0 --- /dev/null +++ b/server/startup/migrations/v224.js @@ -0,0 +1,11 @@ +import { Migrations } from '../../../app/migrations/server'; +import { Permissions } from '../../../app/models/server'; + +const roleName = 'app'; + +Migrations.add({ + version: 224, + up() { + Permissions.update({ _id: 'message-impersonate' }, { $addToSet: { roles: roleName } }); + }, +}); From 35b497c1753629b5c1c7923e683e57d0f6a1a1fb Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Fri, 14 May 2021 10:43:06 -0300 Subject: [PATCH 13/17] [IMPROVE] Add support to queries in `channels.members` and `groups.members` endpoints (#21414) Co-authored-by: Diego Sampaio --- app/api/server/v1/channels.js | 42 ++++++++++++++------------ app/api/server/v1/groups.js | 49 ++++++++++++++++++------------- app/models/server/models/Users.js | 2 +- server/lib/findUsersOfRoom.ts | 38 ++++++++++++++++++++++++ server/methods/getUsersOfRoom.js | 34 ++++----------------- 5 files changed, 95 insertions(+), 70 deletions(-) create mode 100644 server/lib/findUsersOfRoom.ts diff --git a/app/api/server/v1/channels.js b/app/api/server/v1/channels.js index ab7ea3350068..58a75e2667df 100644 --- a/app/api/server/v1/channels.js +++ b/app/api/server/v1/channels.js @@ -1,13 +1,15 @@ import { Meteor } from 'meteor/meteor'; +import { Match, check } from 'meteor/check'; import _ from 'underscore'; -import { Rooms, Subscriptions, Messages, Uploads, Integrations, Users } from '../../../models'; +import { Rooms, Subscriptions, Messages, Uploads, Integrations, Users } from '../../../models/server'; import { hasPermission, hasAtLeastOnePermission, hasAllPermission } from '../../../authorization/server'; import { mountIntegrationQueryBasedOnPermissions } from '../../../integrations/server/lib/mountQueriesBasedOnPermission'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { API } from '../api'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { Team } from '../../../../server/sdk'; +import { findUsersOfRoom } from '../../../../server/lib/findUsersOfRoom'; // Returns the channel IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property @@ -575,29 +577,31 @@ API.v1.addRoute('channels.members', { authRequired: true }, { return API.v1.unauthorized(); } - const { offset, count } = this.getPaginationItems(); + const { offset: skip, count: limit } = this.getPaginationItems(); const { sort = {} } = this.parseJsonQuery(); - const subscriptions = Subscriptions.findByRoomId(findResult._id, { - fields: { 'u._id': 1 }, - sort: { 'u.username': sort.username != null ? sort.username : 1 }, - skip: offset, - limit: count, - }); - - const total = subscriptions.count(); + check(this.queryParams, Match.ObjectIncluding({ + status: Match.Maybe([String]), + filter: Match.Maybe(String), + })); + const { status, filter } = this.queryParams; - const members = subscriptions.fetch().map((s) => s.u && s.u._id); + const cursor = findUsersOfRoom({ + rid: findResult._id, + ...status && { status: { $in: status } }, + skip, + limit, + filter, + ...sort?.username && { sort: { username: sort.username } }, + }); - const users = Users.find({ _id: { $in: members } }, { - fields: { _id: 1, username: 1, name: 1, status: 1, statusText: 1, utcOffset: 1 }, - sort: { username: sort.username != null ? sort.username : 1 }, - }).fetch(); + const total = cursor.count(); + const members = cursor.fetch(); return API.v1.success({ - members: users, - count: users.length, - offset, + members, + count: members.length, + offset: skip, total, }); }, diff --git a/app/api/server/v1/groups.js b/app/api/server/v1/groups.js index f0f77ea0c437..ce0f06fc1813 100644 --- a/app/api/server/v1/groups.js +++ b/app/api/server/v1/groups.js @@ -1,6 +1,6 @@ import _ from 'underscore'; import { Meteor } from 'meteor/meteor'; -import { Match } from 'meteor/check'; +import { Match, check } from 'meteor/check'; import { mountIntegrationQueryBasedOnPermissions } from '../../../integrations/server/lib/mountQueriesBasedOnPermission'; import { Subscriptions, Rooms, Messages, Uploads, Integrations, Users } from '../../../models/server'; @@ -8,6 +8,7 @@ import { hasPermission, hasAtLeastOnePermission, canAccessRoom, hasAllPermission import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { API } from '../api'; import { Team } from '../../../../server/sdk'; +import { findUsersOfRoom } from '../../../../server/lib/findUsersOfRoom'; // Returns the private group subscription IF found otherwise it will return the failure of why it didn't. Check the `statusCode` property export function findPrivateGroupByIdOrName({ params, userId, checkedArchived = true }) { @@ -23,6 +24,7 @@ export function findPrivateGroupByIdOrName({ params, userId, checkedArchived = t fname: 1, prid: 1, archived: 1, + broadcast: 1, }, }; const room = params.roomId @@ -54,6 +56,7 @@ export function findPrivateGroupByIdOrName({ params, userId, checkedArchived = t ro: room.ro, t: room.t, name: roomName, + broadcast: room.broadcast, }; } @@ -491,36 +494,40 @@ API.v1.addRoute('groups.listAll', { authRequired: true }, { API.v1.addRoute('groups.members', { authRequired: true }, { get() { - const findResult = findPrivateGroupByIdOrName({ params: this.requestParams(), userId: this.userId }); - const room = Rooms.findOneById(findResult.rid, { fields: { broadcast: 1 } }); + const findResult = findPrivateGroupByIdOrName({ + params: this.requestParams(), + userId: this.userId, + }); - if (room.broadcast && !hasPermission(this.userId, 'view-broadcast-member-list')) { + if (findResult.broadcast && !hasPermission(this.userId, 'view-broadcast-member-list')) { return API.v1.unauthorized(); } - const { offset, count } = this.getPaginationItems(); + const { offset: skip, count: limit } = this.getPaginationItems(); const { sort = {} } = this.parseJsonQuery(); - const subscriptions = Subscriptions.findByRoomId(findResult.rid, { - fields: { 'u._id': 1 }, - sort: { 'u.username': sort.username != null ? sort.username : 1 }, - skip: offset, - limit: count, - }); - - const total = subscriptions.count(); + check(this.queryParams, Match.ObjectIncluding({ + status: Match.Maybe([String]), + filter: Match.Maybe(String), + })); + const { status, filter } = this.queryParams; - const members = subscriptions.fetch().map((s) => s.u && s.u._id); + const cursor = findUsersOfRoom({ + rid: findResult.rid, + ...status && { status: { $in: status } }, + skip, + limit, + filter, + ...sort?.username && { sort: { username: sort.username } }, + }); - const users = Users.find({ _id: { $in: members } }, { - fields: { _id: 1, username: 1, name: 1, status: 1, statusText: 1, utcOffset: 1 }, - sort: { username: sort.username != null ? sort.username : 1 }, - }).fetch(); + const total = cursor.count(); + const members = cursor.fetch(); return API.v1.success({ - members: users, - count: users.length, - offset, + members, + count: members.length, + offset: skip, total, }); }, diff --git a/app/models/server/models/Users.js b/app/models/server/models/Users.js index 6a31157e71cf..575e71254660 100644 --- a/app/models/server/models/Users.js +++ b/app/models/server/models/Users.js @@ -774,7 +774,7 @@ export class Users extends Base { } // if the search term is empty, don't need to have the $or statement (because it would be an empty regex) - if (searchTerm === '') { + if (!searchTerm) { const query = { $and: [ { diff --git a/server/lib/findUsersOfRoom.ts b/server/lib/findUsersOfRoom.ts new file mode 100644 index 000000000000..7c6d61433eca --- /dev/null +++ b/server/lib/findUsersOfRoom.ts @@ -0,0 +1,38 @@ +import type { Mongo } from 'meteor/mongo'; + +import { Users } from '../../app/models/server'; +import { settings } from '../../app/settings/server'; +import { IUser } from '../../definition/IUser'; + +type FindUsersParam = { + rid: string; + status?: string; + skip?: number; + limit?: number; + filter?: string; + sort?: Record; +}; + +export function findUsersOfRoom({ rid, status, skip = 0, limit = 0, filter = '', sort = {} }: FindUsersParam): Mongo.Cursor { + const options = { + fields: { + name: 1, + username: 1, + nickname: 1, + status: 1, + avatarETag: 1, + _updatedAt: 1, + }, + sort: { + statusConnection: -1, + ...sort || { [settings.get('UI_Use_Real_Name') ? 'name' : 'username']: 1 }, + }, + ...skip > 0 && { skip }, + ...limit > 0 && { limit }, + }; + + return Users.findByActiveUsersExcept(filter, undefined, options, undefined, [{ + __rooms: rid, + ...status && { status }, + }]); +} diff --git a/server/methods/getUsersOfRoom.js b/server/methods/getUsersOfRoom.js index efeea2233a45..d17d91013cb6 100644 --- a/server/methods/getUsersOfRoom.js +++ b/server/methods/getUsersOfRoom.js @@ -1,35 +1,11 @@ import { Meteor } from 'meteor/meteor'; -import { Subscriptions, Users } from '../../app/models/server'; -import { hasPermission } from '../../app/authorization'; -import { settings } from '../../app/settings'; - -function findUsers({ rid, status, skip, limit, filter = '' }) { - const options = { - fields: { - name: 1, - username: 1, - nickname: 1, - status: 1, - avatarETag: 1, - _updatedAt: 1, - }, - sort: { - statusConnection: -1, - [settings.get('UI_Use_Real_Name') ? 'name' : 'username']: 1, - }, - ...skip > 0 && { skip }, - ...limit > 0 && { limit }, - }; - - return Users.findByActiveUsersExcept(filter, undefined, options, undefined, [{ - __rooms: rid, - ...status && { status }, - }]).fetch(); -} +import { Subscriptions } from '../../app/models/server'; +import { hasPermission } from '../../app/authorization/server'; +import { findUsersOfRoom } from '../lib/findUsersOfRoom'; Meteor.methods({ - async getUsersOfRoom(rid, showAll, { limit, skip } = {}, filter) { + getUsersOfRoom(rid, showAll, { limit, skip } = {}, filter) { const userId = Meteor.userId(); if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getUsersOfRoom' }); @@ -46,7 +22,7 @@ Meteor.methods({ const total = Subscriptions.findByRoomIdWhenUsernameExists(rid).count(); - const users = await findUsers({ rid, status: !showAll ? { $ne: 'offline' } : undefined, limit, skip, filter }); + const users = findUsersOfRoom({ rid, status: !showAll ? { $ne: 'offline' } : undefined, limit, skip, filter }).fetch(); return { total, From f53825557cbf760e42c315f43faf8b8a950250e1 Mon Sep 17 00:00:00 2001 From: Matheus Barbosa Silva <36537004+matheusbsilva137@users.noreply.github.com> Date: Fri, 14 May 2021 10:43:34 -0300 Subject: [PATCH 14/17] [IMPROVE] Add support to queries in the `im.members` endpoint (#21471) Co-authored-by: Diego Sampaio --- app/api/server/v1/im.js | 37 +++++++++++++++-------- tests/end-to-end/api/04-direct-message.js | 35 +++++++++++++++++++++ 2 files changed, 59 insertions(+), 13 deletions(-) diff --git a/app/api/server/v1/im.js b/app/api/server/v1/im.js index f61426949b08..b367596b4698 100644 --- a/app/api/server/v1/im.js +++ b/app/api/server/v1/im.js @@ -1,9 +1,10 @@ import { Meteor } from 'meteor/meteor'; +import { Match, check } from 'meteor/check'; -import { Subscriptions, Uploads, Users, Messages, Rooms } from '../../../models'; -import { hasPermission } from '../../../authorization'; +import { Subscriptions, Uploads, Users, Messages, Rooms } from '../../../models/server'; +import { hasPermission } from '../../../authorization/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; -import { settings } from '../../../settings'; +import { settings } from '../../../settings/server'; import { API } from '../api'; import { getDirectMessageByNameOrIdWithOptionToJoin } from '../../../lib/server/functions/getDirectMessageByNameOrIdWithOptionToJoin'; @@ -202,22 +203,32 @@ API.v1.addRoute(['dm.members', 'im.members'], { authRequired: true }, { const { offset, count } = this.getPaginationItems(); const { sort } = this.parseJsonQuery(); - const cursor = Subscriptions.findByRoomId(findResult.room._id, { - sort: { 'u.username': sort && sort.username ? sort.username : 1 }, + + check(this.queryParams, Match.ObjectIncluding({ + status: Match.Maybe([String]), + filter: Match.Maybe(String), + })); + const { status, filter } = this.queryParams; + + const extraQuery = { + _id: { $in: findResult.room.uids }, + ...status && { status: { $in: status } }, + }; + + const options = { + sort: { username: sort && sort.username ? sort.username : 1 }, + fields: { _id: 1, username: 1, name: 1, status: 1, statusText: 1, utcOffset: 1 }, skip: offset, limit: count, - }); + }; - const total = cursor.count(); - const members = cursor.fetch().map((s) => s.u && s.u.username); + const cursor = Users.findByActiveUsersExcept(filter, [], options, null, [extraQuery]); - const users = Users.find({ username: { $in: members } }, { - fields: { _id: 1, username: 1, name: 1, status: 1, statusText: 1, utcOffset: 1 }, - sort: { username: sort && sort.username ? sort.username : 1 }, - }).fetch(); + const members = cursor.fetch(); + const total = cursor.count(); return API.v1.success({ - members: users, + members, count: members.length, offset, total, diff --git a/tests/end-to-end/api/04-direct-message.js b/tests/end-to-end/api/04-direct-message.js index 3899e77c70a7..c46c125f6a38 100644 --- a/tests/end-to-end/api/04-direct-message.js +++ b/tests/end-to-end/api/04-direct-message.js @@ -441,5 +441,40 @@ describe('[Direct Messages]', function() { }) .end(done); }); + it('should return and array with one member', (done) => { + request.get(api('im.members')) + .set(credentials) + .query({ + username: 'rocket.cat', + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('count').and.to.be.equal(1); + expect(res.body).to.have.property('offset').and.to.be.equal(0); + expect(res.body).to.have.property('total').and.to.be.equal(2); + expect(res.body).to.have.property('members').and.to.have.lengthOf(1); + }) + .end(done); + }); + it('should return and array with one member queried by status', (done) => { + request.get(api('im.members')) + .set(credentials) + .query({ + roomId: directMessage._id, + 'status[]': ['online'], + }) + .expect('Content-Type', 'application/json') + .expect(200) + .expect((res) => { + expect(res.body).to.have.property('success', true); + expect(res.body).to.have.property('count').and.to.be.equal(1); + expect(res.body).to.have.property('offset').and.to.be.equal(0); + expect(res.body).to.have.property('total').and.to.be.equal(2); + expect(res.body).to.have.property('members').and.to.have.lengthOf(1); + }) + .end(done); + }); }); }); From d12ce70be6cf6816383ac346edf8eb5d16f4a42f Mon Sep 17 00:00:00 2001 From: Darshil Patel <55157259+Darshilp326@users.noreply.github.com> Date: Fri, 14 May 2021 19:19:55 +0530 Subject: [PATCH 15/17] [FIX] Removed fields from User Info for which the user doesn't have permissions. (#20923) Co-authored-by: dougfabris --- app/lib/server/functions/getFullUserData.js | 1 + .../room/contextualBar/UserInfo/UserInfo.js | 25 +++++++++++-------- .../UserInfo/UserInfoWithData.js | 2 ++ 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/app/lib/server/functions/getFullUserData.js b/app/lib/server/functions/getFullUserData.js index 06489353d59e..9113c777e81e 100644 --- a/app/lib/server/functions/getFullUserData.js +++ b/app/lib/server/functions/getFullUserData.js @@ -92,6 +92,7 @@ export function getFullUserDataByIdOrUsername({ userId, filterId, filterUsername fields, }; const user = Users.findOneByIdOrUsername(filterId || filterUsername, options); + user.canViewAllInfo = canViewAllInfo; return myself ? user : removePasswordInfo(user); } diff --git a/client/views/room/contextualBar/UserInfo/UserInfo.js b/client/views/room/contextualBar/UserInfo/UserInfo.js index a28516342d77..0282ffa3f490 100644 --- a/client/views/room/contextualBar/UserInfo/UserInfo.js +++ b/client/views/room/contextualBar/UserInfo/UserInfo.js @@ -14,6 +14,7 @@ import Avatar from './Avatar'; function UserInfo({ username, bio, + canViewAllInfo, email, verified, showRealNames, @@ -32,9 +33,7 @@ function UserInfo({ ...props }) { const t = useTranslation(); - const timeAgo = useTimeAgo(); - const customFieldsToShowSetting = useSetting('Accounts_CustomFieldsToShowInUserInfo'); const customFieldsToShowObj = JSON.parse(customFieldsToShowSetting); @@ -64,7 +63,7 @@ function UserInfo({ - {!!roles && ( + {roles.length !== 0 && ( {t('Roles')} {roles} @@ -87,10 +86,12 @@ function UserInfo({ )} - - {t('Last_login')} - {lastLogin ? timeAgo(lastLogin) : t('Never')} - + {canViewAllInfo && ( + + {t('Last_login')} + {lastLogin ? timeAgo(lastLogin) : t('Never')} + + )} {name && ( @@ -152,10 +153,12 @@ function UserInfo({ ) : null, )} - - {t('Created_at')} - {timeAgo(createdAt)} - + {createdAt && ( + + {t('Created_at')} + {timeAgo(createdAt)} + + )} diff --git a/client/views/room/contextualBar/UserInfo/UserInfoWithData.js b/client/views/room/contextualBar/UserInfo/UserInfoWithData.js index b723f6b5fad7..d8dddabd2b17 100644 --- a/client/views/room/contextualBar/UserInfo/UserInfoWithData.js +++ b/client/views/room/contextualBar/UserInfo/UserInfoWithData.js @@ -53,6 +53,7 @@ function UserInfoWithData({ utcOffset, lastLogin, nickname, + canViewAllInfo, } = user; return { _id, @@ -63,6 +64,7 @@ function UserInfoWithData({ roles && getRoles(roles).map((role, index) => {role}), bio, + canViewAllInfo, phone: user.phone, customFields: user.customFields, verified: getUserEmailVerified(user), From 1202b45da1d60d06326c3692eb26e56fe6a222ed Mon Sep 17 00:00:00 2001 From: Douglas Fabris Date: Fri, 14 May 2021 10:50:44 -0300 Subject: [PATCH 16/17] [FIX] Unpin message reactivity (#22029) --- app/message-pin/client/views/pinnedMessages.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/message-pin/client/views/pinnedMessages.js b/app/message-pin/client/views/pinnedMessages.js index 1058a23fafda..eb7c934deee8 100644 --- a/app/message-pin/client/views/pinnedMessages.js +++ b/app/message-pin/client/views/pinnedMessages.js @@ -39,7 +39,7 @@ Template.pinnedMessages.onCreated(function() { pinned: true, rid: this.data.rid, _updatedAt: { - $gt: new Date(), + $gte: new Date(), }, }; From 4713f3a0b3957f0154faef66ce187ecf4bb046dd Mon Sep 17 00:00:00 2001 From: Kevin Aleman Date: Fri, 14 May 2021 08:00:02 -0600 Subject: [PATCH 17/17] [FIX] Scenarios where 2FA enforcement was not working properly (#22017) --- app/authorization/client/startup.js | 5 ++++- app/ui-master/client/main.js | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/app/authorization/client/startup.js b/app/authorization/client/startup.js index 9c01ff37ab98..5f26b56bbdf4 100644 --- a/app/authorization/client/startup.js +++ b/app/authorization/client/startup.js @@ -12,7 +12,10 @@ import { registerAdminSidebarItem } from '../../../client/views/admin'; Meteor.startup(() => { CachedCollectionManager.onLogin(async () => { const { roles } = await APIClient.v1.get('roles.list'); - roles.forEach((role) => Roles.insert(role)); + // if a role is checked before this collection is populated, it will return undefined + for await (const role of roles) { + await Roles.upsert({ _id: role._id }, role); + } }); registerAdminSidebarItem({ diff --git a/app/ui-master/client/main.js b/app/ui-master/client/main.js index 56323dee9765..c4401c688619 100644 --- a/app/ui-master/client/main.js +++ b/app/ui-master/client/main.js @@ -75,9 +75,10 @@ Template.main.helpers({ if (!user || (user.services.totp !== undefined && user.services.totp.enabled) || (user.services.email2fa !== undefined && user.services.email2fa.enabled)) { return false; } + const is2faEnabled = settings.get('Accounts_TwoFactorAuthentication_Enabled'); const mandatoryRole = Roles.findOne({ _id: { $in: user.roles }, mandatory2fa: true }); - return mandatoryRole !== undefined; + return mandatoryRole !== undefined && is2faEnabled; }, CustomScriptLoggedOut: () => { const script = settings.get('Custom_Script_Logged_Out') || '';