diff --git a/.github/history-manual.json b/.github/history-manual.json index de60527e3d41..6258a78a3d94 100644 --- a/.github/history-manual.json +++ b/.github/history-manual.json @@ -123,5 +123,12 @@ "sampaiodiego", "pierre-lehnen-rc" ] + }], + "4.1.1": [{ + "title": "[FIX] Security Hotfix (https://docs.rocket.chat/guides/security/security-updates)", + "userLogin": "sampaiodiego", + "contributors": [ + "sampaiodiego" + ] }] } diff --git a/.github/history.json b/.github/history.json index 890111d8e940..5f3b0c6a3950 100644 --- a/.github/history.json +++ b/.github/history.json @@ -67032,6 +67032,116 @@ "5.0" ], "pull_requests": [] + }, + "4.1.1": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.1", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23607", + "title": "[FIX] App update flow failing in HA setups", + "userLogin": "d-gubert", + "description": "The flow for app updates is broken in specific scenarios with HA setups. Here we change the method calls in the Apps-Engine to avoid race conditions", + "milestone": "4.1.1", + "contributors": [ + "d-gubert" + ] + }, + { + "pr": "23627", + "title": "[FIX] LDAP users not being re-activated on login", + "userLogin": "pierre-lehnen-rc", + "milestone": "4.1.1", + "contributors": [ + "pierre-lehnen-rc" + ] + }, + { + "pr": "23608", + "title": "[FIX] Advanced LDAP Sync Features", + "userLogin": "pierre-lehnen-rc", + "milestone": "4.1.1", + "contributors": [ + "pierre-lehnen-rc", + "web-flow" + ] + } + ] + }, + "3.18.3": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.27.1", + "mongo_versions": [ + "3.4", + "3.6", + "4.0", + "4.2" + ], + "pull_requests": [] + }, + "4.0.6": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.0", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [] + }, + "4.1.2": { + "node_version": "12.22.1", + "npm_version": "6.14.1", + "apps_engine_version": "1.28.1", + "mongo_versions": [ + "3.6", + "4.0", + "4.2", + "4.4", + "5.0" + ], + "pull_requests": [ + { + "pr": "23487", + "title": "[FIX] Notifications are not being filtered", + "userLogin": "matheusbsilva137", + "description": "- Add a migration to update the `Accounts_Default_User_Preferences_pushNotifications` setting's value to the `Accounts_Default_User_Preferences_mobileNotifications` setting's value;\r\n - Remove the `Accounts_Default_User_Preferences_mobileNotifications` setting (replaced by `Accounts_Default_User_Preferences_pushNotifications`);\r\n - Rename 'mobileNotifications' user's preference to 'pushNotifications'.", + "milestone": "4.1.2", + "contributors": [ + "matheusbsilva137" + ] + }, + { + "pr": "23661", + "title": "[FIX] Performance issues when running Omnichannel job queue dispatcher", + "userLogin": "renatobecker", + "milestone": "4.1.2", + "contributors": [ + "renatobecker" + ] + }, + { + "pr": "23587", + "title": "[FIX] Omnichannel status being changed on page refresh", + "userLogin": "KevLehman", + "milestone": "4.1.2", + "contributors": [ + "KevLehman" + ] + } + ] } } } \ No newline at end of file diff --git a/HISTORY.md b/HISTORY.md index e3aa63d8aa17..7ee96dd1faa2 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,6 +1,62 @@ +# 4.1.2 +`2021-11-08 ยท 3 ๐Ÿ› ยท 3 ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป` + +### Engine versions +- Node: `12.22.1` +- NPM: `6.14.1` +- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` +- Apps-Engine: `1.28.1` + +### ๐Ÿ› Bug fixes + + +- Notifications are not being filtered ([#23487](https://github.com/RocketChat/Rocket.Chat/pull/23487)) + + - Add a migration to update the `Accounts_Default_User_Preferences_pushNotifications` setting's value to the `Accounts_Default_User_Preferences_mobileNotifications` setting's value; + - Remove the `Accounts_Default_User_Preferences_mobileNotifications` setting (replaced by `Accounts_Default_User_Preferences_pushNotifications`); + - Rename 'mobileNotifications' user's preference to 'pushNotifications'. + +- Omnichannel status being changed on page refresh ([#23587](https://github.com/RocketChat/Rocket.Chat/pull/23587)) + +- Performance issues when running Omnichannel job queue dispatcher ([#23661](https://github.com/RocketChat/Rocket.Chat/pull/23661)) + +### ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป Core Team ๐Ÿค“ + +- [@KevLehman](https://github.com/KevLehman) +- [@matheusbsilva137](https://github.com/matheusbsilva137) +- [@renatobecker](https://github.com/renatobecker) + +# 4.1.1 +`2021-11-05 ยท 4 ๐Ÿ› ยท 3 ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป` + +### Engine versions +- Node: `12.22.1` +- NPM: `6.14.1` +- MongoDB: `3.6, 4.0, 4.2, 4.4, 5.0` +- Apps-Engine: `1.28.1` + +### ๐Ÿ› Bug fixes + + +- Advanced LDAP Sync Features ([#23608](https://github.com/RocketChat/Rocket.Chat/pull/23608)) + +- App update flow failing in HA setups ([#23607](https://github.com/RocketChat/Rocket.Chat/pull/23607)) + + The flow for app updates is broken in specific scenarios with HA setups. Here we change the method calls in the Apps-Engine to avoid race conditions + +- LDAP users not being re-activated on login ([#23627](https://github.com/RocketChat/Rocket.Chat/pull/23627)) + +- Security Hotfix (https://docs.rocket.chat/guides/security/security-updates) + +### ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป Core Team ๐Ÿค“ + +- [@d-gubert](https://github.com/d-gubert) +- [@pierre-lehnen-rc](https://github.com/pierre-lehnen-rc) +- [@sampaiodiego](https://github.com/sampaiodiego) + # 4.1.0 -`2021-10-27 ยท 1 ๐ŸŽ‰ ยท 4 ๐Ÿš€ ยท 25 ๐Ÿ› ยท 38 ๐Ÿ” ยท 23 ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป` +`2021-10-28 ยท 1 ๐ŸŽ‰ ยท 4 ๐Ÿš€ ยท 25 ๐Ÿ› ยท 38 ๐Ÿ” ยท 23 ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป` ### Engine versions - Node: `12.22.1` @@ -6345,7 +6401,7 @@ - [@sampaiodiego](https://github.com/sampaiodiego) # 3.8.0 -`2020-11-14 ยท 14 ๐ŸŽ‰ ยท 4 ๐Ÿš€ ยท 40 ๐Ÿ› ยท 54 ๐Ÿ” ยท 30 ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป` +`2020-11-13 ยท 14 ๐ŸŽ‰ ยท 4 ๐Ÿš€ ยท 40 ๐Ÿ› ยท 54 ๐Ÿ” ยท 30 ๐Ÿ‘ฉโ€๐Ÿ’ป๐Ÿ‘จโ€๐Ÿ’ป` ### Engine versions - Node: `12.18.4` diff --git a/app/api/server/lib/integrations.js b/app/api/server/lib/integrations.ts similarity index 65% rename from app/api/server/lib/integrations.js rename to app/api/server/lib/integrations.ts index 55db33a636a5..ef5cab57ed94 100644 --- a/app/api/server/lib/integrations.js +++ b/app/api/server/lib/integrations.ts @@ -1,7 +1,9 @@ import { Integrations } from '../../../models/server/raw'; import { hasPermissionAsync } from '../../../authorization/server/functions/hasPermission'; +import { IIntegration } from '../../../../definition/IIntegration'; +import { IUser } from '../../../../definition/IUser'; -const hasIntegrationsPermission = async (userId, integration) => { +const hasIntegrationsPermission = async (userId: string, integration: IIntegration): Promise => { const type = integration.type === 'webhook-incoming' ? 'incoming' : 'outgoing'; if (await hasPermissionAsync(userId, `manage-${ type }-integrations`)) { @@ -15,7 +17,15 @@ const hasIntegrationsPermission = async (userId, integration) => { return false; }; -export const findOneIntegration = async ({ userId, integrationId, createdBy }) => { +export const findOneIntegration = async ({ + userId, + integrationId, + createdBy, +}: { + userId: string; + integrationId: string; + createdBy: IUser; +}): Promise => { const integration = await Integrations.findOneByIdAndCreatedByIfExists({ _id: integrationId, createdBy }); if (!integration) { throw new Error('The integration does not exists.'); diff --git a/app/api/server/lib/webdav.js b/app/api/server/lib/webdav.js deleted file mode 100644 index cf5a3c8ea8f1..000000000000 --- a/app/api/server/lib/webdav.js +++ /dev/null @@ -1,14 +0,0 @@ -import { WebdavAccounts } from '../../../models/server/raw'; - -export async function findWebdavAccountsByUserId({ uid }) { - return { - accounts: await WebdavAccounts.findWithUserId(uid, { - fields: { - _id: 1, - username: 1, - server_url: 1, - name: 1, - }, - }).toArray(), - }; -} diff --git a/app/api/server/lib/webdav.ts b/app/api/server/lib/webdav.ts new file mode 100644 index 000000000000..fe2f17185bb6 --- /dev/null +++ b/app/api/server/lib/webdav.ts @@ -0,0 +1,16 @@ +import { WebdavAccounts } from '../../../models/server/raw'; +import { IWebdavAccount } from '../../../../definition/IWebdavAccount'; + +export async function findWebdavAccountsByUserId({ uid }: { uid: string }): Promise<{ accounts: IWebdavAccount[] }> { + return { + accounts: await WebdavAccounts.findWithUserId(uid, { + projection: { + _id: 1, + username: 1, + // eslint-disable-next-line @typescript-eslint/camelcase + server_url: 1, + name: 1, + }, + }).toArray(), + }; +} diff --git a/app/api/server/v1/channels.js b/app/api/server/v1/channels.js index 8d360a5de505..3e0139ec80b0 100644 --- a/app/api/server/v1/channels.js +++ b/app/api/server/v1/channels.js @@ -2,7 +2,8 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import _ from 'underscore'; -import { Rooms, Subscriptions, Messages, Uploads, Integrations, Users } from '../../../models/server'; +import { Rooms, Subscriptions, Messages, Users } from '../../../models/server'; +import { Integrations, Uploads } from '../../../models/server/raw'; import { canAccessRoom, hasPermission, hasAtLeastOnePermission, hasAllPermission } from '../../../authorization/server'; import { mountIntegrationQueryBasedOnPermissions } from '../../../integrations/server/lib/mountQueriesBasedOnPermission'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; @@ -285,28 +286,28 @@ API.v1.addRoute('channels.files', { authRequired: true }, { return file; }; - Meteor.runAsUser(this.userId, () => { - Meteor.call('canAccessRoom', findResult._id, this.userId); - }); + if (!canAccessRoom(findResult, { _id: this.userId })) { + return API.v1.unauthorized(); + } const { offset, count } = this.getPaginationItems(); const { sort, fields, query } = this.parseJsonQuery(); const ourQuery = Object.assign({}, query, { rid: findResult._id }); - const files = Uploads.find(ourQuery, { + const files = Promise.await(Uploads.find(ourQuery, { sort: sort || { name: 1 }, skip: offset, limit: count, fields, - }).fetch(); + }).toArray()); return API.v1.success({ files: files.map(addUserObjectToEveryObject), count: files.length, offset, - total: Uploads.find(ourQuery).count(), + total: Promise.await(Uploads.find(ourQuery).count()), }); }, }); @@ -340,21 +341,24 @@ API.v1.addRoute('channels.getIntegrations', { authRequired: true }, { } const { offset, count } = this.getPaginationItems(); - const { sort, fields, query } = this.parseJsonQuery(); + const { sort, fields: projection, query } = this.parseJsonQuery(); ourQuery = Object.assign(mountIntegrationQueryBasedOnPermissions(this.userId), query, ourQuery); - const integrations = Integrations.find(ourQuery, { + const cursor = Integrations.find(ourQuery, { sort: sort || { _createdAt: 1 }, skip: offset, limit: count, - fields, - }).fetch(); + projection, + }); + + const integrations = Promise.await(cursor.toArray()); + const total = Promise.await(cursor.count()); return API.v1.success({ integrations, count: integrations.length, offset, - total: Integrations.find(ourQuery).count(), + total, }); }, }); diff --git a/app/api/server/v1/chat.js b/app/api/server/v1/chat.js index db41290a16f3..eba0e4e3f668 100644 --- a/app/api/server/v1/chat.js +++ b/app/api/server/v1/chat.js @@ -3,7 +3,7 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { Messages } from '../../../models'; -import { canAccessRoom, hasPermission } from '../../../authorization'; +import { canAccessRoom, hasPermission } from '../../../authorization/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { processWebhookMessage } from '../../../lib/server'; import { executeSendMessage } from '../../../lib/server/methods/sendMessage'; @@ -404,12 +404,12 @@ API.v1.addRoute('chat.getPinnedMessages', { authRequired: true }, { if (!roomId) { throw new Meteor.Error('error-roomId-param-not-provided', 'The required "roomId" query param is missing.'); } - const room = Meteor.call('canAccessRoom', roomId, this.userId); - if (!room) { + + if (!canAccessRoom({ _id: roomId }, { _id: this.userId })) { throw new Meteor.Error('error-not-allowed', 'Not allowed'); } - const cursor = Messages.findPinnedByRoom(room._id, { + const cursor = Messages.findPinnedByRoom(roomId, { skip: offset, limit: count, }); diff --git a/app/api/server/v1/commands.js b/app/api/server/v1/commands.js index 51059b11221e..a9f6c290d8dd 100644 --- a/app/api/server/v1/commands.js +++ b/app/api/server/v1/commands.js @@ -2,8 +2,9 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; import objectPath from 'object-path'; -import { slashCommands } from '../../../utils'; -import { Messages } from '../../../models'; +import { slashCommands } from '../../../utils/server'; +import { Messages } from '../../../models/server'; +import { canAccessRoom } from '../../../authorization/server'; import { API } from '../api'; API.v1.addRoute('commands.get', { authRequired: true }, { @@ -189,8 +190,9 @@ API.v1.addRoute('commands.run', { authRequired: true }, { return API.v1.failure('The command provided does not exist (or is disabled).'); } - // This will throw an error if they can't or the room is invalid - Meteor.call('canAccessRoom', body.roomId, user._id); + if (!canAccessRoom({ _id: body.roomId }, user)) { + return API.v1.unauthorized(); + } const params = body.params ? body.params : ''; const message = { @@ -238,8 +240,9 @@ API.v1.addRoute('commands.preview', { authRequired: true }, { return API.v1.failure('The command provided does not exist (or is disabled).'); } - // This will throw an error if they can't or the room is invalid - Meteor.call('canAccessRoom', query.roomId, user._id); + if (!canAccessRoom({ _id: query.roomId }, user)) { + return API.v1.unauthorized(); + } const params = query.params ? query.params : ''; @@ -288,8 +291,9 @@ API.v1.addRoute('commands.preview', { authRequired: true }, { return API.v1.failure('The command provided does not exist (or is disabled).'); } - // This will throw an error if they can't or the room is invalid - Meteor.call('canAccessRoom', body.roomId, user._id); + if (!canAccessRoom({ _id: body.roomId }, user)) { + return API.v1.unauthorized(); + } const params = body.params ? body.params : ''; const message = { diff --git a/app/api/server/v1/email-inbox.js b/app/api/server/v1/email-inbox.js index e7452fc5ffe1..61368a2d0a8a 100644 --- a/app/api/server/v1/email-inbox.js +++ b/app/api/server/v1/email-inbox.js @@ -3,7 +3,7 @@ import { check, Match } from 'meteor/check'; import { API } from '../api'; import { findEmailInboxes, findOneEmailInbox, insertOneOrUpdateEmailInbox } from '../lib/emailInbox'; import { hasPermission } from '../../../authorization/server/functions/hasPermission'; -import { EmailInbox } from '../../../models'; +import { EmailInbox } from '../../../models/server/raw'; import Users from '../../../models/server/models/Users'; import { sendTestEmailToInbox } from '../../../../server/features/EmailInbox/EmailInbox_Outgoing'; @@ -79,12 +79,12 @@ API.v1.addRoute('email-inbox/:_id', { authRequired: true }, { const { _id } = this.urlParams; if (!_id) { throw new Error('error-invalid-param'); } - const emailInboxes = EmailInbox.findOneById(_id); + const emailInboxes = Promise.await(EmailInbox.findOneById(_id)); if (!emailInboxes) { return API.v1.notFound(); } - EmailInbox.removeById(_id); + Promise.await(EmailInbox.removeById(_id)); return API.v1.success({ _id }); }, }); diff --git a/app/api/server/v1/emoji-custom.js b/app/api/server/v1/emoji-custom.js index 403cca1d189c..092e41c1de97 100644 --- a/app/api/server/v1/emoji-custom.js +++ b/app/api/server/v1/emoji-custom.js @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import { EmojiCustom } from '../../../models/server'; +import { EmojiCustom } from '../../../models/server/raw'; import { API } from '../api'; import { getUploadFormData } from '../lib/getUploadFormData'; import { findEmojisCustom } from '../lib/emoji-custom'; @@ -19,15 +19,15 @@ API.v1.addRoute('emoji-custom.list', { authRequired: true }, { } return API.v1.success({ emojis: { - update: EmojiCustom.find({ ...query, _updatedAt: { $gt: updatedSinceDate } }).fetch(), - remove: EmojiCustom.trashFindDeletedAfter(updatedSinceDate).fetch(), + update: Promise.await(EmojiCustom.find({ ...query, _updatedAt: { $gt: updatedSinceDate } }).toArray()), + remove: Promise.await(EmojiCustom.trashFindDeletedAfter(updatedSinceDate).toArray()), }, }); } return API.v1.success({ emojis: { - update: EmojiCustom.find(query).fetch(), + update: Promise.await(EmojiCustom.find(query).toArray()), remove: [], }, }); @@ -88,7 +88,7 @@ API.v1.addRoute('emoji-custom.update', { authRequired: true }, { throw new Meteor.Error('The required "_id" query param is missing.'); } - const emojiToUpdate = EmojiCustom.findOneById(fields._id); + const emojiToUpdate = Promise.await(EmojiCustom.findOneById(fields._id)); if (!emojiToUpdate) { throw new Meteor.Error('Emoji not found.'); } diff --git a/app/api/server/v1/groups.js b/app/api/server/v1/groups.js index 4cf5d029ad6b..141bb94d49d6 100644 --- a/app/api/server/v1/groups.js +++ b/app/api/server/v1/groups.js @@ -3,7 +3,8 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { mountIntegrationQueryBasedOnPermissions } from '../../../integrations/server/lib/mountQueriesBasedOnPermission'; -import { Subscriptions, Rooms, Messages, Uploads, Integrations, Users } from '../../../models/server'; +import { Subscriptions, Rooms, Messages, Users } from '../../../models/server'; +import { Integrations, Uploads } from '../../../models/server/raw'; import { hasPermission, hasAtLeastOnePermission, canAccessRoom, hasAllPermission } from '../../../authorization/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { API } from '../api'; @@ -272,18 +273,18 @@ API.v1.addRoute('groups.files', { authRequired: true }, { const ourQuery = Object.assign({}, query, { rid: findResult.rid }); - const files = Uploads.find(ourQuery, { + const files = Promise.await(Uploads.find(ourQuery, { sort: sort || { name: 1 }, skip: offset, limit: count, fields, - }).fetch(); + }).toArray()); return API.v1.success({ files: files.map(addUserObjectToEveryObject), count: files.length, offset, - total: Uploads.find(ourQuery).count(), + total: Promise.await(Uploads.find(ourQuery).count()), }); }, }); @@ -312,21 +313,24 @@ API.v1.addRoute('groups.getIntegrations', { authRequired: true }, { } const { offset, count } = this.getPaginationItems(); - const { sort, fields, query } = this.parseJsonQuery(); + const { sort, fields: projection, query } = this.parseJsonQuery(); const ourQuery = Object.assign(mountIntegrationQueryBasedOnPermissions(this.userId), query, { channel: { $in: channelsToSearch } }); - const integrations = Integrations.find(ourQuery, { + const cursor = Integrations.find(ourQuery, { sort: sort || { _createdAt: 1 }, skip: offset, limit: count, - fields, - }).fetch(); + projection, + }); + + const integrations = Promise.await(cursor.toArray()); + const total = Promise.await(cursor.count()); return API.v1.success({ integrations, count: integrations.length, offset, - total: Integrations.find(ourQuery).count(), + total, }); }, }); diff --git a/app/api/server/v1/im.js b/app/api/server/v1/im.js index 21d164ee862b..41d3d5dfb273 100644 --- a/app/api/server/v1/im.js +++ b/app/api/server/v1/im.js @@ -1,8 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import { Subscriptions, Uploads, Users, Messages, Rooms } from '../../../models/server'; -import { hasPermission } from '../../../authorization/server'; +import { Subscriptions, Users, Messages, Rooms } from '../../../models/server'; +import { Uploads } from '../../../models/server/raw'; +import { canAccessRoom, hasPermission } from '../../../authorization/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { settings } from '../../../settings/server'; import { API } from '../api'; @@ -19,7 +20,7 @@ function findDirectMessageRoom(params, user, allowAdminOverride) { nameOrId: params.username || params.roomId, }); - const canAccess = Meteor.call('canAccessRoom', room._id, user._id) + const canAccess = canAccessRoom(room, user) || (allowAdminOverride && hasPermission(user._id, 'view-room-administration')); if (!canAccess || !room || room.t !== 'd') { throw new Meteor.Error('error-room-not-found', 'The required "roomId" or "username" param provided does not match any direct message'); @@ -148,18 +149,18 @@ API.v1.addRoute(['dm.files', 'im.files'], { authRequired: true }, { const ourQuery = Object.assign({}, query, { rid: findResult.room._id }); - const files = Uploads.find(ourQuery, { + const files = Promise.await(Uploads.find(ourQuery, { sort: sort || { name: 1 }, skip: offset, limit: count, fields, - }).fetch(); + }).toArray()); return API.v1.success({ files: files.map(addUserObjectToEveryObject), count: files.length, offset, - total: Uploads.find(ourQuery).count(), + total: Promise.await(Uploads.find(ourQuery).count()), }); }, }); diff --git a/app/api/server/v1/instances.ts b/app/api/server/v1/instances.ts index e6586a7c12a7..d3db3489537a 100644 --- a/app/api/server/v1/instances.ts +++ b/app/api/server/v1/instances.ts @@ -1,7 +1,7 @@ import { getInstanceConnection } from '../../../../server/stream/streamBroadcast'; import { hasPermission } from '../../../authorization/server'; import { API } from '../api'; -import InstanceStatus from '../../../models/server/models/InstanceStatus'; +import { InstanceStatus } from '../../../models/server/raw'; import { IInstanceStatus } from '../../../../definition/IInstanceStatus'; API.v1.addRoute('instances.get', { authRequired: true }, { @@ -10,7 +10,7 @@ API.v1.addRoute('instances.get', { authRequired: true }, { return API.v1.unauthorized(); } - const instances = InstanceStatus.find().fetch(); + const instances = Promise.await(InstanceStatus.find().toArray()); return API.v1.success({ instances: instances.map((instance: IInstanceStatus) => { diff --git a/app/api/server/v1/integrations.js b/app/api/server/v1/integrations.js index 480c3e8743eb..c05544eb4b82 100644 --- a/app/api/server/v1/integrations.js +++ b/app/api/server/v1/integrations.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { hasAtLeastOnePermission } from '../../../authorization/server'; -import { IntegrationHistory, Integrations } from '../../../models'; +import { Integrations, IntegrationHistory } from '../../../models/server/raw'; import { API } from '../api'; import { mountIntegrationHistoryQueryBasedOnPermissions, mountIntegrationQueryBasedOnPermissions } from '../../../integrations/server/lib/mountQueriesBasedOnPermission'; import { findOneIntegration } from '../lib/integrations'; @@ -63,21 +63,24 @@ API.v1.addRoute('integrations.history', { authRequired: true }, { const { id } = this.queryParams; const { offset, count } = this.getPaginationItems(); - const { sort, fields, query } = this.parseJsonQuery(); + const { sort, fields: projection, query } = this.parseJsonQuery(); const ourQuery = Object.assign(mountIntegrationHistoryQueryBasedOnPermissions(this.userId, id), query); - const history = IntegrationHistory.find(ourQuery, { + const cursor = IntegrationHistory.find(ourQuery, { sort: sort || { _updatedAt: -1 }, skip: offset, limit: count, - fields, - }).fetch(); + projection, + }); + + const history = Promise.await(cursor.toArray()); + const total = Promise.await(cursor.count()); return API.v1.success({ history, offset, items: history.length, - total: IntegrationHistory.find(ourQuery).count(), + total, }); }, }); @@ -94,21 +97,25 @@ API.v1.addRoute('integrations.list', { authRequired: true }, { } const { offset, count } = this.getPaginationItems(); - const { sort, fields, query } = this.parseJsonQuery(); + const { sort, fields: projection, query } = this.parseJsonQuery(); const ourQuery = Object.assign(mountIntegrationQueryBasedOnPermissions(this.userId), query); - const integrations = Integrations.find(ourQuery, { + const cursor = Integrations.find(ourQuery, { sort: sort || { ts: -1 }, skip: offset, limit: count, - fields, - }).fetch(); + projection, + }); + + const total = Promise.await(cursor.count()); + + const integrations = Promise.await(cursor.toArray()); return API.v1.success({ integrations, offset, items: integrations.length, - total: Integrations.find(ourQuery).count(), + total, }); }, }); @@ -138,9 +145,9 @@ API.v1.addRoute('integrations.remove', { authRequired: true }, { switch (this.bodyParams.type) { case 'webhook-outgoing': if (this.bodyParams.target_url) { - integration = Integrations.findOne({ urls: this.bodyParams.target_url }); + integration = Promise.await(Integrations.findOne({ urls: this.bodyParams.target_url })); } else if (this.bodyParams.integrationId) { - integration = Integrations.findOne({ _id: this.bodyParams.integrationId }); + integration = Promise.await(Integrations.findOne({ _id: this.bodyParams.integrationId })); } if (!integration) { @@ -155,7 +162,7 @@ API.v1.addRoute('integrations.remove', { authRequired: true }, { integration, }); case 'webhook-incoming': - integration = Integrations.findOne({ _id: this.bodyParams.integrationId }); + integration = Promise.await(Integrations.findOne({ _id: this.bodyParams.integrationId })); if (!integration) { return API.v1.failure('No integration found.'); @@ -217,9 +224,9 @@ API.v1.addRoute('integrations.update', { authRequired: true }, { switch (this.bodyParams.type) { case 'webhook-outgoing': if (this.bodyParams.target_url) { - integration = Integrations.findOne({ urls: this.bodyParams.target_url }); + integration = Promise.await(Integrations.findOne({ urls: this.bodyParams.target_url })); } else if (this.bodyParams.integrationId) { - integration = Integrations.findOne({ _id: this.bodyParams.integrationId }); + integration = Promise.await(Integrations.findOne({ _id: this.bodyParams.integrationId })); } if (!integration) { @@ -229,10 +236,10 @@ API.v1.addRoute('integrations.update', { authRequired: true }, { Meteor.call('updateOutgoingIntegration', integration._id, this.bodyParams); return API.v1.success({ - integration: Integrations.findOne({ _id: integration._id }), + integration: Promise.await(Integrations.findOne({ _id: integration._id })), }); case 'webhook-incoming': - integration = Integrations.findOne({ _id: this.bodyParams.integrationId }); + integration = Promise.await(Integrations.findOne({ _id: this.bodyParams.integrationId })); if (!integration) { return API.v1.failure('No integration found.'); @@ -241,7 +248,7 @@ API.v1.addRoute('integrations.update', { authRequired: true }, { Meteor.call('updateIncomingIntegration', integration._id, this.bodyParams); return API.v1.success({ - integration: Integrations.findOne({ _id: integration._id }), + integration: Promise.await(Integrations.findOne({ _id: integration._id })), }); default: return API.v1.failure('Invalid integration type.'); diff --git a/app/api/server/v1/invites.js b/app/api/server/v1/invites.js index fd17ec366190..f901247547db 100644 --- a/app/api/server/v1/invites.js +++ b/app/api/server/v1/invites.js @@ -7,7 +7,7 @@ import { validateInviteToken } from '../../../invites/server/functions/validateI API.v1.addRoute('listInvites', { authRequired: true }, { get() { - const result = listInvites(this.userId); + const result = Promise.await(listInvites(this.userId)); return API.v1.success(result); }, }); @@ -15,7 +15,7 @@ API.v1.addRoute('listInvites', { authRequired: true }, { API.v1.addRoute('findOrCreateInvite', { authRequired: true }, { post() { const { rid, days, maxUses } = this.bodyParams; - const result = findOrCreateInvite(this.userId, { rid, days, maxUses }); + const result = Promise.await(findOrCreateInvite(this.userId, { rid, days, maxUses })); return API.v1.success(result); }, @@ -24,7 +24,7 @@ API.v1.addRoute('findOrCreateInvite', { authRequired: true }, { API.v1.addRoute('removeInvite/:_id', { authRequired: true }, { delete() { const { _id } = this.urlParams; - const result = removeInvite(this.userId, { _id }); + const result = Promise.await(removeInvite(this.userId, { _id })); return API.v1.success(result); }, @@ -34,7 +34,7 @@ API.v1.addRoute('useInviteToken', { authRequired: true }, { post() { const { token } = this.bodyParams; // eslint-disable-next-line react-hooks/rules-of-hooks - const result = useInviteToken(this.userId, token); + const result = Promise.await(useInviteToken(this.userId, token)); return API.v1.success(result); }, @@ -46,7 +46,7 @@ API.v1.addRoute('validateInviteToken', { authRequired: false }, { let valid = true; try { - validateInviteToken(token); + Promise.await(validateInviteToken(token)); } catch (e) { valid = false; } diff --git a/app/api/server/v1/permissions.js b/app/api/server/v1/permissions.ts similarity index 74% rename from app/api/server/v1/permissions.js rename to app/api/server/v1/permissions.ts index 4ac1661f0786..c2aab9afda54 100644 --- a/app/api/server/v1/permissions.js +++ b/app/api/server/v1/permissions.ts @@ -1,31 +1,29 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import { hasPermission } from '../../../authorization'; -import { Permissions, Roles } from '../../../models/server'; +import { hasPermission } from '../../../authorization/server'; import { API } from '../api'; +import { Permissions, Roles } from '../../../models/server/raw'; API.v1.addRoute('permissions.listAll', { authRequired: true }, { get() { const { updatedSince } = this.queryParams; - let updatedSinceDate; + let updatedSinceDate: Date | undefined; if (updatedSince) { if (isNaN(Date.parse(updatedSince))) { throw new Meteor.Error('error-roomId-param-invalid', 'The "updatedSince" query parameter must be a valid date.'); - } else { - updatedSinceDate = new Date(updatedSince); } + updatedSinceDate = new Date(updatedSince); } - let result; - Meteor.runAsUser(this.userId, () => { result = Meteor.call('permissions/get', updatedSinceDate); }); + const result = Promise.await(Meteor.call('permissions/get', updatedSinceDate)); if (Array.isArray(result)) { - result = { + return API.v1.success({ update: result, remove: [], - }; + }); } return API.v1.success(result); @@ -52,14 +50,14 @@ API.v1.addRoute('permissions.update', { authRequired: true }, { Object.keys(this.bodyParams.permissions).forEach((key) => { const element = this.bodyParams.permissions[key]; - if (!Permissions.findOneById(element._id)) { + if (!Promise.await(Permissions.findOneById(element._id))) { permissionNotFound = true; } Object.keys(element.roles).forEach((key) => { - const subelement = element.roles[key]; + const subElement = element.roles[key]; - if (!Roles.findOneById(subelement)) { + if (!Promise.await(Roles.findOneById(subElement))) { roleNotFound = true; } }); @@ -77,7 +75,7 @@ API.v1.addRoute('permissions.update', { authRequired: true }, { Permissions.createOrUpdate(element._id, element.roles); }); - const result = Meteor.runAsUser(this.userId, () => Meteor.call('permissions/get')); + const result = Promise.await(Meteor.call('permissions/get')); return API.v1.success({ permissions: result, diff --git a/app/api/server/v1/roles.js b/app/api/server/v1/roles.ts similarity index 76% rename from app/api/server/v1/roles.js rename to app/api/server/v1/roles.ts index 39d89164e4ab..8d87ac7f25c7 100644 --- a/app/api/server/v1/roles.js +++ b/app/api/server/v1/roles.ts @@ -1,15 +1,16 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import { Roles, Users } from '../../../models'; +import { Users } from '../../../models/server'; import { API } from '../api'; import { getUsersInRole, hasPermission, hasRole } from '../../../authorization/server'; import { settings } from '../../../settings/server/index'; import { api } from '../../../../server/sdk/api'; +import { Roles } from '../../../models/server/raw'; API.v1.addRoute('roles.list', { authRequired: true }, { get() { - const roles = Roles.find({}, { fields: { _updatedAt: 0 } }).fetch(); + const roles = Promise.await(Roles.find({}, { fields: { _updatedAt: 0 } }).toArray()); return API.v1.success({ roles }); }, @@ -25,8 +26,8 @@ API.v1.addRoute('roles.sync', { authRequired: true }, { return API.v1.success({ roles: { - update: Roles.findByUpdatedDate(new Date(updatedSince), { fields: API.v1.defaultFieldsToExclude }).fetch(), - remove: Roles.trashFindDeletedAfter(new Date(updatedSince)).fetch(), + update: Promise.await(Roles.findByUpdatedDate(new Date(updatedSince), { fields: API.v1.defaultFieldsToExclude }).toArray()), + remove: Promise.await(Roles.trashFindDeletedAfter(new Date(updatedSince)).toArray()), }, }); }, @@ -52,15 +53,15 @@ API.v1.addRoute('roles.create', { authRequired: true }, { throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed'); } - if (Roles.findOneByIdOrName(roleData.name)) { + if (Promise.await(Roles.findOneByIdOrName(roleData.name))) { throw new Meteor.Error('error-duplicate-role-names-not-allowed', 'Role name already exists'); } if (['Users', 'Subscriptions'].includes(roleData.scope) === false) { roleData.scope = 'Users'; } - - const roleId = Roles.createWithRandomId(roleData.name, roleData.scope, roleData.description, false, roleData.mandatory2fa); + const a = Roles.createWithRandomId(roleData.name, roleData.scope, roleData.description, false, roleData.mandatory2fa); + const roleId = Promise.await(a).insertedId; if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { @@ -70,7 +71,7 @@ API.v1.addRoute('roles.create', { authRequired: true }, { } return API.v1.success({ - role: Roles.findOneByIdOrName(roleId, { fields: API.v1.defaultFieldsToExclude }), + role: Promise.await(Roles.findOneByIdOrName(roleId, { fields: API.v1.defaultFieldsToExclude })), }); }, }); @@ -95,7 +96,7 @@ API.v1.addRoute('roles.addUserToRole', { authRequired: true }, { }); return API.v1.success({ - role: Roles.findOneByIdOrName(this.bodyParams.roleName, { fields: API.v1.defaultFieldsToExclude }), + role: Promise.await(Roles.findOneByIdOrName(this.bodyParams.roleName, { fields: API.v1.defaultFieldsToExclude })), }); }, }); @@ -121,13 +122,14 @@ API.v1.addRoute('roles.getUsersInRole', { authRequired: true }, { if (roomId && !hasPermission(this.userId, 'view-other-user-channels')) { throw new Meteor.Error('error-not-allowed', 'Not allowed'); } - const users = getUsersInRole(role, roomId, { + const users = Promise.await(getUsersInRole(role, roomId, { limit: count, sort: { username: 1 }, skip: offset, fields, - }); - return API.v1.success({ users: users.fetch(), total: users.count() }); + })); + + return API.v1.success({ users: Promise.await(users.toArray()), total: Promise.await(users.count()) }); }, }); @@ -149,7 +151,7 @@ API.v1.addRoute('roles.update', { authRequired: true }, { mandatory2fa: this.bodyParams.mandatory2fa, }; - const role = Roles.findOneByIdOrName(roleData.roleId); + const role = Promise.await(Roles.findOneByIdOrName(roleData.roleId)); if (!role) { throw new Meteor.Error('error-invalid-roleId', 'This role does not exist'); @@ -160,7 +162,7 @@ API.v1.addRoute('roles.update', { authRequired: true }, { } if (roleData.name) { - const otherRole = Roles.findOneByIdOrName(roleData.name); + const otherRole = Promise.await(Roles.findOneByIdOrName(roleData.name)); if (otherRole && otherRole._id !== role._id) { throw new Meteor.Error('error-duplicate-role-names-not-allowed', 'Role name already exists'); } @@ -172,7 +174,7 @@ API.v1.addRoute('roles.update', { authRequired: true }, { } } - Roles.updateById(roleData.roleId, roleData.name, roleData.scope, roleData.description, roleData.mandatory2fa); + Promise.await(Roles.updateById(roleData.roleId, roleData.name, roleData.scope, roleData.description, roleData.mandatory2fa)); if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { @@ -182,7 +184,7 @@ API.v1.addRoute('roles.update', { authRequired: true }, { } return API.v1.success({ - role: Roles.findOneByIdOrName(roleData.roleId, { fields: API.v1.defaultFieldsToExclude }), + role: Promise.await(Roles.findOneByIdOrName(roleData.roleId, { fields: API.v1.defaultFieldsToExclude })), }); }, }); @@ -197,7 +199,7 @@ API.v1.addRoute('roles.delete', { authRequired: true }, { throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed'); } - const role = Roles.findOneByIdOrName(this.bodyParams.roleId); + const role = Promise.await(Roles.findOneByIdOrName(this.bodyParams.roleId)); if (!role) { throw new Meteor.Error('error-invalid-roleId', 'This role does not exist'); @@ -207,13 +209,13 @@ API.v1.addRoute('roles.delete', { authRequired: true }, { throw new Meteor.Error('error-role-protected', 'Cannot delete a protected role'); } - const existingUsers = Roles.findUsersInRole(role.name, role.scope); + const existingUsers = Promise.await(Roles.findUsersInRole(role.name, role.scope)); - if (existingUsers && existingUsers.count() > 0) { + if (existingUsers && Promise.await(existingUsers.count()) > 0) { throw new Meteor.Error('error-role-in-use', 'Cannot delete role because it\'s in use'); } - Roles.remove(role._id); + Promise.await(Roles.removeById(role._id)); return API.v1.success(); }, @@ -243,7 +245,7 @@ API.v1.addRoute('roles.removeUserFromRole', { authRequired: true }, { throw new Meteor.Error('error-invalid-user', 'There is no user with this username'); } - const role = Roles.findOneByIdOrName(data.roleName); + const role = Promise.await(Roles.findOneByIdOrName(data.roleName)); if (!role) { throw new Meteor.Error('error-invalid-roleId', 'This role does not exist'); @@ -254,13 +256,13 @@ API.v1.addRoute('roles.removeUserFromRole', { authRequired: true }, { } if (role._id === 'admin') { - const adminCount = Roles.findUsersInRole('admin').count(); + const adminCount = Promise.await(Promise.await(Roles.findUsersInRole('admin')).count()); if (adminCount === 1) { throw new Meteor.Error('error-admin-required', 'You need to have at least one admin'); } } - Roles.removeUserRoles(user._id, role.name, data.scope); + Promise.await(Roles.removeUserRoles(user._id, [role.name], data.scope)); if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { diff --git a/app/api/server/v1/rooms.js b/app/api/server/v1/rooms.js index eee7230a0bbf..9310ac8c7b22 100644 --- a/app/api/server/v1/rooms.js +++ b/app/api/server/v1/rooms.js @@ -65,9 +65,7 @@ API.v1.addRoute('rooms.get', { authRequired: true }, { API.v1.addRoute('rooms.upload/:rid', { authRequired: true }, { post() { - const room = Meteor.call('canAccessRoom', this.urlParams.rid, this.userId); - - if (!room) { + if (!canAccessRoom({ _id: this.urlParams.rid }, { _id: this.userId })) { return API.v1.unauthorized(); } @@ -191,9 +189,11 @@ API.v1.addRoute('rooms.info', { authRequired: true }, { get() { const room = findRoomByIdOrName({ params: this.requestParams() }); const { fields } = this.parseJsonQuery(); - if (!Meteor.call('canAccessRoom', room._id, this.userId, {})) { + + if (!room || !canAccessRoom(room, { _id: this.userId })) { return API.v1.failure('not-allowed', 'Not Allowed'); } + return API.v1.success({ room: Rooms.findOneByIdOrName(room._id, { fields }) }); }, }); @@ -244,9 +244,11 @@ API.v1.addRoute('rooms.getDiscussions', { authRequired: true }, { const room = findRoomByIdOrName({ params: this.requestParams() }); const { offset, count } = this.getPaginationItems(); const { sort, fields, query } = this.parseJsonQuery(); - if (!Meteor.call('canAccessRoom', room._id, this.userId, {})) { + + if (!room || !canAccessRoom(room, { _id: this.userId })) { return API.v1.failure('not-allowed', 'Not Allowed'); } + const ourQuery = Object.assign(query, { prid: room._id }); const discussions = Rooms.find(ourQuery, { diff --git a/app/api/server/v1/settings.js b/app/api/server/v1/settings.ts similarity index 54% rename from app/api/server/v1/settings.js rename to app/api/server/v1/settings.ts index b9c720f3522a..1f60f2ea2c58 100644 --- a/app/api/server/v1/settings.js +++ b/app/api/server/v1/settings.ts @@ -3,24 +3,65 @@ import { Match, check } from 'meteor/check'; import { ServiceConfiguration } from 'meteor/service-configuration'; import _ from 'underscore'; -import { Settings } from '../../../models/server'; -import { hasPermission } from '../../../authorization'; +import { Settings } from '../../../models/server/raw'; +import { hasPermission } from '../../../authorization/server'; import { API } from '../api'; import { SettingsEvents, settings } from '../../../settings/server'; import { setValue } from '../../../settings/server/raw'; +import { ISetting, ISettingColor, isSettingAction, isSettingColor } from '../../../../definition/ISetting'; -const fetchSettings = (query, sort, offset, count, fields) => { - const settings = Settings.find(query, { + +const fetchSettings = async (query: Parameters[0], sort: Parameters[1]['sort'], offset: Parameters[1]['skip'], count: Parameters[1]['limit'], fields: Parameters[1]['projection']): Promise => { + const settings = await Settings.find(query, { sort: sort || { _id: 1 }, skip: offset, limit: count, - fields: Object.assign({ _id: 1, value: 1, enterprise: 1, invalidValue: 1, modules: 1 }, fields), - }).fetch(); + projection: { _id: 1, value: 1, enterprise: 1, invalidValue: 1, modules: 1, ...fields }, + }).toArray() as unknown as ISetting[]; + SettingsEvents.emit('fetch-settings', settings); return settings; }; +type OauthCustomConfiguration = { + _id: string; + clientId?: string; + custom: unknown; + service?: string; + serverURL: unknown; + tokenPath: unknown; + identityPath: unknown; + authorizePath: unknown; + scope: unknown; + loginStyle: unknown; + tokenSentVia: unknown; + identityTokenSentVia: unknown; + keyField: unknown; + usernameField: unknown; + emailField: unknown; + nameField: unknown; + avatarField: unknown; + rolesClaim: unknown; + groupsClaim: unknown; + mapChannels: unknown; + channelsMap: unknown; + channelsAdmin: unknown; + mergeUsers: unknown; + mergeRoles: unknown; + accessTokenParam: unknown; + showButton: unknown; + + appId: unknown; + consumerKey: unknown; + + clientConfig: unknown; + buttonLabelText: unknown; + buttonLabelColor: unknown; + buttonColor: unknown; +} +const isOauthCustomConfiguration = (config: any): config is OauthCustomConfiguration => Boolean(config); + // settings endpoints API.v1.addRoute('settings.public', { authRequired: false }, { get() { @@ -34,7 +75,7 @@ API.v1.addRoute('settings.public', { authRequired: false }, { ourQuery = Object.assign({}, query, ourQuery); - const settings = fetchSettings(ourQuery, sort, offset, count, fields); + const settings = Promise.await(fetchSettings(ourQuery, sort, offset, count, fields)); return API.v1.success({ settings, @@ -47,11 +88,15 @@ API.v1.addRoute('settings.public', { authRequired: false }, { API.v1.addRoute('settings.oauth', { authRequired: false }, { get() { - const mountOAuthServices = () => { + const mountOAuthServices = (): object => { const oAuthServicesEnabled = ServiceConfiguration.configurations.find({}, { fields: { secret: 0 } }).fetch(); return oAuthServicesEnabled.map((service) => { - if (service.custom || ['saml', 'cas', 'wordpress'].includes(service.service)) { + if (!isOauthCustomConfiguration(service)) { + return service; + } + + if (service.custom || (service.service && ['saml', 'cas', 'wordpress'].includes(service.service))) { return { ...service }; } @@ -93,7 +138,7 @@ API.v1.addRoute('settings', { authRequired: true }, { const { offset, count } = this.getPaginationItems(); const { sort, fields, query } = this.parseJsonQuery(); - let ourQuery = { + let ourQuery: Parameters[0] = { hidden: { $ne: true }, }; @@ -103,7 +148,7 @@ API.v1.addRoute('settings', { authRequired: true }, { ourQuery = Object.assign({}, query, ourQuery); - const settings = fetchSettings(ourQuery, sort, offset, count, fields); + const settings = Promise.await(fetchSettings(ourQuery, sort, offset, count, fields)); return API.v1.success({ settings, @@ -119,26 +164,34 @@ API.v1.addRoute('settings/:_id', { authRequired: true }, { if (!hasPermission(this.userId, 'view-privileged-setting')) { return API.v1.unauthorized(); } - - return API.v1.success(_.pick(Settings.findOneNotHiddenById(this.urlParams._id), '_id', 'value')); + const setting = Promise.await(Settings.findOneNotHiddenById(this.urlParams._id)); + if (!setting) { + return API.v1.failure(); + } + return API.v1.success(_.pick(setting, '_id', 'value')); }, post: { twoFactorRequired: true, - action() { + action(this: any): void { if (!hasPermission(this.userId, 'edit-privileged-setting')) { return API.v1.unauthorized(); } // allow special handling of particular setting types - const setting = Settings.findOneNotHiddenById(this.urlParams._id); - if (setting.type === 'action' && this.bodyParams && this.bodyParams.execute) { + const setting = Promise.await(Settings.findOneNotHiddenById(this.urlParams._id)); + + if (!setting) { + return API.v1.failure(); + } + + if (isSettingAction(setting) && this.bodyParams && this.bodyParams.execute) { // execute the configured method Meteor.call(setting.value); return API.v1.success(); } - if (setting.type === 'color' && this.bodyParams && this.bodyParams.editor && this.bodyParams.value) { - Settings.updateOptionsById(this.urlParams._id, { editor: this.bodyParams.editor }); + if (isSettingColor(setting) && this.bodyParams && this.bodyParams.editor && this.bodyParams.value) { + Settings.updateOptionsById(this.urlParams._id, { editor: this.bodyParams.editor }); Settings.updateValueNotHiddenById(this.urlParams._id, this.bodyParams.value); return API.v1.success(); } @@ -146,8 +199,12 @@ API.v1.addRoute('settings/:_id', { authRequired: true }, { check(this.bodyParams, { value: Match.Any, }); - if (Settings.updateValueNotHiddenById(this.urlParams._id, this.bodyParams.value)) { - settings.set(Settings.findOneNotHiddenById(this.urlParams._id)); + if (Promise.await(Settings.updateValueNotHiddenById(this.urlParams._id, this.bodyParams.value))) { + const s = Promise.await(Settings.findOneNotHiddenById(this.urlParams._id)); + if (!s) { + return API.v1.failure(); + } + settings.set(s); setValue(this.urlParams._id, this.bodyParams.value); return API.v1.success(); } diff --git a/app/apps/server/bridges/internal.ts b/app/apps/server/bridges/internal.ts index 0d09646f3893..154adbdeb104 100644 --- a/app/apps/server/bridges/internal.ts +++ b/app/apps/server/bridges/internal.ts @@ -2,8 +2,9 @@ import { InternalBridge } from '@rocket.chat/apps-engine/server/bridges/Internal import { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import { AppServerOrchestrator } from '../orchestrator'; -import { Subscriptions, Settings } from '../../../models/server'; +import { Subscriptions } from '../../../models/server'; import { ISubscription } from '../../../../definition/ISubscription'; +import { Settings } from '../../../models/server/raw'; export class AppInternalBridge extends InternalBridge { // eslint-disable-next-line no-empty-function @@ -30,7 +31,7 @@ export class AppInternalBridge extends InternalBridge { } protected async getWorkspacePublicKey(): Promise { - const publicKeySetting = Settings.findById('Cloud_Workspace_PublicKey').fetch()[0]; + const publicKeySetting = await Settings.findOneById('Cloud_Workspace_PublicKey'); return this.orch.getConverters()?.get('settings').convertToApp(publicKeySetting); } diff --git a/app/apps/server/bridges/settings.ts b/app/apps/server/bridges/settings.ts index ad1c234b0af2..ba0626434ff9 100644 --- a/app/apps/server/bridges/settings.ts +++ b/app/apps/server/bridges/settings.ts @@ -1,7 +1,7 @@ import { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import { ServerSettingBridge } from '@rocket.chat/apps-engine/server/bridges/ServerSettingBridge'; -import { Settings } from '../../../models/server'; +import { Settings } from '../../../models/server/raw'; import { AppServerOrchestrator } from '../orchestrator'; export class AppSettingBridge extends ServerSettingBridge { @@ -13,9 +13,8 @@ export class AppSettingBridge extends ServerSettingBridge { protected async getAll(appId: string): Promise> { this.orch.debugLog(`The App ${ appId } is getting all the settings.`); - return Settings.find({ secret: false }) - .fetch() - .map((s: ISetting) => this.orch.getConverters()?.get('settings').convertToApp(s)); + const settings = await Settings.find({ secret: false }).toArray(); + return settings.map((s) => this.orch.getConverters()?.get('settings').convertToApp(s)); } protected async getOneById(id: string, appId: string): Promise { @@ -46,8 +45,8 @@ export class AppSettingBridge extends ServerSettingBridge { protected async isReadableById(id: string, appId: string): Promise { this.orch.debugLog(`The App ${ appId } is checking if they can read the setting ${ id }.`); - - return !Settings.findOneById(id).secret; + const setting = await Settings.findOneById(id); + return Boolean(setting && !setting.secret); } protected async updateOne(setting: ISetting & { id: string }, appId: string): Promise { diff --git a/app/apps/server/communication/rest.js b/app/apps/server/communication/rest.js index 6d84f3797b95..1386ba39cfa8 100644 --- a/app/apps/server/communication/rest.js +++ b/app/apps/server/communication/rest.js @@ -6,9 +6,10 @@ import { getUploadFormData } from '../../../api/server/lib/getUploadFormData'; import { getWorkspaceAccessToken, getUserCloudAccessToken } from '../../../cloud/server'; import { settings } from '../../../settings/server'; import { Info } from '../../../utils'; -import { Settings, Users } from '../../../models/server'; +import { Users } from '../../../models/server'; import { Apps } from '../orchestrator'; import { formatAppInstanceForRest } from '../../lib/misc/formatAppInstanceForRest'; +import { Settings } from '../../../models/server/raw'; const appsEngineVersionForMarketplace = Info.marketplaceApiVersion.replace(/-.*/g, ''); const getDefaultHeaders = () => ({ @@ -67,7 +68,7 @@ export class AppsRestApi { // Gets the Apps from the marketplace if (this.queryParams.marketplace) { const headers = getDefaultHeaders(); - const token = getWorkspaceAccessToken(); + const token = Promise.await(getWorkspaceAccessToken()); if (token) { headers.Authorization = `Bearer ${ token }`; } @@ -91,7 +92,7 @@ export class AppsRestApi { if (this.queryParams.categories) { const headers = getDefaultHeaders(); - const token = getWorkspaceAccessToken(); + const token = Promise.await(getWorkspaceAccessToken()); if (token) { headers.Authorization = `Bearer ${ token }`; } @@ -187,7 +188,7 @@ export class AppsRestApi { }); const marketplacePromise = new Promise((resolve, reject) => { - const token = getWorkspaceAccessToken(); + const token = Promise.await(getWorkspaceAccessToken()); HTTP.get(`${ baseUrl }/v1/apps/${ this.bodyParams.appId }?appVersion=${ this.bodyParams.version }`, { headers: { @@ -307,7 +308,7 @@ export class AppsRestApi { const baseUrl = orchestrator.getMarketplaceUrl(); const headers = {}; - const token = getWorkspaceAccessToken(); + const token = Promise.await(getWorkspaceAccessToken()); if (token) { headers.Authorization = `Bearer ${ token }`; } @@ -337,7 +338,7 @@ export class AppsRestApi { const baseUrl = orchestrator.getMarketplaceUrl(); const headers = {}; // DO NOT ATTACH THE FRAMEWORK/ENGINE VERSION HERE. - const token = getWorkspaceAccessToken(); + const token = Promise.await(getWorkspaceAccessToken()); if (token) { headers.Authorization = `Bearer ${ token }`; } @@ -363,7 +364,7 @@ export class AppsRestApi { const baseUrl = orchestrator.getMarketplaceUrl(); const headers = getDefaultHeaders(); - const token = getWorkspaceAccessToken(); + const token = Promise.await(getWorkspaceAccessToken()); if (token) { headers.Authorization = `Bearer ${ token }`; } @@ -507,12 +508,12 @@ export class AppsRestApi { const baseUrl = orchestrator.getMarketplaceUrl(); const headers = getDefaultHeaders(); - const token = getWorkspaceAccessToken(); + const token = Promise.await(getWorkspaceAccessToken()); if (token) { headers.Authorization = `Bearer ${ token }`; } - const [workspaceIdSetting] = Settings.findById('Cloud_Workspace_Id').fetch(); + const workspaceIdSetting = Promise.await(Settings.findOneById('Cloud_Workspace_Id')); let result; try { diff --git a/app/apps/server/converters/settings.js b/app/apps/server/converters/settings.js index 82ffcd2b2f0f..bc5949bc7ccd 100644 --- a/app/apps/server/converters/settings.js +++ b/app/apps/server/converters/settings.js @@ -1,14 +1,14 @@ import { SettingType } from '@rocket.chat/apps-engine/definition/settings'; -import { Settings } from '../../../models'; +import { Settings } from '../../../models/server/raw'; export class AppSettingsConverter { constructor(orch) { this.orch = orch; } - convertById(settingId) { - const setting = Settings.findOneNotHiddenById(settingId); + async convertById(settingId) { + const setting = await Settings.findOneNotHiddenById(settingId); return this.convertToApp(setting); } diff --git a/app/apps/server/converters/uploads.js b/app/apps/server/converters/uploads.js index d95f5d10067f..efbda7ae5fd1 100644 --- a/app/apps/server/converters/uploads.js +++ b/app/apps/server/converters/uploads.js @@ -1,5 +1,5 @@ import { transformMappedData } from '../../lib/misc/transformMappedData'; -import Uploads from '../../../models/server/models/Uploads'; +import { Uploads } from '../../../models/server/raw'; export class AppUploadsConverter { constructor(orch) { @@ -7,7 +7,7 @@ export class AppUploadsConverter { } convertById(id) { - const upload = Uploads.findOneById(id); + const upload = Promise.await(Uploads.findOneById(id)); return this.convertToApp(upload); } diff --git a/app/apps/server/cron.js b/app/apps/server/cron.js index 3201612ea6b6..d38eebe060b2 100644 --- a/app/apps/server/cron.js +++ b/app/apps/server/cron.js @@ -6,8 +6,9 @@ import { AppStatus } from '@rocket.chat/apps-engine/definition/AppStatus'; import { Apps } from './orchestrator'; import { getWorkspaceAccessToken } from '../../cloud/server'; -import { Settings, Users } from '../../models/server'; +import { Users } from '../../models/server'; import { sendMessagesToAdmins } from '../../../server/lib/sendMessagesToAdmins'; +import { Settings } from '../../models/server/raw'; const notifyAdminsAboutInvalidApps = Meteor.bindEnvironment(function _notifyAdminsAboutInvalidApps(apps) { @@ -27,7 +28,7 @@ const notifyAdminsAboutInvalidApps = Meteor.bindEnvironment(function _notifyAdmi const rocketCatMessage = 'There is one or more apps in an invalid state. Go to Administration > Apps to review.'; const link = '/admin/apps'; - sendMessagesToAdmins({ + Promise.await(sendMessagesToAdmins({ msgs: ({ adminUser }) => ({ msg: `*${ TAPi18n.__(title, adminUser.language) }*\n${ TAPi18n.__(rocketCatMessage, adminUser.language) }` }), banners: ({ adminUser }) => { Users.removeBannerById(adminUser._id, { id }); @@ -41,7 +42,7 @@ const notifyAdminsAboutInvalidApps = Meteor.bindEnvironment(function _notifyAdmi link, }]; }, - }); + })); return apps; }); @@ -59,19 +60,19 @@ const notifyAdminsAboutRenewedApps = Meteor.bindEnvironment(function _notifyAdmi const rocketCatMessage = 'There is one or more disabled apps with valid licenses. Go to Administration > Apps to review.'; - sendMessagesToAdmins({ + Promise.await(sendMessagesToAdmins({ msgs: ({ adminUser }) => ({ msg: `${ TAPi18n.__(rocketCatMessage, adminUser.language) }` }), - }); + })); }); export const appsUpdateMarketplaceInfo = Meteor.bindEnvironment(function _appsUpdateMarketplaceInfo() { - const token = getWorkspaceAccessToken(); + const token = Promise.await(getWorkspaceAccessToken()); const baseUrl = Apps.getMarketplaceUrl(); - const [workspaceIdSetting] = Settings.findById('Cloud_Workspace_Id').fetch(); + const workspaceIdSetting = Promise.await(Settings.getValueById('Cloud_Workspace_Id')); const currentSeats = Users.getActiveLocalUserCount(); - const fullUrl = `${ baseUrl }/v1/workspaces/${ workspaceIdSetting.value }/apps?seats=${ currentSeats }`; + const fullUrl = `${ baseUrl }/v1/workspaces/${ workspaceIdSetting }/apps?seats=${ currentSeats }`; const options = { headers: { Authorization: `Bearer ${ token }`, diff --git a/app/authentication/server/lib/restrictLoginAttempts.ts b/app/authentication/server/lib/restrictLoginAttempts.ts index d3ca8f78e5ca..6561f3d3f12c 100644 --- a/app/authentication/server/lib/restrictLoginAttempts.ts +++ b/app/authentication/server/lib/restrictLoginAttempts.ts @@ -1,12 +1,10 @@ import moment from 'moment'; import { ILoginAttempt } from '../ILoginAttempt'; -import { ServerEvents, Users, Rooms } from '../../../models/server/raw'; -import { IServerEventType } from '../../../../definition/IServerEvent'; -import { IUser } from '../../../../definition/IUser'; +import { ServerEvents, Users, Rooms, Sessions } from '../../../models/server/raw'; +import { IServerEventType, IServerEvent } from '../../../../definition/IServerEvent'; import { settings } from '../../../settings/server'; import { addMinutesToADate } from '../../../../lib/utils/addMinutesToADate'; -import Sessions from '../../../models/server/raw/Sessions'; import { getClientAddress } from '../../../../server/lib/getClientAddress'; import { sendMessage } from '../../../lib/server/functions'; import { Logger } from '../../../logger/server'; @@ -52,7 +50,7 @@ export const isValidLoginAttemptByIp = async (ip: string): Promise => { return true; } - const lastLogin = await Sessions.findLastLoginByIp(ip) as {loginAt?: Date} | undefined; + const lastLogin = await Sessions.findLastLoginByIp(ip); let failedAttemptsSinceLastLogin; if (!lastLogin || !lastLogin.loginAt) { @@ -128,7 +126,7 @@ export const isValidAttemptByUser = async (login: ILoginAttempt): Promise => { - const user: Partial = { + const user: IServerEvent['u'] = { _id: login.user?._id, username: login.user?.username || login.methodArguments[0].user?.username, }; @@ -142,10 +140,15 @@ export const saveFailedLoginAttempts = async (login: ILoginAttempt): Promise => { + const user: IServerEvent['u'] = { + _id: login.user?._id, + username: login.user?.username || login.methodArguments[0].user?.username, + }; + await ServerEvents.insertOne({ ip: getClientAddress(login.connection), t: IServerEventType.LOGIN, ts: new Date(), - u: login.user, + u: user, }); }; diff --git a/app/authentication/server/startup/index.js b/app/authentication/server/startup/index.js index ff9e6b02f431..ed98b62aaf28 100644 --- a/app/authentication/server/startup/index.js +++ b/app/authentication/server/startup/index.js @@ -8,8 +8,8 @@ import { escapeRegExp, escapeHTML } from '@rocket.chat/string-helpers'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; import { callbacks } from '../../../callbacks/server'; -import { Roles, Users, Settings } from '../../../models/server'; -import { Users as UsersRaw } from '../../../models/server/raw'; +import { Users, Settings } from '../../../models/server'; +import { Roles, Users as UsersRaw } from '../../../models/server/raw'; import { addUserRoles } from '../../../authorization/server'; import { getAvatarSuggestionForUser } from '../../../lib/server/functions'; import { @@ -186,8 +186,7 @@ Accounts.onCreateUser(function(options, user = {}) { if (!user.active) { const destinations = []; - - Roles.findUsersInRole('admin').forEach((adminUser) => { + Promise.await(Roles.findUsersInRole('admin').toArray()).forEach((adminUser) => { if (Array.isArray(adminUser.emails)) { adminUser.emails.forEach((email) => { destinations.push(`${ adminUser.name }<${ email.address }>`); diff --git a/app/authorization/client/hasPermission.ts b/app/authorization/client/hasPermission.ts index 744903bc5062..23a0aeeca8a6 100644 --- a/app/authorization/client/hasPermission.ts +++ b/app/authorization/client/hasPermission.ts @@ -7,11 +7,11 @@ import { IUser } from '../../../definition/IUser'; import { IRole } from '../../../definition/IRole'; import { IPermission } from '../../../definition/IPermission'; -const isValidScope = (scope: IRole['scope']): scope is keyof typeof Models => +const isValidScope = (scope: IRole['scope']): boolean => typeof scope === 'string' && scope in Models; const createPermissionValidator = (quantifier: (predicate: (permissionId: IPermission['_id']) => boolean) => boolean) => - (permissionIds: IPermission['_id'][], scope: IRole['scope'], userId: IUser['_id']): boolean => { + (permissionIds: IPermission['_id'][], scope: string | undefined, userId: IUser['_id']): boolean => { const user: IUser | null = Models.Users.findOneById(userId, { fields: { roles: 1 } }); const checkEachPermission = quantifier.bind(permissionIds); @@ -34,7 +34,7 @@ const createPermissionValidator = (quantifier: (predicate: (permissionId: IPermi return false; } - const model = Models[roleScope]; + const model = Models[roleScope as keyof typeof Models]; return model.isUserInRole && model.isUserInRole(userId, roleName, scope); }); }); @@ -46,8 +46,8 @@ const all = createPermissionValidator(Array.prototype.every); const validatePermissions = ( permissions: IPermission['_id'] | IPermission['_id'][], - scope: IRole['scope'], - predicate: (permissionIds: IPermission['_id'][], scope: IRole['scope'], userId: IUser['_id']) => boolean, + scope: string | undefined, + predicate: (permissionIds: IPermission['_id'][], scope: string | undefined, userId: IUser['_id']) => boolean, userId?: IUser['_id'] | null, ): boolean => { userId = userId ?? Meteor.userId(); @@ -65,17 +65,17 @@ const validatePermissions = ( export const hasAllPermission = ( permissions: IPermission['_id'] | IPermission['_id'][], - scope?: IRole['scope'], + scope?: string, ): boolean => validatePermissions(permissions, scope, all); export const hasAtLeastOnePermission = ( permissions: IPermission['_id'] | IPermission['_id'][], - scope?: IRole['scope'], + scope?: string, ): boolean => validatePermissions(permissions, scope, atLeastOne); export const userHasAllPermission = ( permissions: IPermission['_id'] | IPermission['_id'][], - scope?: IRole['scope'], + scope?: string, userId?: IUser['_id'] | null, ): boolean => validatePermissions(permissions, scope, all, userId); diff --git a/app/authorization/server/functions/addUserRoles.js b/app/authorization/server/functions/addUserRoles.ts similarity index 54% rename from app/authorization/server/functions/addUserRoles.js rename to app/authorization/server/functions/addUserRoles.ts index 46302e81eb8d..dda983ff6a3c 100644 --- a/app/authorization/server/functions/addUserRoles.js +++ b/app/authorization/server/functions/addUserRoles.ts @@ -2,9 +2,11 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { getRoles } from './getRoles'; -import { Users, Roles } from '../../../models'; +import { Users } from '../../../models/server'; +import { IRole, IUser } from '../../../../definition/IUser'; +import { Roles } from '../../../models/server/raw'; -export const addUserRoles = (userId, roleNames, scope) => { +export const addUserRoles = (userId: IUser['_id'], roleNames: IRole['name'][], scope?: string): boolean => { if (!userId || !roleNames) { return false; } @@ -16,17 +18,19 @@ export const addUserRoles = (userId, roleNames, scope) => { }); } - roleNames = [].concat(roleNames); + if (!Array.isArray(roleNames)) { // TODO: remove this check + roleNames = [roleNames]; + } + const existingRoleNames = _.pluck(getRoles(), '_id'); const invalidRoleNames = _.difference(roleNames, existingRoleNames); if (!_.isEmpty(invalidRoleNames)) { for (const role of invalidRoleNames) { - Roles.createOrUpdate(role); + Promise.await(Roles.createOrUpdate(role)); } } - Roles.addUserRoles(userId, roleNames, scope); - + Promise.await(Roles.addUserRoles(userId, roleNames, scope)); return true; }; diff --git a/app/authorization/server/functions/getRoles.js b/app/authorization/server/functions/getRoles.js deleted file mode 100644 index 9d20c72d29a9..000000000000 --- a/app/authorization/server/functions/getRoles.js +++ /dev/null @@ -1,3 +0,0 @@ -import { Roles } from '../../../models'; - -export const getRoles = () => Roles.find().fetch(); diff --git a/app/authorization/server/functions/getRoles.ts b/app/authorization/server/functions/getRoles.ts new file mode 100644 index 000000000000..27de1000bb0a --- /dev/null +++ b/app/authorization/server/functions/getRoles.ts @@ -0,0 +1,4 @@ +import { IRole } from '../../../../definition/IUser'; +import { Roles } from '../../../models/server/raw'; + +export const getRoles = (): IRole[] => Promise.await(Roles.find().toArray()); diff --git a/app/authorization/server/functions/getUsersInRole.js b/app/authorization/server/functions/getUsersInRole.js deleted file mode 100644 index 27c369acf9ff..000000000000 --- a/app/authorization/server/functions/getUsersInRole.js +++ /dev/null @@ -1,3 +0,0 @@ -import { Roles } from '../../../models'; - -export const getUsersInRole = (roleName, scope, options) => Roles.findUsersInRole(roleName, scope, options); diff --git a/app/authorization/server/functions/getUsersInRole.ts b/app/authorization/server/functions/getUsersInRole.ts new file mode 100644 index 000000000000..8a8bcadf3dba --- /dev/null +++ b/app/authorization/server/functions/getUsersInRole.ts @@ -0,0 +1,14 @@ + + +import { Cursor, FindOneOptions, WithoutProjection } from 'mongodb'; + +import { IRole, IUser } from '../../../../definition/IUser'; +import { Roles } from '../../../models/server/raw'; + +export function getUsersInRole(name: IRole['name'], scope?: string): Promise>; + +export function getUsersInRole(name: IRole['name'], scope: string | undefined, options: WithoutProjection>): Promise>; + +export function getUsersInRole

(name: IRole['name'], scope: string | undefined, options: FindOneOptions

): Promise>; + +export function getUsersInRole

(name: IRole['name'], scope: string | undefined, options?: any | undefined): Promise> { return Roles.findUsersInRole(name, scope, options); } diff --git a/app/authorization/server/functions/removeUserFromRoles.js b/app/authorization/server/functions/removeUserFromRoles.js index b08c2778addb..a55d722bb891 100644 --- a/app/authorization/server/functions/removeUserFromRoles.js +++ b/app/authorization/server/functions/removeUserFromRoles.js @@ -2,7 +2,8 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { getRoles } from './getRoles'; -import { Users, Roles } from '../../../models'; +import { Users } from '../../../models/server'; +import { Roles } from '../../../models/server/raw'; export const removeUserFromRoles = (userId, roleNames, scope) => { if (!userId || !roleNames) { @@ -27,7 +28,7 @@ export const removeUserFromRoles = (userId, roleNames, scope) => { }); } - Roles.removeUserRoles(userId, roleNames, scope); + Promise.await(Roles.removeUserRoles(userId, roleNames, scope)); return true; }; diff --git a/app/authorization/server/functions/upsertPermissions.js b/app/authorization/server/functions/upsertPermissions.ts similarity index 87% rename from app/authorization/server/functions/upsertPermissions.js rename to app/authorization/server/functions/upsertPermissions.ts index 76e97fcef480..ad4c06c22471 100644 --- a/app/authorization/server/functions/upsertPermissions.js +++ b/app/authorization/server/functions/upsertPermissions.ts @@ -1,11 +1,11 @@ /* eslint no-multi-spaces: 0 */ -import Roles from '../../../models/server/models/Roles'; -import Permissions from '../../../models/server/models/Permissions'; -import Settings from '../../../models/server/models/Settings'; import { settings } from '../../../settings/server'; import { getSettingPermissionId, CONSTANTS } from '../../lib'; +import { Permissions, Roles, Settings } from '../../../models/server/raw'; +import { IPermission } from '../../../../definition/IPermission'; +import { ISetting } from '../../../../definition/ISetting'; -export const upsertPermissions = () => { +export const upsertPermissions = async (): Promise => { // Note: // 1.if we need to create a role that can only edit channel message, but not edit group message // then we can define edit--message instead of edit-message @@ -94,6 +94,7 @@ export const upsertPermissions = () => { { _id: 'create-invite-links', roles: ['admin', 'owner', 'moderator'] }, { _id: 'view-l-room', roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'] }, { _id: 'view-livechat-manager', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, + { _id: 'view-omnichannel-contact-center', roles: ['livechat-manager', 'livechat-agent', 'livechat-monitor', 'admin'] }, { _id: 'edit-omnichannel-contact', roles: ['livechat-manager', 'livechat-agent', 'admin'] }, { _id: 'view-livechat-rooms', roles: ['livechat-manager', 'livechat-monitor', 'admin'] }, { _id: 'close-livechat-room', roles: ['livechat-manager', 'livechat-monitor', 'livechat-agent', 'admin'] }, @@ -152,8 +153,8 @@ export const upsertPermissions = () => { ]; - for (const permission of permissions) { - Permissions.create(permission._id, permission.roles); + for await (const permission of permissions) { + await Permissions.create(permission._id, permission.roles); } const defaultRoles = [ @@ -170,29 +171,30 @@ export const upsertPermissions = () => { { name: 'livechat-manager', scope: 'Users', description: 'Livechat Manager' }, ]; - for (const role of defaultRoles) { - Roles.createOrUpdate(role.name, role.scope, role.description, true, false); + for await (const role of defaultRoles) { + await Roles.createOrUpdate(role.name, role.scope as 'Users' | 'Subscriptions', role.description, true, false); } - const getPreviousPermissions = function(settingId) { - const previousSettingPermissions = {}; + const getPreviousPermissions = async function(settingId?: string): Promise> { + const previousSettingPermissions: { + [key: string]: IPermission; + } = {}; - const selector = { level: CONSTANTS.SETTINGS_LEVEL }; - if (settingId) { - selector.settingId = settingId; - } + const selector = { level: 'settings' as const, ...settingId && { settingId } }; - Permissions.find(selector).forEach( - function(permission) { + await Permissions.find(selector).forEach( + function(permission: IPermission) { previousSettingPermissions[permission._id] = permission; }); return previousSettingPermissions; }; - const createSettingPermission = function(setting, previousSettingPermissions) { + const createSettingPermission = async function(setting: ISetting, previousSettingPermissions: { + [key: string]: IPermission; + }): Promise { const permissionId = getSettingPermissionId(setting._id); - const permission = { - level: CONSTANTS.SETTINGS_LEVEL, + const permission: Omit = { + level: CONSTANTS.SETTINGS_LEVEL as 'settings' | undefined, // copy those setting-properties which are needed to properly publish the setting-based permissions settingId: setting._id, group: setting.group, @@ -211,19 +213,19 @@ export const upsertPermissions = () => { permission.sectionPermissionId = getSettingPermissionId(setting.section); } - const existent = Permissions.findOne({ + const existent = await Permissions.findOne({ _id: permissionId, ...permission, }, { fields: { _id: 1 } }); if (!existent) { try { - Permissions.upsert({ _id: permissionId }, { $set: permission }); + await Permissions.update({ _id: permissionId }, { $set: permission }, { upsert: true }); } catch (e) { if (!e.message.includes('E11000')) { // E11000 refers to a MongoDB error that can occur when using unique indexes for upserts // https://docs.mongodb.com/manual/reference/method/db.collection.update/#use-unique-indexes - Permissions.upsert({ _id: permissionId }, { $set: permission }); + await Permissions.update({ _id: permissionId }, { $set: permission }, { upsert: true }); } } } @@ -231,17 +233,17 @@ export const upsertPermissions = () => { delete previousSettingPermissions[permissionId]; }; - const createPermissionsForExistingSettings = function() { - const previousSettingPermissions = getPreviousPermissions(); + const createPermissionsForExistingSettings = async function(): Promise { + const previousSettingPermissions = await getPreviousPermissions(); - Settings.findNotHidden().fetch().forEach((setting) => { + (await Settings.findNotHidden().toArray()).forEach((setting) => { createSettingPermission(setting, previousSettingPermissions); }); // remove permissions for non-existent settings - for (const obsoletePermission in previousSettingPermissions) { + for await (const obsoletePermission of Object.keys(previousSettingPermissions)) { if (previousSettingPermissions.hasOwnProperty(obsoletePermission)) { - Permissions.remove({ _id: obsoletePermission }); + await Permissions.deleteOne({ _id: obsoletePermission }); } } }; @@ -250,9 +252,9 @@ export const upsertPermissions = () => { createPermissionsForExistingSettings(); // register a callback for settings for be create in higher-level-packages - settings.on('*', function([settingId]) { - const previousSettingPermissions = getPreviousPermissions(settingId); - const setting = Settings.findOneById(settingId); + settings.on('*', async function([settingId]) { + const previousSettingPermissions = await getPreviousPermissions(settingId); + const setting = await Settings.findOneById(settingId); if (setting) { if (!setting.hidden) { createSettingPermission(setting, previousSettingPermissions); diff --git a/app/authorization/server/methods/addPermissionToRole.js b/app/authorization/server/methods/addPermissionToRole.ts similarity index 72% rename from app/authorization/server/methods/addPermissionToRole.js rename to app/authorization/server/methods/addPermissionToRole.ts index 5ca74ed3dbc9..42990b114437 100644 --- a/app/authorization/server/methods/addPermissionToRole.js +++ b/app/authorization/server/methods/addPermissionToRole.ts @@ -1,11 +1,12 @@ import { Meteor } from 'meteor/meteor'; -import { Permissions } from '../../../models/server'; + import { hasPermission } from '../functions/hasPermission'; import { CONSTANTS, AuthorizationUtils } from '../../lib'; +import { Permissions } from '../../../models/server/raw'; Meteor.methods({ - 'authorization:addPermissionToRole'(permissionId, role) { + async 'authorization:addPermissionToRole'(permissionId, role) { if (AuthorizationUtils.isPermissionRestrictedForRole(permissionId, role)) { throw new Meteor.Error('error-action-not-allowed', 'Permission is restricted', { method: 'authorization:addPermissionToRole', @@ -14,7 +15,14 @@ Meteor.methods({ } const uid = Meteor.userId(); - const permission = Permissions.findOneById(permissionId); + const permission = await Permissions.findOneById(permissionId); + + if (!permission) { + throw new Meteor.Error('error-invalid-permission', 'Permission does not exist', { + method: 'authorization:addPermissionToRole', + action: 'Adding_permission', + }); + } if (!uid || !hasPermission(uid, 'access-permissions') || (permission.level === CONSTANTS.SETTINGS_LEVEL && !hasPermission(uid, 'access-setting-permissions'))) { throw new Meteor.Error('error-action-not-allowed', 'Adding permission is not allowed', { diff --git a/app/authorization/server/methods/addUserToRole.js b/app/authorization/server/methods/addUserToRole.ts similarity index 84% rename from app/authorization/server/methods/addUserToRole.js rename to app/authorization/server/methods/addUserToRole.ts index a7fdd21ec24d..3182d327ff47 100644 --- a/app/authorization/server/methods/addUserToRole.js +++ b/app/authorization/server/methods/addUserToRole.ts @@ -1,13 +1,14 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; -import { Users, Roles } from '../../../models/server'; +import { Users } from '../../../models/server'; import { settings } from '../../../settings/server'; import { hasPermission } from '../functions/hasPermission'; import { api } from '../../../../server/sdk/api'; +import { Roles } from '../../../models/server/raw'; Meteor.methods({ - 'authorization:addUserToRole'(roleName, username, scope) { + async 'authorization:addUserToRole'(roleName, username, scope) { if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'access-permissions')) { throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed', { method: 'authorization:addUserToRole', @@ -41,13 +42,13 @@ Meteor.methods({ } // verify if user can be added to given scope - if (scope && !Roles.canAddUserToRole(user._id, roleName, scope)) { + if (scope && !await Roles.canAddUserToRole(user._id, roleName, scope)) { throw new Meteor.Error('error-invalid-user', 'User is not part of given room', { method: 'authorization:addUserToRole', }); } - const add = Roles.addUserRoles(user._id, roleName, scope); + const add = await Roles.addUserRoles(user._id, [roleName], scope); if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { diff --git a/app/authorization/server/methods/deleteRole.js b/app/authorization/server/methods/deleteRole.ts similarity index 67% rename from app/authorization/server/methods/deleteRole.js rename to app/authorization/server/methods/deleteRole.ts index 8613e1761b0a..8925942b23f3 100644 --- a/app/authorization/server/methods/deleteRole.js +++ b/app/authorization/server/methods/deleteRole.ts @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; -import * as Models from '../../../models/server'; +import { Roles } from '../../../models/server/raw'; import { hasPermission } from '../functions/hasPermission'; Meteor.methods({ - 'authorization:deleteRole'(roleName) { + async 'authorization:deleteRole'(roleName) { if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'access-permissions')) { throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed', { method: 'authorization:deleteRole', @@ -12,7 +12,7 @@ Meteor.methods({ }); } - const role = Models.Roles.findOne(roleName); + const role = await Roles.findOne(roleName); if (!role) { throw new Meteor.Error('error-invalid-role', 'Invalid role', { method: 'authorization:deleteRole', @@ -25,16 +25,14 @@ Meteor.methods({ }); } - const roleScope = role.scope || 'Users'; - const model = Models[roleScope]; - const existingUsers = model && model.findUsersInRoles && model.findUsersInRoles(roleName); + const users = await(await Roles.findUsersInRole(roleName)).count(); - if (existingUsers && existingUsers.count() > 0) { + if (users > 0) { throw new Meteor.Error('error-role-in-use', 'Cannot delete role because it\'s in use', { method: 'authorization:deleteRole', }); } - return Models.Roles.remove(role.name); + return Roles.removeById(role.name); }, }); diff --git a/app/authorization/server/methods/removeRoleFromPermission.js b/app/authorization/server/methods/removeRoleFromPermission.ts similarity index 70% rename from app/authorization/server/methods/removeRoleFromPermission.js rename to app/authorization/server/methods/removeRoleFromPermission.ts index e0aa20ed34db..c31592a0ceca 100644 --- a/app/authorization/server/methods/removeRoleFromPermission.js +++ b/app/authorization/server/methods/removeRoleFromPermission.ts @@ -1,13 +1,18 @@ import { Meteor } from 'meteor/meteor'; -import { Permissions } from '../../../models/server'; import { hasPermission } from '../functions/hasPermission'; import { CONSTANTS } from '../../lib'; +import { Permissions } from '../../../models/server/raw'; Meteor.methods({ - 'authorization:removeRoleFromPermission'(permissionId, role) { + async 'authorization:removeRoleFromPermission'(permissionId, role) { const uid = Meteor.userId(); - const permission = Permissions.findOneById(permissionId); + const permission = await Permissions.findOneById(permissionId); + + + if (!permission) { + throw new Meteor.Error('error-permission-not-found', 'Permission not found', { method: 'authorization:removeRoleFromPermission' }); + } if (!uid || !hasPermission(uid, 'access-permissions') || (permission.level === CONSTANTS.SETTINGS_LEVEL && !hasPermission(uid, 'access-setting-permissions'))) { throw new Meteor.Error('error-action-not-allowed', 'Removing permission is not allowed', { diff --git a/app/authorization/server/methods/removeUserFromRole.js b/app/authorization/server/methods/removeUserFromRole.js index 9a36a8895870..d98ff825af9b 100644 --- a/app/authorization/server/methods/removeUserFromRole.js +++ b/app/authorization/server/methods/removeUserFromRole.js @@ -1,13 +1,13 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; -import { Roles } from '../../../models/server'; import { settings } from '../../../settings/server'; import { hasPermission } from '../functions/hasPermission'; import { api } from '../../../../server/sdk/api'; +import { Roles } from '../../../models/server/raw'; Meteor.methods({ - 'authorization:removeUserFromRole'(roleName, username, scope) { + async 'authorization:removeUserFromRole'(roleName, username, scope) { if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'access-permissions')) { throw new Meteor.Error('error-action-not-allowed', 'Access permissions is not allowed', { method: 'authorization:removeUserFromRole', @@ -44,7 +44,7 @@ Meteor.methods({ }, }).count(); - const userIsAdmin = user.roles.indexOf('admin') > -1; + const userIsAdmin = user.roles?.indexOf('admin') > -1; if (adminCount === 1 && userIsAdmin) { throw new Meteor.Error('error-action-not-allowed', 'Leaving the app without admins is not allowed', { method: 'removeUserFromRole', @@ -53,7 +53,7 @@ Meteor.methods({ } } - const remove = Roles.removeUserRoles(user._id, roleName, scope); + const remove = await Roles.removeUserRoles(user._id, [roleName], scope); if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { type: 'removed', diff --git a/app/authorization/server/methods/saveRole.js b/app/authorization/server/methods/saveRole.ts similarity index 80% rename from app/authorization/server/methods/saveRole.js rename to app/authorization/server/methods/saveRole.ts index 5e09f211240d..04f431ba9906 100644 --- a/app/authorization/server/methods/saveRole.js +++ b/app/authorization/server/methods/saveRole.ts @@ -1,12 +1,12 @@ import { Meteor } from 'meteor/meteor'; -import { Roles } from '../../../models/server'; import { settings } from '../../../settings/server'; import { hasPermission } from '../functions/hasPermission'; import { api } from '../../../../server/sdk/api'; +import { Roles } from '../../../models/server/raw'; Meteor.methods({ - 'authorization:saveRole'(roleData) { + async 'authorization:saveRole'(roleData) { if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'access-permissions')) { throw new Meteor.Error('error-action-not-allowed', 'Accessing permissions is not allowed', { method: 'authorization:saveRole', @@ -24,7 +24,7 @@ Meteor.methods({ roleData.scope = 'Users'; } - const update = Roles.createOrUpdate(roleData.name, roleData.scope, roleData.description, false, roleData.mandatory2fa); + const update = await Roles.createOrUpdate(roleData.name, roleData.scope, roleData.description, false, roleData.mandatory2fa); if (settings.get('UI_DisplayRoles')) { api.broadcast('user.roleUpdate', { type: 'changed', diff --git a/app/authorization/server/streamer/permissions/index.js b/app/authorization/server/streamer/permissions/index.js deleted file mode 100644 index edffbdfe3e73..000000000000 --- a/app/authorization/server/streamer/permissions/index.js +++ /dev/null @@ -1,25 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import Permissions from '../../../../models/server/models/Permissions'; - -Meteor.methods({ - 'permissions/get'(updatedAt) { - // TODO: should we return this for non logged users? - // TODO: we could cache this collection - - const records = Permissions.find().fetch(); - - if (updatedAt instanceof Date) { - return { - update: records.filter((record) => record._updatedAt > updatedAt), - remove: Permissions.trashFindDeletedAfter( - updatedAt, - {}, - { fields: { _id: 1, _deletedAt: 1 } }, - ).fetch(), - }; - } - - return records; - }, -}); diff --git a/app/authorization/server/streamer/permissions/index.ts b/app/authorization/server/streamer/permissions/index.ts new file mode 100644 index 000000000000..5494f8f1f78e --- /dev/null +++ b/app/authorization/server/streamer/permissions/index.ts @@ -0,0 +1,28 @@ +import { Meteor } from 'meteor/meteor'; +import { check, Match } from 'meteor/check'; + +import { Permissions } from '../../../../models/server/raw'; + +Meteor.methods({ + async 'permissions/get'(updatedAt: Date) { + check(updatedAt, Match.Maybe(Date)); + + // TODO: should we return this for non logged users? + // TODO: we could cache this collection + + const records = await Permissions.find(updatedAt && { _updatedAt: { $gt: updatedAt } }).toArray(); + + if (updatedAt instanceof Date) { + return { + update: records, + remove: await Permissions.trashFindDeletedAfter( + updatedAt, + {}, + { fields: { _id: 1, _deletedAt: 1 } }, + ).toArray(), + }; + } + + return records; + }, +}); diff --git a/app/autotranslate/server/permissions.js b/app/autotranslate/server/permissions.js deleted file mode 100644 index 64ce0028fa87..000000000000 --- a/app/autotranslate/server/permissions.js +++ /dev/null @@ -1,11 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Permissions } from '../../models'; - -Meteor.startup(() => { - if (Permissions) { - if (!Permissions.findOne({ _id: 'auto-translate' })) { - Permissions.insert({ _id: 'auto-translate', roles: ['admin'] }); - } - } -}); diff --git a/app/autotranslate/server/permissions.ts b/app/autotranslate/server/permissions.ts new file mode 100644 index 000000000000..5ce05e8f1ef7 --- /dev/null +++ b/app/autotranslate/server/permissions.ts @@ -0,0 +1,9 @@ +import { Meteor } from 'meteor/meteor'; + +import { Permissions } from '../../models/server/raw'; + +Meteor.startup(async () => { + if (!await Permissions.findOne({ _id: 'auto-translate' })) { + Permissions.create('auto-translate', ['admin']); + } +}); diff --git a/app/cas/server/cas_server.js b/app/cas/server/cas_server.js index 646e87a8f053..cc569eeab441 100644 --- a/app/cas/server/cas_server.js +++ b/app/cas/server/cas_server.js @@ -10,7 +10,8 @@ import CAS from 'cas'; import { logger } from './cas_rocketchat'; import { settings } from '../../settings'; -import { Rooms, CredentialTokens } from '../../models/server'; +import { Rooms } from '../../models/server'; +import { CredentialTokens } from '../../models/server/raw'; import { _setRealName } from '../../lib'; import { createRoom } from '../../lib/server/functions/createRoom'; @@ -43,7 +44,7 @@ const casTicket = function(req, token, callback) { service: `${ appUrl }/_cas/${ token }`, }); - cas.validate(ticketId, Meteor.bindEnvironment(function(err, status, username, details) { + cas.validate(ticketId, Meteor.bindEnvironment(async function(err, status, username, details) { if (err) { logger.error(`error when trying to validate: ${ err.message }`); } else if (status) { @@ -54,11 +55,11 @@ const casTicket = function(req, token, callback) { if (details && details.attributes) { _.extend(user_info, { attributes: details.attributes }); } - CredentialTokens.create(token, user_info); + await CredentialTokens.create(token, user_info); } else { logger.error(`Unable to validate ticket: ${ ticketId }`); } - // logger.debug("Receveied response: " + JSON.stringify(details, null , 4)); + // logger.debug("Received response: " + JSON.stringify(details, null , 4)); callback(); })); @@ -114,7 +115,8 @@ Accounts.registerLoginHandler(function(options) { return undefined; } - const credentials = CredentialTokens.findOneById(options.cas.credentialToken); + // TODO: Sync wrapper due to the chain conversion to async models + const credentials = Promise.await(CredentialTokens.findOneNotExpiredById(options.cas.credentialToken)); if (credentials === undefined) { throw new Meteor.Error(Accounts.LoginCancelledError.numericError, 'no matching login attempt found'); diff --git a/app/channel-settings/server/functions/saveRoomName.js b/app/channel-settings/server/functions/saveRoomName.js index 5d3197d133b3..0cc31cfa77b4 100644 --- a/app/channel-settings/server/functions/saveRoomName.js +++ b/app/channel-settings/server/functions/saveRoomName.js @@ -1,6 +1,7 @@ import { Meteor } from 'meteor/meteor'; -import { Rooms, Messages, Subscriptions, Integrations } from '../../../models/server'; +import { Rooms, Messages, Subscriptions } from '../../../models/server'; +import { Integrations } from '../../../models/server/raw'; import { roomTypes, getValidRoomName } from '../../../utils/server'; import { callbacks } from '../../../callbacks/server'; import { checkUsernameAvailability } from '../../../lib/server/functions'; @@ -19,7 +20,7 @@ const updateRoomName = (rid, displayName, isDiscussion) => { return Rooms.setNameById(rid, slugifiedRoomName, displayName) && Subscriptions.updateNameAndAlertByRoomId(rid, slugifiedRoomName, displayName); }; -export const saveRoomName = function(rid, displayName, user, sendMessage = true) { +export async function saveRoomName(rid, displayName, user, sendMessage = true) { const room = Rooms.findOneById(rid); if (roomTypes.getConfig(room.t).preventRenaming()) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { @@ -35,10 +36,10 @@ export const saveRoomName = function(rid, displayName, user, sendMessage = true) return; } - Integrations.updateRoomName(room.name, displayName); + await Integrations.updateRoomName(room.name, displayName); if (sendMessage) { Messages.createRoomRenamedWithRoomIdRoomNameAndUser(rid, displayName, user); } callbacks.run('afterRoomNameChange', { rid, name: displayName, oldName: room.name }); return displayName; -}; +} diff --git a/app/channel-settings/server/methods/saveRoomSettings.js b/app/channel-settings/server/methods/saveRoomSettings.js index 811c492fb70d..59c0bb239f79 100644 --- a/app/channel-settings/server/methods/saveRoomSettings.js +++ b/app/channel-settings/server/methods/saveRoomSettings.js @@ -128,7 +128,7 @@ const validators = { const settingSavers = { roomName({ value, rid, user, room }) { - if (!saveRoomName(rid, value, user)) { + if (!Promise.await(saveRoomName(rid, value, user))) { return; } @@ -231,13 +231,13 @@ const settingSavers = { favorite({ value, rid }) { Rooms.saveFavoriteById(rid, value.favorite, value.defaultValue); }, - roomAvatar({ value, rid, user }) { - setRoomAvatar(rid, value, user); + async roomAvatar({ value, rid, user }) { + await setRoomAvatar(rid, value, user); }, }; Meteor.methods({ - saveRoomSettings(rid, settings, value) { + async saveRoomSettings(rid, settings, value) { const userId = Meteor.userId(); if (!userId) { @@ -313,10 +313,10 @@ Meteor.methods({ }); // saving data - Object.keys(settings).forEach((setting) => { + for await (const setting of Object.keys(settings)) { const value = settings[setting]; - const saver = settingSavers[setting]; + const saver = await settingSavers[setting]; if (saver) { saver({ value, @@ -325,7 +325,7 @@ Meteor.methods({ user, }); } - }); + } Meteor.defer(function() { const room = Rooms.findOneById(rid); diff --git a/app/cloud/server/functions/buildRegistrationData.js b/app/cloud/server/functions/buildRegistrationData.js index d8ecff67687f..5346558e23ab 100644 --- a/app/cloud/server/functions/buildRegistrationData.js +++ b/app/cloud/server/functions/buildRegistrationData.js @@ -1,10 +1,11 @@ import { settings } from '../../../settings/server'; -import { Users, Statistics } from '../../../models/server'; +import { Users } from '../../../models/server'; +import { Statistics } from '../../../models/server/raw'; import { statistics } from '../../../statistics'; import { LICENSE_VERSION } from '../license'; -export function buildWorkspaceRegistrationData() { - const stats = Statistics.findLast() || statistics.get(); +export async function buildWorkspaceRegistrationData() { + const stats = await Statistics.findLast() || statistics.get(); const address = settings.get('Site_Url'); const siteName = settings.get('Site_Name'); diff --git a/app/cloud/server/functions/startRegisterWorkspace.js b/app/cloud/server/functions/startRegisterWorkspace.js index bb533c79c580..2f9e4f90b789 100644 --- a/app/cloud/server/functions/startRegisterWorkspace.js +++ b/app/cloud/server/functions/startRegisterWorkspace.js @@ -7,18 +7,17 @@ import { Settings } from '../../../models'; import { buildWorkspaceRegistrationData } from './buildRegistrationData'; import { SystemLogger } from '../../../../server/lib/logger/system'; - -export function startRegisterWorkspace(resend = false) { +export async function startRegisterWorkspace(resend = false) { const { workspaceRegistered, connectToCloud } = retrieveRegistrationStatus(); if ((workspaceRegistered && connectToCloud) || process.env.TEST_MODE) { - syncWorkspace(true); + await syncWorkspace(true); return true; } Settings.updateValueById('Register_Server', true); - const regInfo = buildWorkspaceRegistrationData(); + const regInfo = await buildWorkspaceRegistrationData(); const cloudUrl = settings.get('Cloud_Url'); diff --git a/app/cloud/server/functions/syncWorkspace.js b/app/cloud/server/functions/syncWorkspace.js index 03f67acf4a4b..1b1021402e25 100644 --- a/app/cloud/server/functions/syncWorkspace.js +++ b/app/cloud/server/functions/syncWorkspace.js @@ -10,13 +10,13 @@ import { getAndCreateNpsSurvey } from '../../../../server/services/nps/getAndCre import { NPS, Banner } from '../../../../server/sdk'; import { SystemLogger } from '../../../../server/lib/logger/system'; -export function syncWorkspace(reconnectCheck = false) { +export async function syncWorkspace(reconnectCheck = false) { const { workspaceRegistered, connectToCloud } = retrieveRegistrationStatus(); if (!workspaceRegistered || (!connectToCloud && !reconnectCheck)) { return false; } - const info = buildWorkspaceRegistrationData(); + const info = await buildWorkspaceRegistrationData(); const workspaceUrl = settings.get('Cloud_Workspace_Registration_Client_Uri'); @@ -64,11 +64,11 @@ export function syncWorkspace(reconnectCheck = false) { const startAt = new Date(data.nps.startAt); - Promise.await(NPS.create({ + await NPS.create({ npsId, startAt, expireAt: new Date(expireAt), - })); + }); const now = new Date(); @@ -79,19 +79,19 @@ export function syncWorkspace(reconnectCheck = false) { // add banners if (data.banners) { - for (const banner of data.banners) { + for await (const banner of data.banners) { const { createdAt, expireAt, startAt, } = banner; - Promise.await(Banner.create({ + await Banner.create({ ...banner, createdAt: new Date(createdAt), expireAt: new Date(expireAt), startAt: new Date(startAt), - })); + }); } } diff --git a/app/cloud/server/methods.js b/app/cloud/server/methods.js index 7723566601f5..83847711a603 100644 --- a/app/cloud/server/methods.js +++ b/app/cloud/server/methods.js @@ -26,7 +26,7 @@ Meteor.methods({ return retrieveRegistrationStatus(); }, - 'cloud:getWorkspaceRegisterData'() { + async 'cloud:getWorkspaceRegisterData'() { if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'cloud:getWorkspaceRegisterData' }); } @@ -35,9 +35,9 @@ Meteor.methods({ throw new Meteor.Error('error-not-authorized', 'Not authorized', { method: 'cloud:getWorkspaceRegisterData' }); } - return Buffer.from(JSON.stringify(buildWorkspaceRegistrationData())).toString('base64'); + return Buffer.from(JSON.stringify(await buildWorkspaceRegistrationData())).toString('base64'); }, - 'cloud:registerWorkspace'() { + async 'cloud:registerWorkspace'() { if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'cloud:startRegister' }); } @@ -48,7 +48,7 @@ Meteor.methods({ return startRegisterWorkspace(); }, - 'cloud:syncWorkspace'() { + async 'cloud:syncWorkspace'() { if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'cloud:syncWorkspace' }); } diff --git a/app/crowd/server/crowd.js b/app/crowd/server/crowd.js index d5b4b5b97ef3..dba130ef489e 100644 --- a/app/crowd/server/crowd.js +++ b/app/crowd/server/crowd.js @@ -208,7 +208,7 @@ export class CROWD { if (settings.get('CROWD_Remove_Orphaned_Users') === true) { logger.info('Removing user:', crowd_username); Meteor.defer(function() { - deleteUser(user._id); + Promise.await(deleteUser(user._id)); logger.info('User removed:', crowd_username); }); } diff --git a/app/custom-sounds/server/methods/deleteCustomSound.js b/app/custom-sounds/server/methods/deleteCustomSound.js index b72c852bacfc..d168fac12d1d 100644 --- a/app/custom-sounds/server/methods/deleteCustomSound.js +++ b/app/custom-sounds/server/methods/deleteCustomSound.js @@ -1,16 +1,16 @@ import { Meteor } from 'meteor/meteor'; -import { CustomSounds } from '../../../models'; +import { CustomSounds } from '../../../models/server/raw'; import { hasPermission } from '../../../authorization'; import { Notifications } from '../../../notifications'; import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds'; Meteor.methods({ - deleteCustomSound(_id) { + async deleteCustomSound(_id) { let sound = null; if (hasPermission(this.userId, 'manage-sounds')) { - sound = CustomSounds.findOneById(_id); + sound = await CustomSounds.findOneById(_id); } else { throw new Meteor.Error('not_authorized'); } @@ -20,7 +20,7 @@ Meteor.methods({ } RocketChatFileCustomSoundsInstance.deleteFile(`${ sound._id }.${ sound.extension }`); - CustomSounds.removeById(_id); + await CustomSounds.removeById(_id); Notifications.notifyAll('deleteCustomSound', { soundData: sound }); return true; diff --git a/app/custom-sounds/server/methods/insertOrUpdateSound.js b/app/custom-sounds/server/methods/insertOrUpdateSound.js index d3fe25e0173b..b1fa7c749747 100644 --- a/app/custom-sounds/server/methods/insertOrUpdateSound.js +++ b/app/custom-sounds/server/methods/insertOrUpdateSound.js @@ -3,12 +3,12 @@ import s from 'underscore.string'; import { check } from 'meteor/check'; import { hasPermission } from '../../../authorization'; -import { CustomSounds } from '../../../models'; +import { CustomSounds } from '../../../models/server/raw'; import { Notifications } from '../../../notifications'; import { RocketChatFileCustomSoundsInstance } from '../startup/custom-sounds'; Meteor.methods({ - insertOrUpdateSound(soundData) { + async insertOrUpdateSound(soundData) { if (!hasPermission(this.userId, 'manage-sounds')) { throw new Meteor.Error('not_authorized'); } @@ -34,9 +34,9 @@ Meteor.methods({ if (soundData._id) { check(soundData._id, String); - matchingResults = CustomSounds.findByNameExceptId(soundData.name, soundData._id).fetch(); + matchingResults = await CustomSounds.findByNameExceptId(soundData.name, soundData._id).toArray(); } else { - matchingResults = CustomSounds.findByName(soundData.name).fetch(); + matchingResults = await CustomSounds.findByName(soundData.name).toArray(); } if (matchingResults.length > 0) { @@ -50,7 +50,7 @@ Meteor.methods({ extension: soundData.extension, }; - const _id = CustomSounds.create(createSound); + const _id = await (await CustomSounds.create(createSound)).insertedId; createSound._id = _id; return _id; @@ -61,7 +61,7 @@ Meteor.methods({ } if (soundData.name !== soundData.previousName) { - CustomSounds.setName(soundData._id, soundData.name); + await CustomSounds.setName(soundData._id, soundData.name); Notifications.notifyAll('updateCustomSound', { soundData }); } diff --git a/app/custom-sounds/server/methods/listCustomSounds.js b/app/custom-sounds/server/methods/listCustomSounds.js index 90bf6db20435..475da52286be 100644 --- a/app/custom-sounds/server/methods/listCustomSounds.js +++ b/app/custom-sounds/server/methods/listCustomSounds.js @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; -import { CustomSounds } from '../../../models'; +import { CustomSounds } from '../../../models/server/raw'; Meteor.methods({ - listCustomSounds() { - return CustomSounds.find({}).fetch(); + async listCustomSounds() { + return CustomSounds.find({}).toArray(); }, }); diff --git a/app/discussion/server/permissions.js b/app/discussion/server/permissions.ts similarity index 87% rename from app/discussion/server/permissions.js rename to app/discussion/server/permissions.ts index 3d54e4c66b16..da3ac2ee2290 100644 --- a/app/discussion/server/permissions.js +++ b/app/discussion/server/permissions.ts @@ -1,6 +1,7 @@ import { Meteor } from 'meteor/meteor'; -import { Permissions } from '../../models'; +import { Permissions } from '../../models/server/raw'; + Meteor.startup(() => { // Add permissions for discussion diff --git a/app/e2e/server/methods/getUsersOfRoomWithoutKey.js b/app/e2e/server/methods/getUsersOfRoomWithoutKey.js index a686af5e88c4..2139ac8fde7e 100644 --- a/app/e2e/server/methods/getUsersOfRoomWithoutKey.js +++ b/app/e2e/server/methods/getUsersOfRoomWithoutKey.js @@ -1,16 +1,23 @@ import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; -import { Subscriptions, Users } from '../../../models'; +import { canAccessRoom } from '../../../authorization/server'; +import { Subscriptions, Users } from '../../../models/server'; Meteor.methods({ 'e2e.getUsersOfRoomWithoutKey'(rid) { + check(rid, String); + const userId = Meteor.userId(); if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'e2e.getUsersOfRoomWithoutKey' }); } - const room = Meteor.call('canAccessRoom', rid, userId); - if (!room) { + if (!rid) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.getUsersOfRoomWithoutKey' }); + } + + if (!canAccessRoom({ _id: rid }, { _id: userId })) { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.getUsersOfRoomWithoutKey' }); } diff --git a/app/e2e/server/methods/setRoomKeyID.js b/app/e2e/server/methods/setRoomKeyID.js index a273e803b934..e2f8aafa059b 100644 --- a/app/e2e/server/methods/setRoomKeyID.js +++ b/app/e2e/server/methods/setRoomKeyID.js @@ -1,19 +1,29 @@ import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; -import { Rooms } from '../../../models'; +import { canAccessRoom } from '../../../authorization/server'; +import { Rooms } from '../../../models/server'; Meteor.methods({ 'e2e.setRoomKeyID'(rid, keyID) { + check(rid, String); + check(keyID, String); + const userId = Meteor.userId(); if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'e2e.setRoomKeyID' }); } - const room = Meteor.call('canAccessRoom', rid, userId); - if (!room) { + if (!rid) { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.setRoomKeyID' }); } + if (!canAccessRoom({ _id: rid }, { _id: userId })) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'e2e.setRoomKeyID' }); + } + + const room = Rooms.findOneById(rid, { fields: { e2eKeyId: 1 } }); + if (room.e2eKeyId) { throw new Meteor.Error('error-room-e2e-key-already-exists', 'E2E Key ID already exists', { method: 'e2e.setRoomKeyID' }); } diff --git a/app/emoji-custom/server/methods/deleteEmojiCustom.js b/app/emoji-custom/server/methods/deleteEmojiCustom.js index 7393f245b459..2964c5ff6cd6 100644 --- a/app/emoji-custom/server/methods/deleteEmojiCustom.js +++ b/app/emoji-custom/server/methods/deleteEmojiCustom.js @@ -2,22 +2,22 @@ import { Meteor } from 'meteor/meteor'; import { api } from '../../../../server/sdk/api'; import { hasPermission } from '../../../authorization'; -import { EmojiCustom } from '../../../models'; +import { EmojiCustom } from '../../../models/server/raw'; import { RocketChatFileEmojiCustomInstance } from '../startup/emoji-custom'; Meteor.methods({ - deleteEmojiCustom(emojiID) { + async deleteEmojiCustom(emojiID) { if (!hasPermission(this.userId, 'manage-emoji')) { throw new Meteor.Error('not_authorized'); } - const emoji = EmojiCustom.findOneById(emojiID); + const emoji = await EmojiCustom.findOneById(emojiID); if (emoji == null) { throw new Meteor.Error('Custom_Emoji_Error_Invalid_Emoji', 'Invalid emoji', { method: 'deleteEmojiCustom' }); } RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${ emoji.name }.${ emoji.extension }`)); - EmojiCustom.removeById(emojiID); + await EmojiCustom.removeById(emojiID); api.broadcast('emoji.deleteCustom', emoji); return true; diff --git a/app/emoji-custom/server/methods/insertOrUpdateEmoji.js b/app/emoji-custom/server/methods/insertOrUpdateEmoji.js index b96b40b2fbd0..23843c81cec9 100644 --- a/app/emoji-custom/server/methods/insertOrUpdateEmoji.js +++ b/app/emoji-custom/server/methods/insertOrUpdateEmoji.js @@ -4,12 +4,12 @@ import s from 'underscore.string'; import limax from 'limax'; import { hasPermission } from '../../../authorization'; -import { EmojiCustom } from '../../../models'; +import { EmojiCustom } from '../../../models/server/raw'; import { RocketChatFileEmojiCustomInstance } from '../startup/emoji-custom'; import { api } from '../../../../server/sdk/api'; Meteor.methods({ - insertOrUpdateEmoji(emojiData) { + async insertOrUpdateEmoji(emojiData) { if (!hasPermission(this.userId, 'manage-emoji')) { throw new Meteor.Error('not_authorized'); } @@ -50,14 +50,14 @@ Meteor.methods({ let matchingResults = []; if (emojiData._id) { - matchingResults = EmojiCustom.findByNameOrAliasExceptID(emojiData.name, emojiData._id).fetch(); - for (const alias of emojiData.aliases) { - matchingResults = matchingResults.concat(EmojiCustom.findByNameOrAliasExceptID(alias, emojiData._id).fetch()); + matchingResults = await EmojiCustom.findByNameOrAliasExceptID(emojiData.name, emojiData._id).toArray(); + for await (const alias of emojiData.aliases) { + matchingResults = matchingResults.concat(await EmojiCustom.findByNameOrAliasExceptID(alias, emojiData._id).toArray()); } } else { - matchingResults = EmojiCustom.findByNameOrAlias(emojiData.name).fetch(); - for (const alias of emojiData.aliases) { - matchingResults = matchingResults.concat(EmojiCustom.findByNameOrAlias(alias).fetch()); + matchingResults = await EmojiCustom.findByNameOrAlias(emojiData.name).toArray(); + for await (const alias of emojiData.aliases) { + matchingResults = matchingResults.concat(await EmojiCustom.findByNameOrAlias(alias).toArray()); } } @@ -77,7 +77,7 @@ Meteor.methods({ extension: emojiData.extension, }; - const _id = EmojiCustom.create(createEmoji); + const _id = (await EmojiCustom.create(createEmoji)).insertedId; api.broadcast('emoji.updateCustom', createEmoji); @@ -90,7 +90,7 @@ Meteor.methods({ RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${ emojiData.previousName }.${ emojiData.extension }`)); RocketChatFileEmojiCustomInstance.deleteFile(encodeURIComponent(`${ emojiData.previousName }.${ emojiData.previousExtension }`)); - EmojiCustom.setExtension(emojiData._id, emojiData.extension); + await EmojiCustom.setExtension(emojiData._id, emojiData.extension); } else if (emojiData.name !== emojiData.previousName) { const rs = RocketChatFileEmojiCustomInstance.getFileWithReadStream(encodeURIComponent(`${ emojiData.previousName }.${ emojiData.previousExtension }`)); if (rs !== null) { @@ -104,13 +104,13 @@ Meteor.methods({ } if (emojiData.name !== emojiData.previousName) { - EmojiCustom.setName(emojiData._id, emojiData.name); + await EmojiCustom.setName(emojiData._id, emojiData.name); } if (emojiData.aliases) { - EmojiCustom.setAliases(emojiData._id, emojiData.aliases); + await EmojiCustom.setAliases(emojiData._id, emojiData.aliases); } else { - EmojiCustom.setAliases(emojiData._id, []); + await EmojiCustom.setAliases(emojiData._id, []); } api.broadcast('emoji.updateCustom', emojiData); diff --git a/app/emoji-custom/server/methods/listEmojiCustom.js b/app/emoji-custom/server/methods/listEmojiCustom.js index d06b382af85e..d66aeee1a6ad 100644 --- a/app/emoji-custom/server/methods/listEmojiCustom.js +++ b/app/emoji-custom/server/methods/listEmojiCustom.js @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; -import { EmojiCustom } from '../../../models'; +import { EmojiCustom } from '../../../models/server/raw'; Meteor.methods({ - listEmojiCustom(options = {}) { - return EmojiCustom.find(options).fetch(); + async listEmojiCustom(options = {}) { + return EmojiCustom.find(options).toArray(); }, }); diff --git a/app/federation/server/endpoints/dispatch.js b/app/federation/server/endpoints/dispatch.js index ae392ac8aac8..333a30bbeebf 100644 --- a/app/federation/server/endpoints/dispatch.js +++ b/app/federation/server/endpoints/dispatch.js @@ -4,12 +4,13 @@ import { API } from '../../../api/server'; import { serverLogger } from '../lib/logger'; import { contextDefinitions, eventTypes } from '../../../models/server/models/FederationEvents'; import { - FederationRoomEvents, FederationServers, + FederationRoomEvents, Messages, Rooms, Subscriptions, Users, } from '../../../models/server'; +import { FederationServers } from '../../../models/server/raw'; import { normalizers } from '../normalizers'; import { deleteRoom } from '../../../lib/server/functions'; import { Notifications } from '../../../notifications/server'; @@ -139,7 +140,7 @@ const eventHandlers = { // Refresh the servers list if (federationAltered) { - FederationServers.refreshServers(); + await FederationServers.refreshServers(); // Update the room's federation property Rooms.update({ _id: roomId }, { $set: { 'federation.domains': domainsAfterAdd } }); @@ -163,7 +164,7 @@ const eventHandlers = { Subscriptions.removeByRoomIdAndUserId(roomId, user._id); // Refresh the servers list - FederationServers.refreshServers(); + await FederationServers.refreshServers(); // Update the room's federation property Rooms.update({ _id: roomId }, { $set: { 'federation.domains': domainsAfterRemoval } }); @@ -186,7 +187,7 @@ const eventHandlers = { Subscriptions.removeByRoomIdAndUserId(roomId, user._id); // Refresh the servers list - FederationServers.refreshServers(); + await FederationServers.refreshServers(); // Update the room's federation property Rooms.update({ _id: roomId }, { $set: { 'federation.domains': domainsAfterRemoval } }); @@ -226,7 +227,7 @@ const eventHandlers = { const { federation: { origin } } = denormalizedMessage; - const { upload, buffer } = getUpload(origin, denormalizedMessage.file._id); + const { upload, buffer } = await getUpload(origin, denormalizedMessage.file._id); const oldUploadId = upload._id; @@ -444,7 +445,7 @@ const eventHandlers = { }; API.v1.addRoute('federation.events.dispatch', { authRequired: false, rateLimiterOptions: { numRequestsAllowed: 30, intervalTimeInMS: 1000 } }, { - async post() { + post() { if (!isFederationEnabled()) { return API.v1.failure('Federation not enabled'); } @@ -454,7 +455,7 @@ API.v1.addRoute('federation.events.dispatch', { authRequired: false, rateLimiter let payload; try { - payload = decryptIfNeeded(this.request, this.bodyParams); + payload = Promise.await(decryptIfNeeded(this.request, this.bodyParams)); } catch (err) { return API.v1.failure('Could not decrypt payload'); } @@ -472,7 +473,7 @@ API.v1.addRoute('federation.events.dispatch', { authRequired: false, rateLimiter let eventResult; if (eventHandlers[event.type]) { - eventResult = await eventHandlers[event.type](event); + eventResult = Promise.await(eventHandlers[event.type](event)); } // If there was an error handling the event, take action @@ -480,7 +481,7 @@ API.v1.addRoute('federation.events.dispatch', { authRequired: false, rateLimiter try { serverLogger.debug({ msg: 'federation.events.dispatch => Event has missing parents', event }); - requestEventsFromLatest(event.origin, getFederationDomain(), contextDefinitions.defineType(event), event.context, eventResult.latestEventIds); + Promise.await(requestEventsFromLatest(event.origin, getFederationDomain(), contextDefinitions.defineType(event), event.context, eventResult.latestEventIds)); // And stop handling the events break; diff --git a/app/federation/server/endpoints/requestFromLatest.js b/app/federation/server/endpoints/requestFromLatest.js index cac0168c8c12..84fd69f88d3a 100644 --- a/app/federation/server/endpoints/requestFromLatest.js +++ b/app/federation/server/endpoints/requestFromLatest.js @@ -8,7 +8,7 @@ import { isFederationEnabled } from '../lib/isFederationEnabled'; import { dispatchEvents } from '../handler'; API.v1.addRoute('federation.events.requestFromLatest', { authRequired: false }, { - async post() { + post() { if (!isFederationEnabled()) { return API.v1.failure('Federation not enabled'); } @@ -18,7 +18,7 @@ API.v1.addRoute('federation.events.requestFromLatest', { authRequired: false }, let payload; try { - payload = decryptIfNeeded(this.request, this.bodyParams); + payload = Promise.await(decryptIfNeeded(this.request, this.bodyParams)); } catch (err) { return API.v1.failure('Could not decrypt payload'); } @@ -54,7 +54,7 @@ API.v1.addRoute('federation.events.requestFromLatest', { authRequired: false }, } // Dispatch all the events, on the same request - dispatchEvents([fromDomain], missingEvents); + Promise.await(dispatchEvents([fromDomain], missingEvents)); return API.v1.success(); }, diff --git a/app/federation/server/endpoints/uploads.js b/app/federation/server/endpoints/uploads.js index 7735a630f15e..a997b2aff307 100644 --- a/app/federation/server/endpoints/uploads.js +++ b/app/federation/server/endpoints/uploads.js @@ -1,5 +1,5 @@ import { API } from '../../../api/server'; -import { Uploads } from '../../../models/server'; +import { Uploads } from '../../../models/server/raw'; import { FileUpload } from '../../../file-upload/server'; import { isFederationEnabled } from '../lib/isFederationEnabled'; @@ -11,7 +11,7 @@ API.v1.addRoute('federation.uploads', { authRequired: false }, { const { upload_id } = this.requestParams(); - const upload = Uploads.findOneById(upload_id); + const upload = Promise.await(Uploads.findOneById(upload_id)); if (!upload) { return API.v1.failure('There is no such file in this server'); diff --git a/app/federation/server/functions/addUser.js b/app/federation/server/functions/addUser.js index eebd1656260b..314b7893fbc1 100644 --- a/app/federation/server/functions/addUser.js +++ b/app/federation/server/functions/addUser.js @@ -1,15 +1,16 @@ import { Meteor } from 'meteor/meteor'; import * as federationErrors from './errors'; -import { FederationServers, Users } from '../../../models/server'; +import { Users } from '../../../models/server'; +import { FederationServers } from '../../../models/server/raw'; import { getUserByUsername } from '../handler'; -export function addUser(query) { +export async function addUser(query) { if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'addUser' }); } - const user = getUserByUsername(query); + const user = await getUserByUsername(query); if (!user) { throw federationErrors.userNotFound(query); @@ -22,7 +23,7 @@ export function addUser(query) { userId = Users.create(user); // Refresh the servers list - FederationServers.refreshServers(); + await FederationServers.refreshServers(); } catch (err) { // This might get called twice by the createDirectMessage method // so we need to handle the situation accordingly diff --git a/app/federation/server/functions/dashboard.js b/app/federation/server/functions/dashboard.js index 137ef802c5dc..3f256bf3d88a 100644 --- a/app/federation/server/functions/dashboard.js +++ b/app/federation/server/functions/dashboard.js @@ -1,21 +1,22 @@ import { Meteor } from 'meteor/meteor'; -import { FederationServers, FederationRoomEvents, Users } from '../../../models/server'; +import { FederationRoomEvents, Users } from '../../../models/server'; +import { FederationServers } from '../../../models/server/raw'; -export function getStatistics() { +export async function getStatistics() { const numberOfEvents = FederationRoomEvents.find().count(); const numberOfFederatedUsers = Users.findRemote().count(); - const numberOfServers = FederationServers.find().count(); + const numberOfServers = await FederationServers.find().count(); return { numberOfEvents, numberOfFederatedUsers, numberOfServers }; } -export function federationGetOverviewData() { +export async function federationGetOverviewData() { if (!Meteor.userId()) { throw new Meteor.Error('not-authorized'); } - const { numberOfEvents, numberOfFederatedUsers, numberOfServers } = getStatistics(); + const { numberOfEvents, numberOfFederatedUsers, numberOfServers } = await getStatistics(); return { data: [{ @@ -31,12 +32,12 @@ export function federationGetOverviewData() { }; } -export function federationGetServers() { +export async function federationGetServers() { if (!Meteor.userId()) { throw new Meteor.Error('not-authorized'); } - const servers = FederationServers.find().fetch(); + const servers = await FederationServers.find().toArray(); return { data: servers, diff --git a/app/federation/server/functions/helpers.js b/app/federation/server/functions/helpers.js deleted file mode 100644 index 4113b6014edd..000000000000 --- a/app/federation/server/functions/helpers.js +++ /dev/null @@ -1,69 +0,0 @@ -import { Settings, Subscriptions, Users } from '../../../models/server'; -import { STATUS_ENABLED, STATUS_REGISTERING } from '../constants'; - -export const getNameAndDomain = (fullyQualifiedName) => fullyQualifiedName.split('@'); -export const isFullyQualified = (name) => name.indexOf('@') !== -1; - -export function isRegisteringOrEnabled() { - const status = Settings.findOneById('FEDERATION_Status'); - return [STATUS_ENABLED, STATUS_REGISTERING].includes(status && status.value); -} - -export function updateStatus(status) { - Settings.updateValueById('FEDERATION_Status', status); -} - -export function updateEnabled(enabled) { - Settings.updateValueById('FEDERATION_Enabled', enabled); -} - -export const checkRoomType = (room) => room.t === 'p' || room.t === 'd'; -export const checkRoomDomainsLength = (domains) => domains.length <= (process.env.FEDERATED_DOMAINS_LENGTH || 10); - -export const hasExternalDomain = ({ federation }) => { - // same test as isFederated(room) - if (!federation) { - return false; - } - - return federation.domains - .some((domain) => domain !== federation.origin); -}; - -export const isLocalUser = ({ federation }, localDomain) => - !federation || federation.origin === localDomain; - -export const getFederatedRoomData = (room) => { - let hasFederatedUser = false; - - let users = null; - let subscriptions = null; - - if (room.t === 'd') { - // Check if there is a federated user on this room - hasFederatedUser = room.usernames.some(isFullyQualified); - } else { - // Find all subscriptions of this room - subscriptions = Subscriptions.findByRoomIdWhenUsernameExists(room._id).fetch(); - subscriptions = subscriptions.reduce((acc, s) => { - acc[s.u._id] = s; - - return acc; - }, {}); - - // Get all user ids - const userIds = Object.keys(subscriptions); - - // Load all the users - users = Users.findUsersWithUsernameByIds(userIds).fetch(); - - // Check if there is a federated user on this room - hasFederatedUser = users.some((u) => isFullyQualified(u.username)); - } - - return { - hasFederatedUser, - users, - subscriptions, - }; -}; diff --git a/app/federation/server/functions/helpers.ts b/app/federation/server/functions/helpers.ts new file mode 100644 index 000000000000..e8cb5b3e5170 --- /dev/null +++ b/app/federation/server/functions/helpers.ts @@ -0,0 +1,77 @@ +import { IRoom, isDirectMessageRoom } from '../../../../definition/IRoom'; +import { ISubscription } from '../../../../definition/ISubscription'; +import { IRegisterUser, IUser } from '../../../../definition/IUser'; +import { Subscriptions, Users } from '../../../models/server'; +import { Settings } from '../../../models/server/raw'; +import { STATUS_ENABLED, STATUS_REGISTERING } from '../constants'; + +export const getNameAndDomain = (fullyQualifiedName: string): string [] => fullyQualifiedName.split('@'); + +export const isFullyQualified = (name: string): boolean => name.indexOf('@') !== -1; + +export async function isRegisteringOrEnabled(): Promise { + const value = await Settings.getValueById('FEDERATION_Status'); + return typeof value === 'string' && [STATUS_ENABLED, STATUS_REGISTERING].includes(value); +} + +export async function updateStatus(status: string): Promise { + await Settings.updateValueById('FEDERATION_Status', status); +} + +export async function updateEnabled(enabled: boolean): Promise { + await Settings.updateValueById('FEDERATION_Enabled', enabled); +} + +export const checkRoomType = (room: IRoom): boolean => room.t === 'p' || room.t === 'd'; +export const checkRoomDomainsLength = (domains: unknown[]): boolean => domains.length <= (process.env.FEDERATED_DOMAINS_LENGTH || 10); + +export const hasExternalDomain = ({ federation }: { federation: { origin: string; domains: string[] } }): boolean => { + // same test as isFederated(room) + if (!federation) { + return false; + } + + return federation.domains + .some((domain) => domain !== federation.origin); +}; + +export const isLocalUser = ({ federation }: { federation: { origin: string } }, localDomain: string): boolean => + !federation || federation.origin === localDomain; + +export const getFederatedRoomData = (room: IRoom): { + hasFederatedUser: boolean; + users: IUser[]; + subscriptions: { [k: string]: ISubscription } | undefined; +} => { + if (isDirectMessageRoom(room)) { + // Check if there is a federated user on this room + + return { + users: [], + hasFederatedUser: room.usernames.some(isFullyQualified), + subscriptions: undefined, + }; + } + + // Find all subscriptions of this room + const s = Subscriptions.findByRoomIdWhenUsernameExists(room._id).fetch() as ISubscription[]; + const subscriptions = s.reduce((acc, s) => { + acc[s.u._id] = s; + return acc; + }, {} as { [k: string]: ISubscription }); + + // Get all user ids + const userIds = Object.keys(subscriptions); + + // Load all the users + const users: IRegisterUser[] = Users.findUsersWithUsernameByIds(userIds).fetch(); + + // Check if there is a federated user on this room + const hasFederatedUser = users.some((u) => isFullyQualified(u.username)); + + return { + hasFederatedUser, + users, + subscriptions, + }; +}; diff --git a/app/federation/server/handler/index.js b/app/federation/server/handler/index.js index 7827ddf063a7..46aec0b7dcea 100644 --- a/app/federation/server/handler/index.js +++ b/app/federation/server/handler/index.js @@ -5,7 +5,7 @@ import { clientLogger } from '../lib/logger'; import { isFederationEnabled } from '../lib/isFederationEnabled'; import { federationRequestToPeer } from '../lib/http'; -export function federationSearchUsers(query) { +export async function federationSearchUsers(query) { if (!isFederationEnabled()) { throw disabled('client.searchUsers'); } @@ -16,12 +16,12 @@ export function federationSearchUsers(query) { const uri = `/api/v1/federation.users.search?${ qs.stringify({ username, domain: peerDomain }) }`; - const { data: { users } } = federationRequestToPeer('GET', peerDomain, uri); + const { data: { users } } = await federationRequestToPeer('GET', peerDomain, uri); return users; } -export function getUserByUsername(query) { +export async function getUserByUsername(query) { if (!isFederationEnabled()) { throw disabled('client.searchUsers'); } @@ -32,12 +32,12 @@ export function getUserByUsername(query) { const uri = `/api/v1/federation.users.getByUsername?${ qs.stringify({ username }) }`; - const { data: { user } } = federationRequestToPeer('GET', peerDomain, uri); + const { data: { user } } = await federationRequestToPeer('GET', peerDomain, uri); return user; } -export function requestEventsFromLatest(domain, fromDomain, contextType, contextQuery, latestEventIds) { +export async function requestEventsFromLatest(domain, fromDomain, contextType, contextQuery, latestEventIds) { if (!isFederationEnabled()) { throw disabled('client.requestEventsFromLatest'); } @@ -46,11 +46,11 @@ export function requestEventsFromLatest(domain, fromDomain, contextType, context const uri = '/api/v1/federation.events.requestFromLatest'; - federationRequestToPeer('POST', domain, uri, { fromDomain, contextType, contextQuery, latestEventIds }); + await federationRequestToPeer('POST', domain, uri, { fromDomain, contextType, contextQuery, latestEventIds }); } -export function dispatchEvents(domains, events) { +export async function dispatchEvents(domains, events) { if (!isFederationEnabled()) { throw disabled('client.dispatchEvents'); } @@ -61,17 +61,17 @@ export function dispatchEvents(domains, events) { const uri = '/api/v1/federation.events.dispatch'; - for (const domain of domains) { - federationRequestToPeer('POST', domain, uri, { events }, { ignoreErrors: true }); + for await (const domain of domains) { + await federationRequestToPeer('POST', domain, uri, { events }, { ignoreErrors: true }); } } -export function dispatchEvent(domains, event) { - dispatchEvents([...new Set(domains)], [event]); +export async function dispatchEvent(domains, event) { + await dispatchEvents([...new Set(domains)], [event]); } -export function getUpload(domain, fileId) { - const { data: { upload, buffer } } = federationRequestToPeer('GET', domain, `/api/v1/federation.uploads?${ qs.stringify({ upload_id: fileId }) }`); +export async function getUpload(domain, fileId) { + const { data: { upload, buffer } } = await federationRequestToPeer('GET', domain, `/api/v1/federation.uploads?${ qs.stringify({ upload_id: fileId }) }`); return { upload, buffer: Buffer.from(buffer) }; } diff --git a/app/federation/server/hooks/afterCreateDirectRoom.js b/app/federation/server/hooks/afterCreateDirectRoom.js index ac05794e1c2e..79e6fc992836 100644 --- a/app/federation/server/hooks/afterCreateDirectRoom.js +++ b/app/federation/server/hooks/afterCreateDirectRoom.js @@ -41,7 +41,7 @@ async function afterCreateDirectRoom(room, extras) { })); // Dispatch the events - dispatchEvents(normalizedRoom.federation.domains, [genesisEvent, ...events]); + await dispatchEvents(normalizedRoom.federation.domains, [genesisEvent, ...events]); } catch (err) { await deleteRoom(room._id); diff --git a/app/federation/server/hooks/afterCreateRoom.js b/app/federation/server/hooks/afterCreateRoom.js index 75dfeeac6575..905e108740cf 100644 --- a/app/federation/server/hooks/afterCreateRoom.js +++ b/app/federation/server/hooks/afterCreateRoom.js @@ -47,7 +47,7 @@ export async function doAfterCreateRoom(room, users, subscriptions) { const genesisEvent = await FederationRoomEvents.createGenesisEvent(getFederationDomain(), normalizedRoom); // Dispatch the events - dispatchEvents(normalizedRoom.federation.domains, [genesisEvent, ...addUserEvents]); + await dispatchEvents(normalizedRoom.federation.domains, [genesisEvent, ...addUserEvents]); } async function afterCreateRoom(roomOwner, room) { diff --git a/app/federation/server/lib/crypt.js b/app/federation/server/lib/crypt.js index 7a231a13fb91..5a7685a2e9e0 100644 --- a/app/federation/server/lib/crypt.js +++ b/app/federation/server/lib/crypt.js @@ -1,19 +1,19 @@ -import { FederationKeys } from '../../../models/server'; +import { FederationKeys } from '../../../models/server/raw'; import { getFederationDomain } from './getFederationDomain'; import { search } from './dns'; import { cryptLogger } from './logger'; -export function decrypt(data, peerKey) { +export async function decrypt(data, peerKey) { // // Decrypt the payload const payloadBuffer = Buffer.from(data); // Decrypt with the peer's public key try { - data = FederationKeys.loadKey(peerKey, 'public').decryptPublic(payloadBuffer); + data = (await FederationKeys.loadKey(peerKey, 'public')).decryptPublic(payloadBuffer); // Decrypt with the local private key - data = FederationKeys.getPrivateKey().decrypt(data); + data = (await FederationKeys.getPrivateKey()).decrypt(data); } catch (err) { cryptLogger.error(err); @@ -23,7 +23,7 @@ export function decrypt(data, peerKey) { return JSON.parse(data.toString()); } -export function decryptIfNeeded(request, bodyParams) { +export async function decryptIfNeeded(request, bodyParams) { // // Look for the domain that sent this event const remotePeerDomain = request.headers['x-federation-domain']; @@ -48,17 +48,17 @@ export function decryptIfNeeded(request, bodyParams) { return decrypt(bodyParams, peerKey); } -export function encrypt(data, peerKey) { +export async function encrypt(data, peerKey) { if (!data) { return data; } try { // Encrypt with the peer's public key - data = FederationKeys.loadKey(peerKey, 'public').encrypt(data); + data = (await FederationKeys.loadKey(peerKey, 'public')).encrypt(data); // Encrypt with the local private key - return FederationKeys.getPrivateKey().encryptPrivate(data); + return (await FederationKeys.getPrivateKey()).encryptPrivate(data); } catch (err) { cryptLogger.error(err); diff --git a/app/federation/server/lib/dns.js b/app/federation/server/lib/dns.js index 0c4e2f348e1b..0080ddae625b 100644 --- a/app/federation/server/lib/dns.js +++ b/app/federation/server/lib/dns.js @@ -17,12 +17,12 @@ const memoizedDnsResolveTXT = mem(dnsResolveTXT, { maxAge: cacheMaxAge }); const hubUrl = process.env.NODE_ENV === 'development' ? 'http://localhost:8080' : 'https://hub.rocket.chat'; -export function registerWithHub(peerDomain, url, publicKey) { +export async function registerWithHub(peerDomain, url, publicKey) { const body = { domain: peerDomain, url, public_key: publicKey }; try { // If there is no DNS entry for that, get from the Hub - federationRequest('POST', `${ hubUrl }/api/v1/peers`, body); + await federationRequest('POST', `${ hubUrl }/api/v1/peers`, body); return true; } catch (err) { @@ -32,12 +32,12 @@ export function registerWithHub(peerDomain, url, publicKey) { } } -export function searchHub(peerDomain) { +export async function searchHub(peerDomain) { try { dnsLogger.debug(`searchHub: peerDomain=${ peerDomain }`); // If there is no DNS entry for that, get from the Hub - const { data: { peer } } = federationRequest('GET', `${ hubUrl }/api/v1/peers?search=${ peerDomain }`); + const { data: { peer } } = await federationRequest('GET', `${ hubUrl }/api/v1/peers?search=${ peerDomain }`); if (!peer) { dnsLogger.debug(`searchHub: could not find peerDomain=${ peerDomain }`); diff --git a/app/federation/server/lib/http.js b/app/federation/server/lib/http.js index 542a2d32ef9e..e18d09b8e86d 100644 --- a/app/federation/server/lib/http.js +++ b/app/federation/server/lib/http.js @@ -6,14 +6,14 @@ import { getFederationDomain } from './getFederationDomain'; import { search } from './dns'; import { encrypt } from './crypt'; -export function federationRequest(method, url, body, headers, peerKey = null) { +export async function federationRequest(method, url, body, headers, peerKey = null) { let data = null; if ((method === 'POST' || method === 'PUT') && body) { data = EJSON.toJSONValue(body); if (peerKey) { - data = encrypt(data, peerKey); + data = await encrypt(data, peerKey); } } @@ -22,7 +22,7 @@ export function federationRequest(method, url, body, headers, peerKey = null) { return MeteorHTTP.call(method, url, { data, timeout: 2000, headers: { ...headers, 'x-federation-domain': getFederationDomain() } }); } -export function federationRequestToPeer(method, peerDomain, uri, body, options = {}) { +export async function federationRequestToPeer(method, peerDomain, uri, body, options = {}) { const ignoreErrors = peerDomain === getFederationDomain() ? false : options.ignoreErrors; const { url: baseUrl, publicKey } = search(peerDomain); @@ -39,7 +39,7 @@ export function federationRequestToPeer(method, peerDomain, uri, body, options = try { httpLogger.debug({ msg: 'federationRequestToPeer', url: `${ baseUrl }${ uri }` }); - result = federationRequest(method, `${ baseUrl }${ uri }`, body, options.headers || {}, peerKey); + result = await federationRequest(method, `${ baseUrl }${ uri }`, body, options.headers || {}, peerKey); } catch (err) { httpLogger.error({ msg: `${ ignoreErrors ? '[IGNORED] ' : '' }Error`, err }); diff --git a/app/federation/server/startup/generateKeys.js b/app/federation/server/startup/generateKeys.js index 012cdd0b48f4..32eaacc30418 100644 --- a/app/federation/server/startup/generateKeys.js +++ b/app/federation/server/startup/generateKeys.js @@ -1,6 +1,8 @@ -import { FederationKeys } from '../../../models/server'; +import { FederationKeys } from '../../../models/server/raw'; // Create key pair if needed -if (!FederationKeys.getPublicKey()) { - FederationKeys.generateKeys(); -} +(async () => { + if (!await FederationKeys.getPublicKey()) { + await FederationKeys.generateKeys(); + } +})(); diff --git a/app/federation/server/startup/settings.ts b/app/federation/server/startup/settings.ts index cfa7fda19e6a..36ade9e70eeb 100644 --- a/app/federation/server/startup/settings.ts +++ b/app/federation/server/startup/settings.ts @@ -7,11 +7,11 @@ import { getFederationDiscoveryMethod } from '../lib/getFederationDiscoveryMetho import { registerWithHub } from '../lib/dns'; import { enableCallbacks, disableCallbacks } from '../lib/callbacks'; import { setupLogger } from '../lib/logger'; -import { FederationKeys } from '../../../models/server'; +import { FederationKeys } from '../../../models/server/raw'; import { STATUS_ENABLED, STATUS_REGISTERING, STATUS_ERROR_REGISTERING, STATUS_DISABLED } from '../constants'; -Meteor.startup(function() { - const federationPublicKey = FederationKeys.getPublicKeyString(); +Meteor.startup(async function() { + const federationPublicKey = await FederationKeys.getPublicKeyString(); settingsRegistry.addGroup('Federation', function() { this.add('FEDERATION_Enabled', false, { @@ -36,7 +36,7 @@ Meteor.startup(function() { // disableReset: true, }); - this.add('FEDERATION_Public_Key', federationPublicKey, { + this.add('FEDERATION_Public_Key', federationPublicKey || '', { readonly: true, type: 'string', multiline: true, @@ -65,26 +65,26 @@ Meteor.startup(function() { }); }); -const updateSettings = function(): void { +const updateSettings = async function(): Promise { // Get the key pair - if (getFederationDiscoveryMethod() === 'hub' && !isRegisteringOrEnabled()) { + if (getFederationDiscoveryMethod() === 'hub' && !Promise.await(isRegisteringOrEnabled())) { // Register with hub try { - updateStatus(STATUS_REGISTERING); + await updateStatus(STATUS_REGISTERING); - registerWithHub(getFederationDomain(), settings.get('Site_Url'), FederationKeys.getPublicKeyString()); + await registerWithHub(getFederationDomain(), settings.get('Site_Url'), await FederationKeys.getPublicKeyString()); - updateStatus(STATUS_ENABLED); + await updateStatus(STATUS_ENABLED); } catch (err) { // Disable federation - updateEnabled(false); + await updateEnabled(false); - updateStatus(STATUS_ERROR_REGISTERING); + await updateStatus(STATUS_ERROR_REGISTERING); } - } else { - updateStatus(STATUS_ENABLED); + return; } + await updateStatus(STATUS_ENABLED); }; // Add settings listeners @@ -92,11 +92,11 @@ settings.watch('FEDERATION_Enabled', function enableOrDisable(value) { setupLogger.info(`Federation is ${ value ? 'enabled' : 'disabled' }`); if (value) { - updateSettings(); + Promise.await(updateSettings()); enableCallbacks(); } else { - updateStatus(STATUS_DISABLED); + Promise.await(updateStatus(STATUS_DISABLED)); disableCallbacks(); } diff --git a/app/file-upload/server/lib/FileUpload.js b/app/file-upload/server/lib/FileUpload.js index aa8d49ab408e..d361ed3e2294 100644 --- a/app/file-upload/server/lib/FileUpload.js +++ b/app/file-upload/server/lib/FileUpload.js @@ -2,6 +2,7 @@ import fs from 'fs'; import stream from 'stream'; import { Meteor } from 'meteor/meteor'; +import { Mongo } from 'meteor/mongo'; import streamBuffers from 'stream-buffers'; import Future from 'fibers/future'; import sharp from 'sharp'; @@ -13,9 +14,7 @@ import filesize from 'filesize'; import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; import { settings } from '../../../settings/server'; -import Uploads from '../../../models/server/models/Uploads'; -import UserDataFiles from '../../../models/server/models/UserDataFiles'; -import Avatars from '../../../models/server/models/Avatars'; +import { Avatars, UserDataFiles, Uploads } from '../../../models/server/raw'; import Users from '../../../models/server/models/Users'; import Rooms from '../../../models/server/models/Rooms'; import Settings from '../../../models/server/models/Settings'; @@ -41,6 +40,9 @@ settings.watch('FileUpload_MaxFileSize', function(value) { } }); +const AvatarModel = new Mongo.Collection(Avatars.col.collectionName); +const UserDataFilesModel = new Mongo.Collection(UserDataFiles.col.collectionName); +const UploadsModel = new Mongo.Collection(Uploads.col.collectionName); export const FileUpload = { handlers: {}, @@ -139,7 +141,7 @@ export const FileUpload = { defaultUploads() { return { - collection: Uploads.model, + collection: UploadsModel, filter: new UploadFS.Filter({ onCheck: FileUpload.validateFileUpload, }), @@ -161,7 +163,7 @@ export const FileUpload = { defaultAvatars() { return { - collection: Avatars.model, + collection: AvatarModel, filter: new UploadFS.Filter({ onCheck: FileUpload.validateAvatarUpload, }), @@ -176,7 +178,7 @@ export const FileUpload = { defaultUserDataFiles() { return { - collection: UserDataFiles.model, + collection: UserDataFilesModel, getPath(file) { return `${ settings.get('uniqueID') }/uploads/userData/${ file.userId }`; }, @@ -254,7 +256,7 @@ export const FileUpload = { }, resizeImagePreview(file) { - file = Uploads.findOneById(file._id); + file = Promise.await(Uploads.findOneById(file._id)); file = FileUpload.addExtensionTo(file); const image = FileUpload.getStore('Uploads')._store.getReadStream(file._id, file); @@ -279,7 +281,7 @@ export const FileUpload = { return; } - file = Uploads.findOneById(file._id); + file = Promise.await(Uploads.findOneById(file._id)); file = FileUpload.addExtensionTo(file); const store = FileUpload.getStore('Uploads'); const image = store._store.getReadStream(file._id, file); @@ -378,11 +380,11 @@ export const FileUpload = { } // update file record to match user's username const user = Users.findOneById(file.userId); - const oldAvatar = Avatars.findOneByName(user.username); + const oldAvatar = Promise.await(Avatars.findOneByName(user.username)); if (oldAvatar) { - Avatars.deleteFile(oldAvatar._id); + Promise.await(Avatars.deleteFile(oldAvatar._id)); } - Avatars.updateFileNameById(file._id, user.username); + Promise.await(Avatars.updateFileNameById(file._id, user.username)); // console.log('upload finished ->', file); }, @@ -571,11 +573,11 @@ export class FileUploadClass { this.store.delete(fileId); } - return this.model.deleteFile(fileId); + return Promise.await(this.model.deleteFile(fileId)); } deleteById(fileId) { - const file = this.model.findOneById(fileId); + const file = Promise.await(this.model.findOneById(fileId)); if (!file) { return; @@ -587,7 +589,7 @@ export class FileUploadClass { } deleteByName(fileName) { - const file = this.model.findOneByName(fileName); + const file = Promise.await(this.model.findOneByName(fileName)); if (!file) { return; @@ -600,7 +602,7 @@ export class FileUploadClass { deleteByRoomId(rid) { - const file = this.model.findOneByRoomId(rid); + const file = Promise.await(this.model.findOneByRoomId(rid)); if (!file) { return; diff --git a/app/file-upload/server/lib/requests.js b/app/file-upload/server/lib/requests.js index 80a3b4213b38..3b2e8dad19d7 100644 --- a/app/file-upload/server/lib/requests.js +++ b/app/file-upload/server/lib/requests.js @@ -1,13 +1,13 @@ import { WebApp } from 'meteor/webapp'; import { FileUpload } from './FileUpload'; -import { Uploads } from '../../../models'; +import { Uploads } from '../../../models/server/raw'; -WebApp.connectHandlers.use(FileUpload.getPath(), function(req, res, next) { +WebApp.connectHandlers.use(FileUpload.getPath(), async function(req, res, next) { const match = /^\/([^\/]+)\/(.*)/.exec(req.url); if (match && match[1]) { - const file = Uploads.findOneById(match[1]); + const file = await Uploads.findOneById(match[1]); if (file) { if (!FileUpload.requestCanAccessFiles(req)) { diff --git a/app/file-upload/server/methods/getS3FileUrl.js b/app/file-upload/server/methods/getS3FileUrl.js index f68f720d171b..cfffdfcc032a 100644 --- a/app/file-upload/server/methods/getS3FileUrl.js +++ b/app/file-upload/server/methods/getS3FileUrl.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { UploadFS } from 'meteor/jalik:ufs'; import { settings } from '../../../settings/server'; -import { Uploads } from '../../../models'; +import { Uploads } from '../../../models/server/raw'; let protectedFiles; @@ -11,11 +11,11 @@ settings.watch('FileUpload_ProtectFiles', function(value) { }); Meteor.methods({ - getS3FileUrl(fileId) { + async getS3FileUrl(fileId) { if (protectedFiles && !Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'sendFileMessage' }); } - const file = Uploads.findOneById(fileId); + const file = await Uploads.findOneById(fileId); return UploadFS.getStore('AmazonS3:Uploads').getRedirectURL(file); }, diff --git a/app/file-upload/server/methods/sendFileMessage.ts b/app/file-upload/server/methods/sendFileMessage.ts index 80dec7bca683..886f9167e07e 100644 --- a/app/file-upload/server/methods/sendFileMessage.ts +++ b/app/file-upload/server/methods/sendFileMessage.ts @@ -3,8 +3,7 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import _ from 'underscore'; -import { Uploads } from '../../../models/server'; -import { Rooms } from '../../../models/server/raw'; +import { Rooms, Uploads } from '../../../models/server/raw'; import { callbacks } from '../../../callbacks/server'; import { FileUpload } from '../lib/FileUpload'; import { canAccessRoom } from '../../../authorization/server/functions/canAccessRoom'; @@ -35,7 +34,7 @@ Meteor.methods({ tmid: Match.Optional(String), }); - Uploads.updateFileComplete(file._id, user._id, _.omit(file, '_id')); + await Uploads.updateFileComplete(file._id, user._id, _.omit(file, '_id')); const fileUrl = FileUpload.getPath(`${ file._id }/${ encodeURI(file.name) }`); diff --git a/app/integrations/server/api/api.js b/app/integrations/server/api/api.js index 17a04b6c3582..eb223c67c9ad 100644 --- a/app/integrations/server/api/api.js +++ b/app/integrations/server/api/api.js @@ -14,6 +14,7 @@ import { incomingLogger } from '../logger'; import { processWebhookMessage } from '../../../lib/server'; import { API, APIClass, defaultRateLimiterOptions } from '../../../api/server'; import * as Models from '../../../models/server'; +import { Integrations } from '../../../models/server/raw'; import { settings } from '../../../settings/server'; const compiledScripts = {}; @@ -129,9 +130,10 @@ function removeIntegration(options, user) { incomingLogger.info('Remove integration'); incomingLogger.debug(options); - const integrationToRemove = Models.Integrations.findOne({ - urls: options.target_url, - }); + const integrationToRemove = Promise.await(Integrations.findOneByUrl(options.target_url)); + if (!integrationToRemove) { + return API.v1.failure('integration-not-found'); + } Meteor.runAsUser(user._id, () => Meteor.call('deleteOutgoingIntegration', integrationToRemove._id)); @@ -373,10 +375,10 @@ const Api = new WebHookAPI({ } } - this.integration = Models.Integrations.findOne({ + this.integration = Promise.await(Integrations.findOne({ _id: this.request.params.integrationId, token: decodeURIComponent(this.request.params.token), - }); + })); if (!this.integration) { incomingLogger.info(`Invalid integration id ${ this.request.params.integrationId } or token ${ this.request.params.token }`); diff --git a/app/integrations/server/lib/triggerHandler.js b/app/integrations/server/lib/triggerHandler.js index 33cc33ddb24d..27f71a4e5729 100644 --- a/app/integrations/server/lib/triggerHandler.js +++ b/app/integrations/server/lib/triggerHandler.js @@ -10,6 +10,7 @@ import Fiber from 'fibers'; import Future from 'fibers/future'; import * as Models from '../../../models/server'; +import { Integrations, IntegrationHistory } from '../../../models/server/raw'; import { settings } from '../../../settings/server'; import { getRoomByNameOrIdWithOptionToJoin, processWebhookMessage } from '../../../lib/server'; import { outgoingLogger } from '../logger'; @@ -22,7 +23,7 @@ export class RocketChatIntegrationHandler { this.compiledScripts = {}; this.triggers = {}; - Models.Integrations.find({ type: 'webhook-outgoing' }).fetch().forEach((data) => this.addIntegration(data)); + Promise.await(Integrations.find({ type: 'webhook-outgoing' }).forEach((data) => this.addIntegration(data))); } addIntegration(record) { @@ -142,11 +143,11 @@ export class RocketChatIntegrationHandler { } if (historyId) { - Models.IntegrationHistory.update({ _id: historyId }, { $set: history }); + Promise.await(IntegrationHistory.updateOne({ _id: historyId }, { $set: history })); return historyId; } history._createdAt = new Date(); - return Models.IntegrationHistory.insert(Object.assign({ _id: Random.id() }, history)); + return Promise.await(IntegrationHistory.insertOne({ _id: Random.id(), ...history })); } // Trigger is the trigger, nameOrId is a string which is used to try and find a room, room is a room, message is a message, and data contains "user_name" if trigger.impersonateUser is truthful. @@ -715,7 +716,7 @@ export class RocketChatIntegrationHandler { if (result.statusCode === 410) { this.updateHistory({ historyId, step: 'after-process-http-status-410', error: true }); outgoingLogger.error(`Disabling the Integration "${ trigger.name }" because the status code was 401 (Gone).`); - Models.Integrations.update({ _id: trigger._id }, { $set: { enabled: false } }); + Promise.await(Integrations.updateOne({ _id: trigger._id }, { $set: { enabled: false } })); return; } diff --git a/app/integrations/server/methods/clearIntegrationHistory.js b/app/integrations/server/methods/clearIntegrationHistory.ts similarity index 70% rename from app/integrations/server/methods/clearIntegrationHistory.js rename to app/integrations/server/methods/clearIntegrationHistory.ts index 87eec581e37a..f4ef3e974b96 100644 --- a/app/integrations/server/methods/clearIntegrationHistory.js +++ b/app/integrations/server/methods/clearIntegrationHistory.ts @@ -1,17 +1,20 @@ import { Meteor } from 'meteor/meteor'; -import { hasPermission } from '../../../authorization'; -import { IntegrationHistory, Integrations } from '../../../models'; +import { hasPermission } from '../../../authorization/server'; +import { IntegrationHistory, Integrations } from '../../../models/server/raw'; import notifications from '../../../notifications/server/lib/Notifications'; Meteor.methods({ - clearIntegrationHistory(integrationId) { + async clearIntegrationHistory(integrationId) { let integration; if (hasPermission(this.userId, 'manage-outgoing-integrations') || hasPermission(this.userId, 'manage-outgoing-integrations', 'bot')) { - integration = Integrations.findOne(integrationId); + integration = await Integrations.findOneById(integrationId); } else if (hasPermission(this.userId, 'manage-own-outgoing-integrations') || hasPermission(this.userId, 'manage-own-outgoing-integrations', 'bot')) { - integration = Integrations.findOne(integrationId, { fields: { '_createdBy._id': this.userId } }); + integration = await Integrations.findOne({ + _id: integrationId, + '_createdBy._id': this.userId, + }); } else { throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'clearIntegrationHistory' }); } @@ -20,7 +23,7 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'clearIntegrationHistory' }); } - IntegrationHistory.removeByIntegrationId(integrationId); + await IntegrationHistory.removeByIntegrationId(integrationId); notifications.streamIntegrationHistory.emit(integrationId, { type: 'removed' }); diff --git a/app/integrations/server/methods/incoming/addIncomingIntegration.js b/app/integrations/server/methods/incoming/addIncomingIntegration.js index 6e86dacd5700..23b339ed48fb 100644 --- a/app/integrations/server/methods/incoming/addIncomingIntegration.js +++ b/app/integrations/server/methods/incoming/addIncomingIntegration.js @@ -4,13 +4,14 @@ import { Babel } from 'meteor/babel-compiler'; import _ from 'underscore'; import s from 'underscore.string'; -import { hasPermission, hasAllPermission } from '../../../../authorization'; -import { Users, Rooms, Integrations, Roles, Subscriptions } from '../../../../models'; +import { hasPermission, hasAllPermission } from '../../../../authorization/server'; +import { Users, Rooms, Subscriptions } from '../../../../models/server'; +import { Integrations, Roles } from '../../../../models/server/raw'; const validChannelChars = ['@', '#']; Meteor.methods({ - addIncomingIntegration(integration) { + async addIncomingIntegration(integration) { if (!hasPermission(this.userId, 'manage-incoming-integrations') && !hasPermission(this.userId, 'manage-own-incoming-integrations')) { throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'addIncomingIntegration' }); } @@ -95,9 +96,11 @@ Meteor.methods({ integration._createdAt = new Date(); integration._createdBy = Users.findOne(this.userId, { fields: { username: 1 } }); - Roles.addUserRoles(user._id, 'bot'); + await Roles.addUserRoles(user._id, 'bot'); - integration._id = Integrations.insert(integration); + const result = await Integrations.insertOne(integration); + + integration._id = result.insertedId; return integration; }, diff --git a/app/integrations/server/methods/incoming/deleteIncomingIntegration.js b/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts similarity index 56% rename from app/integrations/server/methods/incoming/deleteIncomingIntegration.js rename to app/integrations/server/methods/incoming/deleteIncomingIntegration.ts index 96c25116a10d..bbd158f20ae0 100644 --- a/app/integrations/server/methods/incoming/deleteIncomingIntegration.js +++ b/app/integrations/server/methods/incoming/deleteIncomingIntegration.ts @@ -1,16 +1,19 @@ import { Meteor } from 'meteor/meteor'; -import { hasPermission } from '../../../../authorization'; -import { Integrations } from '../../../../models'; +import { hasPermission } from '../../../../authorization/server'; +import { Integrations } from '../../../../models/server/raw'; Meteor.methods({ - deleteIncomingIntegration(integrationId) { + async deleteIncomingIntegration(integrationId) { let integration; if (hasPermission(this.userId, 'manage-incoming-integrations')) { - integration = Integrations.findOne(integrationId); + integration = Integrations.findOneById(integrationId); } else if (hasPermission(this.userId, 'manage-own-incoming-integrations')) { - integration = Integrations.findOne(integrationId, { fields: { '_createdBy._id': this.userId } }); + integration = Integrations.findOne({ + _id: integrationId, + '_createdBy._id': this.userId, + }); } else { throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'deleteIncomingIntegration' }); } @@ -19,7 +22,7 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'deleteIncomingIntegration' }); } - Integrations.remove({ _id: integrationId }); + await Integrations.removeById(integrationId); return true; }, diff --git a/app/integrations/server/methods/incoming/updateIncomingIntegration.js b/app/integrations/server/methods/incoming/updateIncomingIntegration.js index 5e7b3517ba0e..fc5a6d384b95 100644 --- a/app/integrations/server/methods/incoming/updateIncomingIntegration.js +++ b/app/integrations/server/methods/incoming/updateIncomingIntegration.js @@ -3,13 +3,14 @@ import { Babel } from 'meteor/babel-compiler'; import _ from 'underscore'; import s from 'underscore.string'; -import { Integrations, Rooms, Users, Roles, Subscriptions } from '../../../../models'; -import { hasAllPermission, hasPermission } from '../../../../authorization'; +import { Rooms, Users, Subscriptions } from '../../../../models/server'; +import { Integrations, Roles } from '../../../../models/server/raw'; +import { hasAllPermission, hasPermission } from '../../../../authorization/server'; const validChannelChars = ['@', '#']; Meteor.methods({ - updateIncomingIntegration(integrationId, integration) { + async updateIncomingIntegration(integrationId, integration) { if (!_.isString(integration.channel) || integration.channel.trim() === '') { throw new Meteor.Error('error-invalid-channel', 'Invalid channel', { method: 'updateIncomingIntegration' }); } @@ -25,9 +26,9 @@ Meteor.methods({ let currentIntegration; if (hasPermission(this.userId, 'manage-incoming-integrations')) { - currentIntegration = Integrations.findOne(integrationId); + currentIntegration = await Integrations.findOneById(integrationId); } else if (hasPermission(this.userId, 'manage-own-incoming-integrations')) { - currentIntegration = Integrations.findOne({ _id: integrationId, '_createdBy._id': this.userId }); + currentIntegration = await Integrations.findOne({ _id: integrationId, '_createdBy._id': this.userId }); } else { throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'updateIncomingIntegration' }); } @@ -43,14 +44,14 @@ Meteor.methods({ integration.scriptCompiled = Babel.compile(integration.script, babelOptions).code; integration.scriptError = undefined; - Integrations.update(integrationId, { + await Integrations.updateOne({ _id: integrationId }, { $set: { scriptCompiled: integration.scriptCompiled }, $unset: { scriptError: 1 }, }); } catch (e) { integration.scriptCompiled = undefined; integration.scriptError = _.pick(e, 'name', 'message', 'stack'); - Integrations.update(integrationId, { + await Integrations.updateOne({ _id: integrationId }, { $set: { scriptError: integration.scriptError, }, @@ -100,9 +101,9 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-post-as-user', 'Invalid Post As User', { method: 'updateIncomingIntegration' }); } - Roles.addUserRoles(user._id, 'bot'); + await Roles.addUserRoles(user._id, 'bot'); - Integrations.update(integrationId, { + await Integrations.updateOne({ _id: integrationId }, { $set: { enabled: integration.enabled, name: integration.name, @@ -117,6 +118,6 @@ Meteor.methods({ }, }); - return Integrations.findOne(integrationId); + return Integrations.findOneById(integrationId); }, }); diff --git a/app/integrations/server/methods/outgoing/addOutgoingIntegration.js b/app/integrations/server/methods/outgoing/addOutgoingIntegration.js index 5baf6e88cda4..ae6f1aa6933d 100644 --- a/app/integrations/server/methods/outgoing/addOutgoingIntegration.js +++ b/app/integrations/server/methods/outgoing/addOutgoingIntegration.js @@ -1,11 +1,12 @@ import { Meteor } from 'meteor/meteor'; -import { hasPermission } from '../../../../authorization'; -import { Users, Integrations } from '../../../../models'; +import { hasPermission } from '../../../../authorization/server'; +import { Users } from '../../../../models/server'; +import { Integrations } from '../../../../models/server/raw'; import { integrations } from '../../../lib/rocketchat'; Meteor.methods({ - addOutgoingIntegration(integration) { + async addOutgoingIntegration(integration) { if (!hasPermission(this.userId, 'manage-outgoing-integrations') && !hasPermission(this.userId, 'manage-own-outgoing-integrations') && !hasPermission(this.userId, 'manage-outgoing-integrations', 'bot') @@ -17,7 +18,9 @@ Meteor.methods({ integration._createdAt = new Date(); integration._createdBy = Users.findOne(this.userId, { fields: { username: 1 } }); - integration._id = Integrations.insert(integration); + + const result = await Integrations.insertOne(integration); + integration._id = result.insertedId; return integration; }, diff --git a/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.js b/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts similarity index 63% rename from app/integrations/server/methods/outgoing/deleteOutgoingIntegration.js rename to app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts index 07823b22bb2c..a63e845eaa77 100644 --- a/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.js +++ b/app/integrations/server/methods/outgoing/deleteOutgoingIntegration.ts @@ -1,16 +1,19 @@ import { Meteor } from 'meteor/meteor'; -import { hasPermission } from '../../../../authorization'; -import { IntegrationHistory, Integrations } from '../../../../models'; +import { hasPermission } from '../../../../authorization/server'; +import { IntegrationHistory, Integrations } from '../../../../models/server/raw'; Meteor.methods({ - deleteOutgoingIntegration(integrationId) { + async deleteOutgoingIntegration(integrationId) { let integration; if (hasPermission(this.userId, 'manage-outgoing-integrations') || hasPermission(this.userId, 'manage-outgoing-integrations', 'bot')) { - integration = Integrations.findOne(integrationId); + integration = Integrations.findOneById(integrationId); } else if (hasPermission(this.userId, 'manage-own-outgoing-integrations') || hasPermission(this.userId, 'manage-own-outgoing-integrations', 'bot')) { - integration = Integrations.findOne(integrationId, { fields: { '_createdBy._id': this.userId } }); + integration = Integrations.findOne({ + _id: integrationId, + '_createdBy._id': this.userId, + }); } else { throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'deleteOutgoingIntegration' }); } @@ -19,8 +22,8 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'deleteOutgoingIntegration' }); } - Integrations.remove({ _id: integrationId }); - IntegrationHistory.removeByIntegrationId(integrationId); + await Integrations.removeById(integrationId); + await IntegrationHistory.removeByIntegrationId(integrationId); return true; }, diff --git a/app/integrations/server/methods/outgoing/replayOutgoingIntegration.js b/app/integrations/server/methods/outgoing/replayOutgoingIntegration.ts similarity index 69% rename from app/integrations/server/methods/outgoing/replayOutgoingIntegration.js rename to app/integrations/server/methods/outgoing/replayOutgoingIntegration.ts index 8d88cde3ea28..bf3136525bc9 100644 --- a/app/integrations/server/methods/outgoing/replayOutgoingIntegration.js +++ b/app/integrations/server/methods/outgoing/replayOutgoingIntegration.ts @@ -1,17 +1,20 @@ import { Meteor } from 'meteor/meteor'; -import { hasPermission } from '../../../../authorization'; -import { Integrations, IntegrationHistory } from '../../../../models'; +import { hasPermission } from '../../../../authorization/server'; +import { Integrations, IntegrationHistory } from '../../../../models/server/raw'; import { triggerHandler } from '../../lib/triggerHandler'; Meteor.methods({ - replayOutgoingIntegration({ integrationId, historyId }) { + async replayOutgoingIntegration({ integrationId, historyId }) { let integration; if (hasPermission(this.userId, 'manage-outgoing-integrations') || hasPermission(this.userId, 'manage-outgoing-integrations', 'bot')) { - integration = Integrations.findOne(integrationId); + integration = await Integrations.findOneById(integrationId); } else if (hasPermission(this.userId, 'manage-own-outgoing-integrations') || hasPermission(this.userId, 'manage-own-outgoing-integrations', 'bot')) { - integration = Integrations.findOne(integrationId, { fields: { '_createdBy._id': this.userId } }); + integration = await Integrations.findOne({ + _id: integrationId, + '_createdBy._id': this.userId, + }); } else { throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'replayOutgoingIntegration' }); } @@ -20,7 +23,7 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-integration', 'Invalid integration', { method: 'replayOutgoingIntegration' }); } - const history = IntegrationHistory.findOneByIntegrationIdAndHistoryId(integration._id, historyId); + const history = await IntegrationHistory.findOneByIntegrationIdAndHistoryId(integration._id, historyId); if (!history) { throw new Meteor.Error('error-invalid-integration-history', 'Invalid Integration History', { method: 'replayOutgoingIntegration' }); diff --git a/app/integrations/server/methods/outgoing/updateOutgoingIntegration.js b/app/integrations/server/methods/outgoing/updateOutgoingIntegration.js index 981a7890bc29..e9e4bc1ba968 100644 --- a/app/integrations/server/methods/outgoing/updateOutgoingIntegration.js +++ b/app/integrations/server/methods/outgoing/updateOutgoingIntegration.js @@ -1,11 +1,12 @@ import { Meteor } from 'meteor/meteor'; -import { hasPermission } from '../../../../authorization'; -import { Integrations, Users } from '../../../../models'; +import { hasPermission } from '../../../../authorization/server'; +import { Users } from '../../../../models/server'; +import { Integrations } from '../../../../models/server/raw'; import { integrations } from '../../../lib/rocketchat'; Meteor.methods({ - updateOutgoingIntegration(integrationId, integration) { + async updateOutgoingIntegration(integrationId, integration) { integration = integrations.validateOutgoing(integration, this.userId); if (!integration.token || integration.token.trim() === '') { @@ -15,9 +16,9 @@ Meteor.methods({ let currentIntegration; if (hasPermission(this.userId, 'manage-outgoing-integrations')) { - currentIntegration = Integrations.findOne(integrationId); + currentIntegration = await Integrations.findOneById(integrationId); } else if (hasPermission(this.userId, 'manage-own-outgoing-integrations')) { - currentIntegration = Integrations.findOne({ _id: integrationId, '_createdBy._id': this.userId }); + currentIntegration = await Integrations.findOne({ _id: integrationId, '_createdBy._id': this.userId }); } else { throw new Meteor.Error('not_authorized', 'Unauthorized', { method: 'updateOutgoingIntegration' }); } @@ -26,18 +27,18 @@ Meteor.methods({ throw new Meteor.Error('invalid_integration', '[methods] updateOutgoingIntegration -> integration not found'); } if (integration.scriptCompiled) { - Integrations.update(integrationId, { + await Integrations.updateOne({ _id: integrationId }, { $set: { scriptCompiled: integration.scriptCompiled }, $unset: { scriptError: 1 }, }); } else { - Integrations.update(integrationId, { + await Integrations.updateOne({ _id: integrationId }, { $set: { scriptError: integration.scriptError }, $unset: { scriptCompiled: 1 }, }); } - Integrations.update(integrationId, { + await Integrations.updateOne({ _id: integrationId }, { $set: { event: integration.event, enabled: integration.enabled, @@ -65,6 +66,6 @@ Meteor.methods({ }, }); - return Integrations.findOne(integrationId); + return Integrations.findOneById(integrationId); }, }); diff --git a/app/invites/server/functions/findOrCreateInvite.js b/app/invites/server/functions/findOrCreateInvite.js index c875a53906d0..3b608e7121e0 100644 --- a/app/invites/server/functions/findOrCreateInvite.js +++ b/app/invites/server/functions/findOrCreateInvite.js @@ -3,7 +3,8 @@ import { Random } from 'meteor/random'; import { hasPermission } from '../../../authorization'; import { Notifications } from '../../../notifications'; -import { Invites, Subscriptions, Rooms } from '../../../models/server'; +import { Subscriptions, Rooms } from '../../../models/server'; +import { Invites } from '../../../models/server/raw'; import { settings } from '../../../settings'; import { getURL } from '../../../utils/lib/getURL'; import { roomTypes, RoomMemberActions } from '../../../utils/server'; @@ -23,7 +24,7 @@ function getInviteUrl(invite) { const possibleDays = [0, 1, 7, 15, 30]; const possibleUses = [0, 1, 5, 10, 25, 50, 100]; -export const findOrCreateInvite = (userId, invite) => { +export const findOrCreateInvite = async (userId, invite) => { if (!userId || !invite) { return false; } @@ -57,7 +58,7 @@ export const findOrCreateInvite = (userId, invite) => { } // Before anything, let's check if there's an existing invite with the same settings for the same channel and user and that has not yet expired. - const existing = Invites.findOneByUserRoomMaxUsesAndExpiration(userId, invite.rid, maxUses, days); + const existing = await Invites.findOneByUserRoomMaxUsesAndExpiration(userId, invite.rid, maxUses, days); // If an existing invite was found, return it's _id instead of creating a new one. if (existing) { @@ -86,7 +87,7 @@ export const findOrCreateInvite = (userId, invite) => { uses: 0, }; - Invites.create(createInvite); + await Invites.insertOne(createInvite); Notifications.notifyUser(userId, 'updateInvites', { invite: createInvite }); createInvite.url = getInviteUrl(createInvite); diff --git a/app/invites/server/functions/listInvites.js b/app/invites/server/functions/listInvites.js index 476a5f729e09..10d67435237d 100644 --- a/app/invites/server/functions/listInvites.js +++ b/app/invites/server/functions/listInvites.js @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; -import { hasPermission } from '../../../authorization'; -import { Invites } from '../../../models'; +import { hasPermission } from '../../../authorization/server'; +import { Invites } from '../../../models/server/raw'; -export const listInvites = (userId) => { +export const listInvites = async (userId) => { if (!userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'listInvites' }); } @@ -12,5 +12,5 @@ export const listInvites = (userId) => { throw new Meteor.Error('not_authorized'); } - return Invites.find({}).fetch(); + return Invites.find({}).toArray(); }; diff --git a/app/invites/server/functions/removeInvite.js b/app/invites/server/functions/removeInvite.js index eadbe67966b3..0ea066a8e287 100644 --- a/app/invites/server/functions/removeInvite.js +++ b/app/invites/server/functions/removeInvite.js @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization'; -import Invites from '../../../models/server/models/Invites'; +import { Invites } from '../../../models/server/raw'; -export const removeInvite = (userId, invite) => { +export const removeInvite = async (userId, invite) => { if (!userId || !invite) { return false; } @@ -17,13 +17,13 @@ export const removeInvite = (userId, invite) => { } // Before anything, let's check if there's an existing invite - const existing = Invites.findOneById(invite._id); + const existing = await Invites.findOneById(invite._id); if (!existing) { throw new Meteor.Error('invalid-invitation-id', 'Invalid Invitation _id', { method: 'removeInvite' }); } - Invites.removeById(invite._id); + await Invites.removeById(invite._id); return true; }; diff --git a/app/invites/server/functions/useInviteToken.js b/app/invites/server/functions/useInviteToken.js index 3cf638fd3e94..6fcef0a40788 100644 --- a/app/invites/server/functions/useInviteToken.js +++ b/app/invites/server/functions/useInviteToken.js @@ -1,11 +1,12 @@ import { Meteor } from 'meteor/meteor'; -import { Invites, Users, Subscriptions } from '../../../models/server'; +import { Users, Subscriptions } from '../../../models/server'; +import { Invites } from '../../../models/server/raw'; import { validateInviteToken } from './validateInviteToken'; import { addUserToRoom } from '../../../lib/server/functions/addUserToRoom'; import { roomTypes, RoomMemberActions } from '../../../utils/server'; -export const useInviteToken = (userId, token) => { +export const useInviteToken = async (userId, token) => { if (!userId) { throw new Meteor.Error('error-invalid-user', 'The user is invalid', { method: 'useInviteToken', field: 'userId' }); } @@ -14,7 +15,7 @@ export const useInviteToken = (userId, token) => { throw new Meteor.Error('error-invalid-token', 'The invite token is invalid.', { method: 'useInviteToken', field: 'token' }); } - const { inviteData, room } = validateInviteToken(token); + const { inviteData, room } = await validateInviteToken(token); if (!roomTypes.getConfig(room.t).allowMemberAction(room, RoomMemberActions.INVITE)) { throw new Meteor.Error('error-room-type-not-allowed', 'Can\'t join room of this type via invite', { method: 'useInviteToken', field: 'token' }); @@ -25,7 +26,7 @@ export const useInviteToken = (userId, token) => { const subscription = Subscriptions.findOneByRoomIdAndUserId(room._id, user._id, { fields: { _id: 1 } }); if (!subscription) { - Invites.increaseUsageById(inviteData._id); + await Invites.increaseUsageById(inviteData._id); } // If the user already has an username, then join the invite room, diff --git a/app/invites/server/functions/validateInviteToken.js b/app/invites/server/functions/validateInviteToken.js index dda8add8b612..81febb439442 100644 --- a/app/invites/server/functions/validateInviteToken.js +++ b/app/invites/server/functions/validateInviteToken.js @@ -1,13 +1,14 @@ import { Meteor } from 'meteor/meteor'; -import { Invites, Rooms } from '../../../models'; +import { Rooms } from '../../../models'; +import { Invites } from '../../../models/server/raw'; -export const validateInviteToken = (token) => { +export const validateInviteToken = async (token) => { if (!token || typeof token !== 'string') { throw new Meteor.Error('error-invalid-token', 'The invite token is invalid.', { method: 'validateInviteToken', field: 'token' }); } - const inviteData = Invites.findOneById(token); + const inviteData = await Invites.findOneById(token); if (!inviteData) { throw new Meteor.Error('error-invalid-token', 'The invite token is invalid.', { method: 'validateInviteToken', field: 'token' }); } diff --git a/app/lib/server/functions/deleteMessage.ts b/app/lib/server/functions/deleteMessage.ts index 8f698842e205..96e563b1a464 100644 --- a/app/lib/server/functions/deleteMessage.ts +++ b/app/lib/server/functions/deleteMessage.ts @@ -2,14 +2,15 @@ import { Meteor } from 'meteor/meteor'; import { FileUpload } from '../../../file-upload/server'; import { settings } from '../../../settings/server'; -import { Messages, Uploads, Rooms } from '../../../models/server'; +import { Messages, Rooms } from '../../../models/server'; +import { Uploads } from '../../../models/server/raw'; import { Notifications } from '../../../notifications/server'; import { callbacks } from '../../../callbacks/server'; import { Apps } from '../../../apps/server'; import { IMessage } from '../../../../definition/IMessage'; import { IUser } from '../../../../definition/IUser'; -export const deleteMessage = function(message: IMessage, user: IUser): void { +export const deleteMessage = async function(message: IMessage, user: IUser): Promise { const deletedMsg = Messages.findOneById(message._id); const isThread = deletedMsg.tcount > 0; const keepHistory = settings.get('Message_KeepHistory') || isThread; @@ -36,9 +37,9 @@ export const deleteMessage = function(message: IMessage, user: IUser): void { Messages.setHiddenById(message._id, true); } - files.forEach((file) => { - file?._id && Uploads.update(file._id, { $set: { _hidden: true } }); - }); + for await (const file of files) { + file?._id && await Uploads.update({ _id: file._id }, { $set: { _hidden: true } }); + } } else { if (!showDeletedStatus) { Messages.removeById(message._id); diff --git a/app/lib/server/functions/deleteUser.js b/app/lib/server/functions/deleteUser.js index 680517db5405..4193774e1136 100644 --- a/app/lib/server/functions/deleteUser.js +++ b/app/lib/server/functions/deleteUser.js @@ -2,7 +2,8 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { FileUpload } from '../../../file-upload/server'; -import { Users, Subscriptions, Messages, Rooms, Integrations, FederationServers } from '../../../models/server'; +import { Users, Subscriptions, Messages, Rooms } from '../../../models/server'; +import { FederationServers, Integrations } from '../../../models/server/raw'; import { settings } from '../../../settings/server'; import { updateGroupDMsName } from './updateGroupDMsName'; import { relinquishRoomOwnerships } from './relinquishRoomOwnerships'; @@ -10,7 +11,7 @@ import { getSubscribedRoomsForUserWithDetails, shouldRemoveOrChangeOwner } from import { getUserSingleOwnedRooms } from './getUserSingleOwnedRooms'; import { api } from '../../../../server/sdk/api'; -export const deleteUser = function(userId, confirmRelinquish = false) { +export async function deleteUser(userId, confirmRelinquish = false) { const user = Users.findOneById(userId, { fields: { username: 1, avatarOrigin: 1, federation: 1 }, }); @@ -36,7 +37,7 @@ export const deleteUser = function(userId, confirmRelinquish = false) { // Users without username can't do anything, so there is nothing to remove if (user.username != null) { - relinquishRoomOwnerships(userId, subscribedRooms); + await relinquishRoomOwnerships(userId, subscribedRooms); const messageErasureType = settings.get('Message_ErasureType'); switch (messageErasureType) { @@ -64,7 +65,7 @@ export const deleteUser = function(userId, confirmRelinquish = false) { FileUpload.getStore('Avatars').deleteByName(user.username); } - Integrations.disableByUserId(userId); // Disables all the integrations which rely on the user being deleted. + await Integrations.disableByUserId(userId); // Disables all the integrations which rely on the user being deleted. api.broadcast('user.deleted', user); } @@ -75,5 +76,5 @@ export const deleteUser = function(userId, confirmRelinquish = false) { updateGroupDMsName(user); // Refresh the servers list - FederationServers.refreshServers(); -}; + await FederationServers.refreshServers(); +} diff --git a/app/lib/server/functions/relinquishRoomOwnerships.js b/app/lib/server/functions/relinquishRoomOwnerships.js index 7c56e3bc05a5..f5c403f1b2b1 100644 --- a/app/lib/server/functions/relinquishRoomOwnerships.js +++ b/app/lib/server/functions/relinquishRoomOwnerships.js @@ -1,5 +1,6 @@ import { FileUpload } from '../../../file-upload/server'; -import { Subscriptions, Messages, Rooms, Roles } from '../../../models/server'; +import { Subscriptions, Messages, Rooms } from '../../../models/server'; +import { Roles } from '../../../models/server/raw'; const bulkRoomCleanUp = (rids) => { // no bulk deletion for files @@ -12,11 +13,14 @@ const bulkRoomCleanUp = (rids) => { ])); }; -export const relinquishRoomOwnerships = function(userId, subscribedRooms, removeDirectMessages = true) { +export const relinquishRoomOwnerships = async function(userId, subscribedRooms, removeDirectMessages = true) { // change owners - subscribedRooms - .filter(({ shouldChangeOwner }) => shouldChangeOwner) - .forEach(({ newOwner, rid }) => Roles.addUserRoles(newOwner, ['owner'], rid)); + const changeOwner = subscribedRooms + .filter(({ shouldChangeOwner }) => shouldChangeOwner); + + for await (const { newOwner, rid } of changeOwner) { + await Roles.addUserRoles(newOwner, ['owner'], rid); + } const roomIdsToRemove = subscribedRooms.filter(({ shouldBeRemoved }) => shouldBeRemoved).map(({ rid }) => rid); diff --git a/app/lib/server/functions/setRoomAvatar.js b/app/lib/server/functions/setRoomAvatar.js index 9b0ea487c758..540de2799201 100644 --- a/app/lib/server/functions/setRoomAvatar.js +++ b/app/lib/server/functions/setRoomAvatar.js @@ -2,13 +2,14 @@ import { Meteor } from 'meteor/meteor'; import { RocketChatFile } from '../../../file'; import { FileUpload } from '../../../file-upload'; -import { Rooms, Avatars, Messages } from '../../../models/server'; +import { Rooms, Messages } from '../../../models/server'; +import { Avatars } from '../../../models/server/raw'; import { api } from '../../../../server/sdk/api'; -export const setRoomAvatar = function(rid, dataURI, user) { +export const setRoomAvatar = async function(rid, dataURI, user) { const fileStore = FileUpload.getStore('Avatars'); - const current = Avatars.findOneByRoomId(rid); + const current = await Avatars.findOneByRoomId(rid); if (!dataURI) { fileStore.deleteByRoomId(rid); diff --git a/app/lib/server/functions/setUserActiveStatus.js b/app/lib/server/functions/setUserActiveStatus.js index 8089fbf7d1cf..77a137695fdd 100644 --- a/app/lib/server/functions/setUserActiveStatus.js +++ b/app/lib/server/functions/setUserActiveStatus.js @@ -63,7 +63,7 @@ export function setUserActiveStatus(userId, active, confirmRelinquish = false) { } closeOmnichannelConversations(user, livechatSubscribedRooms); - relinquishRoomOwnerships(user, chatSubscribedRooms, false); + Promise.await(relinquishRoomOwnerships(user, chatSubscribedRooms, false)); } if (active && !user.active) { diff --git a/app/lib/server/functions/setUsername.js b/app/lib/server/functions/setUsername.js index 8795fb6b01fc..97a05291f5d1 100644 --- a/app/lib/server/functions/setUsername.js +++ b/app/lib/server/functions/setUsername.js @@ -3,7 +3,8 @@ import s from 'underscore.string'; import { Accounts } from 'meteor/accounts-base'; import { settings } from '../../../settings'; -import { Users, Invites } from '../../../models/server'; +import { Users } from '../../../models/server'; +import { Invites } from '../../../models/server/raw'; import { hasPermission } from '../../../authorization'; import { RateLimiter } from '../lib'; import { addUserToRoom } from './addUserToRoom'; @@ -70,7 +71,7 @@ export const _setUsername = function(userId, u, fullUser) { // If it's the first username and the user has an invite Token, then join the invite room if (!previousUsername && user.inviteToken) { - const inviteData = Invites.findOneById(user.inviteToken); + const inviteData = Promise.await(Invites.findOneById(user.inviteToken)); if (inviteData && inviteData.rid) { addUserToRoom(inviteData.rid, user); } diff --git a/app/lib/server/lib/getRoomRoles.js b/app/lib/server/lib/getRoomRoles.js index 9c3718628782..6ed6527c5368 100644 --- a/app/lib/server/lib/getRoomRoles.js +++ b/app/lib/server/lib/getRoomRoles.js @@ -1,7 +1,8 @@ import _ from 'underscore'; import { settings } from '../../../settings'; -import { Subscriptions, Users, Roles } from '../../../models'; +import { Subscriptions, Users } from '../../../models'; +import { Roles } from '../../../models/server/raw'; export function getRoomRoles(rid) { const options = { @@ -17,7 +18,7 @@ export function getRoomRoles(rid) { const UI_Use_Real_Name = settings.get('UI_Use_Real_Name') === true; - const roles = Roles.find({ scope: 'Subscriptions', description: { $exists: 1, $ne: '' } }).fetch(); + const roles = Promise.await(Roles.find({ scope: 'Subscriptions', description: { $exists: 1, $ne: '' } }).toArray()); const subscriptions = Subscriptions.findByRoomIdAndRoles(rid, _.pluck(roles, '_id'), options).fetch(); if (!UI_Use_Real_Name) { diff --git a/app/lib/server/lib/processDirectEmail.js b/app/lib/server/lib/processDirectEmail.js index 3b97938d4694..98313697028f 100644 --- a/app/lib/server/lib/processDirectEmail.js +++ b/app/lib/server/lib/processDirectEmail.js @@ -5,7 +5,7 @@ import moment from 'moment'; import { settings } from '../../../settings/server'; import { Rooms, Messages, Users, Subscriptions } from '../../../models/server'; import { metrics } from '../../../metrics/server'; -import { hasPermission } from '../../../authorization/server'; +import { canAccessRoom, hasPermission } from '../../../authorization/server'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { sendMessage as _sendMessage } from '../functions'; @@ -55,29 +55,25 @@ export const processDirectEmail = function(email) { } message.rid = prevMessage.rid; - const room = Meteor.call('canAccessRoom', message.rid, user._id); - if (!room) { + const room = Rooms.findOneById(message.rid); + + if (!canAccessRoom(room, user)) { return false; } - const roomInfo = Rooms.findOneById(message.rid, { - t: 1, - name: 1, - }); - // check mention - if (message.msg.indexOf(`@${ prevMessage.u.username }`) === -1 && roomInfo.t !== 'd') { + if (message.msg.indexOf(`@${ prevMessage.u.username }`) === -1 && room.t !== 'd') { message.msg = `@${ prevMessage.u.username } ${ message.msg }`; } // reply message link let prevMessageLink = `[ ](${ Meteor.absoluteUrl().replace(/\/$/, '') }`; - if (roomInfo.t === 'c') { - prevMessageLink += `/channel/${ roomInfo.name }?msg=${ email.headers.mid }) `; - } else if (roomInfo.t === 'd') { + if (room.t === 'c') { + prevMessageLink += `/channel/${ room.name }?msg=${ email.headers.mid }) `; + } else if (room.t === 'd') { prevMessageLink += `/direct/${ prevMessage.u.username }?msg=${ email.headers.mid }) `; - } else if (roomInfo.t === 'p') { - prevMessageLink += `/group/${ roomInfo.name }?msg=${ email.headers.mid }) `; + } else if (room.t === 'p') { + prevMessageLink += `/group/${ room.name }?msg=${ email.headers.mid }) `; } // add reply message link message.msg = prevMessageLink + message.msg; diff --git a/app/lib/server/methods/deleteMessage.js b/app/lib/server/methods/deleteMessage.js index 086b9caee7f9..8be7da4e5e45 100644 --- a/app/lib/server/methods/deleteMessage.js +++ b/app/lib/server/methods/deleteMessage.js @@ -6,7 +6,7 @@ import { Messages } from '../../../models'; import { deleteMessage } from '../functions'; Meteor.methods({ - deleteMessage(message) { + async deleteMessage(message) { check(message, Match.ObjectIncluding({ _id: String, })); diff --git a/app/lib/server/methods/deleteUserOwnAccount.js b/app/lib/server/methods/deleteUserOwnAccount.js index 1ff7494a8751..2d7f269b08f7 100644 --- a/app/lib/server/methods/deleteUserOwnAccount.js +++ b/app/lib/server/methods/deleteUserOwnAccount.js @@ -9,7 +9,7 @@ import { Users } from '../../../models'; import { deleteUser } from '../functions'; Meteor.methods({ - deleteUserOwnAccount(password, confirmRelinquish) { + async deleteUserOwnAccount(password, confirmRelinquish) { check(password, String); if (!Meteor.userId()) { @@ -39,7 +39,7 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-username', 'Invalid username', { method: 'deleteUserOwnAccount' }); } - deleteUser(userId, confirmRelinquish); + await deleteUser(userId, confirmRelinquish); return true; }, diff --git a/app/lib/server/methods/getChannelHistory.js b/app/lib/server/methods/getChannelHistory.js index 25c645231038..80237842b118 100644 --- a/app/lib/server/methods/getChannelHistory.js +++ b/app/lib/server/methods/getChannelHistory.js @@ -2,8 +2,8 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; import _ from 'underscore'; -import { hasPermission } from '../../../authorization/server'; -import { Subscriptions, Messages } from '../../../models/server'; +import { canAccessRoom, hasPermission } from '../../../authorization/server'; +import { Subscriptions, Messages, Rooms } from '../../../models/server'; import { settings } from '../../../settings/server'; import { normalizeMessagesForUser } from '../../../utils/server/lib/normalizeMessagesForUser'; import { getHiddenSystemMessages } from '../lib/getHiddenSystemMessages'; @@ -17,11 +17,15 @@ Meteor.methods({ } const fromUserId = Meteor.userId(); - const room = Meteor.call('canAccessRoom', rid, fromUserId); + const room = Rooms.findOneById(rid); if (!room) { return false; } + if (!canAccessRoom(room, { _id: fromUserId })) { + return false; + } + // Make sure they can access the room if (room.t === 'c' && !hasPermission(fromUserId, 'preview-c-room') && !Subscriptions.findOneByRoomIdAndUserId(rid, fromUserId, { fields: { _id: 1 } })) { return false; diff --git a/app/lib/server/methods/getMessages.js b/app/lib/server/methods/getMessages.js index d4b3ce4bd20a..f8ccbbbb6f74 100644 --- a/app/lib/server/methods/getMessages.js +++ b/app/lib/server/methods/getMessages.js @@ -1,28 +1,22 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { Messages } from '../../../models'; +import { canAccessRoom } from '../../../authorization/server'; +import { Messages } from '../../../models/server'; Meteor.methods({ getMessages(messages) { check(messages, [String]); - const cache = {}; + const msgs = Messages.findVisibleByIds(messages).fetch(); - return messages.map((msgId) => { - const msg = Messages.findOneById(msgId); + const user = { _id: Meteor.userId() }; - if (!msg || !msg.rid) { - return undefined; - } + const rids = [...new Set(msgs.map((m) => m.rid))]; + if (!rids.every((_id) => canAccessRoom({ _id }, user))) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getSingleMessage' }); + } - cache[msg.rid] = cache[msg.rid] || Meteor.call('canAccessRoom', msg.rid, Meteor.userId()); - - if (!cache[msg.rid]) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getSingleMessage' }); - } - - return msg; - }); + return msgs; }, }); diff --git a/app/lib/server/methods/getSingleMessage.js b/app/lib/server/methods/getSingleMessage.js index 399aa335cab3..604ac2f1b4f7 100644 --- a/app/lib/server/methods/getSingleMessage.js +++ b/app/lib/server/methods/getSingleMessage.js @@ -1,7 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { Messages } from '../../../models'; +import { canAccessRoom } from '../../../authorization/server'; +import { Messages } from '../../../models/server'; Meteor.methods({ getSingleMessage(msgId) { @@ -13,7 +14,7 @@ Meteor.methods({ return undefined; } - if (!Meteor.call('canAccessRoom', msg.rid, Meteor.userId())) { + if (!canAccessRoom({ _id: msg.rid }, { _id: Meteor.userId() })) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getSingleMessage' }); } diff --git a/app/lib/server/methods/leaveRoom.js b/app/lib/server/methods/leaveRoom.ts similarity index 76% rename from app/lib/server/methods/leaveRoom.js rename to app/lib/server/methods/leaveRoom.ts index 561d7bdb548a..cce12a25b8f8 100644 --- a/app/lib/server/methods/leaveRoom.js +++ b/app/lib/server/methods/leaveRoom.ts @@ -1,13 +1,14 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { hasPermission, hasRole, getUsersInRole } from '../../../authorization'; -import { Subscriptions, Rooms } from '../../../models'; +import { hasPermission, hasRole } from '../../../authorization/server'; +import { Subscriptions, Rooms } from '../../../models/server'; import { removeUserFromRoom } from '../functions'; import { roomTypes, RoomMemberActions } from '../../../utils/server'; +import { Roles } from '../../../models/server/raw'; Meteor.methods({ - leaveRoom(rid) { + async leaveRoom(rid) { check(rid, String); if (!Meteor.userId()) { @@ -17,7 +18,7 @@ Meteor.methods({ const room = Rooms.findOneById(rid); const user = Meteor.user(); - if (!roomTypes.getConfig(room.t).allowMemberAction(room, RoomMemberActions.LEAVE)) { + if (!user || !roomTypes.getConfig(room.t).allowMemberAction(room, RoomMemberActions.LEAVE)) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'leaveRoom' }); } @@ -32,7 +33,8 @@ Meteor.methods({ // If user is room owner, check if there are other owners. If there isn't anyone else, warn user to set a new owner. if (hasRole(user._id, 'owner', room._id)) { - const numOwners = getUsersInRole('owner', room._id).count(); + const cursor = await Roles.findUsersInRole('owner', room._id); + const numOwners = Promise.await(cursor.count()); if (numOwners === 1) { throw new Meteor.Error('error-you-are-last-owner', 'You are the last owner. Please set new owner before leaving the room.', { method: 'leaveRoom' }); } diff --git a/app/lib/server/methods/refreshOAuthService.js b/app/lib/server/methods/refreshOAuthService.ts similarity index 66% rename from app/lib/server/methods/refreshOAuthService.js rename to app/lib/server/methods/refreshOAuthService.ts index e0ef565cb45e..14dbeaa1fcd6 100644 --- a/app/lib/server/methods/refreshOAuthService.js +++ b/app/lib/server/methods/refreshOAuthService.ts @@ -1,11 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { ServiceConfiguration } from 'meteor/service-configuration'; -import { hasPermission } from '../../../authorization'; -import { Settings } from '../../../models'; +import { hasPermission } from '../../../authorization/server'; +import { Settings } from '../../../models/server/raw'; Meteor.methods({ - refreshOAuthService() { + async refreshOAuthService() { if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'refreshOAuthService' }); } @@ -16,6 +16,6 @@ Meteor.methods({ ServiceConfiguration.configurations.remove({}); - Settings.update({ _id: /^(Accounts_OAuth_|SAML_|CAS_|Blockstack_).+/ }, { $set: { _updatedAt: new Date() } }, { multi: true }); + await Settings.update({ _id: /^(Accounts_OAuth_|SAML_|CAS_|Blockstack_).+/ }, { $set: { _updatedAt: new Date() } }, { multi: true }); }, }); diff --git a/app/lib/server/methods/removeOAuthService.ts b/app/lib/server/methods/removeOAuthService.ts new file mode 100644 index 000000000000..6b3f1ff535ae --- /dev/null +++ b/app/lib/server/methods/removeOAuthService.ts @@ -0,0 +1,54 @@ +import { capitalize } from '@rocket.chat/string-helpers'; +import { Meteor } from 'meteor/meteor'; +import { check } from 'meteor/check'; + +import { hasPermission } from '../../../authorization/server'; +import { Settings } from '../../../models/server/raw'; + + +Meteor.methods({ + async removeOAuthService(name) { + check(name, String); + + if (!Meteor.userId()) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'removeOAuthService' }); + } + + if (hasPermission(Meteor.userId(), 'add-oauth-service') !== true) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'removeOAuthService' }); + } + + name = name.toLowerCase().replace(/[^a-z0-9_]/g, ''); + name = capitalize(name); + await Promise.all([ + Settings.removeById(`Accounts_OAuth_Custom-${ name }`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-url`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-token_path`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-identity_path`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-authorize_path`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-scope`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-access_token_param`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-token_sent_via`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-identity_token_sent_via`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-id`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-secret`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-button_label_text`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-button_label_color`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-button_color`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-login_style`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-key_field`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-username_field`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-email_field`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-name_field`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-avatar_field`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-roles_claim`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-merge_roles`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-merge_users`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-show_button`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-groups_claim`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-channels_admin`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-map_channels`), + Settings.removeById(`Accounts_OAuth_Custom-${ name }-groups_channel_map`), + ]); + }, +}); diff --git a/app/lib/server/methods/saveSetting.js b/app/lib/server/methods/saveSetting.js index b375b1ad5952..993915db53f6 100644 --- a/app/lib/server/methods/saveSetting.js +++ b/app/lib/server/methods/saveSetting.js @@ -3,11 +3,11 @@ import { Match, check } from 'meteor/check'; import { hasPermission, hasAllPermission } from '../../../authorization/server'; import { getSettingPermissionId } from '../../../authorization/lib'; -import { Settings } from '../../../models'; import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired'; +import { Settings } from '../../../models/server/raw'; Meteor.methods({ - saveSetting: twoFactorRequired(function(_id, value, editor) { + saveSetting: twoFactorRequired(async function(_id, value, editor) { const uid = Meteor.userId(); if (!uid) { throw new Meteor.Error('error-action-not-allowed', 'Editing settings is not allowed', { @@ -26,7 +26,7 @@ Meteor.methods({ // Verify the _id passed in is a string. check(_id, String); - const setting = Settings.db.findOneById(_id); + const setting = await Settings.findOneById(_id); // Verify the value is what it should be switch (setting.type) { @@ -44,7 +44,7 @@ Meteor.methods({ break; } - Settings.updateValueAndEditorById(_id, value, editor); + await Settings.updateValueAndEditorById(_id, value, editor); return true; }), }); diff --git a/app/lib/server/methods/saveSettings.js b/app/lib/server/methods/saveSettings.js index 6b99b3c7665c..6da862bd9aa5 100644 --- a/app/lib/server/methods/saveSettings.js +++ b/app/lib/server/methods/saveSettings.js @@ -1,13 +1,13 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import { hasPermission } from '../../../authorization'; -import { Settings } from '../../../models'; +import { hasPermission } from '../../../authorization/server'; import { getSettingPermissionId } from '../../../authorization/lib'; import { twoFactorRequired } from '../../../2fa/server/twoFactorRequired'; +import { Settings } from '../../../models/server/raw'; Meteor.methods({ - saveSettings: twoFactorRequired(function(params = []) { + saveSettings: twoFactorRequired(async function(params = []) { const uid = Meteor.userId(); const settingsNotAllowed = []; if (uid === null) { @@ -18,16 +18,16 @@ Meteor.methods({ const editPrivilegedSetting = hasPermission(uid, 'edit-privileged-setting'); const manageSelectedSettings = hasPermission(uid, 'manage-selected-settings'); - params.forEach(({ _id, value }) => { + await Promise.all(params.map(async ({ _id, value }) => { // Verify the _id passed in is a string. check(_id, String); if (!editPrivilegedSetting && !(manageSelectedSettings && hasPermission(uid, getSettingPermissionId(_id)))) { return settingsNotAllowed.push(_id); } - const setting = Settings.db.findOneById(_id); + const setting = await Settings.findOneById(_id); // Verify the value is what it should be - switch (setting.type) { + switch (setting?.type) { case 'roomPick': check(value, Match.OneOf([Object], '')); break; @@ -44,7 +44,7 @@ Meteor.methods({ check(value, String); break; } - }); + })); if (settingsNotAllowed.length) { throw new Meteor.Error('error-action-not-allowed', 'Editing settings is not allowed', { @@ -53,8 +53,8 @@ Meteor.methods({ }); } - params.forEach(({ _id, value, editor }) => Settings.updateValueById(_id, value, editor)); + await Promise.all(params.map(({ _id, value, editor }) => Settings.updateValueById(_id, value, editor))); return true; - }), + }, {}), }); diff --git a/app/livechat/client/views/app/livechatReadOnly.js b/app/livechat/client/views/app/livechatReadOnly.js index c751c701e9ea..74dce229f25c 100644 --- a/app/livechat/client/views/app/livechatReadOnly.js +++ b/app/livechat/client/views/app/livechatReadOnly.js @@ -64,7 +64,7 @@ Template.livechatReadOnly.onCreated(function() { this.preparing = new ReactiveVar(true); this.updateInquiry = async ({ clientAction, ...inquiry }) => { - if (clientAction === 'removed' || !await callWithErrorHandling('canAccessRoom', inquiry.rid, Meteor.userId())) { + if (clientAction === 'removed') { // this will force to refresh the room // since the client wont get notified of room changes when chats are on queue (no one assigned) // a better approach should be performed when refactoring these templates to use react diff --git a/app/livechat/server/api/lib/livechat.js b/app/livechat/server/api/lib/livechat.js index a7a29598250f..5720112b5463 100644 --- a/app/livechat/server/api/lib/livechat.js +++ b/app/livechat/server/api/lib/livechat.js @@ -1,7 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; -import { LivechatRooms, LivechatVisitors, LivechatDepartment, LivechatTrigger, EmojiCustom } from '../../../../models/server'; +import { LivechatRooms, LivechatVisitors, LivechatDepartment, LivechatTrigger } from '../../../../models/server'; +import { EmojiCustom } from '../../../../models/server/raw'; import { Livechat } from '../../lib/Livechat'; import { callbacks } from '../../../../callbacks/server'; import { normalizeAgent } from '../../lib/Helper'; @@ -86,12 +87,12 @@ export function normalizeHttpHeaderData(headers = {}) { const httpHeaders = Object.assign({}, headers); return { httpHeaders }; } -export function settings() { +export async function settings() { const initSettings = Livechat.getInitSettings(); const triggers = findTriggers(); const departments = findDepartments(); const sound = `${ Meteor.absoluteUrl() }sounds/chime.mp3`; - const emojis = EmojiCustom.find().fetch(); + const emojis = await EmojiCustom.find().toArray(); return { enabled: initSettings.Livechat_enabled, settings: { diff --git a/app/livechat/server/api/v1/config.js b/app/livechat/server/api/v1/config.js index e43509d4ba3c..f1a49c1ed395 100644 --- a/app/livechat/server/api/v1/config.js +++ b/app/livechat/server/api/v1/config.js @@ -17,7 +17,7 @@ API.v1.addRoute('livechat/config', { return API.v1.success({ config: { enabled: false } }); } - const config = settings(); + const config = Promise.await(settings()); const { token, department } = this.queryParams; const status = Livechat.online(department); diff --git a/app/livechat/server/api/v1/message.js b/app/livechat/server/api/v1/message.js index 178f571da490..0fd39bc5d086 100644 --- a/app/livechat/server/api/v1/message.js +++ b/app/livechat/server/api/v1/message.js @@ -108,7 +108,7 @@ API.v1.addRoute('livechat/message/:_id', { } if (message.file) { - message = normalizeMessageFileUpload(message); + message = Promise.await(normalizeMessageFileUpload(message)); } return API.v1.success({ message }); @@ -151,7 +151,7 @@ API.v1.addRoute('livechat/message/:_id', { if (result) { let message = Messages.findOneById(_id); if (message.file) { - message = normalizeMessageFileUpload(message); + message = Promise.await(normalizeMessageFileUpload(message)); } return API.v1.success({ message }); @@ -191,7 +191,7 @@ API.v1.addRoute('livechat/message/:_id', { throw new Meteor.Error('invalid-message'); } - const result = Livechat.deleteMessage({ guest, message }); + const result = Promise.await(Livechat.deleteMessage({ guest, message })); if (result) { return API.v1.success({ message: { @@ -251,7 +251,7 @@ API.v1.addRoute('livechat/messages.history/:rid', { const messages = loadMessageHistory({ userId: guest._id, rid, end, limit, ls, sort, offset, text }) .messages - .map(normalizeMessageFileUpload); + .map((...args) => Promise.await(normalizeMessageFileUpload(...args))); return API.v1.success({ messages }); } catch (e) { return API.v1.failure(e); diff --git a/app/livechat/server/api/v1/room.js b/app/livechat/server/api/v1/room.js index 5dcbf8cf1dd0..88c9e4717110 100644 --- a/app/livechat/server/api/v1/room.js +++ b/app/livechat/server/api/v1/room.js @@ -166,7 +166,7 @@ API.v1.addRoute('livechat/room.survey', { throw new Meteor.Error('invalid-room'); } - const config = settings(); + const config = Promise.await(settings()); if (!config.survey || !config.survey.items || !config.survey.values) { throw new Meteor.Error('invalid-livechat-config'); } diff --git a/app/livechat/server/api/v1/videoCall.js b/app/livechat/server/api/v1/videoCall.js index 38b9c2d66491..7e30b10eadd7 100644 --- a/app/livechat/server/api/v1/videoCall.js +++ b/app/livechat/server/api/v1/videoCall.js @@ -35,7 +35,7 @@ API.v1.addRoute('livechat/video.call/:token', { }, }; const { room } = getRoom({ guest, rid, roomInfo }); - const config = settings(); + const config = Promise.await(settings()); if (!config.theme || !config.theme.actionLinks) { throw new Meteor.Error('invalid-livechat-config'); } diff --git a/app/livechat/server/business-hour/AbstractBusinessHour.ts b/app/livechat/server/business-hour/AbstractBusinessHour.ts index 3b564b2c4499..01768b5240d0 100644 --- a/app/livechat/server/business-hour/AbstractBusinessHour.ts +++ b/app/livechat/server/business-hour/AbstractBusinessHour.ts @@ -47,7 +47,7 @@ export abstract class AbstractBusinessHourBehavior { } async changeAgentActiveStatus(agentId: string, status: string): Promise { - return this.UsersRepository.setLivechatStatus(agentId, status); + return this.UsersRepository.setLivechatStatusIf(agentId, status, { livechatStatusSystemModified: true }, { livechatStatusSystemModified: true }); } } diff --git a/app/livechat/server/hooks/saveAnalyticsData.js b/app/livechat/server/hooks/saveAnalyticsData.js index 4ca8832c153d..96d336c228ca 100644 --- a/app/livechat/server/hooks/saveAnalyticsData.js +++ b/app/livechat/server/hooks/saveAnalyticsData.js @@ -14,7 +14,7 @@ callbacks.add('afterSaveMessage', function(message, room) { } if (message.file) { - message = normalizeMessageFileUpload(message); + message = Promise.await(normalizeMessageFileUpload(message)); } const now = new Date(); diff --git a/app/livechat/server/hooks/sendToCRM.js b/app/livechat/server/hooks/sendToCRM.js index ac1ec663904b..94e4cfcd22eb 100644 --- a/app/livechat/server/hooks/sendToCRM.js +++ b/app/livechat/server/hooks/sendToCRM.js @@ -84,7 +84,7 @@ function sendToCRM(type, room, includeMessages = true) { } const { u } = message; - postData.messages.push(normalizeMessageFileUpload({ u, ...msg })); + postData.messages.push(Promise.await(normalizeMessageFileUpload({ u, ...msg }))); }); } diff --git a/app/livechat/server/hooks/sendToFacebook.js b/app/livechat/server/hooks/sendToFacebook.js index 1af4767e3656..7c1b00f31211 100644 --- a/app/livechat/server/hooks/sendToFacebook.js +++ b/app/livechat/server/hooks/sendToFacebook.js @@ -29,7 +29,7 @@ callbacks.add('afterSaveMessage', function(message, room) { } if (message.file) { - message = normalizeMessageFileUpload(message); + message = Promise.await(normalizeMessageFileUpload(message)); } OmniChannel.reply({ diff --git a/app/livechat/server/lib/Helper.js b/app/livechat/server/lib/Helper.js index fb42f3949724..4ed3a225136e 100644 --- a/app/livechat/server/lib/Helper.js +++ b/app/livechat/server/lib/Helper.js @@ -40,6 +40,7 @@ export const createLivechatRoom = (rid, name, guest, roomInfo = {}, extraData = const extraRoomInfo = callbacks.run('livechat.beforeRoom', roomInfo, extraData); const { _id, username, token, department: departmentId, status = 'online' } = guest; + const newRoomAt = new Date(); logger.debug(`Creating livechat room for visitor ${ _id }`); @@ -47,10 +48,10 @@ export const createLivechatRoom = (rid, name, guest, roomInfo = {}, extraData = _id: rid, msgs: 0, usersCount: 1, - lm: new Date(), + lm: newRoomAt, fname: name, t: 'l', - ts: new Date(), + ts: newRoomAt, departmentId, v: { _id, @@ -67,6 +68,7 @@ export const createLivechatRoom = (rid, name, guest, roomInfo = {}, extraData = type: OmnichannelSourceType.OTHER, alias: 'unknown', }, + queuedAt: newRoomAt, }, extraRoomInfo); const roomId = Rooms.insert(room); diff --git a/app/livechat/server/lib/Livechat.js b/app/livechat/server/lib/Livechat.js index e7145d7def0d..aa399564bfa5 100644 --- a/app/livechat/server/lib/Livechat.js +++ b/app/livechat/server/lib/Livechat.js @@ -40,6 +40,7 @@ import { normalizeTransferredByData, parseAgentCustomFields, updateDepartmentAge import { Apps, AppEvents } from '../../../apps/server'; import { businessHourManager } from '../business-hour'; import notifications from '../../../notifications/server/lib/Notifications'; +import { Users as UsersRaw } from '../../../models/server/raw'; const logger = new Logger('Livechat'); @@ -221,7 +222,7 @@ export const Livechat = { return true; }, - deleteMessage({ guest, message }) { + async deleteMessage({ guest, message }) { Livechat.logger.debug(`Attempting to delete a message by visitor ${ guest._id }`); check(message, Match.ObjectIncluding({ _id: String })); @@ -238,7 +239,7 @@ export const Livechat = { throw new Meteor.Error('error-action-not-allowed', 'Message deleting not allowed', { method: 'livechatDeleteMessage' }); } - deleteMessage(message, guest); + await deleteMessage(message, guest); return true; }, @@ -886,6 +887,12 @@ export const Livechat = { return user; }, + setUserStatusLivechatIf(userId, status, condition, fields) { + const user = Promise.await(UsersRaw.setLivechatStatusIf(userId, status, condition, fields)); + callbacks.runAsync('livechat.setUserStatusLivechat', { userId, status }); + return user; + }, + cleanGuestHistory(_id) { const guest = LivechatVisitors.findOneById(_id); if (!guest) { diff --git a/app/livechat/server/lib/QueueManager.js b/app/livechat/server/lib/QueueManager.js index c3a10ebb5bc4..711aa84351f9 100644 --- a/app/livechat/server/lib/QueueManager.js +++ b/app/livechat/server/lib/QueueManager.js @@ -7,7 +7,7 @@ import { callbacks } from '../../../callbacks/server'; import { Logger } from '../../../logger'; import { RoutingManager } from './RoutingManager'; -const logger = new Logger('QueueMananger'); +const logger = new Logger('QueueManager'); export const saveQueueInquiry = (inquiry) => { LivechatInquiry.queueInquiry(inquiry._id); diff --git a/app/livechat/server/sendMessageBySMS.js b/app/livechat/server/sendMessageBySMS.js index 6094d1dc62a3..5e3bd1f13342 100644 --- a/app/livechat/server/sendMessageBySMS.js +++ b/app/livechat/server/sendMessageBySMS.js @@ -31,7 +31,7 @@ callbacks.add('afterSaveMessage', function(message, room) { let extraData; if (message.file) { - message = normalizeMessageFileUpload(message); + message = Promise.await(normalizeMessageFileUpload(message)); const { fileUpload, rid, u: { _id: userId } = {} } = message; extraData = Object.assign({}, { rid, userId, fileUpload }); } diff --git a/app/livechat/server/startup.js b/app/livechat/server/startup.js index 50b9bd3dc0ec..729eb4f92979 100644 --- a/app/livechat/server/startup.js +++ b/app/livechat/server/startup.js @@ -62,5 +62,5 @@ Meteor.startup(async () => { RoutingManager.setMethodNameAndStartQueue(value); }); - Accounts.onLogout(({ user }) => user?.roles?.includes('livechat-agent') && !user?.roles?.includes('bot') && Livechat.setUserStatusLivechat(user._id, 'not-available')); + Accounts.onLogout(({ user }) => user?.roles?.includes('livechat-agent') && !user?.roles?.includes('bot') && Livechat.setUserStatusLivechatIf(user._id, 'not-available', {}, { livechatStatusSystemModified: true })); }); diff --git a/app/livechat/server/statistics/LivechatAgentActivityMonitor.js b/app/livechat/server/statistics/LivechatAgentActivityMonitor.js index 1066e112f027..2c894afe4f1f 100644 --- a/app/livechat/server/statistics/LivechatAgentActivityMonitor.js +++ b/app/livechat/server/statistics/LivechatAgentActivityMonitor.js @@ -3,7 +3,8 @@ import { Meteor } from 'meteor/meteor'; import { SyncedCron } from 'meteor/littledata:synced-cron'; import { callbacks } from '../../../callbacks/server'; -import { LivechatAgentActivity, Sessions, Users } from '../../../models/server'; +import { LivechatAgentActivity, Users } from '../../../models/server'; +import { Sessions } from '../../../models/server/raw'; const formatDate = (dateTime = new Date()) => ({ date: parseInt(moment(dateTime).format('YYYYMMDD')), @@ -12,7 +13,6 @@ const formatDate = (dateTime = new Date()) => ({ export class LivechatAgentActivityMonitor { constructor() { this._started = false; - this._handleMeteorConnection = this._handleMeteorConnection.bind(this); this._handleAgentStatusChanged = this._handleAgentStatusChanged.bind(this); this._handleUserStatusLivechatChanged = this._handleUserStatusLivechatChanged.bind(this); this._name = 'Livechat Agent Activity Monitor'; @@ -41,7 +41,7 @@ export class LivechatAgentActivityMonitor { return; } this._startMonitoring(); - Meteor.onConnection(this._handleMeteorConnection); + Meteor.onConnection((connection) => this._handleMeteorConnection(connection)); callbacks.add('livechat.agentStatusChanged', this._handleAgentStatusChanged); callbacks.add('livechat.setUserStatusLivechat', this._handleUserStatusLivechatChanged); this._started = true; @@ -75,12 +75,12 @@ export class LivechatAgentActivityMonitor { } } - _handleMeteorConnection(connection) { + async _handleMeteorConnection(connection) { if (!this.isRunning()) { return; } - const session = Sessions.findOne({ sessionId: connection.id }); + const session = await Sessions.findOne({ sessionId: connection.id }); if (!session) { return; } diff --git a/app/message-pin/server/pinMessage.js b/app/message-pin/server/pinMessage.js index 4543496ad2a0..545a350b7d6a 100644 --- a/app/message-pin/server/pinMessage.js +++ b/app/message-pin/server/pinMessage.js @@ -1,11 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { settings } from '../../settings'; -import { callbacks } from '../../callbacks'; -import { isTheLastMessage } from '../../lib'; +import { settings } from '../../settings/server'; +import { callbacks } from '../../callbacks/server'; +import { isTheLastMessage } from '../../lib/server'; import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL'; -import { hasPermission } from '../../authorization'; +import { canAccessRoom, hasPermission } from '../../authorization/server'; import { Subscriptions, Messages, Users, Rooms } from '../../models'; const recursiveRemove = (msg, deep = 1) => { @@ -72,7 +72,11 @@ Meteor.methods({ if (settings.get('Message_KeepHistory')) { Messages.cloneAndSaveAsHistoryById(message._id, me); } - const room = Meteor.call('canAccessRoom', originalMessage.rid, Meteor.userId()); + + const room = Rooms.findOneById(originalMessage.rid); + if (!canAccessRoom(room, { _id: Meteor.userId() })) { + throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'pinMessage' }); + } originalMessage.pinned = true; originalMessage.pinnedAt = pinnedAt || Date.now; @@ -166,7 +170,12 @@ Meteor.methods({ username: me.username, }; originalMessage = callbacks.run('beforeSaveMessage', originalMessage); - const room = Meteor.call('canAccessRoom', originalMessage.rid, Meteor.userId()); + + const room = Rooms.findOneById(originalMessage.rid, { fields: { lastMessage: 1 } }); + if (!canAccessRoom(room, { _id: Meteor.userId() })) { + throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'unpinMessage' }); + } + if (isTheLastMessage(room, message)) { Rooms.setLastMessagePinned(room._id, originalMessage.pinnedBy, originalMessage.pinned); } diff --git a/app/message-star/server/starMessage.js b/app/message-star/server/starMessage.js index 4d22d6727835..097b640f31ef 100644 --- a/app/message-star/server/starMessage.js +++ b/app/message-star/server/starMessage.js @@ -1,8 +1,9 @@ import { Meteor } from 'meteor/meteor'; -import { settings } from '../../settings'; -import { isTheLastMessage } from '../../lib'; -import { Subscriptions, Rooms, Messages } from '../../models'; +import { settings } from '../../settings/server'; +import { isTheLastMessage } from '../../lib/server'; +import { canAccessRoom } from '../../authorization/server'; +import { Subscriptions, Rooms, Messages } from '../../models/server'; Meteor.methods({ starMessage(message) { @@ -26,7 +27,12 @@ Meteor.methods({ if (!Messages.findOneByRoomIdAndMessageId(message.rid, message._id)) { return false; } - const room = Meteor.call('canAccessRoom', message.rid, Meteor.userId()); + + const room = Rooms.findOneById(message.rid, { fields: { lastMessage: 1 } }); + if (!canAccessRoom(room, { _id: Meteor.userId() })) { + throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'starMessage' }); + } + if (isTheLastMessage(room, message)) { Rooms.updateLastMessageStar(room._id, Meteor.userId(), message.starred); } diff --git a/app/meteor-accounts-saml/server/lib/SAML.ts b/app/meteor-accounts-saml/server/lib/SAML.ts index 5de64393352a..a0c60ff1d3a5 100644 --- a/app/meteor-accounts-saml/server/lib/SAML.ts +++ b/app/meteor-accounts-saml/server/lib/SAML.ts @@ -8,7 +8,8 @@ import fiber from 'fibers'; import { escapeRegExp, escapeHTML } from '@rocket.chat/string-helpers'; import { settings } from '../../../settings/server'; -import { Users, Rooms, CredentialTokens } from '../../../models/server'; +import { Users, Rooms } from '../../../models/server'; +import { CredentialTokens } from '../../../models/server/raw'; import { IUser } from '../../../../definition/IUser'; import { IIncomingMessage } from '../../../../definition/IIncomingMessage'; import { saveUserIdentity, createRoom, generateUsernameSuggestion, addUserToRoom } from '../../../lib/server/functions'; @@ -55,20 +56,20 @@ export class SAML { } } - public static hasCredential(credentialToken: string): boolean { - return CredentialTokens.findOneById(credentialToken) != null; + public static async hasCredential(credentialToken: string): Promise { + return await CredentialTokens.findOneNotExpiredById(credentialToken) != null; } - public static retrieveCredential(credentialToken: string): Record | undefined { + public static async retrieveCredential(credentialToken: string): Promise | undefined> { // The credentialToken in all these functions corresponds to SAMLs inResponseTo field and is mandatory to check. - const data = CredentialTokens.findOneById(credentialToken); + const data = await CredentialTokens.findOneNotExpiredById(credentialToken); if (data) { return data.userInfo; } } - public static storeCredential(credentialToken: string, loginResult: object): void { - CredentialTokens.create(credentialToken, loginResult); + public static async storeCredential(credentialToken: string, loginResult: {profile: Record}): Promise { + await CredentialTokens.create(credentialToken, loginResult); } public static insertOrUpdateSAMLUser(userObject: ISAMLUser): {userId: string; token: string} { @@ -380,7 +381,7 @@ export class SAML { private static processValidateAction(req: IIncomingMessage, res: ServerResponse, service: IServiceProviderOptions, _samlObject: ISAMLAction): void { const serviceProvider = new SAMLServiceProvider(service); SAMLUtils.relayState = req.body.RelayState; - serviceProvider.validateResponse(req.body.SAMLResponse, (err, profile/* , loggedOut*/) => { + serviceProvider.validateResponse(req.body.SAMLResponse, async (err, profile/* , loggedOut*/) => { try { if (err) { SAMLUtils.error(err); @@ -400,7 +401,7 @@ export class SAML { profile, }; - this.storeCredential(credentialToken, loginResult); + await this.storeCredential(credentialToken, loginResult); const url = `${ Meteor.absoluteUrl('home') }?saml_idp_credentialToken=${ credentialToken }`; res.writeHead(302, { Location: url, diff --git a/app/meteor-accounts-saml/server/loginHandler.ts b/app/meteor-accounts-saml/server/loginHandler.ts index 6b73c7f386ec..edb58716d974 100644 --- a/app/meteor-accounts-saml/server/loginHandler.ts +++ b/app/meteor-accounts-saml/server/loginHandler.ts @@ -17,7 +17,7 @@ Accounts.registerLoginHandler('saml', function(loginRequest) { return undefined; } - const loginResult = SAML.retrieveCredential(loginRequest.credentialToken); + const loginResult = Promise.await(SAML.retrieveCredential(loginRequest.credentialToken)); SAMLUtils.log({ msg: 'RESULT', loginResult }); if (!loginResult) { diff --git a/app/metrics/server/lib/collectMetrics.js b/app/metrics/server/lib/collectMetrics.js index 98267f9ca855..f72437df2f04 100644 --- a/app/metrics/server/lib/collectMetrics.js +++ b/app/metrics/server/lib/collectMetrics.js @@ -10,7 +10,7 @@ import { Facts } from 'meteor/facts-base'; import { Info, getOplogInfo } from '../../../utils/server'; import { getControl } from '../../../../server/lib/migrations'; import { settings } from '../../../settings/server'; -import { Statistics } from '../../../models/server'; +import { Statistics } from '../../../models/server/raw'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { metrics } from './metrics'; import { getAppsStatistics } from '../../../statistics/server/lib/getAppsStatistics'; @@ -42,7 +42,7 @@ const setPrometheusData = async () => { const oplogQueue = getOplogInfo().mongo._oplogHandle?._entryQueue?.length || 0; metrics.oplogQueue.set(oplogQueue); - const statistics = Statistics.findLast(); + const statistics = await Statistics.findLast(); if (!statistics) { return; } diff --git a/app/models/server/index.js b/app/models/server/index.js index 8c9094d654ef..5507cf53f0aa 100644 --- a/app/models/server/index.js +++ b/app/models/server/index.js @@ -1,31 +1,11 @@ import { Base } from './models/_Base'; import { BaseDb } from './models/_BaseDb'; -import Avatars from './models/Avatars'; -import ExportOperations from './models/ExportOperations'; import Messages from './models/Messages'; -import Reports from './models/Reports'; import Rooms from './models/Rooms'; import Settings from './models/Settings'; import Subscriptions from './models/Subscriptions'; -import Uploads from './models/Uploads'; -import UserDataFiles from './models/UserDataFiles'; import Users from './models/Users'; -import Sessions from './models/Sessions'; -import Statistics from './models/Statistics'; -import Permissions from './models/Permissions'; -import Roles from './models/Roles'; -import CustomSounds from './models/CustomSounds'; -import CustomUserStatus from './models/CustomUserStatus'; import Imports from './models/Imports'; -import Integrations from './models/Integrations'; -import IntegrationHistory from './models/IntegrationHistory'; -import Invites from './models/Invites'; -import CredentialTokens from './models/CredentialTokens'; -import EmojiCustom from './models/EmojiCustom'; -import OAuthApps from './models/OAuthApps'; -import OEmbedCache from './models/OEmbedCache'; -import SmarshHistory from './models/SmarshHistory'; -import WebdavAccounts from './models/WebdavAccounts'; import LivechatCustomField from './models/LivechatCustomField'; import LivechatDepartment from './models/LivechatDepartment'; import LivechatDepartmentAgents from './models/LivechatDepartmentAgents'; @@ -35,50 +15,24 @@ import LivechatTrigger from './models/LivechatTrigger'; import LivechatVisitors from './models/LivechatVisitors'; import LivechatAgentActivity from './models/LivechatAgentActivity'; import LivechatInquiry from './models/LivechatInquiry'; -import ReadReceipts from './models/ReadReceipts'; import LivechatExternalMessage from './models/LivechatExternalMessages'; import OmnichannelQueue from './models/OmnichannelQueue'; -import Analytics from './models/Analytics'; -import EmailInbox from './models/EmailInbox'; import ImportData from './models/ImportData'; export { AppsLogsModel } from './models/apps-logs-model'; export { AppsPersistenceModel } from './models/apps-persistence-model'; export { AppsModel } from './models/apps-model'; -export { FederationDNSCache } from './models/FederationDNSCache'; export { FederationRoomEvents } from './models/FederationRoomEvents'; -export { FederationKeys } from './models/FederationKeys'; -export { FederationServers } from './models/FederationServers'; export { Base, BaseDb, - Avatars, - ExportOperations, Messages, - Reports, Rooms, Settings, Subscriptions, - Uploads, - UserDataFiles, Users, - Sessions, - Statistics, - Permissions, - Roles, - CustomSounds, - CustomUserStatus, Imports, - Integrations, - IntegrationHistory, - Invites, - CredentialTokens, - EmojiCustom, - OAuthApps, - OEmbedCache, - SmarshHistory, - WebdavAccounts, LivechatCustomField, LivechatDepartment, LivechatDepartmentAgents, @@ -87,11 +41,8 @@ export { LivechatTrigger, LivechatVisitors, LivechatAgentActivity, - ReadReceipts, LivechatExternalMessage, LivechatInquiry, - Analytics, OmnichannelQueue, - EmailInbox, ImportData, }; diff --git a/app/models/server/models/Analytics.js b/app/models/server/models/Analytics.js deleted file mode 100644 index c521fda8923e..000000000000 --- a/app/models/server/models/Analytics.js +++ /dev/null @@ -1,11 +0,0 @@ -import { Base } from './_Base'; - -export class Analytics extends Base { - constructor() { - super('analytics'); - this.tryEnsureIndex({ date: 1 }); - this.tryEnsureIndex({ 'room._id': 1, date: 1 }, { unique: true }); - } -} - -export default new Analytics(); diff --git a/app/models/server/models/Avatars.js b/app/models/server/models/Avatars.js deleted file mode 100644 index b0e7e8cb5da1..000000000000 --- a/app/models/server/models/Avatars.js +++ /dev/null @@ -1,92 +0,0 @@ -import _ from 'underscore'; -import s from 'underscore.string'; -import { InstanceStatus } from 'meteor/konecty:multiple-instances-status'; - -import { Base } from './_Base'; - -export class Avatars extends Base { - constructor() { - super('avatars'); - - this.model.before.insert((userId, doc) => { - doc.instanceId = InstanceStatus.id(); - }); - - this.tryEnsureIndex({ name: 1 }, { sparse: true }); - this.tryEnsureIndex({ rid: 1 }, { sparse: true }); - } - - insertAvatarFileInit(name, userId, store, file, extra) { - const fileData = { - _id: name, - name, - userId, - store, - complete: false, - uploading: true, - progress: 0, - extension: s.strRightBack(file.name, '.'), - uploadedAt: new Date(), - }; - - _.extend(fileData, file, extra); - - return this.insertOrUpsert(fileData); - } - - updateFileComplete(fileId, userId, file) { - if (!fileId) { - return; - } - - const filter = { - _id: fileId, - userId, - }; - - const update = { - $set: { - complete: true, - uploading: false, - progress: 1, - }, - }; - - update.$set = _.extend(file, update.$set); - - if (this.model.direct && this.model.direct.update) { - return this.model.direct.update(filter, update); - } - return this.update(filter, update); - } - - findOneByName(name) { - return this.findOne({ name }); - } - - findOneByRoomId(rid) { - return this.findOne({ rid }); - } - - updateFileNameById(fileId, name) { - const filter = { _id: fileId }; - const update = { - $set: { - name, - }, - }; - if (this.model.direct && this.model.direct.update) { - return this.model.direct.update(filter, update); - } - return this.update(filter, update); - } - - deleteFile(fileId) { - if (this.model.direct && this.model.direct.remove) { - return this.model.direct.remove({ _id: fileId }); - } - return this.remove({ _id: fileId }); - } -} - -export default new Avatars(); diff --git a/app/models/server/models/CredentialTokens.js b/app/models/server/models/CredentialTokens.js deleted file mode 100644 index 7659538e032e..000000000000 --- a/app/models/server/models/CredentialTokens.js +++ /dev/null @@ -1,32 +0,0 @@ -import { Base } from './_Base'; - -export class CredentialTokens extends Base { - constructor() { - super('credential_tokens'); - - this.tryEnsureIndex({ expireAt: 1 }, { sparse: 1, expireAfterSeconds: 0 }); - } - - create(_id, userInfo) { - const validForMilliseconds = 60000; // Valid for 60 seconds - const token = { - _id, - userInfo, - expireAt: new Date(Date.now() + validForMilliseconds), - }; - - this.insert(token); - return token; - } - - findOneById(_id) { - const query = { - _id, - expireAt: { $gt: new Date() }, - }; - - return this.findOne(query); - } -} - -export default new CredentialTokens(); diff --git a/app/models/server/models/CustomSounds.js b/app/models/server/models/CustomSounds.js deleted file mode 100644 index b9971b954229..000000000000 --- a/app/models/server/models/CustomSounds.js +++ /dev/null @@ -1,56 +0,0 @@ -import { Base } from './_Base'; - -class CustomSounds extends Base { - constructor() { - super('custom_sounds'); - - this.tryEnsureIndex({ name: 1 }); - } - - // find one - findOneById(_id, options) { - return this.findOne(_id, options); - } - - // find - findByName(name, options) { - const query = { - name, - }; - - return this.find(query, options); - } - - findByNameExceptId(name, except, options) { - const query = { - _id: { $nin: [except] }, - name, - }; - - return this.find(query, options); - } - - // update - setName(_id, name) { - const update = { - $set: { - name, - }, - }; - - return this.update({ _id }, update); - } - - // INSERT - create(data) { - return this.insert(data); - } - - - // REMOVE - removeById(_id) { - return this.remove(_id); - } -} - -export default new CustomSounds(); diff --git a/app/models/server/models/CustomUserStatus.js b/app/models/server/models/CustomUserStatus.js deleted file mode 100644 index eb3a586da6ba..000000000000 --- a/app/models/server/models/CustomUserStatus.js +++ /dev/null @@ -1,71 +0,0 @@ -import { Base } from './_Base'; - -class CustomUserStatus extends Base { - constructor() { - super('custom_user_status'); - - this.tryEnsureIndex({ name: 1 }); - } - - // find one - findOneById(_id, options) { - return this.findOne(_id, options); - } - - // find one by name - findOneByName(name, options) { - return this.findOne({ name }, options); - } - - // find - findByName(name, options) { - const query = { - name, - }; - - return this.find(query, options); - } - - findByNameExceptId(name, except, options) { - const query = { - _id: { $nin: [except] }, - name, - }; - - return this.find(query, options); - } - - // update - setName(_id, name) { - const update = { - $set: { - name, - }, - }; - - return this.update({ _id }, update); - } - - setStatusType(_id, statusType) { - const update = { - $set: { - statusType, - }, - }; - - return this.update({ _id }, update); - } - - // INSERT - create(data) { - return this.insert(data); - } - - - // REMOVE - removeById(_id) { - return this.remove(_id); - } -} - -export default new CustomUserStatus(); diff --git a/app/models/server/models/EmailInbox.js b/app/models/server/models/EmailInbox.js deleted file mode 100644 index 490628be3383..000000000000 --- a/app/models/server/models/EmailInbox.js +++ /dev/null @@ -1,27 +0,0 @@ -import { Base } from './_Base'; - -export class EmailInbox extends Base { - constructor() { - super('email_inbox'); - - this.tryEnsureIndex({ email: 1 }, { unique: true }); - } - - findOneById(_id, options) { - return this.findOne(_id, options); - } - - create(data) { - return this.insert(data); - } - - updateById(_id, data) { - return this.update({ _id }, data); - } - - removeById(_id) { - return this.remove(_id); - } -} - -export default new EmailInbox(); diff --git a/app/models/server/models/EmojiCustom.js b/app/models/server/models/EmojiCustom.js deleted file mode 100644 index d0cd7d7bc4cb..000000000000 --- a/app/models/server/models/EmojiCustom.js +++ /dev/null @@ -1,91 +0,0 @@ -import { Base } from './_Base'; - -class EmojiCustom extends Base { - constructor() { - super('custom_emoji'); - - this.tryEnsureIndex({ name: 1 }); - this.tryEnsureIndex({ aliases: 1 }); - this.tryEnsureIndex({ extension: 1 }); - } - - // find one - findOneById(_id, options) { - return this.findOne(_id, options); - } - - // find - findByNameOrAlias(emojiName, options) { - let name = emojiName; - - if (typeof emojiName === 'string') { - name = emojiName.replace(/:/g, ''); - } - - const query = { - $or: [ - { name }, - { aliases: name }, - ], - }; - - return this.find(query, options); - } - - findByNameOrAliasExceptID(name, except, options) { - const query = { - _id: { $nin: [except] }, - $or: [ - { name }, - { aliases: name }, - ], - }; - - return this.find(query, options); - } - - - // update - setName(_id, name) { - const update = { - $set: { - name, - }, - }; - - return this.update({ _id }, update); - } - - setAliases(_id, aliases) { - const update = { - $set: { - aliases, - }, - }; - - return this.update({ _id }, update); - } - - setExtension(_id, extension) { - const update = { - $set: { - extension, - }, - }; - - return this.update({ _id }, update); - } - - // INSERT - create(data) { - return this.insert(data); - } - - - // REMOVE - removeById(_id) { - return this.remove(_id); - } -} - -export default new EmojiCustom(); diff --git a/app/models/server/models/ExportOperations.js b/app/models/server/models/ExportOperations.js deleted file mode 100644 index fb70d38925ca..000000000000 --- a/app/models/server/models/ExportOperations.js +++ /dev/null @@ -1,108 +0,0 @@ -import _ from 'underscore'; - -import { Base } from './_Base'; - -export class ExportOperations extends Base { - constructor() { - super('export_operations'); - - this.tryEnsureIndex({ userId: 1 }); - this.tryEnsureIndex({ status: 1 }); - } - - // FIND - findById(id) { - const query = { _id: id }; - - return this.find(query); - } - - findLastOperationByUser(userId, fullExport = false, options = {}) { - const query = { - userId, - fullExport, - }; - - options.sort = { createdAt: -1 }; - return this.findOne(query, options); - } - - findPendingByUser(userId, options) { - const query = { - userId, - status: { - $nin: ['completed', 'skipped'], - }, - }; - - return this.find(query, options); - } - - findAllPending(options) { - const query = { - status: { $nin: ['completed', 'skipped'] }, - }; - - return this.find(query, options); - } - - findOnePending(options) { - const query = { - status: { $nin: ['completed', 'skipped'] }, - }; - - return this.findOne(query, options); - } - - findAllPendingBeforeMyRequest(requestDay, options) { - const query = { - status: { $nin: ['completed', 'skipped'] }, - createdAt: { $lt: requestDay }, - }; - - return this.find(query, options); - } - - // UPDATE - updateOperation(data) { - const update = { - $set: { - roomList: data.roomList, - status: data.status, - fileList: data.fileList, - generatedFile: data.generatedFile, - fileId: data.fileId, - userNameTable: data.userNameTable, - userData: data.userData, - generatedUserFile: data.generatedUserFile, - generatedAvatar: data.generatedAvatar, - exportPath: data.exportPath, - assetsPath: data.assetsPath, - }, - }; - - return this.update(data._id, update); - } - - - // INSERT - create(data) { - const exportOperation = { - createdAt: new Date(), - }; - - _.extend(exportOperation, data); - - this.insert(exportOperation); - - return exportOperation._id; - } - - - // REMOVE - removeById(_id) { - return this.remove(_id); - } -} - -export default new ExportOperations(); diff --git a/app/models/server/models/FederationDNSCache.js b/app/models/server/models/FederationDNSCache.js deleted file mode 100644 index 155deed53b95..000000000000 --- a/app/models/server/models/FederationDNSCache.js +++ /dev/null @@ -1,13 +0,0 @@ -import { Base } from './_Base'; - -class FederationDNSCacheModel extends Base { - constructor() { - super('federation_dns_cache'); - } - - findOneByDomain(domain) { - return this.findOne({ domain }); - } -} - -export const FederationDNSCache = new FederationDNSCacheModel(); diff --git a/app/models/server/models/FederationKeys.js b/app/models/server/models/FederationKeys.js deleted file mode 100644 index 188f7cdc434e..000000000000 --- a/app/models/server/models/FederationKeys.js +++ /dev/null @@ -1,58 +0,0 @@ -import NodeRSA from 'node-rsa'; - -import { Base } from './_Base'; - -class FederationKeysModel extends Base { - constructor() { - super('federation_keys'); - } - - getKey(type) { - const keyResource = this.findOne({ type }); - - if (!keyResource) { return null; } - - return keyResource.key; - } - - loadKey(keyData, type) { - return new NodeRSA(keyData, `pkcs8-${ type }-pem`); - } - - generateKeys() { - const key = new NodeRSA({ b: 512 }); - - key.generateKeyPair(); - - this.update({ type: 'private' }, { type: 'private', key: key.exportKey('pkcs8-private-pem').replace(/\n|\r/g, '') }, { upsert: true }); - - this.update({ type: 'public' }, { type: 'public', key: key.exportKey('pkcs8-public-pem').replace(/\n|\r/g, '') }, { upsert: true }); - - return { - privateKey: this.getPrivateKey(), - publicKey: this.getPublicKey(), - }; - } - - getPrivateKey() { - const keyData = this.getKey('private'); - - return keyData && this.loadKey(keyData, 'private'); - } - - getPrivateKeyString() { - return this.getKey('private'); - } - - getPublicKey() { - const keyData = this.getKey('public'); - - return keyData && this.loadKey(keyData, 'public'); - } - - getPublicKeyString() { - return this.getKey('public'); - } -} - -export const FederationKeys = new FederationKeysModel(); diff --git a/app/models/server/models/FederationServers.js b/app/models/server/models/FederationServers.js deleted file mode 100644 index 9daf20d5a128..000000000000 --- a/app/models/server/models/FederationServers.js +++ /dev/null @@ -1,26 +0,0 @@ -import { Base } from './_Base'; -import { Users } from '../raw'; - -class FederationServersModel extends Base { - constructor() { - super('federation_servers'); - - this.tryEnsureIndex({ domain: 1 }); - } - - async refreshServers() { - const domains = await Users.getDistinctFederationDomains(); - - domains.forEach((domain) => { - this.update({ domain }, { - $setOnInsert: { - domain, - }, - }, { upsert: true }); - }); - - this.remove({ domain: { $nin: domains } }); - } -} - -export const FederationServers = new FederationServersModel(); diff --git a/app/models/server/models/InstanceStatus.js b/app/models/server/models/InstanceStatus.js deleted file mode 100644 index 344381e44266..000000000000 --- a/app/models/server/models/InstanceStatus.js +++ /dev/null @@ -1,7 +0,0 @@ -import { InstanceStatus } from 'meteor/konecty:multiple-instances-status'; - -import { Base } from './_Base'; - -export class InstanceStatusModel extends Base {} - -export default new InstanceStatusModel(InstanceStatus.getCollection(), { preventSetUpdatedAt: true }); diff --git a/app/models/server/models/IntegrationHistory.js b/app/models/server/models/IntegrationHistory.js deleted file mode 100644 index 817deae0d789..000000000000 --- a/app/models/server/models/IntegrationHistory.js +++ /dev/null @@ -1,43 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Base } from './_Base'; - -export class IntegrationHistory extends Base { - constructor() { - super('integration_history'); - } - - findByType(type, options) { - if (type !== 'outgoing-webhook' || type !== 'incoming-webhook') { - throw new Meteor.Error('invalid-integration-type'); - } - - return this.find({ type }, options); - } - - findByIntegrationId(id, options) { - return this.find({ 'integration._id': id }, options); - } - - findByIntegrationIdAndCreatedBy(id, creatorId, options) { - return this.find({ 'integration._id': id, 'integration._createdBy._id': creatorId }, options); - } - - findOneByIntegrationIdAndHistoryId(integrationId, historyId) { - return this.findOne({ 'integration._id': integrationId, _id: historyId }); - } - - findByEventName(event, options) { - return this.find({ event }, options); - } - - findFailed(options) { - return this.find({ error: true }, options); - } - - removeByIntegrationId(integrationId) { - return this.remove({ 'integration._id': integrationId }); - } -} - -export default new IntegrationHistory(); diff --git a/app/models/server/models/Integrations.js b/app/models/server/models/Integrations.js deleted file mode 100644 index ffbf40c1dcce..000000000000 --- a/app/models/server/models/Integrations.js +++ /dev/null @@ -1,31 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Base } from './_Base'; - -export class Integrations extends Base { - constructor() { - super('integrations'); - - this.tryEnsureIndex({ type: 1 }); - } - - findByType(type, options) { - if (type !== 'webhook-incoming' && type !== 'webhook-outgoing') { - throw new Meteor.Error('invalid-type-to-find'); - } - - return this.find({ type }, options); - } - - disableByUserId(userId) { - return this.update({ userId }, { $set: { enabled: false } }, { multi: true }); - } - - updateRoomName(oldRoomName, newRoomName) { - const hashedOldRoomName = `#${ oldRoomName }`; - const hashedNewRoomName = `#${ newRoomName }`; - return this.update({ channel: hashedOldRoomName }, { $set: { 'channel.$': hashedNewRoomName } }, { multi: true }); - } -} - -export default new Integrations(); diff --git a/app/models/server/models/Invites.js b/app/models/server/models/Invites.js deleted file mode 100644 index 517c1780f0ba..000000000000 --- a/app/models/server/models/Invites.js +++ /dev/null @@ -1,51 +0,0 @@ -import { Base } from './_Base'; - -class Invites extends Base { - constructor() { - super('invites'); - } - - findOneByUserRoomMaxUsesAndExpiration(userId, rid, maxUses, daysToExpire) { - const query = { - rid, - userId, - days: daysToExpire, - maxUses, - }; - - if (daysToExpire > 0) { - query.expires = { - $gt: new Date(), - }; - } - - if (maxUses > 0) { - query.uses = { - $lt: maxUses, - }; - } - - return this.findOne(query); - } - - // INSERT - create(data) { - return this.insert(data); - } - - // REMOVE - removeById(_id) { - return this.remove({ _id }); - } - - // UPDATE - increaseUsageById(_id, uses = 1) { - return this.update({ _id }, { - $inc: { - uses, - }, - }); - } -} - -export default new Invites(); diff --git a/app/models/server/models/LivechatInquiry.js b/app/models/server/models/LivechatInquiry.js index a4d4ec356607..1994e5056cef 100644 --- a/app/models/server/models/LivechatInquiry.js +++ b/app/models/server/models/LivechatInquiry.js @@ -48,7 +48,7 @@ export class LivechatInquiry extends Base { this.update({ _id: inquiryId, }, { - $set: { status: 'taken' }, + $set: { status: 'taken', takenAt: new Date() }, $unset: { defaultAgent: 1, estimatedInactivityCloseTimeAt: 1 }, }); } @@ -71,9 +71,17 @@ export class LivechatInquiry extends Base { return this.update({ _id: inquiryId, }, { - $set: { - status: 'queued', - }, + $set: { status: 'queued', queuedAt: new Date() }, + $unset: { takenAt: 1 }, + }); + } + + queueInquiryAndRemoveDefaultAgent(inquiryId) { + return this.update({ + _id: inquiryId, + }, { + $set: { status: 'queued', queuedAt: new Date() }, + $unset: { takenAt: 1, defaultAgent: 1 }, }); } diff --git a/app/models/server/models/LivechatRooms.js b/app/models/server/models/LivechatRooms.js index dc73c211a2c1..994733ad8bc7 100644 --- a/app/models/server/models/LivechatRooms.js +++ b/app/models/server/models/LivechatRooms.js @@ -20,6 +20,7 @@ export class LivechatRooms extends Base { this.tryEnsureIndex({ 'v.token': 1 }, { sparse: true }); this.tryEnsureIndex({ 'v.token': 1, 'email.thread': 1 }, { sparse: true }); this.tryEnsureIndex({ 'v._id': 1 }, { sparse: true }); + this.tryEnsureIndex({ t: 1, departmentId: 1, closedAt: 1 }, { partialFilterExpression: { closedAt: { $exists: true } } }); } findLivechat(filter = {}, offset = 0, limit = 20) { @@ -719,9 +720,8 @@ export class LivechatRooms extends Base { t: 'l', }; const update = { - $unset: { - servedBy: 1, - }, + $set: { queuedAt: new Date() }, + $unset: { servedBy: 1 }, }; this.update(query, update); diff --git a/app/models/server/models/Messages.js b/app/models/server/models/Messages.js index de09ad1c0b04..3bfff331c4ef 100644 --- a/app/models/server/models/Messages.js +++ b/app/models/server/models/Messages.js @@ -246,6 +246,17 @@ export class Messages extends Base { return this.find(query, options); } + findVisibleByIds(ids, options) { + const query = { + _id: { $in: ids }, + _hidden: { + $ne: true, + }, + }; + + return this.find(query, options); + } + findVisibleThreadByThreadId(tmid, options) { const query = { _hidden: { diff --git a/app/models/server/models/NotificationQueue.js b/app/models/server/models/NotificationQueue.js deleted file mode 100644 index 32eb7524c2c2..000000000000 --- a/app/models/server/models/NotificationQueue.js +++ /dev/null @@ -1,14 +0,0 @@ -import { Base } from './_Base'; - -export class NotificationQueue extends Base { - constructor() { - super('notification_queue'); - this.tryEnsureIndex({ uid: 1 }); - this.tryEnsureIndex({ ts: 1 }, { expireAfterSeconds: 2 * 60 * 60 }); - this.tryEnsureIndex({ schedule: 1 }, { sparse: true }); - this.tryEnsureIndex({ sending: 1 }, { sparse: true }); - this.tryEnsureIndex({ error: 1 }, { sparse: true }); - } -} - -export default new NotificationQueue(); diff --git a/app/models/server/models/OAuthApps.js b/app/models/server/models/OAuthApps.js deleted file mode 100644 index 6aedffb63ae0..000000000000 --- a/app/models/server/models/OAuthApps.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Base } from './_Base'; - -export class OAuthApps extends Base { - constructor() { - super('oauth_apps'); - } -} - -export default new OAuthApps(); diff --git a/app/models/server/models/OEmbedCache.js b/app/models/server/models/OEmbedCache.js deleted file mode 100644 index db4383b9cd34..000000000000 --- a/app/models/server/models/OEmbedCache.js +++ /dev/null @@ -1,39 +0,0 @@ -import { Base } from './_Base'; - -export class OEmbedCache extends Base { - constructor() { - super('oembed_cache'); - this.tryEnsureIndex({ updatedAt: 1 }); - } - - // FIND ONE - findOneById(_id, options) { - const query = { - _id, - }; - return this.findOne(query, options); - } - - // INSERT - createWithIdAndData(_id, data) { - const record = { - _id, - data, - updatedAt: new Date(), - }; - record._id = this.insert(record); - return record; - } - - // REMOVE - removeAfterDate(date) { - const query = { - updatedAt: { - $lte: date, - }, - }; - return this.remove(query); - } -} - -export default new OEmbedCache(); diff --git a/app/models/server/models/Permissions.js b/app/models/server/models/Permissions.js deleted file mode 100644 index 009f29d37f9d..000000000000 --- a/app/models/server/models/Permissions.js +++ /dev/null @@ -1,49 +0,0 @@ -import { Base } from './_Base'; - -export class Permissions extends Base { - // FIND - findByRole(role, options) { - const query = { - roles: role, - }; - - return this.find(query, options); - } - - findOneById(_id) { - return this.findOne({ _id }); - } - - createOrUpdate(name, roles) { - const exists = this.findOne({ - _id: name, - roles, - }, { fields: { _id: 1 } }); - - if (exists) { - return exists._id; - } - - this.upsert({ _id: name }, { $set: { roles } }); - } - - create(name, roles) { - const exists = this.findOneById(name, { fields: { _id: 1 } }); - - if (exists) { - return exists._id; - } - - this.upsert({ _id: name }, { $set: { roles } }); - } - - addRole(permission, role) { - this.update({ _id: permission, roles: { $ne: role } }, { $addToSet: { roles: role } }); - } - - removeRole(permission, role) { - this.update({ _id: permission, roles: role }, { $pull: { roles: role } }); - } -} - -export default new Permissions('permissions'); diff --git a/app/models/server/models/ReadReceipts.js b/app/models/server/models/ReadReceipts.js deleted file mode 100644 index d830f400669f..000000000000 --- a/app/models/server/models/ReadReceipts.js +++ /dev/null @@ -1,25 +0,0 @@ -import { Base } from './_Base'; - -export class ReadReceipts extends Base { - constructor(...args) { - super(...args); - - this.tryEnsureIndex({ - roomId: 1, - userId: 1, - messageId: 1, - }, { - unique: 1, - }); - - this.tryEnsureIndex({ - messageId: 1, - }); - } - - findByMessageId(messageId) { - return this.find({ messageId }); - } -} - -export default new ReadReceipts('message_read_receipt'); diff --git a/app/models/server/models/Reports.js b/app/models/server/models/Reports.js deleted file mode 100644 index 4d2ab019f19b..000000000000 --- a/app/models/server/models/Reports.js +++ /dev/null @@ -1,23 +0,0 @@ -import _ from 'underscore'; - -import { Base } from './_Base'; - -export class Reports extends Base { - constructor() { - super('reports'); - } - - createWithMessageDescriptionAndUserId(message, description, userId, extraData) { - const record = { - message, - description, - ts: new Date(), - userId, - }; - _.extend(record, extraData); - record._id = this.insert(record); - return record; - } -} - -export default new Reports(); diff --git a/app/models/server/models/Roles.js b/app/models/server/models/Roles.js deleted file mode 100644 index e7576191d070..000000000000 --- a/app/models/server/models/Roles.js +++ /dev/null @@ -1,134 +0,0 @@ -import { Base } from './_Base'; -import * as Models from '..'; - - -export class Roles extends Base { - constructor(...args) { - super(...args); - this.tryEnsureIndex({ name: 1 }); - this.tryEnsureIndex({ scope: 1 }); - } - - findUsersInRole(name, scope, options) { - const role = this.findOneByName(name); - const roleScope = (role && role.scope) || 'Users'; - const model = Models[roleScope]; - - return model && model.findUsersInRoles && model.findUsersInRoles(name, scope, options); - } - - isUserInRoles(userId, roles, scope) { - roles = [].concat(roles); - return roles.some((roleName) => { - const role = this.findOneByName(roleName); - const roleScope = (role && role.scope) || 'Users'; - const model = Models[roleScope]; - - return model && model.isUserInRole && model.isUserInRole(userId, roleName, scope); - }); - } - - updateById(_id, name, scope, description, mandatory2fa) { - const queryData = { - name, - scope, - description, - mandatory2fa, - }; - - this.upsert({ _id }, { $set: queryData }); - } - - createWithRandomId(name, scope = 'Users', description = '', protectedRole = true, mandatory2fa = false) { - const role = { - name, - scope, - description, - protected: protectedRole, - mandatory2fa, - }; - - return this.insert(role); - } - - createOrUpdate(name, scope = 'Users', description = '', protectedRole = true, mandatory2fa = false) { - const queryData = { - name, - scope, - description, - protected: protectedRole, - mandatory2fa, - }; - - this.upsert({ _id: name }, { $set: queryData }); - } - - addUserRoles(userId, roles, scope) { - roles = [].concat(roles); - for (const roleName of roles) { - const role = this.findOneByName(roleName); - const roleScope = (role && role.scope) || 'Users'; - const model = Models[roleScope]; - - model && model.addRolesByUserId && model.addRolesByUserId(userId, roleName, scope); - } - return true; - } - - removeUserRoles(userId, roles, scope) { - roles = [].concat(roles); - for (const roleName of roles) { - const role = this.findOneByName(roleName); - const roleScope = (role && role.scope) || 'Users'; - const model = Models[roleScope]; - - model && model.removeRolesByUserId && model.removeRolesByUserId(userId, roleName, scope); - } - return true; - } - - findOneByIdOrName(_idOrName, options) { - const query = { - $or: [{ - _id: _idOrName, - }, { - name: _idOrName, - }], - }; - - return this.findOne(query, options); - } - - findOneByName(name, options) { - const query = { - name, - }; - - return this.findOne(query, options); - } - - findByUpdatedDate(updatedAfterDate, options) { - const query = { - _updatedAt: { $gte: new Date(updatedAfterDate) }, - }; - - return this.find(query, options); - } - - canAddUserToRole(uid, roleName, scope) { - const role = this.findOne({ name: roleName }, { fields: { scope: 1 } }); - if (!role) { - return false; - } - - const model = Models[role.scope]; - if (!model) { - return; - } - - const user = model.isUserInRoleScope(uid, scope); - return !!user; - } -} - -export default new Roles('roles'); diff --git a/app/models/server/models/Rooms.js b/app/models/server/models/Rooms.js index d884208317fc..460bed352f07 100644 --- a/app/models/server/models/Rooms.js +++ b/app/models/server/models/Rooms.js @@ -5,7 +5,6 @@ import { escapeRegExp } from '@rocket.chat/string-helpers'; import { Base } from './_Base'; import Messages from './Messages'; import Subscriptions from './Subscriptions'; -import { getValidRoomName } from '../../../utils'; export class Rooms extends Base { constructor(...args) { @@ -335,6 +334,7 @@ export class Rooms extends Base { let channelName = s.trim(name); try { // TODO evaluate if this function call should be here + const { getValidRoomName } = import('../../../utils/lib/getValidRoomName'); channelName = getValidRoomName(channelName, null, { allowDuplicates: true }); } catch (e) { console.error(e); diff --git a/app/models/server/models/ServerEvents.ts b/app/models/server/models/ServerEvents.ts deleted file mode 100644 index 09b17ac51067..000000000000 --- a/app/models/server/models/ServerEvents.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { Base } from './_Base'; - -export class ServerEvents extends Base { - constructor() { - super('server_events'); - this.tryEnsureIndex({ t: 1, ip: 1, ts: -1 }); - this.tryEnsureIndex({ t: 1, 'u.username': 1, ts: -1 }); - } -} - -export default new ServerEvents(); diff --git a/app/models/server/models/Sessions.mocks.js b/app/models/server/models/Sessions.mocks.js deleted file mode 100644 index ac4b22bf7780..000000000000 --- a/app/models/server/models/Sessions.mocks.js +++ /dev/null @@ -1,16 +0,0 @@ -import mock from 'mock-require'; - -mock('./_Base', { - Base: class Base { - model = { - rawDatabase() { - return { - collection() {}, - options: {}, - }; - }, - } - - tryEnsureIndex() {} - }, -}); diff --git a/app/models/server/models/SmarshHistory.js b/app/models/server/models/SmarshHistory.js deleted file mode 100644 index 9b2b7abc5043..000000000000 --- a/app/models/server/models/SmarshHistory.js +++ /dev/null @@ -1,9 +0,0 @@ -import { Base } from './_Base'; - -export class SmarshHistory extends Base { - constructor() { - super('smarsh_history'); - } -} - -export default new SmarshHistory(); diff --git a/app/models/server/models/Statistics.js b/app/models/server/models/Statistics.js deleted file mode 100644 index 014f8c8180d2..000000000000 --- a/app/models/server/models/Statistics.js +++ /dev/null @@ -1,28 +0,0 @@ -import { Base } from './_Base'; - -export class Statistics extends Base { - constructor() { - super('statistics'); - - this.tryEnsureIndex({ createdAt: -1 }); - } - - // FIND ONE - findOneById(_id, options) { - const query = { _id }; - return this.findOne(query, options); - } - - findLast() { - const options = { - sort: { - createdAt: -1, - }, - limit: 1, - }; - const records = this.find({}, options).fetch(); - return records && records[0]; - } -} - -export default new Statistics(); diff --git a/app/models/server/models/Uploads.js b/app/models/server/models/Uploads.js deleted file mode 100644 index ce56ea6d0c0f..000000000000 --- a/app/models/server/models/Uploads.js +++ /dev/null @@ -1,146 +0,0 @@ -import _ from 'underscore'; -import s from 'underscore.string'; -import { InstanceStatus } from 'meteor/konecty:multiple-instances-status'; -import { escapeRegExp } from '@rocket.chat/string-helpers'; - -import { Base } from './_Base'; - -const fillTypeGroup = (fileData) => { - if (!fileData.type) { - return; - } - - fileData.typeGroup = fileData.type.split('/').shift(); -}; - -export class Uploads extends Base { - constructor() { - super('uploads'); - - this.model.before.insert((userId, doc) => { - doc.instanceId = InstanceStatus.id(); - }); - - this.tryEnsureIndex({ rid: 1 }); - this.tryEnsureIndex({ uploadedAt: 1 }); - this.tryEnsureIndex({ typeGroup: 1 }); - } - - findNotHiddenFilesOfRoom(roomId, searchText, fileType, limit) { - const fileQuery = { - rid: roomId, - complete: true, - uploading: false, - _hidden: { - $ne: true, - }, - }; - - if (searchText) { - fileQuery.name = { $regex: new RegExp(escapeRegExp(searchText), 'i') }; - } - - if (fileType && fileType !== 'all') { - fileQuery.typeGroup = fileType; - } - - const fileOptions = { - limit, - sort: { - uploadedAt: -1, - }, - fields: { - _id: 1, - userId: 1, - rid: 1, - name: 1, - description: 1, - type: 1, - url: 1, - uploadedAt: 1, - typeGroup: 1, - }, - }; - - return this.find(fileQuery, fileOptions); - } - - insert(fileData, ...args) { - fillTypeGroup(fileData); - return super.insert(fileData, ...args); - } - - update(filter, update, ...args) { - if (update.$set) { - fillTypeGroup(update.$set); - } else if (update.type) { - fillTypeGroup(update); - } - - return super.update(filter, update, ...args); - } - - insertFileInit(userId, store, file, extra) { - const fileData = { - userId, - store, - complete: false, - uploading: true, - progress: 0, - extension: s.strRightBack(file.name, '.'), - uploadedAt: new Date(), - }; - - _.extend(fileData, file, extra); - - if (this.model.direct && this.model.direct.insert != null) { - fillTypeGroup(fileData); - file = this.model.direct.insert(fileData); - } else { - file = this.insert(fileData); - } - - return file; - } - - updateFileComplete(fileId, userId, file) { - let result; - if (!fileId) { - return; - } - - const filter = { - _id: fileId, - userId, - }; - - const update = { - $set: { - complete: true, - uploading: false, - progress: 1, - }, - }; - - update.$set = _.extend(file, update.$set); - - if (this.model.direct && this.model.direct.update != null) { - fillTypeGroup(update.$set); - - result = this.model.direct.update(filter, update); - } else { - result = this.update(filter, update); - } - - return result; - } - - deleteFile(fileId) { - if (this.model.direct && this.model.direct.remove != null) { - return this.model.direct.remove({ _id: fileId }); - } - return this.remove({ _id: fileId }); - } -} - -export default new Uploads(); diff --git a/app/models/server/models/UserDataFiles.js b/app/models/server/models/UserDataFiles.js deleted file mode 100644 index a877188ff03b..000000000000 --- a/app/models/server/models/UserDataFiles.js +++ /dev/null @@ -1,44 +0,0 @@ -import _ from 'underscore'; - -import { Base } from './_Base'; - -export class UserDataFiles extends Base { - constructor() { - super('user_data_files'); - - this.tryEnsureIndex({ userId: 1 }); - } - - // FIND - findById(id) { - const query = { _id: id }; - return this.find(query); - } - - findLastFileByUser(userId, options = {}) { - const query = { - userId, - }; - - options.sort = { _updatedAt: -1 }; - return this.findOne(query, options); - } - - // INSERT - create(data) { - const userDataFile = { - createdAt: new Date(), - }; - - _.extend(userDataFile, data); - - return this.insert(userDataFile); - } - - // REMOVE - removeById(_id) { - return this.remove(_id); - } -} - -export default new UserDataFiles(); diff --git a/app/models/server/models/Users.js b/app/models/server/models/Users.js index f74d4de0c90f..6b755bd01a02 100644 --- a/app/models/server/models/Users.js +++ b/app/models/server/models/Users.js @@ -262,6 +262,7 @@ export class Users extends Base { const update = { $set: { statusLivechat: status, + livechatStatusSystemModified: false, }, }; diff --git a/app/models/server/models/UsersSessions.js b/app/models/server/models/UsersSessions.js deleted file mode 100644 index 43aec902d343..000000000000 --- a/app/models/server/models/UsersSessions.js +++ /dev/null @@ -1,7 +0,0 @@ -import { UsersSessions } from 'meteor/konecty:user-presence'; - -import { Base } from './_Base'; - -export class UsersSessionsModel extends Base {} - -export default new UsersSessionsModel(UsersSessions, { preventSetUpdatedAt: true }); diff --git a/app/models/server/models/WebdavAccounts.js b/app/models/server/models/WebdavAccounts.js deleted file mode 100644 index 09df0b64a3a6..000000000000 --- a/app/models/server/models/WebdavAccounts.js +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Webdav Accounts model - */ -import { Base } from './_Base'; - -export class WebdavAccounts extends Base { - constructor() { - super('webdav_accounts'); - - this.tryEnsureIndex({ user_id: 1 }); - } - - findWithUserId(user_id, options) { - const query = { user_id }; - return this.find(query, options); - } - - removeByUserAndId(_id, user_id) { - return this.remove({ _id, user_id }); - } - - removeById(_id) { - return this.remove({ _id }); - } -} - -export default new WebdavAccounts(); diff --git a/app/models/server/models/_BaseDb.js b/app/models/server/models/_BaseDb.js index 6c9473c81fb8..f72bb6c84557 100644 --- a/app/models/server/models/_BaseDb.js +++ b/app/models/server/models/_BaseDb.js @@ -30,31 +30,14 @@ const actions = { d: 'remove', }; -export class BaseDb extends EventEmitter { - constructor(model, baseModel, options = {}) { +export class BaseDbWatch extends EventEmitter { + constructor(collectionName) { super(); - - if (Match.test(model, String)) { - this.name = model; - this.collectionName = this.baseName + this.name; - this.model = new Mongo.Collection(this.collectionName); - } else { - this.name = model._name; - this.collectionName = this.name; - this.model = model; - } - - this.baseModel = baseModel; - - this.preventSetUpdatedAt = !!options.preventSetUpdatedAt; - - this.wrapModel(); + this.collectionName = collectionName; if (!process.env.DISABLE_DB_WATCH) { this.initDbWatch(); } - - this.tryEnsureIndex({ _updatedAt: 1 }, options._updatedAtIndexOptions); } initDbWatch() { @@ -97,6 +80,104 @@ export class BaseDb extends EventEmitter { } } + processOplogRecord({ id, op }) { + const action = actions[op.op]; + metrics.oplog.inc({ + collection: this.collectionName, + op: action, + }); + + if (action === 'insert') { + this.emit('change', { + action, + clientAction: 'inserted', + id: op.o._id, + data: op.o, + oplog: true, + }); + return; + } + + if (action === 'update') { + if (!op.o.$set && !op.o.$unset) { + this.emit('change', { + action, + clientAction: 'updated', + id, + data: op.o, + oplog: true, + }); + return; + } + + const diff = {}; + if (op.o.$set) { + for (const key in op.o.$set) { + if (op.o.$set.hasOwnProperty(key)) { + diff[key] = op.o.$set[key]; + } + } + } + const unset = {}; + if (op.o.$unset) { + for (const key in op.o.$unset) { + if (op.o.$unset.hasOwnProperty(key)) { + diff[key] = undefined; + unset[key] = 1; + } + } + } + + this.emit('change', { + action, + clientAction: 'updated', + id, + diff, + unset, + oplog: true, + }); + return; + } + + if (action === 'remove') { + this.emit('change', { + action, + clientAction: 'removed', + id, + oplog: true, + }); + } + } +} + + +export class BaseDb extends BaseDbWatch { + constructor(model, baseModel, options = {}) { + const collectionName = Match.test(model, String) ? baseName + model : model._name; + + super(collectionName); + + this.collectionName = collectionName; + + if (Match.test(model, String)) { + this.name = model; + this.collectionName = this.baseName + this.name; + this.model = new Mongo.Collection(this.collectionName); + } else { + this.name = model._name; + this.collectionName = this.name; + this.model = model; + } + + this.baseModel = baseModel; + + this.preventSetUpdatedAt = !!options.preventSetUpdatedAt; + + this.wrapModel(); + + this.tryEnsureIndex({ _updatedAt: 1 }, options._updatedAtIndexOptions); + } + get baseName() { return baseName; } @@ -204,75 +285,6 @@ export class BaseDb extends EventEmitter { ); } - processOplogRecord({ id, op }) { - const action = actions[op.op]; - metrics.oplog.inc({ - collection: this.collectionName, - op: action, - }); - - if (action === 'insert') { - this.emit('change', { - action, - clientAction: 'inserted', - id: op.o._id, - data: op.o, - oplog: true, - }); - return; - } - - if (action === 'update') { - if (!op.o.$set && !op.o.$unset) { - this.emit('change', { - action, - clientAction: 'updated', - id, - data: op.o, - oplog: true, - }); - return; - } - - const diff = {}; - if (op.o.$set) { - for (const key in op.o.$set) { - if (op.o.$set.hasOwnProperty(key)) { - diff[key] = op.o.$set[key]; - } - } - } - const unset = {}; - if (op.o.$unset) { - for (const key in op.o.$unset) { - if (op.o.$unset.hasOwnProperty(key)) { - diff[key] = undefined; - unset[key] = 1; - } - } - } - - this.emit('change', { - action, - clientAction: 'updated', - id, - diff, - unset, - oplog: true, - }); - return; - } - - if (action === 'remove') { - this.emit('change', { - action, - clientAction: 'removed', - id, - oplog: true, - }); - } - } - insert(record, ...args) { this.setUpdatedAt(record); diff --git a/app/models/server/raw/Analytics.js b/app/models/server/raw/Analytics.js deleted file mode 100644 index 81e0ad335c26..000000000000 --- a/app/models/server/raw/Analytics.js +++ /dev/null @@ -1,151 +0,0 @@ -import { Random } from 'meteor/random'; - -import { BaseRaw } from './BaseRaw'; -import Analytics from '../models/Analytics'; -import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; - -export class AnalyticsRaw extends BaseRaw { - saveMessageSent({ room, date }) { - return this.update({ date, 'room._id': room._id, type: 'messages' }, { - $set: { - room: { _id: room._id, name: room.fname || room.name, t: room.t, usernames: room.usernames || [] }, - }, - $setOnInsert: { - _id: Random.id(), - date, - type: 'messages', - }, - $inc: { messages: 1 }, - }, { upsert: true }); - } - - saveUserData({ date }) { - return this.update({ date, type: 'users' }, { - $setOnInsert: { - _id: Random.id(), - date, - type: 'users', - }, - $inc: { users: 1 }, - }, { upsert: true }); - } - - saveMessageDeleted({ room, date }) { - return this.update({ date, 'room._id': room._id }, { - $inc: { messages: -1 }, - }); - } - - getMessagesSentTotalByDate({ start, end, options = {} }) { - const params = [ - { - $match: { - type: 'messages', - date: { $gte: start, $lte: end }, - }, - }, - { - $group: { - _id: '$date', - messages: { $sum: '$messages' }, - }, - }, - ]; - if (options.sort) { - params.push({ $sort: options.sort }); - } - if (options.count) { - params.push({ $limit: options.count }); - } - return this.col.aggregate(params).toArray(); - } - - getMessagesOrigin({ start, end }) { - const params = [ - { - $match: { - type: 'messages', - date: { $gte: start, $lte: end }, - }, - }, - { - $group: { - _id: { t: '$room.t' }, - messages: { $sum: '$messages' }, - }, - }, - { - $project: { - _id: 0, - t: '$_id.t', - messages: 1, - }, - }, - ]; - return this.col.aggregate(params).toArray(); - } - - getMostPopularChannelsByMessagesSentQuantity({ start, end, options = {} }) { - const params = [ - { - $match: { - type: 'messages', - date: { $gte: start, $lte: end }, - }, - }, - { - $group: { - _id: { t: '$room.t', name: '$room.name', usernames: '$room.usernames' }, - messages: { $sum: '$messages' }, - }, - }, - { - $project: { - _id: 0, - t: '$_id.t', - name: '$_id.name', - usernames: '$_id.usernames', - messages: 1, - }, - }, - ]; - if (options.sort) { - params.push({ $sort: options.sort }); - } - if (options.count) { - params.push({ $limit: options.count }); - } - return this.col.aggregate(params).toArray(); - } - - getTotalOfRegisteredUsersByDate({ start, end, options = {} }) { - const params = [ - { - $match: { - type: 'users', - date: { $gte: start, $lte: end }, - }, - }, - { - $group: { - _id: '$date', - users: { $sum: '$users' }, - }, - }, - ]; - if (options.sort) { - params.push({ $sort: options.sort }); - } - if (options.count) { - params.push({ $limit: options.count }); - } - return this.col.aggregate(params).toArray(); - } - - findByTypeBeforeDate({ type, date }) { - return this.find({ type, date: { $lte: date } }); - } -} - -const db = Analytics.model.rawDatabase(); -export default new AnalyticsRaw(db.collection(Analytics.model._name, { readPreference: readSecondaryPreferred(db) })); diff --git a/app/models/server/raw/Analytics.ts b/app/models/server/raw/Analytics.ts new file mode 100644 index 000000000000..943274853746 --- /dev/null +++ b/app/models/server/raw/Analytics.ts @@ -0,0 +1,146 @@ +import { Random } from 'meteor/random'; +import { AggregationCursor, Cursor, SortOptionObject, UpdateWriteOpResult } from 'mongodb'; + +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { IAnalytic } from '../../../../definition/IAnalytic'; +import { IRoom } from '../../../../definition/IRoom'; + +type T = IAnalytic; + +export class AnalyticsRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { key: { date: 1 } }, + { key: { 'room._id': 1, date: 1 }, unique: true }, + ]; + + saveMessageSent({ room, date }: { room: IRoom; date: IAnalytic['date'] }): Promise { + return this.updateMany({ date, 'room._id': room._id, type: 'messages' }, { + $set: { + room: { + _id: room._id, + name: room.fname || room.name, + t: room.t, + usernames: room.usernames || [], + }, + }, + $setOnInsert: { + _id: Random.id(), + date, + type: 'messages', + }, + $inc: { messages: 1 }, + }, { upsert: true }); + } + + saveUserData({ date }: { date: IAnalytic['date'] }): Promise { + return this.updateMany({ date, type: 'users' }, { + $setOnInsert: { + _id: Random.id(), + date, + type: 'users', + }, + $inc: { users: 1 }, + }, { upsert: true }); + } + + saveMessageDeleted({ room, date }: { room: { _id: string }; date: IAnalytic['date'] }): Promise { + return this.updateMany({ date, 'room._id': room._id }, { + $inc: { messages: -1 }, + }); + } + + getMessagesSentTotalByDate({ start, end, options = {} }: { start: IAnalytic['date']; end: IAnalytic['date']; options?: { sort?: SortOptionObject; count?: number } }): AggregationCursor { + return this.col.aggregate([ + { + $match: { + type: 'messages', + date: { $gte: start, $lte: end }, + }, + }, + { + $group: { + _id: '$date', + messages: { $sum: '$messages' }, + }, + }, + ...options.sort ? [{ $sort: options.sort }] : [], + ...options.count ? [{ $limit: options.count }] : [], + ]); + } + + getMessagesOrigin({ start, end }: { start: IAnalytic['date']; end: IAnalytic['date'] }): AggregationCursor { + const params = [ + { + $match: { + type: 'messages', + date: { $gte: start, $lte: end }, + }, + }, + { + $group: { + _id: { t: '$room.t' }, + messages: { $sum: '$messages' }, + }, + }, + { + $project: { + _id: 0, + t: '$_id.t', + messages: 1, + }, + }, + ]; + return this.col.aggregate(params); + } + + getMostPopularChannelsByMessagesSentQuantity({ start, end, options = {} }: { start: IAnalytic['date']; end: IAnalytic['date']; options?: { sort?: SortOptionObject; count?: number } }): AggregationCursor { + return this.col.aggregate([ + { + $match: { + type: 'messages', + date: { $gte: start, $lte: end }, + }, + }, + { + $group: { + _id: { t: '$room.t', name: '$room.name', usernames: '$room.usernames' }, + messages: { $sum: '$messages' }, + }, + }, + { + $project: { + _id: 0, + t: '$_id.t', + name: '$_id.name', + usernames: '$_id.usernames', + messages: 1, + }, + }, + ...options.sort ? [{ $sort: options.sort }] : [], + ...options.count ? [{ $limit: options.count }] : [], + ]); + } + + getTotalOfRegisteredUsersByDate({ start, end, options = {} }: { start: IAnalytic['date']; end: IAnalytic['date']; options?: { sort?: SortOptionObject; count?: number } }): AggregationCursor { + return this.col.aggregate([ + { + $match: { + type: 'users', + date: { $gte: start, $lte: end }, + }, + }, + { + $group: { + _id: '$date', + users: { $sum: '$users' }, + }, + }, + ...options.sort ? [{ $sort: options.sort }] : [], + ...options.count ? [{ $limit: options.count }] : [], + ]); + } + + findByTypeBeforeDate({ type, date }: { type: T['type']; date: T['date'] }): Cursor { + return this.find({ type, date: { $lte: date } }); + } +} diff --git a/app/models/server/raw/Avatars.ts b/app/models/server/raw/Avatars.ts new file mode 100644 index 000000000000..cc9c5939f3b3 --- /dev/null +++ b/app/models/server/raw/Avatars.ts @@ -0,0 +1,73 @@ +import { DeleteWriteOpResultObject, UpdateWriteOpResult } from 'mongodb'; + +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { IAvatar as T } from '../../../../definition/IAvatar'; + +export class AvatarsRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { key: { name: 1 }, sparse: true }, + { key: { rid: 1 }, sparse: true }, + ]; + + insertAvatarFileInit(name: string, userId: string, store: string, file: {name: string}, extra: object): Promise { + const fileData = { + name, + userId, + store, + complete: false, + uploading: true, + progress: 0, + extension: file.name.split('.').pop(), + uploadedAt: new Date(), + }; + + Object.assign(fileData, file, extra); + + return this.updateOne({ _id: name }, fileData, { upsert: true }); + } + + updateFileComplete(fileId: string, userId: string, file: object): Promise | undefined { + if (!fileId) { + return; + } + + const filter = { + _id: fileId, + userId, + }; + + const update = { + $set: { + complete: true, + uploading: false, + progress: 1, + }, + }; + + update.$set = Object.assign(file, update.$set); + + return this.updateOne(filter, update); + } + + async findOneByName(name: string): Promise { + return this.findOne({ name }); + } + + async findOneByRoomId(rid: string): Promise { + return this.findOne({ rid }); + } + + async updateFileNameById(fileId: string, name: string): Promise { + const filter = { _id: fileId }; + const update = { + $set: { + name, + }, + }; + return this.updateOne(filter, update); + } + + async deleteFile(fileId: string): Promise { + return this.deleteOne({ _id: fileId }); + } +} diff --git a/app/models/server/raw/BaseRaw.ts b/app/models/server/raw/BaseRaw.ts index 602d8a62542a..500fe55d6e1a 100644 --- a/app/models/server/raw/BaseRaw.ts +++ b/app/models/server/raw/BaseRaw.ts @@ -1,10 +1,14 @@ import { Collection, CollectionInsertOneOptions, + CommonOptions, Cursor, DeleteWriteOpResultObject, FilterQuery, + FindAndModifyWriteOpResultObject, + FindOneAndUpdateOption, FindOneOptions, + IndexSpecification, InsertOneWriteOpResult, InsertWriteOpResult, ObjectID, @@ -21,6 +25,10 @@ import { import { setUpdatedAt } from '../lib/setUpdatedAt'; +export { + IndexSpecification, +} from 'mongodb'; + // [extracted from @types/mongo] TypeScript Omit (Exclude to be specific) does not work for objects with an "any" indexed type, and breaks discriminated unions type EnhancedOmit = string | number extends keyof T ? T // T has indexed type e.g. { _id: string; [k: string]: any; } or it is "any" @@ -37,7 +45,7 @@ type ExtractIdType = TSchema extends { _id: infer U } // user has defin : U : ObjectId; -type ModelOptionalId = EnhancedOmit & { _id?: ExtractIdType }; +export type ModelOptionalId = EnhancedOmit & { _id?: ExtractIdType }; // InsertionModel forces both _id and _updatedAt to be optional, regardless of how they are declared in T export type InsertionModel = EnhancedOmit, '_updatedAt'> & { _updatedAt?: Date }; @@ -58,13 +66,43 @@ const warnFields = process.env.NODE_ENV !== 'production' export class BaseRaw = undefined> implements IBaseRaw { public readonly defaultFields: C; + protected indexes?: IndexSpecification[]; + protected name: string; + private preventSetUpdatedAt: boolean; + constructor( public readonly col: Collection, public readonly trash?: Collection, + options?: { preventSetUpdatedAt?: boolean }, ) { this.name = this.col.collectionName.replace(baseName, ''); + + if (this.indexes?.length) { + this.col.createIndexes(this.indexes); + } + + this.preventSetUpdatedAt = options?.preventSetUpdatedAt ?? false; + } + + private doNotMixInclusionAndExclusionFields(options: FindOneOptions = {}): FindOneOptions { + const optionsDef = this.ensureDefaultFields(options); + if (optionsDef?.projection === undefined) { + return optionsDef; + } + + const projection: Record = optionsDef?.projection; + const keys = Object.keys(projection); + const removeKeys = keys.filter((key) => projection[key] === 0); + if (keys.length > removeKeys.length) { + removeKeys.forEach((key) => delete projection[key]); + } + + return { + ...optionsDef, + projection, + }; } private ensureDefaultFields(options?: undefined): C extends void ? undefined : WithoutProjection>; @@ -78,26 +116,32 @@ export class BaseRaw = undefined> implements IBase return options; } - const { fields, ...rest } = options || {}; + const { fields: deprecatedFields, projection, ...rest } = options || {}; - if (fields) { + if (deprecatedFields) { warnFields('Using \'fields\' in models is deprecated.', options); } + const fields = { ...deprecatedFields, ...projection }; + return { projection: this.defaultFields, - ...fields && { projection: fields }, + ...fields && Object.values(fields).length && { projection: fields }, ...rest, }; } + public findOneAndUpdate(query: FilterQuery, update: UpdateQuery | T, options?: FindOneAndUpdateOption): Promise> { + return this.col.findOneAndUpdate(query, update, options); + } + async findOneById(_id: string, options?: WithoutProjection> | undefined): Promise; async findOneById

(_id: string, options: FindOneOptions

): Promise

; async findOneById

(_id: string, options?: any): Promise { const query = { _id } as FilterQuery; - const optionsDef = this.ensureDefaultFields(options); + const optionsDef = this.doNotMixInclusionAndExclusionFields(options); return this.col.findOne(query, optionsDef); } @@ -110,13 +154,13 @@ export class BaseRaw = undefined> implements IBase async findOne

(query: FilterQuery | string = {}, options?: any): Promise { const q = typeof query === 'string' ? { _id: query } as FilterQuery : query; - const optionsDef = this.ensureDefaultFields(options); + const optionsDef = this.doNotMixInclusionAndExclusionFields(options); return this.col.findOne(q, optionsDef); } - findUsersInRoles(): void { - throw new Error('[overwrite-function] You must overwrite this function in the extended classes'); - } + // findUsersInRoles(): void { + // throw new Error('[overwrite-function] You must overwrite this function in the extended classes'); + // } find(query?: FilterQuery): Cursor>; @@ -125,22 +169,22 @@ export class BaseRaw = undefined> implements IBase find

(query: FilterQuery, options: FindOneOptions

): Cursor

; find

(query: FilterQuery | undefined = {}, options?: any): Cursor

| Cursor { - const optionsDef = this.ensureDefaultFields(options); + const optionsDef = this.doNotMixInclusionAndExclusionFields(options); return this.col.find(query, optionsDef); } update(filter: FilterQuery, update: UpdateQuery | Partial, options?: UpdateOneOptions & { multi?: boolean }): Promise { - setUpdatedAt(update); + this.setUpdatedAt(update); return this.col.update(filter, update, options); } updateOne(filter: FilterQuery, update: UpdateQuery | Partial, options?: UpdateOneOptions & { multi?: boolean }): Promise { - setUpdatedAt(update); + this.setUpdatedAt(update); return this.col.updateOne(filter, update, options); } updateMany(filter: FilterQuery, update: UpdateQuery | Partial, options?: UpdateManyOptions): Promise { - setUpdatedAt(update); + this.setUpdatedAt(update); return this.col.updateMany(filter, update, options); } @@ -150,7 +194,7 @@ export class BaseRaw = undefined> implements IBase const oid = new ObjectID(); return { _id: oid.toHexString(), ...doc }; } - setUpdatedAt(doc); + this.setUpdatedAt(doc); return doc; }); @@ -164,7 +208,7 @@ export class BaseRaw = undefined> implements IBase doc = { _id: oid.toHexString(), ...doc }; } - setUpdatedAt(doc); + this.setUpdatedAt(doc); // TODO reavaluate following type casting return this.col.insertOne(doc as unknown as OptionalId, options); @@ -175,6 +219,15 @@ export class BaseRaw = undefined> implements IBase return this.col.deleteOne(query); } + deleteOne(filter: FilterQuery, options?: CommonOptions & { bypassDocumentValidation?: boolean }): Promise { + return this.col.deleteOne(filter, options); + } + + deleteMany(filter: FilterQuery, options?: CommonOptions): Promise { + return this.col.deleteMany(filter, options); + } + + // Trash trashFind

(query: FilterQuery, options: FindOneOptions

): Cursor

| undefined { if (!this.trash) { @@ -214,4 +267,29 @@ export class BaseRaw = undefined> implements IBase } return trash.findOne(query, options); } + + private setUpdatedAt(record: UpdateQuery | InsertionModel): void { + if (this.preventSetUpdatedAt) { + return; + } + setUpdatedAt(record); + } + + trashFindDeletedAfter

(deletedAt: Date, query: FilterQuery = {}, options?: any): Cursor { + const q = { + __collection__: this.name, + _deletedAt: { + $gt: deletedAt, + }, + ...query, + } as FilterQuery; + + const { trash } = this; + + if (!trash) { + throw new Error('Trash is not enabled for this collection'); + } + + return trash.find(q, options); + } } diff --git a/app/models/server/raw/CredentialTokens.ts b/app/models/server/raw/CredentialTokens.ts new file mode 100644 index 000000000000..eb6db2786682 --- /dev/null +++ b/app/models/server/raw/CredentialTokens.ts @@ -0,0 +1,29 @@ +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { ICredentialToken as T } from '../../../../definition/ICredentialToken'; + +export class CredentialTokensRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { key: { expireAt: 1 }, sparse: true, expireAfterSeconds: 0 }, + ] + + async create(_id: string, userInfo: T['userInfo']): Promise { + const validForMilliseconds = 60000; // Valid for 60 seconds + const token = { + _id, + userInfo, + expireAt: new Date(Date.now() + validForMilliseconds), + }; + + await this.insertOne(token); + return token; + } + + findOneNotExpiredById(_id: string): Promise { + const query = { + _id, + expireAt: { $gt: new Date() }, + }; + + return this.findOne(query); + } +} diff --git a/app/models/server/raw/CustomSounds.js b/app/models/server/raw/CustomSounds.js deleted file mode 100644 index 54e96f064512..000000000000 --- a/app/models/server/raw/CustomSounds.js +++ /dev/null @@ -1,5 +0,0 @@ -import { BaseRaw } from './BaseRaw'; - -export class CustomSoundsRaw extends BaseRaw { - -} diff --git a/app/models/server/raw/CustomSounds.ts b/app/models/server/raw/CustomSounds.ts new file mode 100644 index 000000000000..c46b7f4b4141 --- /dev/null +++ b/app/models/server/raw/CustomSounds.ts @@ -0,0 +1,44 @@ +import { Cursor, FindOneOptions, InsertOneWriteOpResult, UpdateWriteOpResult, WithId, WithoutProjection } from 'mongodb'; + +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { ICustomSound as T } from '../../../../definition/ICustomSound'; + +export class CustomSoundsRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { key: { name: 1 } }, + ] + + // find + findByName(name: string, options: WithoutProjection>): Cursor { + const query = { + name, + }; + + return this.find(query, options); + } + + findByNameExceptId(name: string, except: string, options: WithoutProjection>): Cursor { + const query = { + _id: { $nin: [except] }, + name, + }; + + return this.find(query, options); + } + + // update + setName(_id: string, name: string): Promise { + const update = { + $set: { + name, + }, + }; + + return this.updateOne({ _id }, update); + } + + // INSERT + create(data: T): Promise>> { + return this.insertOne(data); + } +} diff --git a/app/models/server/raw/CustomUserStatus.js b/app/models/server/raw/CustomUserStatus.js deleted file mode 100644 index 0ffc78d4b396..000000000000 --- a/app/models/server/raw/CustomUserStatus.js +++ /dev/null @@ -1,5 +0,0 @@ -import { BaseRaw } from './BaseRaw'; - -export class CustomUserStatusRaw extends BaseRaw { - -} diff --git a/app/models/server/raw/CustomUserStatus.ts b/app/models/server/raw/CustomUserStatus.ts new file mode 100644 index 000000000000..ad1d3df1ea10 --- /dev/null +++ b/app/models/server/raw/CustomUserStatus.ts @@ -0,0 +1,59 @@ +import { Cursor, FindOneOptions, InsertOneWriteOpResult, UpdateWriteOpResult, WithId, WithoutProjection } from 'mongodb'; + +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { ICustomUserStatus as T } from '../../../../definition/ICustomUserStatus'; + +export class CustomUserStatusRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { key: { name: 1 } }, + ] + + // find one by name + async findOneByName(name: string, options: WithoutProjection>): Promise { + return this.findOne({ name }, options); + } + + // find + findByName(name: string, options: WithoutProjection>): Cursor { + const query = { + name, + }; + + return this.find(query, options); + } + + findByNameExceptId(name: string, except: string, options: WithoutProjection>): Cursor { + const query = { + _id: { $nin: [except] }, + name, + }; + + return this.find(query, options); + } + + // update + setName(_id: string, name: string): Promise { + const update = { + $set: { + name, + }, + }; + + return this.updateOne({ _id }, update); + } + + setStatusType(_id: string, statusType: string): Promise { + const update = { + $set: { + statusType, + }, + }; + + return this.updateOne({ _id }, update); + } + + // INSERT + create(data: T): Promise>> { + return this.insertOne(data); + } +} diff --git a/app/models/server/raw/EmailInbox.ts b/app/models/server/raw/EmailInbox.ts index 1d8d008242fa..53b88792392f 100644 --- a/app/models/server/raw/EmailInbox.ts +++ b/app/models/server/raw/EmailInbox.ts @@ -1,6 +1,8 @@ -import { BaseRaw } from './BaseRaw'; +import { BaseRaw, IndexSpecification } from './BaseRaw'; import { IEmailInbox } from '../../../../definition/IEmailInbox'; export class EmailInboxRaw extends BaseRaw { - // + protected indexes: IndexSpecification[] = [ + { key: { email: 1 }, unique: true }, + ] } diff --git a/app/models/server/raw/EmailMessageHistory.ts b/app/models/server/raw/EmailMessageHistory.ts index 9201d1b3a344..89c54e079ec0 100644 --- a/app/models/server/raw/EmailMessageHistory.ts +++ b/app/models/server/raw/EmailMessageHistory.ts @@ -1,10 +1,15 @@ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ +import { IndexSpecification, InsertOneWriteOpResult, WithId } from 'mongodb'; + import { BaseRaw } from './BaseRaw'; -import { IEmailMessageHistory } from '../../../../definition/IEmailMessageHistory'; +import { IEmailMessageHistory as T } from '../../../../definition/IEmailMessageHistory'; + +export class EmailMessageHistoryRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { key: { createdAt: 1 }, expireAfterSeconds: 60 * 60 * 24 }, + ] -export class EmailMessageHistoryRaw extends BaseRaw { - insertOne({ _id, email }: IEmailMessageHistory) { - return this.col.insertOne({ + async create({ _id, email }: T): Promise>> { + return this.insertOne({ _id, email, createdAt: new Date(), diff --git a/app/models/server/raw/EmojiCustom.js b/app/models/server/raw/EmojiCustom.js deleted file mode 100644 index 80b81d41958b..000000000000 --- a/app/models/server/raw/EmojiCustom.js +++ /dev/null @@ -1,5 +0,0 @@ -import { BaseRaw } from './BaseRaw'; - -export class EmojiCustomRaw extends BaseRaw { - -} diff --git a/app/models/server/raw/EmojiCustom.ts b/app/models/server/raw/EmojiCustom.ts new file mode 100644 index 000000000000..82f5f22fc97e --- /dev/null +++ b/app/models/server/raw/EmojiCustom.ts @@ -0,0 +1,79 @@ +import { Cursor, FindOneOptions, InsertOneWriteOpResult, UpdateWriteOpResult, WithId, WithoutProjection } from 'mongodb'; + +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { IEmojiCustom as T } from '../../../../definition/IEmojiCustom'; + +export class EmojiCustomRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { key: { name: 1 } }, + { key: { aliases: 1 } }, + { key: { extension: 1 } }, + ] + + // find + findByNameOrAlias(emojiName: string, options: WithoutProjection>): Cursor { + let name = emojiName; + + if (typeof emojiName === 'string') { + name = emojiName.replace(/:/g, ''); + } + + const query = { + $or: [ + { name }, + { aliases: name }, + ], + }; + + return this.find(query, options); + } + + findByNameOrAliasExceptID(name: string, except: string, options: WithoutProjection>): Cursor { + const query = { + _id: { $nin: [except] }, + $or: [ + { name }, + { aliases: name }, + ], + }; + + return this.find(query, options); + } + + + // update + setName(_id: string, name: string): Promise { + const update = { + $set: { + name, + }, + }; + + return this.updateOne({ _id }, update); + } + + setAliases(_id: string, aliases: string): Promise { + const update = { + $set: { + aliases, + }, + }; + + return this.updateOne({ _id }, update); + } + + setExtension(_id: string, extension: string): Promise { + const update = { + $set: { + extension, + }, + }; + + return this.updateOne({ _id }, update); + } + + // INSERT + create(data: T): Promise>> { + return this.insertOne(data); + } +} diff --git a/app/models/server/raw/ExportOperations.ts b/app/models/server/raw/ExportOperations.ts new file mode 100644 index 000000000000..d470722d2800 --- /dev/null +++ b/app/models/server/raw/ExportOperations.ts @@ -0,0 +1,68 @@ +import { Cursor, UpdateWriteOpResult } from 'mongodb'; + +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { IExportOperation } from '../../../../definition/IExportOperation'; + +type T = IExportOperation; + +export class ExportOperationsRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { key: { userId: 1 } }, + { key: { status: 1 } }, + ] + + findOnePending(): Promise { + const query = { + status: { $nin: ['completed', 'skipped'] }, + }; + + return this.findOne(query); + } + + async create(data: T): Promise { + const result = await this.insertOne({ + ...data, + createdAt: new Date(), + }); + + return result.insertedId; + } + + findLastOperationByUser(userId: string, fullExport = false): Promise { + const query = { + userId, + fullExport, + }; + + return this.findOne(query, { sort: { createdAt: -1 } }); + } + + findAllPendingBeforeMyRequest(requestDay: Date): Cursor { + const query = { + status: { $nin: ['completed', 'skipped'] }, + createdAt: { $lt: requestDay }, + }; + + return this.find(query); + } + + updateOperation(data: T): Promise { + const update = { + $set: { + roomList: data.roomList, + status: data.status, + fileList: data.fileList, + generatedFile: data.generatedFile, + fileId: data.fileId, + userNameTable: data.userNameTable, + userData: data.userData, + generatedUserFile: data.generatedUserFile, + generatedAvatar: data.generatedAvatar, + exportPath: data.exportPath, + assetsPath: data.assetsPath, + }, + }; + + return this.updateOne({ _id: data._id }, update); + } +} diff --git a/app/models/server/raw/FederationKeys.ts b/app/models/server/raw/FederationKeys.ts new file mode 100644 index 000000000000..7ac06051e4fa --- /dev/null +++ b/app/models/server/raw/FederationKeys.ts @@ -0,0 +1,65 @@ +import NodeRSA from 'node-rsa'; + +import { BaseRaw } from './BaseRaw'; + +type T = { + type: 'private' | 'public'; + key: string; +}; + +export class FederationKeysRaw extends BaseRaw { + async getKey(type: T['type']): Promise { + const keyResource = await this.findOne({ type }); + + if (!keyResource) { return null; } + + return keyResource.key; + } + + loadKey(keyData: NodeRSA.Key, type: T['type']): NodeRSA { + return new NodeRSA(keyData, `pkcs8-${ type }-pem`); + } + + async generateKeys(): Promise<{ privateKey: '' | NodeRSA | null; publicKey: '' | NodeRSA | null }> { + const key = new NodeRSA({ b: 512 }); + + key.generateKeyPair(); + + await this.deleteMany({}); + + await this.insertOne({ + type: 'private', + key: key.exportKey('pkcs8-private-pem').replace(/\n|\r/g, ''), + }); + + await this.insertOne({ + type: 'public', + key: key.exportKey('pkcs8-public-pem').replace(/\n|\r/g, ''), + }); + + return { + privateKey: await this.getPrivateKey(), + publicKey: await this.getPublicKey(), + }; + } + + async getPrivateKey(): Promise<'' | NodeRSA | null> { + const keyData = await this.getKey('private'); + + return keyData && this.loadKey(keyData, 'private'); + } + + getPrivateKeyString(): Promise { + return this.getKey('private'); + } + + async getPublicKey(): Promise<'' | NodeRSA | null> { + const keyData = await this.getKey('public'); + + return keyData && this.loadKey(keyData, 'public'); + } + + getPublicKeyString(): Promise { + return this.getKey('public'); + } +} diff --git a/app/models/server/raw/FederationServers.ts b/app/models/server/raw/FederationServers.ts new file mode 100644 index 000000000000..c559b6413770 --- /dev/null +++ b/app/models/server/raw/FederationServers.ts @@ -0,0 +1,29 @@ +import { UpdateWriteOpResult } from 'mongodb'; + +import { Users } from './index'; +import { IFederationServer } from '../../../../definition/Federation'; +import { BaseRaw, IndexSpecification } from './BaseRaw'; + +export class FederationServersRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { key: { domain: 1 } }, + ] + + saveDomain(domain: string): Promise { + return this.updateOne({ domain }, { + $setOnInsert: { + domain, + }, + }, { upsert: true }); + } + + async refreshServers(): Promise { + const domains = await Users.getDistinctFederationDomains(); + + for await (const domain of domains) { + await this.saveDomain(domain); + } + + await this.deleteMany({ domain: { $nin: domains } }); + } +} diff --git a/app/models/server/raw/IntegrationHistory.ts b/app/models/server/raw/IntegrationHistory.ts index 53f7167db796..923c11c6257e 100644 --- a/app/models/server/raw/IntegrationHistory.ts +++ b/app/models/server/raw/IntegrationHistory.ts @@ -1,4 +1,12 @@ import { BaseRaw } from './BaseRaw'; import { IIntegrationHistory } from '../../../../definition/IIntegrationHistory'; -export class IntegrationHistoryRaw extends BaseRaw {} +export class IntegrationHistoryRaw extends BaseRaw { + removeByIntegrationId(integrationId: string): ReturnType['deleteMany']> { + return this.deleteMany({ 'integration._id': integrationId }); + } + + findOneByIntegrationIdAndHistoryId(integrationId: string, historyId: string): Promise { + return this.findOne({ 'integration._id': integrationId, _id: historyId }); + } +} diff --git a/app/models/server/raw/Integrations.js b/app/models/server/raw/Integrations.js deleted file mode 100644 index ab8e01a5ebae..000000000000 --- a/app/models/server/raw/Integrations.js +++ /dev/null @@ -1,14 +0,0 @@ -import { BaseRaw } from './BaseRaw'; - -export class IntegrationsRaw extends BaseRaw { - findOneByIdAndCreatedByIfExists({ _id, createdBy }) { - const query = { - _id, - }; - if (createdBy) { - query['_createdBy._id'] = createdBy; - } - - return this.findOne(query); - } -} diff --git a/app/models/server/raw/Integrations.ts b/app/models/server/raw/Integrations.ts new file mode 100644 index 000000000000..521507d3bfec --- /dev/null +++ b/app/models/server/raw/Integrations.ts @@ -0,0 +1,36 @@ +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { IIntegration } from '../../../../definition/IIntegration'; + +export class IntegrationsRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { key: { type: 1 } }, + ] + + findOneByUrl(url: string): Promise { + return this.findOne({ url }); + } + + updateRoomName(oldRoomName: string, newRoomName: string): ReturnType['updateMany']> { + const hashedOldRoomName = `#${ oldRoomName }`; + const hashedNewRoomName = `#${ newRoomName }`; + + return this.updateMany({ + channel: hashedOldRoomName, + }, { + $set: { + 'channel.$': hashedNewRoomName, + }, + }); + } + + findOneByIdAndCreatedByIfExists({ _id, createdBy }: { _id: IIntegration['_id']; createdBy: IIntegration['_createdBy'] }): Promise { + return this.findOne({ + _id, + ...createdBy && { '_createdBy._id': createdBy }, + }); + } + + disableByUserId(userId: string): ReturnType['updateMany']> { + return this.updateMany({ userId }, { $set: { enabled: false } }); + } +} diff --git a/app/models/server/raw/Invites.ts b/app/models/server/raw/Invites.ts new file mode 100644 index 000000000000..84d21e4e3e81 --- /dev/null +++ b/app/models/server/raw/Invites.ts @@ -0,0 +1,27 @@ +import type { UpdateWriteOpResult } from 'mongodb'; + +import { BaseRaw } from './BaseRaw'; +import { IInvite } from '../../../../definition/IInvite'; + +type T = IInvite; + +export class InvitesRaw extends BaseRaw { + findOneByUserRoomMaxUsesAndExpiration(userId: string, rid: string, maxUses: number, daysToExpire: number): Promise { + return this.findOne({ + rid, + userId, + days: daysToExpire, + maxUses, + ...daysToExpire > 0 ? { expires: { $gt: new Date() } } : {}, + ...maxUses > 0 ? { uses: { $lt: maxUses } } : {}, + }); + } + + increaseUsageById(_id: string, uses = 1): Promise { + return this.updateOne({ _id }, { + $inc: { + uses, + }, + }); + } +} diff --git a/app/models/server/raw/NotificationQueue.ts b/app/models/server/raw/NotificationQueue.ts index 9aedb9680902..cf80da0b747d 100644 --- a/app/models/server/raw/NotificationQueue.ts +++ b/app/models/server/raw/NotificationQueue.ts @@ -1,25 +1,27 @@ -/* eslint-disable @typescript-eslint/explicit-function-return-type */ -import { - Collection, - ObjectId, -} from 'mongodb'; +import { UpdateWriteOpResult } from 'mongodb'; -import { BaseRaw } from './BaseRaw'; +import { BaseRaw, IndexSpecification } from './BaseRaw'; import { INotification } from '../../../../definition/INotification'; export class NotificationQueueRaw extends BaseRaw { - public readonly col!: Collection; + protected indexes: IndexSpecification[] = [ + { key: { uid: 1 } }, + { key: { ts: 1 }, expireAfterSeconds: 2 * 60 * 60 }, + { key: { schedule: 1 }, sparse: true }, + { key: { sending: 1 }, sparse: true }, + { key: { error: 1 }, sparse: true }, + ]; - unsetSendingById(_id: string) { - return this.col.updateOne({ _id }, { + unsetSendingById(_id: string): Promise { + return this.updateOne({ _id }, { $unset: { sending: 1, }, }); } - setErrorById(_id: string, error: any) { - return this.col.updateOne({ + setErrorById(_id: string, error: any): Promise { + return this.updateOne({ _id, }, { $set: { @@ -31,12 +33,8 @@ export class NotificationQueueRaw extends BaseRaw { }); } - removeById(_id: string) { - return this.col.deleteOne({ _id }); - } - - clearScheduleByUserId(uid: string) { - return this.col.updateMany({ + clearScheduleByUserId(uid: string): Promise { + return this.updateMany({ uid, schedule: { $exists: true }, }, { @@ -47,7 +45,7 @@ export class NotificationQueueRaw extends BaseRaw { } async clearQueueByUserId(uid: string): Promise { - const op = await this.col.deleteMany({ + const op = await this.deleteMany({ uid, }); @@ -83,11 +81,4 @@ export class NotificationQueueRaw extends BaseRaw { return result.value; } - - insertOne(data: Omit) { - return this.col.insertOne({ - _id: new ObjectId().toHexString(), - ...data, - }); - } } diff --git a/app/models/server/raw/OAuthApps.js b/app/models/server/raw/OAuthApps.js deleted file mode 100644 index 68c77a772cdd..000000000000 --- a/app/models/server/raw/OAuthApps.js +++ /dev/null @@ -1,14 +0,0 @@ -import { BaseRaw } from './BaseRaw'; - -export class OAuthAppsRaw extends BaseRaw { - findOneAuthAppByIdOrClientId({ clientId, appId }) { - const query = {}; - if (clientId) { - query.clientId = clientId; - } - if (appId) { - query._id = appId; - } - return this.findOne(query); - } -} diff --git a/app/models/server/raw/OAuthApps.ts b/app/models/server/raw/OAuthApps.ts new file mode 100644 index 000000000000..f70d88616b34 --- /dev/null +++ b/app/models/server/raw/OAuthApps.ts @@ -0,0 +1,11 @@ +import { IOAuthApps as T } from '../../../../definition/IOAuthApps'; +import { BaseRaw } from './BaseRaw'; + +export class OAuthAppsRaw extends BaseRaw { + findOneAuthAppByIdOrClientId({ clientId, appId }: {clientId: string; appId: string}): ReturnType['findOne']> { + return this.findOne({ + ...appId && { _id: appId }, + ...clientId && { clientId }, + }); + } +} diff --git a/app/models/server/raw/OEmbedCache.ts b/app/models/server/raw/OEmbedCache.ts new file mode 100644 index 000000000000..586fb1d7040c --- /dev/null +++ b/app/models/server/raw/OEmbedCache.ts @@ -0,0 +1,31 @@ +import { DeleteWriteOpResultObject } from 'mongodb'; + +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { IOEmbedCache } from '../../../../definition/IOEmbedCache'; + +type T = IOEmbedCache; + +export class OEmbedCacheRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { key: { updatedAt: 1 } }, + ] + + async createWithIdAndData(_id: string, data: any): Promise { + const record = { + _id, + data, + updatedAt: new Date(), + }; + record._id = (await this.insertOne(record)).insertedId; + return record; + } + + removeAfterDate(date: Date): Promise { + const query = { + updatedAt: { + $lte: date, + }, + }; + return this.deleteMany(query); + } +} diff --git a/app/models/server/raw/Permissions.ts b/app/models/server/raw/Permissions.ts index d5321c82c80b..0c5ae1f8533e 100644 --- a/app/models/server/raw/Permissions.ts +++ b/app/models/server/raw/Permissions.ts @@ -2,4 +2,35 @@ import { BaseRaw } from './BaseRaw'; import { IPermission } from '../../../../definition/IPermission'; export class PermissionsRaw extends BaseRaw { + async createOrUpdate(name: string, roles: string[]): Promise { + const exists = await this.findOne>({ + _id: name, + roles, + }, { fields: { _id: 1 } }); + + if (exists) { + return exists._id; + } + + return this.update({ _id: name }, { $set: { roles } }, { upsert: true }).then((result) => result.result._id); + } + + async create(id: string, roles: string[]): Promise { + const exists = await this.findOneById>(id, { fields: { _id: 1 } }); + + if (exists) { + return exists._id; + } + + return this.update({ _id: id }, { $set: { roles } }, { upsert: true }).then((result) => result.result._id); + } + + + async addRole(permission: string, role: string): Promise { + await this.update({ _id: permission, roles: { $ne: role } }, { $addToSet: { roles: role } }); + } + + async removeRole(permission: string, role: string): Promise { + await this.update({ _id: permission, roles: role }, { $pull: { roles: role } }); + } } diff --git a/app/models/server/raw/ReadReceipts.ts b/app/models/server/raw/ReadReceipts.ts new file mode 100644 index 000000000000..12763332ca3e --- /dev/null +++ b/app/models/server/raw/ReadReceipts.ts @@ -0,0 +1,15 @@ +import { Cursor } from 'mongodb'; + +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { ReadReceipt } from '../../../../definition/ReadReceipt'; + +export class ReadReceiptsRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { key: { roomId: 1, userId: 1, messageId: 1 }, unique: true }, + { key: { messageId: 1 } }, + ]; + + findByMessageId(messageId: string): Cursor { + return this.find({ messageId }); + } +} diff --git a/app/models/server/raw/Reports.ts b/app/models/server/raw/Reports.ts new file mode 100644 index 000000000000..9b47fdb8fe9f --- /dev/null +++ b/app/models/server/raw/Reports.ts @@ -0,0 +1,15 @@ +import { BaseRaw } from './BaseRaw'; +import { IReport } from '../../../../definition/IReport'; +import { IMessage } from '../../../../definition/IMessage'; + +export class ReportsRaw extends BaseRaw { + createWithMessageDescriptionAndUserId(message: IMessage, description: string, userId: string): ReturnType['insertOne']> { + const record: Pick = { + message, + description, + ts: new Date(), + userId, + }; + return this.insertOne(record); + } +} diff --git a/app/models/server/raw/Roles.js b/app/models/server/raw/Roles.js deleted file mode 100644 index 7e06551fde56..000000000000 --- a/app/models/server/raw/Roles.js +++ /dev/null @@ -1,31 +0,0 @@ -import { BaseRaw } from './BaseRaw'; - -export class RolesRaw extends BaseRaw { - constructor(col, trash, models) { - super(col, trash); - - this.models = models; - } - - async isUserInRoles(userId, roles, scope) { - if (!Array.isArray(roles)) { - roles = [roles]; - } - - for (let i = 0, total = roles.length; i < total; i++) { - const roleName = roles[i]; - - // eslint-disable-next-line no-await-in-loop - const role = await this.findOne({ name: roleName }, { scope: 1 }); - const roleScope = (role && role.scope) || 'Users'; - const model = this.models[roleScope]; - - // eslint-disable-next-line no-await-in-loop - const permitted = await (model && model.isUserInRole && model.isUserInRole(userId, roleName, scope)); - if (permitted) { - return true; - } - } - return false; - } -} diff --git a/app/models/server/raw/Roles.ts b/app/models/server/raw/Roles.ts new file mode 100644 index 000000000000..db7913eb7e2a --- /dev/null +++ b/app/models/server/raw/Roles.ts @@ -0,0 +1,200 @@ +import type { Collection, Cursor, FilterQuery, FindOneOptions, InsertOneWriteOpResult, UpdateWriteOpResult, WithId, WithoutProjection } from 'mongodb'; + +import { IRole, IUser } from '../../../../definition/IUser'; +import { BaseRaw } from './BaseRaw'; +import { SubscriptionsRaw } from './Subscriptions'; +import { UsersRaw } from './Users'; + +type ScopedModelRoles = { + Subscriptions: SubscriptionsRaw; + Users: UsersRaw; +} + +export class RolesRaw extends BaseRaw { + constructor(public readonly col: Collection, + private readonly models: ScopedModelRoles, public readonly trash?: Collection) { + super(col, trash); + } + + + findByUpdatedDate

(updatedAfterDate: Date, options: FindOneOptions

): Cursor

| Cursor { + const query = { + _updatedAt: { $gte: new Date(updatedAfterDate) }, + }; + + return this.find(query, options); + } + + + createOrUpdate(name: IRole['name'], scope: 'Users' | 'Subscriptions' = 'Users', description = '', protectedRole = true, mandatory2fa = false): Promise { + const queryData = { + name, + scope, + description, + protected: protectedRole, + mandatory2fa, + }; + + return this.updateOne({ _id: name }, { $set: queryData }, { upsert: true }); + } + + async addUserRoles(userId: IUser['_id'], roles: IRole['_id'][], scope?: string): Promise { + if (!Array.isArray(roles)) { + roles = [roles]; + process.env.NODE_ENV === 'development' && console.warn('[WARN] RolesRaw.addUserRoles: roles should be an array'); + } + + for await (const name of roles) { + const role = await this.findOne({ name }, { scope: 1 } as FindOneOptions); + + if (!role) { + process.env.NODE_ENV === 'development' && console.warn(`[WARN] RolesRaw.addUserRoles: role: ${ name } not found`); + continue; + } + switch (role.scope) { + case 'Subscriptions': + await this.models.Subscriptions.addRolesByUserId(userId, [name], scope); + break; + case 'Users': + default: + await this.models.Users.addRolesByUserId(userId, [name]); + } + } + return true; + } + + + async isUserInRoles(userId: IUser['_id'], roles: IRole['_id'][], scope?: string): Promise { + if (!Array.isArray(roles)) { // TODO: remove this check + roles = [roles]; + process.env.NODE_ENV === 'development' && console.warn('[WARN] RolesRaw.isUserInRoles: roles should be an array'); + } + + for await (const roleName of roles) { + const role = await this.findOne({ name: roleName }, { scope: 1 } as FindOneOptions); + + if (!role) { + continue; + } + + switch (role.scope) { + case 'Subscriptions': + if (await this.models.Subscriptions.isUserInRole(userId, roleName, scope)) { + return true; + } + break; + case 'Users': + default: + if (await this.models.Users.isUserInRole(userId, roleName)) { + return true; + } + } + } + return false; + } + + async removeUserRoles(userId: IUser['_id'], roles: IRole['_id'][], scope?: string): Promise { + if (!Array.isArray(roles)) { // TODO: remove this check + roles = [roles]; + process.env.NODE_ENV === 'development' && console.warn('[WARN] RolesRaw.removeUserRoles: roles should be an array'); + } + for await (const roleName of roles) { + const role = await this.findOne({ name: roleName }, { scope: 1 } as FindOneOptions); + + if (!role) { + continue; + } + + switch (role.scope) { + case 'Subscriptions': + scope && await this.models.Subscriptions.removeRolesByUserId(userId, [roleName], scope); + break; + case 'Users': + default: + await this.models.Users.removeRolesByUserId(userId, [roleName]); + } + } + return true; + } + + async findOneByIdOrName(_idOrName: IRole['_id'] | IRole['name'], options?: undefined): Promise; + + async findOneByIdOrName(_idOrName: IRole['_id'] | IRole['name'], options: WithoutProjection>): Promise; + + async findOneByIdOrName

(_idOrName: IRole['_id'] | IRole['name'], options: FindOneOptions

): Promise

; + + findOneByIdOrName

(_idOrName: IRole['_id'] | IRole['name'], options?: any): Promise { + const query: FilterQuery = { + $or: [{ + _id: _idOrName, + }, { + name: _idOrName, + }], + }; + + return this.findOne(query, options); + } + + updateById(_id: IRole['_id'], name: IRole['name'], scope: IRole['scope'], description: IRole['description'], mandatory2fa: IRole['mandatory2fa']): Promise { + const queryData = { + name, + scope, + description, + mandatory2fa, + }; + + return this.updateOne({ _id }, { $set: queryData }, { upsert: true }); + } + + + findUsersInRole(name: IRole['name'], scope?: string): Promise>; + + findUsersInRole(name: IRole['name'], scope: string | undefined, options: WithoutProjection>): Promise>; + + findUsersInRole

(name: IRole['name'], scope: string | undefined, options: FindOneOptions

): Promise>; + + async findUsersInRole

(name: IRole['name'], scope: string | undefined, options?: any | undefined): Promise | Cursor

> { + const role = await this.findOne({ name }, { scope: 1 } as FindOneOptions); + + if (!role) { + throw new Error('RolesRaw.findUsersInRole: role not found'); + } + + switch (role.scope) { + case 'Subscriptions': + return this.models.Subscriptions.findUsersInRoles([name], scope, options); + case 'Users': + default: + return this.models.Users.findUsersInRoles([name], options); + } + } + + + createWithRandomId(name: IRole['name'], scope: 'Users' | 'Subscriptions' = 'Users', description = '', protectedRole = true, mandatory2fa = false): Promise>> { + const role = { + name, + scope, + description, + protected: protectedRole, + mandatory2fa, + }; + + return this.insertOne(role); + } + + + async canAddUserToRole(uid: IUser['_id'], name: IRole['name'], scope?: string): Promise { + const role = await this.findOne({ name }, { fields: { scope: 1 } } as FindOneOptions); + if (!role) { + return false; + } + + switch (role.scope) { + case 'Subscriptions': + return this.models.Subscriptions.isUserInRoleScope(uid, scope); + case 'Users': + default: + return this.models.Users.isUserInRoleScope(uid); + } + } +} diff --git a/app/models/server/raw/Rooms.js b/app/models/server/raw/Rooms.js index 600d55c1fffa..0da61636ef86 100644 --- a/app/models/server/raw/Rooms.js +++ b/app/models/server/raw/Rooms.js @@ -27,10 +27,8 @@ export class RoomsRaw extends BaseRaw { { $match: { t: 'l', - closedAt: { $exists: true }, - metrics: { $exists: true }, - 'metrics.chatDuration': { $exists: true }, ...department && { departmentId: department }, + closedAt: { $exists: true }, }, }, { $sort: { closedAt: -1 } }, diff --git a/app/models/server/raw/ServerEvents.ts b/app/models/server/raw/ServerEvents.ts index f36b44983e19..1bb1342ed885 100644 --- a/app/models/server/raw/ServerEvents.ts +++ b/app/models/server/raw/ServerEvents.ts @@ -1,38 +1,28 @@ -import { Collection, ObjectId } from 'mongodb'; - -import { BaseRaw } from './BaseRaw'; +import { BaseRaw, IndexSpecification } from './BaseRaw'; import { IServerEvent, IServerEventType } from '../../../../definition/IServerEvent'; -import { IUser } from '../../../../definition/IUser'; export class ServerEventsRaw extends BaseRaw { - public readonly col!: Collection; - - async insertOne(data: Omit): Promise { - if (data.u) { - data.u = { _id: data.u._id, username: data.u.username } as IUser; - } - return this.col.insertOne({ - _id: new ObjectId().toHexString(), - ...data, - }); - } + protected indexes: IndexSpecification[] = [ + { key: { t: 1, ip: 1, ts: -1 } }, + { key: { t: 1, 'u.username': 1, ts: -1 } }, + ] async findLastFailedAttemptByIp(ip: string): Promise { - return this.col.findOne({ + return this.findOne({ ip, t: IServerEventType.FAILED_LOGIN_ATTEMPT, }, { sort: { ts: -1 } }); } async findLastFailedAttemptByUsername(username: string): Promise { - return this.col.findOne({ + return this.findOne({ 'u.username': username, t: IServerEventType.FAILED_LOGIN_ATTEMPT, }, { sort: { ts: -1 } }); } async countFailedAttemptsByUsernameSince(username: string, since: Date): Promise { - return this.col.find({ + return this.find({ 'u.username': username, t: IServerEventType.FAILED_LOGIN_ATTEMPT, ts: { @@ -42,7 +32,7 @@ export class ServerEventsRaw extends BaseRaw { } countFailedAttemptsByIpSince(ip: string, since: Date): Promise { - return this.col.find({ + return this.find({ ip, t: IServerEventType.FAILED_LOGIN_ATTEMPT, ts: { @@ -52,14 +42,14 @@ export class ServerEventsRaw extends BaseRaw { } countFailedAttemptsByIp(ip: string): Promise { - return this.col.find({ + return this.find({ ip, t: IServerEventType.FAILED_LOGIN_ATTEMPT, }).count(); } countFailedAttemptsByUsername(username: string): Promise { - return this.col.find({ + return this.find({ 'u.username': username, t: IServerEventType.FAILED_LOGIN_ATTEMPT, }).count(); diff --git a/app/models/server/raw/Sessions.js b/app/models/server/raw/Sessions.js deleted file mode 100644 index 965604fcd0b5..000000000000 --- a/app/models/server/raw/Sessions.js +++ /dev/null @@ -1,285 +0,0 @@ -import { BaseRaw } from './BaseRaw'; -import Sessions from '../models/Sessions'; - -const matchBasedOnDate = (start, end) => { - if (start.year === end.year && start.month === end.month) { - return { - year: start.year, - month: start.month, - day: { $gte: start.day, $lte: end.day }, - }; - } - - if (start.year === end.year) { - return { - year: start.year, - $and: [{ - $or: [{ - month: { $gt: start.month }, - }, { - month: start.month, - day: { $gte: start.day }, - }], - }, { - $or: [{ - month: { $lt: end.month }, - }, { - month: end.month, - day: { $lte: end.day }, - }], - }], - }; - } - - return { - $and: [{ - $or: [{ - year: { $gt: start.year }, - }, { - year: start.year, - month: { $gt: start.month }, - }, { - year: start.year, - month: start.month, - day: { $gte: start.day }, - }], - }, { - $or: [{ - year: { $lt: end.year }, - }, { - year: end.year, - month: { $lt: end.month }, - }, { - year: end.year, - month: end.month, - day: { $lte: end.day }, - }], - }], - }; -}; - -const getGroupSessionsByHour = (_id) => { - const isOpenSession = { $not: ['$session.closedAt'] }; - const isAfterLoginAt = { $gte: ['$range', { $hour: '$session.loginAt' }] }; - const isBeforeClosedAt = { $lte: ['$range', { $hour: '$session.closedAt' }] }; - - const listGroup = { - $group: { - _id, - usersList: { - $addToSet: { - $cond: [ - { - $or: [ - { $and: [isOpenSession, isAfterLoginAt] }, - { $and: [isAfterLoginAt, isBeforeClosedAt] }, - ], - }, - '$session.userId', - '$$REMOVE', - ], - }, - }, - }, - }; - - const countGroup = { - $addFields: { - users: { $size: '$usersList' }, - }, - }; - - return { listGroup, countGroup }; -}; - -const getSortByFullDate = () => ({ - year: -1, - month: -1, - day: -1, -}); - -const getProjectionByFullDate = () => ({ - day: '$_id.day', - month: '$_id.month', - year: '$_id.year', -}); - -export class SessionsRaw extends BaseRaw { - getActiveUsersBetweenDates({ start, end }) { - return this.col.aggregate([ - { - $match: { - ...matchBasedOnDate(start, end), - type: 'user_daily', - }, - }, - { - $group: { - _id: '$userId', - }, - }, - ]).toArray(); - } - - async findLastLoginByIp(ip) { - return (await this.col.find({ - ip, - }, { - sort: { loginAt: -1 }, - limit: 1, - }).toArray())[0]; - } - - getActiveUsersOfPeriodByDayBetweenDates({ start, end }) { - return this.col.aggregate([ - { - $match: { - ...matchBasedOnDate(start, end), - type: 'user_daily', - mostImportantRole: { $ne: 'anonymous' }, - }, - }, - { - $group: { - _id: { - day: '$day', - month: '$month', - year: '$year', - userId: '$userId', - }, - }, - }, - { - $group: { - _id: { - day: '$_id.day', - month: '$_id.month', - year: '$_id.year', - }, - usersList: { - $addToSet: '$_id.userId', - }, - users: { $sum: 1 }, - }, - }, - { - $project: { - _id: 0, - ...getProjectionByFullDate(), - usersList: 1, - users: 1, - }, - }, - { - $sort: { - ...getSortByFullDate(), - }, - }, - ]).toArray(); - } - - getBusiestTimeWithinHoursPeriod({ start, end, groupSize }) { - const match = { - $match: { - type: 'computed-session', - loginAt: { $gte: start, $lte: end }, - }, - }; - const rangeProject = { - $project: { - range: { - $range: [0, 24, groupSize], - }, - session: '$$ROOT', - }, - }; - const unwind = { - $unwind: '$range', - }; - const groups = getGroupSessionsByHour('$range'); - const presentationProject = { - $project: { - _id: 0, - hour: '$_id', - users: 1, - }, - }; - const sort = { - $sort: { - hour: -1, - }, - }; - return this.col.aggregate([match, rangeProject, unwind, groups.listGroup, groups.countGroup, presentationProject, sort]).toArray(); - } - - getTotalOfSessionsByDayBetweenDates({ start, end }) { - return this.col.aggregate([ - { - $match: { - ...matchBasedOnDate(start, end), - type: 'user_daily', - mostImportantRole: { $ne: 'anonymous' }, - }, - }, - { - $group: { - _id: { year: '$year', month: '$month', day: '$day' }, - users: { $sum: 1 }, - }, - }, - { - $project: { - _id: 0, - ...getProjectionByFullDate(), - users: 1, - }, - }, - { - $sort: { - ...getSortByFullDate(), - }, - }, - ]).toArray(); - } - - getTotalOfSessionByHourAndDayBetweenDates({ start, end }) { - const match = { - $match: { - type: 'computed-session', - loginAt: { $gte: start, $lte: end }, - }, - }; - const rangeProject = { - $project: { - range: { - $range: [ - { $hour: '$loginAt' }, - { $sum: [{ $ifNull: [{ $hour: '$closedAt' }, 23] }, 1] }], - }, - session: '$$ROOT', - }, - - }; - const unwind = { - $unwind: '$range', - }; - const groups = getGroupSessionsByHour({ range: '$range', day: '$session.day', month: '$session.month', year: '$session.year' }); - const presentationProject = { - $project: { - _id: 0, - hour: '$_id.range', - ...getProjectionByFullDate(), - users: 1, - }, - }; - const sort = { - $sort: { - ...getSortByFullDate(), - hour: -1, - }, - }; - return this.col.aggregate([match, rangeProject, unwind, groups.listGroup, groups.countGroup, presentationProject, sort]).toArray(); - } -} - -export default new SessionsRaw(Sessions.model.rawCollection()); diff --git a/app/models/server/models/Sessions.tests.js b/app/models/server/raw/Sessions.tests.js similarity index 99% rename from app/models/server/models/Sessions.tests.js rename to app/models/server/raw/Sessions.tests.js index ba0b94c60892..7dd157e9e681 100644 --- a/app/models/server/models/Sessions.tests.js +++ b/app/models/server/raw/Sessions.tests.js @@ -4,8 +4,6 @@ import assert from 'assert'; import { MongoMemoryServer } from 'mongodb-memory-server'; -import './Sessions.mocks.js'; - const { MongoClient } = require('mongodb'); const { aggregates } = require('./Sessions'); diff --git a/app/models/server/models/Sessions.js b/app/models/server/raw/Sessions.ts similarity index 50% rename from app/models/server/models/Sessions.js rename to app/models/server/raw/Sessions.ts index ffb43d6566a5..86e97ebe76a7 100644 --- a/app/models/server/models/Sessions.js +++ b/app/models/server/raw/Sessions.ts @@ -1,8 +1,118 @@ -import { Base } from './_Base'; -import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; +import { AggregationCursor, BulkWriteOperation, BulkWriteOpResultObject, Collection, IndexSpecification, UpdateWriteOpResult, FilterQuery } from 'mongodb'; + +import { ISession as T } from '../../../../definition/ISession'; +import { BaseRaw, ModelOptionalId } from './BaseRaw'; + +type DestructuredDate = {year: number; month: number; day: number}; +type DestructuredDateWithType = {year: number; month: number; day: number; type?: 'month' | 'week'}; +type DestructuredRange = {start: DestructuredDate; end: DestructuredDate}; +type FullReturn = { year: number; month: number; day: number; data: T[] }; + +const matchBasedOnDate = (start: DestructuredDate, end: DestructuredDate): FilterQuery => { + if (start.year === end.year && start.month === end.month) { + return { + year: start.year, + month: start.month, + day: { $gte: start.day, $lte: end.day }, + }; + } + + if (start.year === end.year) { + return { + year: start.year, + $and: [{ + $or: [{ + month: { $gt: start.month }, + }, { + month: start.month, + day: { $gte: start.day }, + }], + }, { + $or: [{ + month: { $lt: end.month }, + }, { + month: end.month, + day: { $lte: end.day }, + }], + }], + }; + } + + return { + $and: [{ + $or: [{ + year: { $gt: start.year }, + }, { + year: start.year, + month: { $gt: start.month }, + }, { + year: start.year, + month: start.month, + day: { $gte: start.day }, + }], + }, { + $or: [{ + year: { $lt: end.year }, + }, { + year: end.year, + month: { $lt: end.month }, + }, { + year: end.year, + month: end.month, + day: { $lte: end.day }, + }], + }], + }; +}; + +const getGroupSessionsByHour = (_id: { range: string; day: string; month: string; year: string } | string): {listGroup: object; countGroup: object} => { + const isOpenSession = { $not: ['$session.closedAt'] }; + const isAfterLoginAt = { $gte: ['$range', { $hour: '$session.loginAt' }] }; + const isBeforeClosedAt = { $lte: ['$range', { $hour: '$session.closedAt' }] }; + + const listGroup = { + $group: { + _id, + usersList: { + $addToSet: { + $cond: [ + { + $or: [ + { $and: [isOpenSession, isAfterLoginAt] }, + { $and: [isAfterLoginAt, isBeforeClosedAt] }, + ], + }, + '$session.userId', + '$$REMOVE', + ], + }, + }, + }, + }; + + const countGroup = { + $addFields: { + users: { $size: '$usersList' }, + }, + }; + + return { listGroup, countGroup }; +}; + +const getSortByFullDate = (): { year: number; month: number; day: number } => ({ + year: -1, + month: -1, + day: -1, +}); + +const getProjectionByFullDate = (): { day: string; month: string; year: string } => ({ + day: '$_id.day', + month: '$_id.month', + year: '$_id.year', +}); export const aggregates = { - dailySessionsOfYesterday(collection, { year, month, day }) { + dailySessionsOfYesterday(collection: Collection, { year, month, day }: DestructuredDate): AggregationCursor { return collection.aggregate([{ $match: { userId: { $exists: true }, @@ -91,7 +201,7 @@ export const aggregates = { }], { allowDiskUse: true }); }, - getUniqueUsersOfYesterday(collection, { year, month, day }) { + async getUniqueUsersOfYesterday(collection: Collection, { year, month, day }: DestructuredDate): Promise { return collection.aggregate([{ $match: { year, @@ -153,7 +263,7 @@ export const aggregates = { }]).toArray(); }, - getUniqueUsersOfLastMonthOrWeek(collection, { year, month, day, type = 'month' }) { + async getUniqueUsersOfLastMonthOrWeek(collection: Collection, { year, month, day, type = 'month' }: DestructuredDateWithType): Promise { return collection.aggregate([{ $match: { type: 'user_daily', @@ -223,7 +333,7 @@ export const aggregates = { }], { allowDiskUse: true }).toArray(); }, - getMatchOfLastMonthOrWeek({ year, month, day, type = 'month' }) { + getMatchOfLastMonthOrWeek({ year, month, day, type = 'month' }: DestructuredDateWithType): FilterQuery { let startOfPeriod; if (type === 'month') { @@ -298,7 +408,7 @@ export const aggregates = { }; }, - getUniqueDevicesOfLastMonthOrWeek(collection, { year, month, day, type = 'month' }) { + async getUniqueDevicesOfLastMonthOrWeek(collection: Collection, { year, month, day, type = 'month' }: DestructuredDateWithType): Promise { return collection.aggregate([{ $match: { type: 'user_daily', @@ -336,7 +446,7 @@ export const aggregates = { }], { allowDiskUse: true }).toArray(); }, - getUniqueDevicesOfYesterday(collection, { year, month, day }) { + getUniqueDevicesOfYesterday(collection: Collection, { year, month, day }: DestructuredDate): Promise { return collection.aggregate([{ $match: { year, @@ -376,7 +486,7 @@ export const aggregates = { }]).toArray(); }, - getUniqueOSOfLastMonthOrWeek(collection, { year, month, day, type = 'month' }) { + getUniqueOSOfLastMonthOrWeek(collection: Collection, { year, month, day, type = 'month' }: DestructuredDateWithType): Promise { return collection.aggregate([{ $match: { type: 'user_daily', @@ -415,7 +525,7 @@ export const aggregates = { }], { allowDiskUse: true }).toArray(); }, - getUniqueOSOfYesterday(collection, { year, month, day }) { + getUniqueOSOfYesterday(collection: Collection, { year, month, day }: DestructuredDate): Promise { return collection.aggregate([{ $match: { year, @@ -457,25 +567,208 @@ export const aggregates = { }, }; -export class Sessions extends Base { - constructor(...args) { - super(...args); - - this.tryEnsureIndex({ instanceId: 1, sessionId: 1, year: 1, month: 1, day: 1 }); - this.tryEnsureIndex({ instanceId: 1, sessionId: 1, userId: 1 }); - this.tryEnsureIndex({ instanceId: 1, sessionId: 1 }); - this.tryEnsureIndex({ sessionId: 1 }); - this.tryEnsureIndex({ userId: 1 }); - this.tryEnsureIndex({ year: 1, month: 1, day: 1, type: 1 }); - this.tryEnsureIndex({ type: 1 }); - this.tryEnsureIndex({ ip: 1, loginAt: 1 }); - this.tryEnsureIndex({ _computedAt: 1 }, { expireAfterSeconds: 60 * 60 * 24 * 45 }); - - const db = this.model.rawDatabase(); - this.secondaryCollection = db.collection(this.model._name, { readPreference: readSecondaryPreferred(db) }); +export class SessionsRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { key: { instanceId: 1, sessionId: 1, year: 1, month: 1, day: 1 } }, + { key: { instanceId: 1, sessionId: 1, userId: 1 } }, + { key: { instanceId: 1, sessionId: 1 } }, + { key: { sessionId: 1 } }, + { key: { userId: 1 } }, + { key: { year: 1, month: 1, day: 1, type: 1 } }, + { key: { type: 1 } }, + { key: { ip: 1, loginAt: 1 } }, + { key: { _computedAt: 1 }, expireAfterSeconds: 60 * 60 * 24 * 45 }, + ] + + private secondaryCollection: Collection; + + constructor( + public readonly col: Collection, + public readonly colSecondary: Collection, + public readonly trash?: Collection, + ) { + super(col, trash); + + this.secondaryCollection = colSecondary; + } + + async getActiveUsersBetweenDates({ start, end }: DestructuredRange): Promise { + return this.col.aggregate([ + { + $match: { + ...matchBasedOnDate(start, end), + type: 'user_daily', + }, + }, + { + $group: { + _id: '$userId', + }, + }, + ]).toArray(); + } + + async findLastLoginByIp(ip: string): Promise { + return this.findOne({ + ip, + }, { + sort: { loginAt: -1 }, + limit: 1, + }); + } + + async getActiveUsersOfPeriodByDayBetweenDates({ start, end }: DestructuredRange): Promise { + return this.col.aggregate([ + { + $match: { + ...matchBasedOnDate(start, end), + type: 'user_daily', + mostImportantRole: { $ne: 'anonymous' }, + }, + }, + { + $group: { + _id: { + day: '$day', + month: '$month', + year: '$year', + userId: '$userId', + }, + }, + }, + { + $group: { + _id: { + day: '$_id.day', + month: '$_id.month', + year: '$_id.year', + }, + usersList: { + $addToSet: '$_id.userId', + }, + users: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + ...getProjectionByFullDate(), + usersList: 1, + users: 1, + }, + }, + { + $sort: { + ...getSortByFullDate(), + }, + }, + ]).toArray(); + } + + async getBusiestTimeWithinHoursPeriod({ start, end, groupSize }: DestructuredRange & {groupSize: number}): Promise { + const match = { + $match: { + type: 'computed-session', + loginAt: { $gte: start, $lte: end }, + }, + }; + const rangeProject = { + $project: { + range: { + $range: [0, 24, groupSize], + }, + session: '$$ROOT', + }, + }; + const unwind = { + $unwind: '$range', + }; + const groups = getGroupSessionsByHour('$range'); + const presentationProject = { + $project: { + _id: 0, + hour: '$_id', + users: 1, + }, + }; + const sort = { + $sort: { + hour: -1, + }, + }; + return this.col.aggregate([match, rangeProject, unwind, groups.listGroup, groups.countGroup, presentationProject, sort]).toArray(); } - getUniqueUsersOfYesterday() { + async getTotalOfSessionsByDayBetweenDates({ start, end }: DestructuredRange): Promise { + return this.col.aggregate([ + { + $match: { + ...matchBasedOnDate(start, end), + type: 'user_daily', + mostImportantRole: { $ne: 'anonymous' }, + }, + }, + { + $group: { + _id: { year: '$year', month: '$month', day: '$day' }, + users: { $sum: 1 }, + }, + }, + { + $project: { + _id: 0, + ...getProjectionByFullDate(), + users: 1, + }, + }, + { + $sort: { + ...getSortByFullDate(), + }, + }, + ]).toArray(); + } + + async getTotalOfSessionByHourAndDayBetweenDates({ start, end }: DestructuredRange): Promise { + const match = { + $match: { + type: 'computed-session', + loginAt: { $gte: start, $lte: end }, + }, + }; + const rangeProject = { + $project: { + range: { + $range: [ + { $hour: '$loginAt' }, + { $sum: [{ $ifNull: [{ $hour: '$closedAt' }, 23] }, 1] }], + }, + session: '$$ROOT', + }, + + }; + const unwind = { + $unwind: '$range', + }; + const groups = getGroupSessionsByHour({ range: '$range', day: '$session.day', month: '$session.month', year: '$session.year' }); + const presentationProject = { + $project: { + _id: 0, + hour: '$_id.range', + ...getProjectionByFullDate(), + users: 1, + }, + }; + const sort = { + $sort: { + ...getSortByFullDate(), + hour: -1, + }, + }; + return this.col.aggregate([match, rangeProject, unwind, groups.listGroup, groups.countGroup, presentationProject, sort]).toArray(); + } + + async getUniqueUsersOfYesterday(): Promise { const date = new Date(); date.setDate(date.getDate() - 1); @@ -487,11 +780,11 @@ export class Sessions extends Base { year, month, day, - data: Promise.await(aggregates.getUniqueUsersOfYesterday(this.secondaryCollection, { year, month, day })), + data: await aggregates.getUniqueUsersOfYesterday(this.secondaryCollection, { year, month, day }), }; } - getUniqueUsersOfLastMonth() { + async getUniqueUsersOfLastMonth(): Promise { const date = new Date(); date.setDate(date.getDate() - 1); @@ -503,11 +796,11 @@ export class Sessions extends Base { year, month, day, - data: Promise.await(aggregates.getUniqueUsersOfLastMonthOrWeek(this.secondaryCollection, { year, month, day })), + data: await aggregates.getUniqueUsersOfLastMonthOrWeek(this.secondaryCollection, { year, month, day }), }; } - getUniqueUsersOfLastWeek() { + async getUniqueUsersOfLastWeek(): Promise { const date = new Date(); date.setDate(date.getDate() - 1); @@ -519,11 +812,11 @@ export class Sessions extends Base { year, month, day, - data: Promise.await(aggregates.getUniqueUsersOfLastMonthOrWeek(this.secondaryCollection, { year, month, day, type: 'week' })), + data: await aggregates.getUniqueUsersOfLastMonthOrWeek(this.secondaryCollection, { year, month, day, type: 'week' }), }; } - getUniqueDevicesOfYesterday() { + async getUniqueDevicesOfYesterday(): Promise { const date = new Date(); date.setDate(date.getDate() - 1); @@ -535,11 +828,11 @@ export class Sessions extends Base { year, month, day, - data: Promise.await(aggregates.getUniqueDevicesOfYesterday(this.secondaryCollection, { year, month, day })), + data: await aggregates.getUniqueDevicesOfYesterday(this.secondaryCollection, { year, month, day }), }; } - getUniqueDevicesOfLastMonth() { + async getUniqueDevicesOfLastMonth(): Promise { const date = new Date(); date.setDate(date.getDate() - 1); @@ -551,11 +844,11 @@ export class Sessions extends Base { year, month, day, - data: Promise.await(aggregates.getUniqueDevicesOfLastMonthOrWeek(this.secondaryCollection, { year, month, day })), + data: await aggregates.getUniqueDevicesOfLastMonthOrWeek(this.secondaryCollection, { year, month, day }), }; } - getUniqueDevicesOfLastWeek() { + async getUniqueDevicesOfLastWeek(): Promise { const date = new Date(); date.setDate(date.getDate() - 1); @@ -567,11 +860,11 @@ export class Sessions extends Base { year, month, day, - data: Promise.await(aggregates.getUniqueDevicesOfLastMonthOrWeek(this.secondaryCollection, { year, month, day, type: 'week' })), + data: await aggregates.getUniqueDevicesOfLastMonthOrWeek(this.secondaryCollection, { year, month, day, type: 'week' }), }; } - getUniqueOSOfYesterday() { + async getUniqueOSOfYesterday(): Promise { const date = new Date(); date.setDate(date.getDate() - 1); @@ -583,11 +876,11 @@ export class Sessions extends Base { year, month, day, - data: Promise.await(aggregates.getUniqueOSOfYesterday(this.secondaryCollection, { year, month, day })), + data: await aggregates.getUniqueOSOfYesterday(this.secondaryCollection, { year, month, day }), }; } - getUniqueOSOfLastMonth() { + async getUniqueOSOfLastMonth(): Promise { const date = new Date(); date.setDate(date.getDate() - 1); @@ -599,11 +892,11 @@ export class Sessions extends Base { year, month, day, - data: Promise.await(aggregates.getUniqueOSOfLastMonthOrWeek(this.secondaryCollection, { year, month, day })), + data: await aggregates.getUniqueOSOfLastMonthOrWeek(this.secondaryCollection, { year, month, day }), }; } - getUniqueOSOfLastWeek() { + async getUniqueOSOfLastWeek(): Promise { const date = new Date(); date.setDate(date.getDate() - 1); @@ -615,11 +908,11 @@ export class Sessions extends Base { year, month, day, - data: Promise.await(aggregates.getUniqueOSOfLastMonthOrWeek(this.secondaryCollection, { year, month, day, type: 'week' })), + data: await aggregates.getUniqueOSOfLastMonthOrWeek(this.secondaryCollection, { year, month, day, type: 'week' }), }; } - createOrUpdate(data = {}) { + async createOrUpdate(data: T): Promise { const { year, month, day, sessionId, instanceId } = data; if (!year || !month || !day || !sessionId || !instanceId) { @@ -628,19 +921,19 @@ export class Sessions extends Base { const now = new Date(); - return this.upsert({ instanceId, sessionId, year, month, day }, { + return this.updateOne({ instanceId, sessionId, year, month, day }, { $set: data, $setOnInsert: { createdAt: now, }, - }); + }, { upsert: true }); } - closeByInstanceIdAndSessionId(instanceId, sessionId) { + async closeByInstanceIdAndSessionId(instanceId: string, sessionId: string): Promise { const query = { instanceId, sessionId, - closedAt: { $exists: 0 }, + closedAt: { $exists: false }, }; const closeTime = new Date(); @@ -651,27 +944,27 @@ export class Sessions extends Base { }, }; - return this.update(query, update); + return this.updateOne(query, update); } - updateActiveSessionsByDateAndInstanceIdAndIds({ year, month, day } = {}, instanceId, sessions, data = {}) { + async updateActiveSessionsByDateAndInstanceIdAndIds({ year, month, day }: Partial = {}, instanceId: string, sessions: string[], data = {}): Promise { const query = { instanceId, year, month, day, sessionId: { $in: sessions }, - closedAt: { $exists: 0 }, + closedAt: { $exists: false }, }; const update = { $set: data, }; - return this.update(query, update, { multi: true }); + return this.updateMany(query, update); } - logoutByInstanceIdAndSessionIdAndUserId(instanceId, sessionId, userId) { + async logoutByInstanceIdAndSessionIdAndUserId(instanceId: string, sessionId: string, userId: string): Promise { const query = { instanceId, sessionId, @@ -686,15 +979,15 @@ export class Sessions extends Base { }, }; - return this.update(query, update, { multi: true }); + return this.updateMany(query, update); } - createBatch(sessions) { + async createBatch(sessions: ModelOptionalId[]): Promise { if (!sessions || sessions.length === 0) { return; } - const ops = []; + const ops: BulkWriteOperation[] = []; sessions.forEach((doc) => { const { year, month, day, sessionId, instanceId } = doc; delete doc._id; @@ -710,8 +1003,6 @@ export class Sessions extends Base { }); }); - return this.model.rawCollection().bulkWrite(ops, { ordered: false }); + return this.col.bulkWrite(ops, { ordered: false }); } } - -export default new Sessions('sessions'); diff --git a/app/models/server/raw/Settings.ts b/app/models/server/raw/Settings.ts index dd475ed9a131..2d3a11b2ccea 100644 --- a/app/models/server/raw/Settings.ts +++ b/app/models/server/raw/Settings.ts @@ -1,18 +1,29 @@ -import { Cursor, WriteOpResult } from 'mongodb'; +import { Cursor, FilterQuery, UpdateQuery, WriteOpResult } from 'mongodb'; import { BaseRaw } from './BaseRaw'; -import { ISetting } from '../../../../definition/ISetting'; +import { ISetting, ISettingColor, ISettingSelectOption } from '../../../../definition/ISetting'; -type T = ISetting; -export class SettingsRaw extends BaseRaw { +export class SettingsRaw extends BaseRaw { async getValueById(_id: string): Promise { const setting = await this.findOne>({ _id }, { projection: { value: 1 } }); return setting?.value; } - findOneNotHiddenById(_id: string): Promise { + findNotHidden({ updatedAfter }: { updatedAfter?: Date } = {}): Cursor { + const query: FilterQuery = { + hidden: { $ne: true }, + }; + + if (updatedAfter) { + query._updatedAt = { $gt: updatedAfter }; + } + + return this.find(query); + } + + findOneNotHiddenById(_id: string): Promise { const query = { _id, hidden: { $ne: true }, @@ -21,7 +32,7 @@ export class SettingsRaw extends BaseRaw { return this.findOne(query); } - findByIds(_id: string[] | string = []): Cursor { + findByIds(_id: string[] | string = []): Cursor { if (typeof _id === 'string') { _id = [_id]; } @@ -35,7 +46,50 @@ export class SettingsRaw extends BaseRaw { return this.find(query); } - updateValueById(_id: string, value: any): Promise { + updateValueById(_id: string, value: T): Promise { + const query = { + blocked: { $ne: true }, + value: { $ne: value }, + _id, + }; + + const update = { + $set: { + value, + }, + }; + + return this.update(query, update); + } + + updateOptionsById(_id: ISetting['_id'], options: UpdateQuery['$set']): Promise { + const query = { + blocked: { $ne: true }, + _id, + }; + + const update = { $set: options }; + + return this.update(query, update); + } + + updateValueNotHiddenById(_id: ISetting['_id'], value: T): Promise { + const query = { + _id, + hidden: { $ne: true }, + blocked: { $ne: true }, + }; + + const update = { + $set: { + value, + }, + }; + + return this.update(query, update); + } + + updateValueAndEditorById(_id: ISetting['_id'], value: T, editor: ISettingColor['editor']): Promise { const query = { blocked: { $ne: true }, value: { $ne: value }, @@ -45,9 +99,58 @@ export class SettingsRaw extends BaseRaw { const update = { $set: { value, + editor, }, }; return this.update(query, update); } + + findNotHiddenPublic(ids: ISetting['_id'][] = []): Cursor< T extends ISettingColor ? Pick : Pick> { + const filter: FilterQuery = { + hidden: { $ne: true }, + public: true, + }; + + if (ids.length > 0) { + filter._id = { $in: ids }; + } + + return this.find(filter, { fields: { _id: 1, value: 1, editor: 1, enterprise: 1, invalidValue: 1, modules: 1, requiredOnWizard: 1 } }) as any; + } + + findSetupWizardSettings(): Cursor { + return this.find({ wizard: { $exists: true } }); + } + + addOptionValueById(_id: ISetting['_id'], option: ISettingSelectOption): Promise { + const query = { + blocked: { $ne: true }, + _id, + }; + + const { key, i18nLabel } = option; + const update = { + $addToSet: { + values: { + key, + i18nLabel, + }, + }, + }; + + return this.update(query, update); + } + + findNotHiddenPublicUpdatedAfter(updatedAt: Date): Cursor { + const filter = { + hidden: { $ne: true }, + public: true, + _updatedAt: { + $gt: updatedAt, + }, + }; + + return this.find(filter, { projection: { _id: 1, value: 1, editor: 1, enterprise: 1, invalidValue: 1, modules: 1, requiredOnWizard: 1 } }); + } } diff --git a/app/models/server/raw/SmarshHistory.ts b/app/models/server/raw/SmarshHistory.ts new file mode 100644 index 000000000000..70c2e3df482d --- /dev/null +++ b/app/models/server/raw/SmarshHistory.ts @@ -0,0 +1,8 @@ +import { BaseRaw } from './BaseRaw'; +import { ISmarshHistory } from '../../../../definition/ISmarshHistory'; + +type T = ISmarshHistory; + +export class SmarshHistoryRaw extends BaseRaw { + +} diff --git a/app/models/server/raw/Statistics.js b/app/models/server/raw/Statistics.js deleted file mode 100644 index 15b3cf39404a..000000000000 --- a/app/models/server/raw/Statistics.js +++ /dev/null @@ -1,14 +0,0 @@ -import { BaseRaw } from './BaseRaw'; - -export class StatisticsRaw extends BaseRaw { - async findLast() { - const options = { - sort: { - createdAt: -1, - }, - limit: 1, - }; - const records = await this.find({}, options).toArray(); - return records && records[0]; - } -} diff --git a/app/models/server/raw/Statistics.ts b/app/models/server/raw/Statistics.ts new file mode 100644 index 000000000000..b3b915a9ebce --- /dev/null +++ b/app/models/server/raw/Statistics.ts @@ -0,0 +1,21 @@ +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { IStatistic } from '../../../../definition/IStatistic'; + +type T = IStatistic; + +export class StatisticsRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { key: { createdAt: -1 } }, + ] + + async findLast(): Promise { + const options = { + sort: { + createdAt: -1, + }, + limit: 1, + }; + const records = await this.find({}, options).toArray(); + return records && records[0]; + } +} diff --git a/app/models/server/raw/Subscriptions.ts b/app/models/server/raw/Subscriptions.ts index 544af563af91..4a02f6185769 100644 --- a/app/models/server/raw/Subscriptions.ts +++ b/app/models/server/raw/Subscriptions.ts @@ -1,10 +1,20 @@ -import { FindOneOptions, Cursor, UpdateQuery, FilterQuery } from 'mongodb'; +import { FindOneOptions, Cursor, UpdateQuery, FilterQuery, UpdateWriteOpResult, Collection, WithoutProjection } from 'mongodb'; +import { compact } from 'lodash'; import { BaseRaw } from './BaseRaw'; import { ISubscription } from '../../../../definition/ISubscription'; +import { IRole, IUser } from '../../../../definition/IUser'; +import { IRoom } from '../../../../definition/IRoom'; +import { UsersRaw } from './Users'; type T = ISubscription; export class SubscriptionsRaw extends BaseRaw { + constructor(public readonly col: Collection, + private readonly models: { Users: UsersRaw }, + public readonly trash?: Collection) { + super(col, trash); + } + findOneByRoomIdAndUserId(rid: string, uid: string, options: FindOneOptions = {}): Promise { const query = { rid, @@ -47,7 +57,7 @@ export class SubscriptionsRaw extends BaseRaw { return cursor.count(); } - async isUserInRole(uid: string, roleName: string, rid: string): Promise { + async isUserInRole(uid: IUser['_id'], roleName: IRole['name'], rid?: IRoom['_id']): Promise { if (rid == null) { return null; } @@ -80,4 +90,77 @@ export class SubscriptionsRaw extends BaseRaw { return this.update(query, update, options); } + + removeRolesByUserId(uid: IUser['_id'], roles: IRole['name'][], rid: IRoom['_id']): Promise { + const query = { + 'u._id': uid, + rid, + }; + + const update = { + $pullAll: { + roles, + }, + }; + + return this.updateOne(query, update); + } + + + findUsersInRoles(name: IRole['name'][], rid: string | undefined): Promise>; + + findUsersInRoles(name: IRole['name'][], rid: string | undefined, options: WithoutProjection>): Promise>; + + findUsersInRoles

(name: IRole['name'][], rid: string | undefined, options: FindOneOptions

): Promise>; + + async findUsersInRoles

(roles: IRole['name'][], rid: IRoom['_id'] | undefined, options?: FindOneOptions

): Promise> { + const query = { + roles: { $in: roles }, + ...rid && { rid }, + }; + + const subscriptions = await this.find(query).toArray(); + + const users = compact(subscriptions.map((subscription) => subscription.u?._id).filter(Boolean)); + + return !options ? this.models.Users.find({ _id: { $in: users } }) : this.models.Users.find({ _id: { $in: users } } as FilterQuery, options); + } + + + addRolesByUserId(uid: IUser['_id'], roles: IRole['name'][], rid?: IRoom['_id']): Promise { + if (!Array.isArray(roles)) { + roles = [roles]; + process.env.NODE_ENV === 'development' && console.warn('[WARN] Subscriptions.addRolesByUserId: roles should be an array'); + } + + const query = { + 'u._id': uid, + rid, + }; + + const update = { + $addToSet: { + roles: { $each: roles }, + }, + }; + + return this.updateOne(query, update); + } + + async isUserInRoleScope(uid: IUser['_id'], rid?: IRoom['_id']): Promise { + const query = { + 'u._id': uid, + rid, + }; + + if (!rid) { + return false; + } + const options = { + fields: { _id: 1 }, + }; + + const found = await this.findOne(query, options); + return !!found; + } } diff --git a/app/models/server/raw/Uploads.ts b/app/models/server/raw/Uploads.ts new file mode 100644 index 000000000000..ad2fd67247c9 --- /dev/null +++ b/app/models/server/raw/Uploads.ts @@ -0,0 +1,116 @@ +// TODO: Lib imports should not exists inside the raw models +import { escapeRegExp } from '@rocket.chat/string-helpers'; +import { CollectionInsertOneOptions, Cursor, DeleteWriteOpResultObject, FilterQuery, InsertOneWriteOpResult, UpdateOneOptions, UpdateQuery, UpdateWriteOpResult, WithId, WriteOpResult } from 'mongodb'; + +import { BaseRaw, IndexSpecification, InsertionModel } from './BaseRaw'; +import { IUpload as T } from '../../../../definition/IUpload'; + +const fillTypeGroup = (fileData: Partial): void => { + if (!fileData.type) { + return; + } + + fileData.typeGroup = fileData.type.split('/').shift(); +}; + +export class UploadsRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { key: { rid: 1 } }, + { key: { uploadedAt: 1 } }, + { key: { typeGroup: 1 } }, + ] + + findNotHiddenFilesOfRoom(roomId: string, searchText: string, fileType: string, limit: number): Cursor { + const fileQuery = { + rid: roomId, + complete: true, + uploading: false, + _hidden: { + $ne: true, + }, + + ...searchText && { name: { $regex: new RegExp(escapeRegExp(searchText), 'i') } }, + ...fileType && fileType !== 'all' && { typeGroup: fileType }, + }; + + const fileOptions = { + limit, + sort: { + uploadedAt: -1, + }, + projection: { + _id: 1, + userId: 1, + rid: 1, + name: 1, + description: 1, + type: 1, + url: 1, + uploadedAt: 1, + typeGroup: 1, + }, + }; + + return this.find(fileQuery, fileOptions); + } + + insert(fileData: InsertionModel, options?: CollectionInsertOneOptions): Promise>> { + fillTypeGroup(fileData); + return super.insertOne(fileData, options); + } + + update(filter: FilterQuery, update: UpdateQuery | Partial, options?: UpdateOneOptions & { multi?: boolean }): Promise { + if ('$set' in update && update.$set) { + fillTypeGroup(update.$set); + } else if ('type' in update && update.type) { + fillTypeGroup(update); + } + + return super.update(filter, update, options); + } + + async insertFileInit(userId: string, store: string, file: {name: string}, extra: object): Promise>> { + const fileData = { + userId, + store, + complete: false, + uploading: true, + progress: 0, + extension: file.name.split('.').pop(), + uploadedAt: new Date(), + ...file, + ...extra, + }; + + fillTypeGroup(fileData); + return this.insert(fileData); + } + + async updateFileComplete(fileId: string, userId: string, file: object): Promise { + if (!fileId) { + return; + } + + const filter = { + _id: fileId, + userId, + }; + + const update = { + $set: { + complete: true, + uploading: false, + progress: 1, + }, + }; + + update.$set = Object.assign(file, update.$set); + + fillTypeGroup(update.$set); + return this.updateOne(filter, update); + } + + async deleteFile(fileId: string): Promise { + return this.deleteOne({ _id: fileId }); + } +} diff --git a/app/models/server/raw/UserDataFiles.ts b/app/models/server/raw/UserDataFiles.ts new file mode 100644 index 000000000000..684135c9d57a --- /dev/null +++ b/app/models/server/raw/UserDataFiles.ts @@ -0,0 +1,29 @@ +import { FindOneOptions, InsertOneWriteOpResult, WithId, WithoutProjection } from 'mongodb'; + +import { BaseRaw, IndexSpecification } from './BaseRaw'; +import { IUserDataFile as T } from '../../../../definition/IUserDataFile'; + +export class UserDataFilesRaw extends BaseRaw { + protected indexes: IndexSpecification[] = [ + { key: { userId: 1 } }, + ] + + findLastFileByUser(userId: string, options: WithoutProjection> = {}): Promise { + const query = { + userId, + }; + + options.sort = { _updatedAt: -1 }; + return this.findOne(query, options); + } + + // INSERT + create(data: T): Promise>> { + const userDataFile = { + createdAt: new Date(), + ...data, + }; + + return this.insertOne(userDataFile); + } +} diff --git a/app/models/server/raw/Users.js b/app/models/server/raw/Users.js index a021a91f25e6..0d6ce77ed78c 100644 --- a/app/models/server/raw/Users.js +++ b/app/models/server/raw/Users.js @@ -11,6 +11,24 @@ export class UsersRaw extends BaseRaw { }; } + addRolesByUserId(uid, roles) { + if (!Array.isArray(roles)) { + roles = [roles]; + process.env.NODE_ENV === 'development' && console.warn('[WARN] Users.addRolesByUserId: roles should be an array'); + } + + const query = { + _id: uid, + }; + + const update = { + $addToSet: { + roles: { $each: roles }, + }, + }; + return this.updateOne(query, update); + } + findUsersInRoles(roles, scope, options) { roles = [].concat(roles); @@ -259,14 +277,16 @@ export class UsersRaw extends BaseRaw { return result.value; } - setLivechatStatus(userId, status) { // TODO: Create class Agent + setLivechatStatusIf(userId, status, conditions = {}, extraFields = {}) { // TODO: Create class Agent const query = { _id: userId, + ...conditions, }; const update = { $set: { statusLivechat: status, + ...extraFields, }, }; @@ -704,4 +724,31 @@ export class UsersRaw extends BaseRaw { $pullAll: { __rooms: rids }, }, { multi: true }); } + + removeRolesByUserId(uid, roles) { + const query = { + _id: uid, + }; + + const update = { + $pullAll: { + roles, + }, + }; + + return this.updateOne(query, update); + } + + async isUserInRoleScope(uid) { + const query = { + _id: uid, + }; + + const options = { + fields: { _id: 1 }, + }; + + const found = await this.findOne(query, options); + return !!found; + } } diff --git a/app/models/server/raw/UsersSessions.ts b/app/models/server/raw/UsersSessions.ts index b89feaa9deb3..3560f1e175d1 100644 --- a/app/models/server/raw/UsersSessions.ts +++ b/app/models/server/raw/UsersSessions.ts @@ -1,4 +1,16 @@ import { BaseRaw } from './BaseRaw'; import { IUserSession } from '../../../../definition/IUserSession'; -export class UsersSessionsRaw extends BaseRaw {} +export class UsersSessionsRaw extends BaseRaw { + clearConnectionsFromInstanceId(instanceId: string[]): ReturnType['updateMany']> { + return this.col.updateMany({}, { + $pull: { + connections: { + instanceId: { + $nin: instanceId, + }, + }, + }, + }); + } +} diff --git a/app/models/server/raw/WebdavAccounts.js b/app/models/server/raw/WebdavAccounts.js deleted file mode 100644 index bcd87761c267..000000000000 --- a/app/models/server/raw/WebdavAccounts.js +++ /dev/null @@ -1,8 +0,0 @@ -import { BaseRaw } from './BaseRaw'; - -export class WebdavAccountsRaw extends BaseRaw { - findWithUserId(user_id, options) { - const query = { user_id }; - return this.find(query, options); - } -} diff --git a/app/models/server/raw/WebdavAccounts.ts b/app/models/server/raw/WebdavAccounts.ts new file mode 100644 index 000000000000..c189cb7e3799 --- /dev/null +++ b/app/models/server/raw/WebdavAccounts.ts @@ -0,0 +1,46 @@ +/* eslint-disable @typescript-eslint/camelcase */ +/** + * Webdav Accounts model + */ +import type { Collection, FindOneOptions, Cursor, DeleteWriteOpResultObject } from 'mongodb'; + +import { BaseRaw } from './BaseRaw'; +import { IWebdavAccount } from '../../../../definition/IWebdavAccount'; + +type T = IWebdavAccount; + +export class WebdavAccountsRaw extends BaseRaw { + constructor( + public readonly col: Collection, + public readonly trash?: Collection, + ) { + super(col, trash); + + this.col.createIndex({ user_id: 1 }); + } + + findOneByIdAndUserId(_id: string, user_id: string, options: FindOneOptions): Promise { + return this.findOne({ _id, user_id }, options); + } + + findOneByUserIdServerUrlAndUsername({ + user_id, + server_url, + username, + }: { + user_id: string; + server_url: string; + username: string; + }, options: FindOneOptions): Promise { + return this.findOne({ user_id, server_url, username }, options); + } + + findWithUserId(user_id: string, options: FindOneOptions): Cursor { + const query = { user_id }; + return this.find(query, options); + } + + removeByUserAndId(_id: string, user_id: string): Promise { + return this.deleteOne({ _id, user_id }); + } +} diff --git a/app/models/server/raw/_Users.d.ts b/app/models/server/raw/_Users.d.ts new file mode 100644 index 000000000000..891392ee3f7e --- /dev/null +++ b/app/models/server/raw/_Users.d.ts @@ -0,0 +1,14 @@ +import { UpdateWriteOpResult } from 'mongodb'; + +import { IRole, IUser } from '../../../../definition/IUser'; +import { BaseRaw } from './BaseRaw'; + +export interface IUserRaw extends BaseRaw { + isUserInRole(uid: IUser['_id'], name: IRole['name']): Promise; + removeRolesByUserId(uid: IUser['_id'], roles: IRole['name'][]): Promise; + findUsersInRoles(roles: IRole['name'][]): Promise; + addRolesByUserId(uid: IUser['_id'], roles: IRole['name'][]): Promise; + isUserInRoleScope(uid: IUser['_id']): Promise; + new(...args: any): IUser; +} +export const UsersRaw: IUserRaw; diff --git a/app/models/server/raw/index.ts b/app/models/server/raw/index.ts index 605218c636be..34789c62ce2f 100644 --- a/app/models/server/raw/index.ts +++ b/app/models/server/raw/index.ts @@ -1,83 +1,81 @@ -import PermissionsModel from '../models/Permissions'; +import { MongoInternals } from 'meteor/mongo'; + +import { AvatarsRaw } from './Avatars'; +import { AnalyticsRaw } from './Analytics'; +import { api } from '../../../../server/sdk/api'; +import { BaseDbWatch, trash } from '../models/_BaseDb'; +import { CredentialTokensRaw } from './CredentialTokens'; +import { CustomSoundsRaw } from './CustomSounds'; +import { CustomUserStatusRaw } from './CustomUserStatus'; +import { EmailInboxRaw } from './EmailInbox'; +import { EmailMessageHistoryRaw } from './EmailMessageHistory'; +import { EmojiCustomRaw } from './EmojiCustom'; +import { ExportOperationsRaw } from './ExportOperations'; +import { FederationKeysRaw } from './FederationKeys'; +import { FederationServersRaw } from './FederationServers'; +import { ImportDataRaw } from './ImportData'; +import { initWatchers } from '../../../../server/modules/watchers/watchers.module'; +import { InstanceStatusRaw } from './InstanceStatus'; +import { IntegrationHistoryRaw } from './IntegrationHistory'; +import { IntegrationsRaw } from './Integrations'; +import { InvitesRaw } from './Invites'; +import { LivechatAgentActivityRaw } from './LivechatAgentActivity'; +import { LivechatBusinessHoursRaw } from './LivechatBusinessHours'; +import { LivechatCustomFieldRaw } from './LivechatCustomField'; +import { LivechatDepartmentAgentsRaw } from './LivechatDepartmentAgents'; +import { LivechatDepartmentRaw } from './LivechatDepartment'; +import { LivechatExternalMessageRaw } from './LivechatExternalMessages'; +import { LivechatInquiryRaw } from './LivechatInquiry'; +import { LivechatRoomsRaw } from './LivechatRooms'; +import { LivechatTriggerRaw } from './LivechatTrigger'; +import { LivechatVisitorsRaw } from './LivechatVisitors'; +import { LoginServiceConfigurationRaw } from './LoginServiceConfiguration'; +import { MessagesRaw } from './Messages'; +import { NotificationQueueRaw } from './NotificationQueue'; +import { OAuthAppsRaw } from './OAuthApps'; +import { OEmbedCacheRaw } from './OEmbedCache'; +import { OmnichannelQueueRaw } from './OmnichannelQueue'; import { PermissionsRaw } from './Permissions'; -import RolesModel from '../models/Roles'; +import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; +import { ReadReceiptsRaw } from './ReadReceipts'; +import { ReportsRaw } from './Reports'; import { RolesRaw } from './Roles'; -import SubscriptionsModel from '../models/Subscriptions'; -import { SubscriptionsRaw } from './Subscriptions'; -import SettingsModel from '../models/Settings'; +import { RoomsRaw } from './Rooms'; +import { ServerEventsRaw } from './ServerEvents'; +import { SessionsRaw } from './Sessions'; import { SettingsRaw } from './Settings'; -import UsersModel from '../models/Users'; +import { SmarshHistoryRaw } from './SmarshHistory'; +import { StatisticsRaw } from './Statistics'; +import { SubscriptionsRaw } from './Subscriptions'; import { UsersRaw } from './Users'; -import SessionsModel from '../models/Sessions'; -import { SessionsRaw } from './Sessions'; -import RoomsModel from '../models/Rooms'; -import { RoomsRaw } from './Rooms'; +import { UsersSessionsRaw } from './UsersSessions'; +import { UserDataFilesRaw } from './UserDataFiles'; +import { UploadsRaw } from './Uploads'; +import { WebdavAccountsRaw } from './WebdavAccounts'; +import ImportDataModel from '../models/ImportData'; +import LivechatAgentActivityModel from '../models/LivechatAgentActivity'; +import LivechatBusinessHoursModel from '../models/LivechatBusinessHours'; import LivechatCustomFieldModel from '../models/LivechatCustomField'; -import { LivechatCustomFieldRaw } from './LivechatCustomField'; -import LivechatTriggerModel from '../models/LivechatTrigger'; -import { LivechatTriggerRaw } from './LivechatTrigger'; -import LivechatDepartmentModel from '../models/LivechatDepartment'; -import { LivechatDepartmentRaw } from './LivechatDepartment'; import LivechatDepartmentAgentsModel from '../models/LivechatDepartmentAgents'; -import { LivechatDepartmentAgentsRaw } from './LivechatDepartmentAgents'; -import LivechatRoomsModel from '../models/LivechatRooms'; -import { LivechatRoomsRaw } from './LivechatRooms'; -import MessagesModel from '../models/Messages'; -import { MessagesRaw } from './Messages'; +import LivechatDepartmentModel from '../models/LivechatDepartment'; import LivechatExternalMessagesModel from '../models/LivechatExternalMessages'; -import { LivechatExternalMessageRaw } from './LivechatExternalMessages'; -import LivechatVisitorsModel from '../models/LivechatVisitors'; -import { LivechatVisitorsRaw } from './LivechatVisitors'; import LivechatInquiryModel from '../models/LivechatInquiry'; -import { LivechatInquiryRaw } from './LivechatInquiry'; -import IntegrationsModel from '../models/Integrations'; -import { IntegrationsRaw } from './Integrations'; -import EmojiCustomModel from '../models/EmojiCustom'; -import { EmojiCustomRaw } from './EmojiCustom'; -import WebdavAccountsModel from '../models/WebdavAccounts'; -import { WebdavAccountsRaw } from './WebdavAccounts'; -import OAuthAppsModel from '../models/OAuthApps'; -import { OAuthAppsRaw } from './OAuthApps'; -import CustomSoundsModel from '../models/CustomSounds'; -import { CustomSoundsRaw } from './CustomSounds'; -import CustomUserStatusModel from '../models/CustomUserStatus'; -import { CustomUserStatusRaw } from './CustomUserStatus'; -import LivechatAgentActivityModel from '../models/LivechatAgentActivity'; -import { LivechatAgentActivityRaw } from './LivechatAgentActivity'; -import StatisticsModel from '../models/Statistics'; -import { StatisticsRaw } from './Statistics'; -import NotificationQueueModel from '../models/NotificationQueue'; -import { NotificationQueueRaw } from './NotificationQueue'; -import LivechatBusinessHoursModel from '../models/LivechatBusinessHours'; -import { LivechatBusinessHoursRaw } from './LivechatBusinessHours'; -import ServerEventModel from '../models/ServerEvents'; -import { UsersSessionsRaw } from './UsersSessions'; -import UsersSessionsModel from '../models/UsersSessions'; -import { ServerEventsRaw } from './ServerEvents'; -import { trash } from '../models/_BaseDb'; +import LivechatRoomsModel from '../models/LivechatRooms'; +import LivechatTriggerModel from '../models/LivechatTrigger'; +import LivechatVisitorsModel from '../models/LivechatVisitors'; import LoginServiceConfigurationModel from '../models/LoginServiceConfiguration'; -import { LoginServiceConfigurationRaw } from './LoginServiceConfiguration'; -import { InstanceStatusRaw } from './InstanceStatus'; -import InstanceStatusModel from '../models/InstanceStatus'; -import { IntegrationHistoryRaw } from './IntegrationHistory'; -import IntegrationHistoryModel from '../models/IntegrationHistory'; +import MessagesModel from '../models/Messages'; import OmnichannelQueueModel from '../models/OmnichannelQueue'; -import { OmnichannelQueueRaw } from './OmnichannelQueue'; -import EmailInboxModel from '../models/EmailInbox'; -import { EmailInboxRaw } from './EmailInbox'; -import EmailMessageHistoryModel from '../models/EmailMessageHistory'; -import { EmailMessageHistoryRaw } from './EmailMessageHistory'; -import { api } from '../../../../server/sdk/api'; -import { initWatchers } from '../../../../server/modules/watchers/watchers.module'; -import ImportDataModel from '../models/ImportData'; -import { ImportDataRaw } from './ImportData'; +import RoomsModel from '../models/Rooms'; +import SettingsModel from '../models/Settings'; +import SubscriptionsModel from '../models/Subscriptions'; +import UsersModel from '../models/Users'; const trashCollection = trash.rawCollection(); -export const Permissions = new PermissionsRaw(PermissionsModel.model.rawCollection(), trashCollection); -export const Subscriptions = new SubscriptionsRaw(SubscriptionsModel.model.rawCollection(), trashCollection); -export const Settings = new SettingsRaw(SettingsModel.model.rawCollection(), trashCollection); export const Users = new UsersRaw(UsersModel.model.rawCollection(), trashCollection); +export const Subscriptions = new SubscriptionsRaw(SubscriptionsModel.model.rawCollection(), { Users }, trashCollection); +export const Settings = new SettingsRaw(SettingsModel.model.rawCollection(), trashCollection); export const Rooms = new RoomsRaw(RoomsModel.model.rawCollection(), trashCollection); export const LivechatCustomField = new LivechatCustomFieldRaw(LivechatCustomFieldModel.model.rawCollection(), trashCollection); export const LivechatTrigger = new LivechatTriggerRaw(LivechatTriggerModel.model.rawCollection(), trashCollection); @@ -88,44 +86,56 @@ export const Messages = new MessagesRaw(MessagesModel.model.rawCollection(), tra export const LivechatExternalMessage = new LivechatExternalMessageRaw(LivechatExternalMessagesModel.model.rawCollection(), trashCollection); export const LivechatVisitors = new LivechatVisitorsRaw(LivechatVisitorsModel.model.rawCollection(), trashCollection); export const LivechatInquiry = new LivechatInquiryRaw(LivechatInquiryModel.model.rawCollection(), trashCollection); -export const Integrations = new IntegrationsRaw(IntegrationsModel.model.rawCollection(), trashCollection); -export const EmojiCustom = new EmojiCustomRaw(EmojiCustomModel.model.rawCollection(), trashCollection); -export const WebdavAccounts = new WebdavAccountsRaw(WebdavAccountsModel.model.rawCollection(), trashCollection); -export const OAuthApps = new OAuthAppsRaw(OAuthAppsModel.model.rawCollection(), trashCollection); -export const CustomSounds = new CustomSoundsRaw(CustomSoundsModel.model.rawCollection(), trashCollection); -export const CustomUserStatus = new CustomUserStatusRaw(CustomUserStatusModel.model.rawCollection(), trashCollection); export const LivechatAgentActivity = new LivechatAgentActivityRaw(LivechatAgentActivityModel.model.rawCollection(), trashCollection); -export const Statistics = new StatisticsRaw(StatisticsModel.model.rawCollection(), trashCollection); -export const NotificationQueue = new NotificationQueueRaw(NotificationQueueModel.model.rawCollection(), trashCollection); export const LivechatBusinessHours = new LivechatBusinessHoursRaw(LivechatBusinessHoursModel.model.rawCollection(), trashCollection); -export const ServerEvents = new ServerEventsRaw(ServerEventModel.model.rawCollection(), trashCollection); -export const Roles = new RolesRaw(RolesModel.model.rawCollection(), trashCollection, { Users, Subscriptions }); -export const UsersSessions = new UsersSessionsRaw(UsersSessionsModel.model.rawCollection(), trashCollection); +// export const Roles = new RolesRaw(RolesModel.model.rawCollection(), { Users, Subscriptions }, trashCollection); export const LoginServiceConfiguration = new LoginServiceConfigurationRaw(LoginServiceConfigurationModel.model.rawCollection(), trashCollection); -export const InstanceStatus = new InstanceStatusRaw(InstanceStatusModel.model.rawCollection(), trashCollection); -export const IntegrationHistory = new IntegrationHistoryRaw(IntegrationHistoryModel.model.rawCollection(), trashCollection); -export const Sessions = new SessionsRaw(SessionsModel.model.rawCollection(), trashCollection); export const OmnichannelQueue = new OmnichannelQueueRaw(OmnichannelQueueModel.model.rawCollection(), trashCollection); -export const EmailInbox = new EmailInboxRaw(EmailInboxModel.model.rawCollection(), trashCollection); -export const EmailMessageHistory = new EmailMessageHistoryRaw(EmailMessageHistoryModel.model.rawCollection(), trashCollection); export const ImportData = new ImportDataRaw(ImportDataModel.model.rawCollection(), trashCollection); +const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; +const prefix = 'rocketchat_'; + +export const Avatars = new AvatarsRaw(db.collection(`${ prefix }avatars`), trashCollection); +export const Analytics = new AnalyticsRaw(db.collection(`${ prefix }analytics`, { readPreference: readSecondaryPreferred(db) }), trashCollection); +export const CustomSounds = new CustomSoundsRaw(db.collection(`${ prefix }custom_sounds`), trashCollection); +export const CustomUserStatus = new CustomUserStatusRaw(db.collection(`${ prefix }custom_user_status`), trashCollection); +export const CredentialTokens = new CredentialTokensRaw(db.collection(`${ prefix }credential_tokens`), trashCollection); +export const EmailInbox = new EmailInboxRaw(db.collection(`${ prefix }email_inbox`), trashCollection); +export const EmailMessageHistory = new EmailMessageHistoryRaw(db.collection(`${ prefix }email_message_history`), trashCollection); +export const EmojiCustom = new EmojiCustomRaw(db.collection(`${ prefix }custom_emoji`), trashCollection); +export const ExportOperations = new ExportOperationsRaw(db.collection(`${ prefix }export_operations`), trashCollection); +export const FederationKeys = new FederationKeysRaw(db.collection(`${ prefix }federation_keys`), trashCollection); +export const FederationServers = new FederationServersRaw(db.collection(`${ prefix }federation_servers`), trashCollection); +export const InstanceStatus = new InstanceStatusRaw(db.collection('instances'), trashCollection, { preventSetUpdatedAt: true }); +export const Integrations = new IntegrationsRaw(db.collection(`${ prefix }integrations`), trashCollection); +export const IntegrationHistory = new IntegrationHistoryRaw(db.collection(`${ prefix }integration_history`), trashCollection); +export const Invites = new InvitesRaw(db.collection(`${ prefix }invites`), trashCollection); +export const NotificationQueue = new NotificationQueueRaw(db.collection(`${ prefix }notification_queue`), trashCollection); +export const OAuthApps = new OAuthAppsRaw(db.collection(`${ prefix }oauth_apps`), trashCollection); +export const OEmbedCache = new OEmbedCacheRaw(db.collection(`${ prefix }oembed_cache`), trashCollection); +export const Permissions = new PermissionsRaw(db.collection(`${ prefix }permissions`), trashCollection); +export const ReadReceipts = new ReadReceiptsRaw(db.collection(`${ prefix }read_receipts`), trashCollection); +export const Reports = new ReportsRaw(db.collection(`${ prefix }reports`), trashCollection); +export const ServerEvents = new ServerEventsRaw(db.collection(`${ prefix }server_events`), trashCollection); +export const Sessions = new SessionsRaw(db.collection(`${ prefix }sessions`), db.collection(`${ prefix }sessions`, { readPreference: readSecondaryPreferred(db) }), trashCollection); +export const Roles = new RolesRaw(db.collection(`${ prefix }roles`), { Users, Subscriptions }, trashCollection); +export const SmarshHistory = new SmarshHistoryRaw(db.collection(`${ prefix }smarsh_history`), trashCollection); +export const Statistics = new StatisticsRaw(db.collection(`${ prefix }statistics`), trashCollection); +export const UsersSessions = new UsersSessionsRaw(db.collection('usersSessions'), trashCollection, { preventSetUpdatedAt: true }); +export const UserDataFiles = new UserDataFilesRaw(db.collection(`${ prefix }user_data_files`), trashCollection); +export const Uploads = new UploadsRaw(db.collection(`${ prefix }uploads`), trashCollection); +export const WebdavAccounts = new WebdavAccountsRaw(db.collection(`${ prefix }webdav_accounts`), trashCollection); + const map = { [Messages.col.collectionName]: MessagesModel, [Users.col.collectionName]: UsersModel, [Subscriptions.col.collectionName]: SubscriptionsModel, [Settings.col.collectionName]: SettingsModel, - [Roles.col.collectionName]: RolesModel, - [Permissions.col.collectionName]: PermissionsModel, [LivechatInquiry.col.collectionName]: LivechatInquiryModel, [LivechatDepartmentAgents.col.collectionName]: LivechatDepartmentAgentsModel, - [UsersSessions.col.collectionName]: UsersSessionsModel, [Rooms.col.collectionName]: RoomsModel, [LoginServiceConfiguration.col.collectionName]: LoginServiceConfigurationModel, - [InstanceStatus.col.collectionName]: InstanceStatusModel, - [IntegrationHistory.col.collectionName]: IntegrationHistoryModel, - [Integrations.col.collectionName]: IntegrationsModel, - [EmailInbox.col.collectionName]: EmailInboxModel, }; if (!process.env.DISABLE_DB_WATCH) { @@ -148,7 +158,7 @@ if (!process.env.DISABLE_DB_WATCH) { }; initWatchers(models, api.broadcastLocal.bind(api), (model, fn) => { - const meteorModel = map[model.col.collectionName]; + const meteorModel = map[model.col.collectionName] || new BaseDbWatch(model.col.collectionName); if (!meteorModel) { return; } diff --git a/app/oauth2-server-config/server/admin/methods/addOAuthApp.js b/app/oauth2-server-config/server/admin/methods/addOAuthApp.js index cb4d73e19f27..688409e5ddf4 100644 --- a/app/oauth2-server-config/server/admin/methods/addOAuthApp.js +++ b/app/oauth2-server-config/server/admin/methods/addOAuthApp.js @@ -3,11 +3,12 @@ import { Random } from 'meteor/random'; import _ from 'underscore'; import { hasPermission } from '../../../../authorization'; -import { Users, OAuthApps } from '../../../../models'; +import { Users } from '../../../../models/server'; +import { OAuthApps } from '../../../../models/server/raw'; import { parseUriList } from '../functions/parseUriList'; Meteor.methods({ - addOAuthApp(application) { + async addOAuthApp(application) { if (!hasPermission(this.userId, 'manage-oauth-apps')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'addOAuthApp' }); } @@ -31,7 +32,7 @@ Meteor.methods({ application.clientSecret = Random.secret(); application._createdAt = new Date(); application._createdBy = Users.findOne(this.userId, { fields: { username: 1 } }); - application._id = OAuthApps.insert(application); + application._id = (await OAuthApps.insertOne(application)).insertedId; return application; }, }); diff --git a/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.js b/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.js index 6c0b1e665de6..d1df82d95704 100644 --- a/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.js +++ b/app/oauth2-server-config/server/admin/methods/deleteOAuthApp.js @@ -1,18 +1,18 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../../authorization'; -import { OAuthApps } from '../../../../models'; +import { OAuthApps } from '../../../../models/server/raw'; Meteor.methods({ - deleteOAuthApp(applicationId) { + async deleteOAuthApp(applicationId) { if (!hasPermission(this.userId, 'manage-oauth-apps')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'deleteOAuthApp' }); } - const application = OAuthApps.findOne(applicationId); + const application = await OAuthApps.findOneById(applicationId); if (application == null) { throw new Meteor.Error('error-application-not-found', 'Application not found', { method: 'deleteOAuthApp' }); } - OAuthApps.remove({ _id: applicationId }); + await OAuthApps.deleteOne({ _id: applicationId }); return true; }, }); diff --git a/app/oauth2-server-config/server/admin/methods/updateOAuthApp.js b/app/oauth2-server-config/server/admin/methods/updateOAuthApp.js index 007f5be2e95c..3a7f88dda09e 100644 --- a/app/oauth2-server-config/server/admin/methods/updateOAuthApp.js +++ b/app/oauth2-server-config/server/admin/methods/updateOAuthApp.js @@ -2,11 +2,12 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; import { hasPermission } from '../../../../authorization'; -import { OAuthApps, Users } from '../../../../models'; +import { OAuthApps } from '../../../../models/server/raw'; +import { Users } from '../../../../models/server'; import { parseUriList } from '../functions/parseUriList'; Meteor.methods({ - updateOAuthApp(applicationId, application) { + async updateOAuthApp(applicationId, application) { if (!hasPermission(this.userId, 'manage-oauth-apps')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'updateOAuthApp' }); } @@ -19,7 +20,7 @@ Meteor.methods({ if (!_.isBoolean(application.active)) { throw new Meteor.Error('error-invalid-arguments', 'Invalid arguments', { method: 'updateOAuthApp' }); } - const currentApplication = OAuthApps.findOne(applicationId); + const currentApplication = await OAuthApps.findOneById(applicationId); if (currentApplication == null) { throw new Meteor.Error('error-application-not-found', 'Application not found', { method: 'updateOAuthApp' }); } @@ -30,7 +31,7 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-redirectUri', 'Invalid redirectUri', { method: 'updateOAuthApp' }); } - OAuthApps.update(applicationId, { + await OAuthApps.updateOne({ _id: applicationId }, { $set: { name: application.name, active: application.active, @@ -43,6 +44,6 @@ Meteor.methods({ }), }, }); - return OAuthApps.findOne(applicationId); + return OAuthApps.findOneById(applicationId); }, }); diff --git a/app/oauth2-server-config/server/oauth/default-services.js b/app/oauth2-server-config/server/oauth/default-services.js deleted file mode 100644 index d39489c9ec85..000000000000 --- a/app/oauth2-server-config/server/oauth/default-services.js +++ /dev/null @@ -1,17 +0,0 @@ -import { OAuthApps } from '../../../models'; - -if (!OAuthApps.findOne('zapier')) { - OAuthApps.insert({ - _id: 'zapier', - name: 'Zapier', - active: true, - clientId: 'zapier', - clientSecret: 'RTK6TlndaCIolhQhZ7_KHIGOKj41RnlaOq_o-7JKwLr', - redirectUri: 'https://zapier.com/dashboard/auth/oauth/return/RocketChatDevAPI/', - _createdAt: new Date(), - _createdBy: { - _id: 'system', - username: 'system', - }, - }); -} diff --git a/app/oauth2-server-config/server/oauth/default-services.ts b/app/oauth2-server-config/server/oauth/default-services.ts new file mode 100644 index 000000000000..05fd8f5c5d35 --- /dev/null +++ b/app/oauth2-server-config/server/oauth/default-services.ts @@ -0,0 +1,20 @@ +import { OAuthApps } from '../../../models/server/raw'; + +async function run(): Promise { + if (!await OAuthApps.findOneById('zapier')) { + await OAuthApps.insertOne({ + _id: 'zapier', + name: 'Zapier', + active: true, + clientId: 'zapier', + clientSecret: 'RTK6TlndaCIolhQhZ7_KHIGOKj41RnlaOq_o-7JKwLr', + redirectUri: 'https://zapier.com/dashboard/auth/oauth/return/RocketChatDevAPI/', + _createdAt: new Date(), + _createdBy: { + _id: 'system', + username: 'system', + }, + }); + } +} +run(); diff --git a/app/oauth2-server-config/server/oauth/oauth2-server.js b/app/oauth2-server-config/server/oauth/oauth2-server.js index 438aaaa2e0e6..c801074db4d5 100644 --- a/app/oauth2-server-config/server/oauth/oauth2-server.js +++ b/app/oauth2-server-config/server/oauth/oauth2-server.js @@ -1,15 +1,18 @@ import { Meteor } from 'meteor/meteor'; +import { Mongo } from 'meteor/mongo'; import { WebApp } from 'meteor/webapp'; import { OAuth2Server } from 'meteor/rocketchat:oauth2-server'; -import { OAuthApps, Users } from '../../../models'; +import { Users } from '../../../models/server'; +import { OAuthApps } from '../../../models/server/raw'; import { API } from '../../../api/server'; const oauth2server = new OAuth2Server({ accessTokensCollectionName: 'rocketchat_oauth_access_tokens', refreshTokensCollectionName: 'rocketchat_oauth_refresh_tokens', authCodesCollectionName: 'rocketchat_oauth_auth_codes', - clientsCollection: OAuthApps.model, + // TODO: Remove workaround. Used to pass meteor collection reference to a package + clientsCollection: new Mongo.Collection(OAuthApps.col.collectionName), debug: true, }); diff --git a/app/oembed/server/server.js b/app/oembed/server/server.js index 05f95cfbc0dc..a3b32443fd1d 100644 --- a/app/oembed/server/server.js +++ b/app/oembed/server/server.js @@ -10,7 +10,8 @@ import ipRangeCheck from 'ip-range-check'; import he from 'he'; import jschardet from 'jschardet'; -import { OEmbedCache, Messages } from '../../models'; +import { Messages } from '../../models/server'; +import { OEmbedCache } from '../../models/server/raw'; import { callbacks } from '../../callbacks'; import { settings } from '../../settings'; import { isURL } from '../../utils/lib/isURL'; @@ -214,8 +215,8 @@ OEmbed.getUrlMeta = function(url, withFragment) { }); }; -OEmbed.getUrlMetaWithCache = function(url, withFragment) { - const cache = OEmbedCache.findOneById(url); +OEmbed.getUrlMetaWithCache = async function(url, withFragment) { + const cache = await OEmbedCache.findOneById(url); if (cache != null) { return cache.data; @@ -223,7 +224,7 @@ OEmbed.getUrlMetaWithCache = function(url, withFragment) { const data = OEmbed.getUrlMeta(url, withFragment); if (data != null) { try { - OEmbedCache.createWithIdAndData(url, data); + await OEmbedCache.createWithIdAndData(url, data); } catch (_error) { SystemLogger.error('OEmbed duplicated record', url); } @@ -262,21 +263,21 @@ const getRelevantMetaTags = function(metaObj) { const insertMaxWidthInOembedHtml = (oembedHtml) => oembedHtml?.replace('iframe', 'iframe style=\"max-width: 100%;width:400px;height:225px\"'); -OEmbed.rocketUrlParser = function(message) { +OEmbed.rocketUrlParser = async function(message) { if (Array.isArray(message.urls)) { - let attachments = []; + const attachments = []; let changed = false; - message.urls.forEach(function(item) { + for await (const item of message.urls) { if (item.ignoreParse === true) { return; } if (!isURL(item.url)) { return; } - const data = OEmbed.getUrlMetaWithCache(item.url); + const data = await OEmbed.getUrlMetaWithCache(item.url); if (data != null) { if (data.attachments) { - attachments = _.union(attachments, data.attachments); + attachments.push(...data.attachments); return; } if (data.meta != null) { @@ -291,7 +292,7 @@ OEmbed.rocketUrlParser = function(message) { item.parsedUrl = data.parsedUrl; changed = true; } - }); + } if (attachments.length) { Messages.setMessageAttachments(message._id, attachments); } @@ -304,7 +305,7 @@ OEmbed.rocketUrlParser = function(message) { settings.watch('API_Embed', function(value) { if (value) { - return callbacks.add('afterSaveMessage', OEmbed.rocketUrlParser, callbacks.priority.LOW, 'API_Embed'); + return callbacks.add('afterSaveMessage', (message) => Promise.await(OEmbed.rocketUrlParser(message)), callbacks.priority.LOW, 'API_Embed'); } return callbacks.remove('afterSaveMessage', 'API_Embed'); }); diff --git a/app/reactions/server/setReaction.js b/app/reactions/server/setReaction.js index 53de3fe70c1f..e5f2a885b308 100644 --- a/app/reactions/server/setReaction.js +++ b/app/reactions/server/setReaction.js @@ -2,11 +2,12 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import _ from 'underscore'; -import { Messages, EmojiCustom, Rooms } from '../../models'; -import { callbacks } from '../../callbacks'; -import { emoji } from '../../emoji'; -import { isTheLastMessage, msgStream } from '../../lib'; -import { hasPermission } from '../../authorization/server/functions/hasPermission'; +import { Messages, Rooms } from '../../models/server'; +import { EmojiCustom } from '../../models/server/raw'; +import { callbacks } from '../../callbacks/server'; +import { emoji } from '../../emoji/server'; +import { isTheLastMessage, msgStream } from '../../lib/server'; +import { canAccessRoom, hasPermission } from '../../authorization/server'; import { api } from '../../../server/sdk/api'; const removeUserReaction = (message, reaction, username) => { @@ -20,7 +21,7 @@ const removeUserReaction = (message, reaction, username) => { async function setReaction(room, user, message, reaction, shouldReact) { reaction = `:${ reaction.replace(/:/g, '') }:`; - if (!emoji.list[reaction] && EmojiCustom.findByNameOrAlias(reaction).count() === 0) { + if (!emoji.list[reaction] && await EmojiCustom.findByNameOrAlias(reaction).count() === 0) { throw new Meteor.Error('error-not-allowed', 'Invalid emoji provided.', { method: 'setReaction' }); } @@ -91,17 +92,19 @@ export const executeSetReaction = async function(reaction, messageId, shouldReac } const message = Messages.findOneById(messageId); - if (!message) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'setReaction' }); } - const room = Meteor.call('canAccessRoom', message.rid, Meteor.userId()); - + const room = Rooms.findOneById(message.rid); if (!room) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'setReaction' }); } + if (!canAccessRoom(room, user)) { + throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'setReaction' }); + } + return setReaction(room, user, message, reaction, shouldReact); }; diff --git a/app/search/server/service/validationService.js b/app/search/server/service/validationService.js index 633d88b7697e..120207bd0e97 100644 --- a/app/search/server/service/validationService.js +++ b/app/search/server/service/validationService.js @@ -1,42 +1,46 @@ import { Meteor } from 'meteor/meteor'; +import mem from 'mem'; import SearchLogger from '../logger/logger'; -import { Users } from '../../../models'; +import { canAccessRoom } from '../../../authorization/server'; +import { Users, Rooms } from '../../../models/server'; class ValidationService { validateSearchResult(result) { - const subscriptionCache = {}; + const getSubscription = mem((rid, uid) => { + if (!rid) { + return; + } - const getSubscription = (rid, uid) => { - if (!subscriptionCache.hasOwnProperty(rid)) { - subscriptionCache[rid] = Meteor.call('canAccessRoom', rid, uid); + const room = Rooms.findOneById(rid); + if (!room) { + return; } - return subscriptionCache[rid]; - }; + if (!canAccessRoom(room, { _id: uid })) { + return; + } - const userCache = {}; + return room; + }); - const getUsername = (uid) => { - if (!userCache.hasOwnProperty(uid)) { - try { - userCache[uid] = Users.findById(uid).fetch()[0].username; - } catch (e) { - userCache[uid] = undefined; - } + const getUser = mem((uid) => { + if (!uid) { + return; } - return userCache[uid]; - }; + return Users.findOneById(uid, { fields: { username: 1 } }); + }); const uid = Meteor.userId(); // get subscription for message if (result.message) { result.message.docs.forEach((msg) => { + const user = getUser(msg.user); const subscription = getSubscription(msg.rid, uid); if (subscription) { msg.r = { name: subscription.name, t: subscription.t }; - msg.username = getUsername(msg.user); + msg.username = user?.username; msg.valid = true; SearchLogger.debug(`user ${ uid } can access ${ msg.rid } ( ${ subscription.t === 'd' ? subscription.username : subscription.name } )`); } else { @@ -44,7 +48,7 @@ class ValidationService { } }); - result.message.docs.filter((msg) => msg.valid); + result.message.docs = result.message.docs.filter((msg) => msg.valid); } if (result.room) { @@ -60,7 +64,7 @@ class ValidationService { } }); - result.room.docs.filter((room) => room.valid); + result.room.docs = result.room.docs.filter((room) => room.valid); } return result; diff --git a/app/smarsh-connector/server/functions/generateEml.js b/app/smarsh-connector/server/functions/generateEml.js index 8633fd1ca0ca..f828ce310387 100644 --- a/app/smarsh-connector/server/functions/generateEml.js +++ b/app/smarsh-connector/server/functions/generateEml.js @@ -4,7 +4,8 @@ import _ from 'underscore'; import moment from 'moment'; import { settings } from '../../../settings'; -import { Rooms, Messages, Users, SmarshHistory } from '../../../models'; +import { Rooms, Messages, Users } from '../../../models/server'; +import { SmarshHistory } from '../../../models/server/raw'; import { MessageTypes } from '../../../ui-utils'; import { smarsh } from '../lib/rocketchat'; import 'moment-timezone'; @@ -31,8 +32,8 @@ smarsh.generateEml = () => { const smarshMissingEmail = settings.get('Smarsh_MissingEmail_Email'); const timeZone = settings.get('Smarsh_Timezone'); - Rooms.find().forEach((room) => { - const smarshHistory = SmarshHistory.findOne({ _id: room._id }); + Rooms.find().forEach(async (room) => { + const smarshHistory = await SmarshHistory.findOne({ _id: room._id }); const query = { rid: room._id }; if (smarshHistory) { diff --git a/app/smarsh-connector/server/functions/sendEmail.js b/app/smarsh-connector/server/functions/sendEmail.js index 9b69b05b3ac1..67fcfde02e67 100644 --- a/app/smarsh-connector/server/functions/sendEmail.js +++ b/app/smarsh-connector/server/functions/sendEmail.js @@ -4,19 +4,18 @@ // subject: 'Rocket.Chat, 17 Users, 24 Messages, 1 File, 799504 Minutes, in #random', // files: ['i3nc9l3mn'] // } -import _ from 'underscore'; import { UploadFS } from 'meteor/jalik:ufs'; import * as Mailer from '../../../mailer'; -import { Uploads } from '../../../models'; +import { Uploads } from '../../../models/server/raw'; import { settings } from '../../../settings'; import { smarsh } from '../lib/rocketchat'; -smarsh.sendEmail = (data) => { +smarsh.sendEmail = async (data) => { const attachments = []; - _.each(data.files, (fileId) => { - const file = Uploads.findOneById(fileId); + for await (const fileId of data.files) { + const file = await Uploads.findOneById(fileId); if (file.store === 'rocketchat_uploads' || file.store === 'fileSystem') { const rs = UploadFS.getStore(file.store).getReadStream(fileId, file); attachments.push({ @@ -24,8 +23,7 @@ smarsh.sendEmail = (data) => { streamSource: rs, }); } - }); - + } Mailer.sendNoWrap({ to: settings.get('Smarsh_Email'), diff --git a/app/statistics/server/lib/SAUMonitor.js b/app/statistics/server/lib/SAUMonitor.js index 36b7036fb5a0..43d41cad7e33 100644 --- a/app/statistics/server/lib/SAUMonitor.js +++ b/app/statistics/server/lib/SAUMonitor.js @@ -4,9 +4,9 @@ import { SyncedCron } from 'meteor/littledata:synced-cron'; import UAParser from 'ua-parser-js'; import { UAParserMobile, UAParserDesktop } from './UAParserCustom'; -import { Sessions } from '../../../models/server'; +import { Sessions } from '../../../models/server/raw'; +import { aggregates } from '../../../models/server/raw/Sessions'; import { Logger } from '../../../logger'; -import { aggregates } from '../../../models/server/models/Sessions'; import { getMostImportantRole } from './getMostImportantRole'; const getDateObj = (dateTime = new Date()) => ({ @@ -32,7 +32,7 @@ export class SAUMonitorClass { this._jobName = 'aggregate-sessions'; } - start(instanceId) { + async start(instanceId) { if (this.isRunning()) { return; } @@ -44,7 +44,7 @@ export class SAUMonitorClass { return; } - this._startMonitoring(() => { + await this._startMonitoring(() => { this._started = true; logger.debug(`[start] - InstanceId: ${ this._instanceId }`); }); @@ -70,12 +70,12 @@ export class SAUMonitorClass { return this._started === true; } - _startMonitoring(callback) { + async _startMonitoring(callback) { try { this._handleAccountEvents(); this._handleOnConnection(); this._startSessionControl(); - this._initActiveServerSessions(); + await this._initActiveServerSessions(); this._startAggregation(); if (callback) { callback(); @@ -94,8 +94,8 @@ export class SAUMonitorClass { return; } - this._timer = Meteor.setInterval(() => { - this._updateActiveSessions(); + this._timer = Meteor.setInterval(async () => { + await this._updateActiveSessions(); }, this._monitorTime); } @@ -110,8 +110,8 @@ export class SAUMonitorClass { } // this._handleSession(connection, getDateObj()); - connection.onClose(() => { - Sessions.closeByInstanceIdAndSessionId(this._instanceId, connection.id); + connection.onClose(async () => { + await Sessions.closeByInstanceIdAndSessionId(this._instanceId, connection.id); }); }); } @@ -121,7 +121,7 @@ export class SAUMonitorClass { return; } - Accounts.onLogin((info) => { + Accounts.onLogin(async (info) => { if (!this.isRunning()) { return; } @@ -132,11 +132,11 @@ export class SAUMonitorClass { const loginAt = new Date(); const params = { userId, roles, mostImportantRole, loginAt, ...getDateObj() }; - this._handleSession(info.connection, params); + await this._handleSession(info.connection, params); this._updateConnectionInfo(info.connection.id, { loginAt }); }); - Accounts.onLogout((info) => { + Accounts.onLogout(async (info) => { if (!this.isRunning()) { return; } @@ -144,17 +144,17 @@ export class SAUMonitorClass { const sessionId = info.connection.id; if (info.user) { const userId = info.user._id; - Sessions.logoutByInstanceIdAndSessionIdAndUserId(this._instanceId, sessionId, userId); + await Sessions.logoutByInstanceIdAndSessionIdAndUserId(this._instanceId, sessionId, userId); } }); } - _handleSession(connection, params) { + async _handleSession(connection, params) { const data = this._getConnectionInfo(connection, params); - Sessions.createOrUpdate(data); + await Sessions.createOrUpdate(data); } - _updateActiveSessions() { + async _updateActiveSessions() { if (!this.isRunning()) { return; } @@ -167,8 +167,8 @@ export class SAUMonitorClass { const beforeDateTime = new Date(this._today.year, this._today.month - 1, this._today.day, 23, 59, 59, 999); const nextDateTime = new Date(currentDay.year, currentDay.month - 1, currentDay.day); - const createSessions = (objects, ids) => { - Sessions.createBatch(objects); + const createSessions = async (objects, ids) => { + await Sessions.createBatch(objects); Meteor.defer(() => { Sessions.updateActiveSessionsByDateAndInstanceIdAndIds({ year, month, day }, this._instanceId, ids, { lastActivityAt: beforeDateTime }); @@ -180,8 +180,8 @@ export class SAUMonitorClass { } // Otherwise, just update the lastActivityAt field - this._applyAllServerSessionsIds((sessions) => { - Sessions.updateActiveSessionsByDateAndInstanceIdAndIds({ year, month, day }, this._instanceId, sessions, { lastActivityAt: currentDateTime }); + await this._applyAllServerSessionsIds(async (sessions) => { + await Sessions.updateActiveSessionsByDateAndInstanceIdAndIds({ year, month, day }, this._instanceId, sessions, { lastActivityAt: currentDateTime }); }); } @@ -266,32 +266,38 @@ export class SAUMonitorClass { }; } - _initActiveServerSessions() { - this._applyAllServerSessions((connectionHandle) => { - this._handleSession(connectionHandle, getDateObj()); + async _initActiveServerSessions() { + await this._applyAllServerSessions(async (connectionHandle) => { + await this._handleSession(connectionHandle, getDateObj()); }); } - _applyAllServerSessions(callback) { + async _applyAllServerSessions(callback) { if (!callback || typeof callback !== 'function') { return; } const sessions = Object.values(Meteor.server.sessions).filter((session) => session.userId); - sessions.forEach((session) => { - callback(session.connectionHandle); - }); + for await (const session of sessions) { + await callback(session.connectionHandle); + } } - _applyAllServerSessionsIds(callback) { + async recursive(callback, sessionIds) { + await callback(sessionIds.splice(0, 500)); + + if (sessionIds.length) { + await this.recursive(callback, sessionIds); + } + } + + async _applyAllServerSessionsIds(callback) { if (!callback || typeof callback !== 'function') { return; } const sessionIds = Object.values(Meteor.server.sessions).filter((session) => session.userId).map((s) => s.id); - while (sessionIds.length) { - callback(sessionIds.splice(0, 500)); - } + await this.recursive(callback, sessionIds); } _updateConnectionInfo(sessionId, data = {}) { @@ -315,8 +321,8 @@ export class SAUMonitorClass { return Promise.all(arr.splice(0, limit).map((item) => { ids.push(item.id); return this._getConnectionInfo(item.connectionHandle, params); - })).then((data) => { - callback(data, ids); + })).then(async (data) => { + await callback(data, ids); return batch(arr, limit); }).catch((e) => { logger.debug(`Error: ${ e.message }`); @@ -333,13 +339,13 @@ export class SAUMonitorClass { SyncedCron.add({ name: this._jobName, schedule: (parser) => parser.text('at 2:00 am'), - job: () => { - this.aggregate(); + job: async () => { + await this.aggregate(); }, }); } - aggregate() { + async aggregate() { if (!this.isRunning()) { return; } @@ -357,16 +363,16 @@ export class SAUMonitorClass { day: { $lte: yesterday.day }, }; - aggregates.dailySessionsOfYesterday(Sessions.model.rawCollection(), yesterday).forEach(Meteor.bindEnvironment((record) => { + await aggregates.dailySessionsOfYesterday(Sessions.col, yesterday).forEach(async (record) => { record._id = `${ record.userId }-${ record.year }-${ record.month }-${ record.day }`; - Sessions.upsert({ _id: record._id }, record); - })); + await Sessions.updateOne({ _id: record._id }, record, { upsert: true }); + }); - Sessions.update(match, { + await Sessions.updateMany(match, { $set: { type: 'computed-session', _computedAt: new Date(), }, - }, { multi: true }); + }); } } diff --git a/app/statistics/server/lib/statistics.js b/app/statistics/server/lib/statistics.js index b4ffb9f7dd25..c9d99c049dc5 100644 --- a/app/statistics/server/lib/statistics.js +++ b/app/statistics/server/lib/statistics.js @@ -3,24 +3,21 @@ import os from 'os'; import _ from 'underscore'; import { Meteor } from 'meteor/meteor'; import { InstanceStatus } from 'meteor/konecty:multiple-instances-status'; +import { MongoInternals } from 'meteor/mongo'; import { - Sessions, Settings, Users, Rooms, Subscriptions, - Uploads, Messages, LivechatVisitors, - Integrations, - Statistics, } from '../../../models/server'; import { settings } from '../../../settings/server'; import { Info, getMongoInfo } from '../../../utils/server'; import { getControl } from '../../../../server/lib/migrations'; import { getStatistics as federationGetStatistics } from '../../../federation/server/functions/dashboard'; -import { NotificationQueue, Users as UsersRaw } from '../../../models/server/raw'; +import { NotificationQueue, Users as UsersRaw, Statistics, Sessions, Integrations, Uploads } from '../../../models/server/raw'; import { readSecondaryPreferred } from '../../../../server/database/readSecondaryPreferred'; import { getAppsStatistics } from './getAppsStatistics'; import { getServicesStatistics } from './getServicesStatistics'; @@ -55,9 +52,11 @@ const getUserLanguages = (totalUsers) => { return languages; }; +const { db } = MongoInternals.defaultRemoteCollectionDriver().mongo; + export const statistics = { get: function _getStatistics() { - const readPreference = readSecondaryPreferred(Uploads.model.rawDatabase()); + const readPreference = readSecondaryPreferred(db); const statistics = {}; @@ -162,8 +161,8 @@ export const statistics = { statistics.enterpriseReady = true; - statistics.uploadsTotal = Uploads.find().count(); - const [result] = Promise.await(Uploads.model.rawCollection().aggregate([{ + statistics.uploadsTotal = Promise.await(Uploads.find().count()); + const [result] = Promise.await(Uploads.col.aggregate([{ $group: { _id: 'total', total: { $sum: '$size' } }, }], { readPreference }).toArray()); statistics.uploadsTotalSize = result ? result.total : 0; @@ -176,20 +175,20 @@ export const statistics = { statistics.mongoVersion = mongoVersion; statistics.mongoStorageEngine = mongoStorageEngine; - statistics.uniqueUsersOfYesterday = Sessions.getUniqueUsersOfYesterday(); - statistics.uniqueUsersOfLastWeek = Sessions.getUniqueUsersOfLastWeek(); - statistics.uniqueUsersOfLastMonth = Sessions.getUniqueUsersOfLastMonth(); - statistics.uniqueDevicesOfYesterday = Sessions.getUniqueDevicesOfYesterday(); - statistics.uniqueDevicesOfLastWeek = Sessions.getUniqueDevicesOfLastWeek(); - statistics.uniqueDevicesOfLastMonth = Sessions.getUniqueDevicesOfLastMonth(); - statistics.uniqueOSOfYesterday = Sessions.getUniqueOSOfYesterday(); - statistics.uniqueOSOfLastWeek = Sessions.getUniqueOSOfLastWeek(); - statistics.uniqueOSOfLastMonth = Sessions.getUniqueOSOfLastMonth(); + statistics.uniqueUsersOfYesterday = Promise.await(Sessions.getUniqueUsersOfYesterday()); + statistics.uniqueUsersOfLastWeek = Promise.await(Sessions.getUniqueUsersOfLastWeek()); + statistics.uniqueUsersOfLastMonth = Promise.await(Sessions.getUniqueUsersOfLastMonth()); + statistics.uniqueDevicesOfYesterday = Promise.await(Sessions.getUniqueDevicesOfYesterday()); + statistics.uniqueDevicesOfLastWeek = Promise.await(Sessions.getUniqueDevicesOfLastWeek()); + statistics.uniqueDevicesOfLastMonth = Promise.await(Sessions.getUniqueDevicesOfLastMonth()); + statistics.uniqueOSOfYesterday = Promise.await(Sessions.getUniqueOSOfYesterday()); + statistics.uniqueOSOfLastWeek = Promise.await(Sessions.getUniqueOSOfLastWeek()); + statistics.uniqueOSOfLastMonth = Promise.await(Sessions.getUniqueOSOfLastMonth()); statistics.apps = getAppsStatistics(); statistics.services = getServicesStatistics(); - const integrations = Promise.await(Integrations.model.rawCollection().find({}, { + const integrations = Promise.await(Integrations.find({}, { projection: { _id: 0, type: 1, @@ -215,10 +214,10 @@ export const statistics = { return statistics; }, - save() { + async save() { const rcStatistics = statistics.get(); rcStatistics.createdAt = new Date(); - Statistics.insert(rcStatistics); + await Statistics.insertOne(rcStatistics); return rcStatistics; }, }; diff --git a/app/threads/server/methods/followMessage.js b/app/threads/server/methods/followMessage.js index 4ab2e59e2945..642c32e3b633 100644 --- a/app/threads/server/methods/followMessage.js +++ b/app/threads/server/methods/followMessage.js @@ -4,6 +4,7 @@ import { check } from 'meteor/check'; import { Messages } from '../../../models/server'; import { RateLimiter } from '../../../lib/server'; import { settings } from '../../../settings/server'; +import { canAccessRoom } from '../../../authorization/server'; import { follow } from '../functions'; Meteor.methods({ @@ -24,8 +25,7 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-message', 'Invalid message', { method: 'followMessage' }); } - const room = Meteor.call('canAccessRoom', message.rid, uid); - if (!room) { + if (!canAccessRoom({ _id: message.rid }, { _id: uid })) { throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'followMessage' }); } diff --git a/app/threads/server/methods/unfollowMessage.js b/app/threads/server/methods/unfollowMessage.js index 743d9bd5e719..a5ed0fa50c6a 100644 --- a/app/threads/server/methods/unfollowMessage.js +++ b/app/threads/server/methods/unfollowMessage.js @@ -4,6 +4,7 @@ import { check } from 'meteor/check'; import { Messages } from '../../../models/server'; import { RateLimiter } from '../../../lib/server'; import { settings } from '../../../settings/server'; +import { canAccessRoom } from '../../../authorization/server'; import { unfollow } from '../functions'; Meteor.methods({ @@ -21,12 +22,11 @@ Meteor.methods({ const message = Messages.findOneById(mid, { fields: { rid: 1, tmid: 1 } }); if (!message) { - throw new Meteor.Error('error-invalid-message', 'Invalid message', { method: 'followMessage' }); + throw new Meteor.Error('error-invalid-message', 'Invalid message', { method: 'unfollowMessage' }); } - const room = Meteor.call('canAccessRoom', message.rid, uid); - if (!room) { - throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'followMessage' }); + if (!canAccessRoom({ _id: message.rid }, { _id: uid })) { + throw new Meteor.Error('error-not-allowed', 'not-allowed', { method: 'unfollowMessage' }); } return unfollow({ rid: message.rid, tmid: message.tmid || message._id, uid }); diff --git a/app/ui-sidenav/client/roomList.js b/app/ui-sidenav/client/roomList.js index de128304d349..cab1983ad98d 100644 --- a/app/ui-sidenav/client/roomList.js +++ b/app/ui-sidenav/client/roomList.js @@ -172,6 +172,7 @@ const mergeSubRoom = (subscription) => { livechatData: 1, departmentId: 1, source: 1, + queuedAt: 1, }, }; @@ -212,6 +213,7 @@ const mergeSubRoom = (subscription) => { departmentId, ts, source, + queuedAt, } = room; subscription.lm = subscription.lr ? new Date(Math.max(subscription.lr, lastRoomUpdate)) : lastRoomUpdate; @@ -249,6 +251,7 @@ const mergeSubRoom = (subscription) => { departmentId, ts, source, + queuedAt, }); }; @@ -291,6 +294,7 @@ const mergeRoomSub = (room) => { departmentId, ts, source, + queuedAt, } = room; Subscriptions.update({ @@ -328,6 +332,7 @@ const mergeRoomSub = (room) => { jitsiTimeout, ts, source, + queuedAt, ...getLowerCaseNames(room, sub.name, sub.fname), }, }); diff --git a/app/user-data-download/server/cronProcessDownloads.js b/app/user-data-download/server/cronProcessDownloads.js index 8fb19b86b08c..35c8de44af20 100644 --- a/app/user-data-download/server/cronProcessDownloads.js +++ b/app/user-data-download/server/cronProcessDownloads.js @@ -10,7 +10,8 @@ import moment from 'moment'; import { v4 as uuidv4 } from 'uuid'; import { settings } from '../../settings/server'; -import { Subscriptions, Rooms, Users, Uploads, Messages, UserDataFiles, ExportOperations, Avatars } from '../../models/server'; +import { Subscriptions, Rooms, Users, Messages } from '../../models/server'; +import { Avatars, ExportOperations, UserDataFiles, Uploads } from '../../models/server/raw'; import { FileUpload } from '../../file-upload/server'; import { DataExport } from './DataExport'; import * as Mailer from '../../mailer'; @@ -186,8 +187,8 @@ const getMessageData = function(msg, hideUsers, userData, usersMap) { return messageObject; }; -export const copyFile = function(attachmentData, assetsPath) { - const file = Uploads.findOneById(attachmentData._id); +export const copyFile = async function(attachmentData, assetsPath) { + const file = await Uploads.findOneById(attachmentData._id); if (!file) { return; } @@ -439,12 +440,12 @@ const generateUserFile = function(exportOperation, userData) { } }; -const generateUserAvatarFile = function(exportOperation, userData) { +const generateUserAvatarFile = async function(exportOperation, userData) { if (!userData) { return; } - const file = Avatars.findOneByName(userData.username); + const file = await Avatars.findOneByName(userData.username); if (!file) { return; } @@ -478,7 +479,7 @@ const continueExportOperation = async function(exportOperation) { } if (!exportOperation.generatedAvatar) { - generateUserAvatarFile(exportOperation, exportOperation.userData); + await generateUserAvatarFile(exportOperation, exportOperation.userData); } if (exportOperation.status === 'exporting-rooms') { @@ -511,9 +512,9 @@ const continueExportOperation = async function(exportOperation) { const generatedFileName = uuidv4(); if (exportOperation.status === 'downloading') { - exportOperation.fileList.forEach((attachmentData) => { - copyFile(attachmentData, exportOperation.assetsPath); - }); + for await (const attachmentData of exportOperation.fileList) { + await copyFile(attachmentData, exportOperation.assetsPath); + } const targetFile = joinPath(zipFolder, `${ generatedFileName }.zip`); if (await fsExists(targetFile)) { @@ -539,17 +540,17 @@ const continueExportOperation = async function(exportOperation) { exportOperation.fileId = fileId; exportOperation.status = 'completed'; - ExportOperations.updateOperation(exportOperation); + await ExportOperations.updateOperation(exportOperation); } - ExportOperations.updateOperation(exportOperation); + await ExportOperations.updateOperation(exportOperation); } catch (e) { console.error(e); } }; async function processDataDownloads() { - const operation = ExportOperations.findOnePending(); + const operation = await ExportOperations.findOnePending(); if (!operation) { return; } @@ -571,7 +572,7 @@ async function processDataDownloads() { await ExportOperations.updateOperation(operation); if (operation.status === 'completed') { - const file = operation.fileId ? UserDataFiles.findOneById(operation.fileId) : UserDataFiles.findLastFileByUser(operation.userId); + const file = operation.fileId ? await UserDataFiles.findOneById(operation.fileId) : await UserDataFiles.findLastFileByUser(operation.userId); if (!file) { return; } diff --git a/app/user-data-download/server/exportDownload.js b/app/user-data-download/server/exportDownload.js index d99eda5c88e6..6ea406de307c 100644 --- a/app/user-data-download/server/exportDownload.js +++ b/app/user-data-download/server/exportDownload.js @@ -1,12 +1,12 @@ import { WebApp } from 'meteor/webapp'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { UserDataFiles } from '../../models'; +import { UserDataFiles } from '../../models/server/raw'; import { DataExport } from './DataExport'; import { settings } from '../../settings/server'; -WebApp.connectHandlers.use(DataExport.getPath(), function(req, res, next) { +WebApp.connectHandlers.use(DataExport.getPath(), async function(req, res, next) { const match = /^\/([^\/]+)/.exec(req.url); if (!settings.get('UserData_EnableDownload')) { @@ -16,7 +16,7 @@ WebApp.connectHandlers.use(DataExport.getPath(), function(req, res, next) { } if (match && match[1]) { - const file = UserDataFiles.findOneById(match[1]); + const file = await UserDataFiles.findOneById(match[1]); if (file) { if (!DataExport.requestCanAccessFiles(req, file.userId)) { res.setHeader('Content-Type', 'text/html; charset=UTF-8'); diff --git a/app/user-status/server/methods/deleteCustomUserStatus.js b/app/user-status/server/methods/deleteCustomUserStatus.js index e81a8140d718..6935266a7475 100644 --- a/app/user-status/server/methods/deleteCustomUserStatus.js +++ b/app/user-status/server/methods/deleteCustomUserStatus.js @@ -1,21 +1,21 @@ import { Meteor } from 'meteor/meteor'; import { hasPermission } from '../../../authorization/server'; -import { CustomUserStatus } from '../../../models/server'; +import { CustomUserStatus } from '../../../models/server/raw'; import { api } from '../../../../server/sdk/api'; Meteor.methods({ - deleteCustomUserStatus(userStatusID) { + async deleteCustomUserStatus(userStatusID) { if (!hasPermission(this.userId, 'manage-user-status')) { throw new Meteor.Error('not_authorized'); } - const userStatus = CustomUserStatus.findOneById(userStatusID); + const userStatus = await CustomUserStatus.findOneById(userStatusID); if (userStatus == null) { throw new Meteor.Error('Custom_User_Status_Error_Invalid_User_Status', 'Invalid user status', { method: 'deleteCustomUserStatus' }); } - CustomUserStatus.removeById(userStatusID); + await CustomUserStatus.removeById(userStatusID); api.broadcast('user.deleteCustomStatus', userStatus); return true; diff --git a/app/user-status/server/methods/insertOrUpdateUserStatus.js b/app/user-status/server/methods/insertOrUpdateUserStatus.js index a01cc751c525..ebc0e918acba 100644 --- a/app/user-status/server/methods/insertOrUpdateUserStatus.js +++ b/app/user-status/server/methods/insertOrUpdateUserStatus.js @@ -2,11 +2,11 @@ import { Meteor } from 'meteor/meteor'; import s from 'underscore.string'; import { hasPermission } from '../../../authorization'; -import { CustomUserStatus } from '../../../models'; +import { CustomUserStatus } from '../../../models/server/raw'; import { api } from '../../../../server/sdk/api'; Meteor.methods({ - insertOrUpdateUserStatus(userStatusData) { + async insertOrUpdateUserStatus(userStatusData) { if (!hasPermission(this.userId, 'manage-user-status')) { throw new Meteor.Error('not_authorized'); } @@ -26,9 +26,9 @@ Meteor.methods({ let matchingResults = []; if (userStatusData._id) { - matchingResults = CustomUserStatus.findByNameExceptId(userStatusData.name, userStatusData._id).fetch(); + matchingResults = await CustomUserStatus.findByNameExceptId(userStatusData.name, userStatusData._id).toArray(); } else { - matchingResults = CustomUserStatus.findByName(userStatusData.name).fetch(); + matchingResults = await CustomUserStatus.findByName(userStatusData.name).toArray(); } if (matchingResults.length > 0) { @@ -47,7 +47,7 @@ Meteor.methods({ statusType: userStatusData.statusType || null, }; - const _id = CustomUserStatus.create(createUserStatus); + const _id = await (await CustomUserStatus.create(createUserStatus)).insertedId; api.broadcast('user.updateCustomStatus', createUserStatus); @@ -56,11 +56,11 @@ Meteor.methods({ // update User status if (userStatusData.name !== userStatusData.previousName) { - CustomUserStatus.setName(userStatusData._id, userStatusData.name); + await CustomUserStatus.setName(userStatusData._id, userStatusData.name); } if (userStatusData.statusType !== userStatusData.previousStatusType) { - CustomUserStatus.setStatusType(userStatusData._id, userStatusData.statusType); + await CustomUserStatus.setStatusType(userStatusData._id, userStatusData.statusType); } api.broadcast('user.updateCustomStatus', userStatusData); diff --git a/app/user-status/server/methods/listCustomUserStatus.js b/app/user-status/server/methods/listCustomUserStatus.js index a47f1ff01bc2..b8ec637d99b6 100644 --- a/app/user-status/server/methods/listCustomUserStatus.js +++ b/app/user-status/server/methods/listCustomUserStatus.js @@ -1,14 +1,14 @@ import { Meteor } from 'meteor/meteor'; -import { CustomUserStatus } from '../../../models'; +import { CustomUserStatus } from '../../../models/server/raw'; Meteor.methods({ - listCustomUserStatus() { + async listCustomUserStatus() { const currentUserId = Meteor.userId(); if (!currentUserId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'listCustomUserStatus' }); } - return CustomUserStatus.find({}).fetch(); + return CustomUserStatus.find({}).toArray(); }, }); diff --git a/app/utils/lib/getURL.tests.js b/app/utils/lib/getURL.tests.js index 73d6d0186550..5ef7af6a4888 100644 --- a/app/utils/lib/getURL.tests.js +++ b/app/utils/lib/getURL.tests.js @@ -1,4 +1,3 @@ -/* eslint-disable complexity */ /* eslint-env mocha */ import 'babel-polyfill'; import assert from 'assert'; @@ -89,12 +88,6 @@ const testCases = (options) => { } } } else if (options._cdn_prefix === '') { - if (options.full && !options.cdn && !options.cloud) { - it('should return with host if full: true', () => { - testPaths(options, (path) => _site_url + path); - }); - } - if (!options.full && options.cdn) { it('should return with cloud host if cdn: true', () => { testPaths(options, (path) => getCloudUrl(_site_url, path)); @@ -106,88 +99,54 @@ const testCases = (options) => { testPaths(options, (path) => getCloudUrl(_site_url, path)); }); } - - if (options.full && options.cdn && !options.cloud) { - it('should return with host if full: true and cdn: true', () => { - testPaths(options, (path) => _site_url + path); - }); - } - } else { - if (options.full && !options.cdn && !options.cloud) { - it('should return with host if full: true', () => { - testPaths(options, (path) => _site_url + path); - }); - } - - if (!options.full && options.cdn && !options.cloud) { - it('should return with cdn prefix if cdn: true', () => { - testPaths(options, (path) => options._cdn_prefix + path); - }); - } - - if (!options.full && !options.cdn) { - it('should return with cloud host if full: fase and cdn: false', () => { - testPaths(options, (path) => getCloudUrl(_site_url, path)); - }); - } - - if (options.full && options.cdn && !options.cloud) { - it('should return with host if full: true and cdn: true', () => { - testPaths(options, (path) => options._cdn_prefix + path); - }); - } + } else if (!options.full && !options.cdn) { + it('should return with cloud host if full: fase and cdn: false', () => { + testPaths(options, (path) => getCloudUrl(_site_url, path)); + }); } }; -const testOptions = (options) => { - testCases({ ...options, cdn: false, full: false, cloud: false }); - testCases({ ...options, cdn: true, full: false, cloud: false }); - testCases({ ...options, cdn: false, full: true, cloud: false }); - testCases({ ...options, cdn: false, full: false, cloud: true }); - testCases({ ...options, cdn: true, full: true, cloud: false }); - testCases({ ...options, cdn: false, full: true, cloud: true }); - testCases({ ...options, cdn: true, full: false, cloud: true }); - testCases({ ...options, cdn: true, full: true, cloud: true }); +const testCasesForOptions = (description, options) => { + describe(description, () => { + testCases({ ...options, cdn: false, full: false, cloud: false }); + testCases({ ...options, cdn: true, full: false, cloud: false }); + testCases({ ...options, cdn: false, full: true, cloud: false }); + testCases({ ...options, cdn: false, full: false, cloud: true }); + testCases({ ...options, cdn: true, full: true, cloud: false }); + testCases({ ...options, cdn: false, full: true, cloud: true }); + testCases({ ...options, cdn: true, full: false, cloud: true }); + testCases({ ...options, cdn: true, full: true, cloud: true }); + }); }; -describe('getURL', () => { - describe('getURL with no CDN, no PREFIX for http://localhost:3000/', () => { - testOptions({ - _cdn_prefix: '', - _root_url_path_prefix: '', - _site_url: 'http://localhost:3000/', - }); +describe.only('getURL', () => { + testCasesForOptions('getURL with no CDN, no PREFIX for http://localhost:3000/', { + _cdn_prefix: '', + _root_url_path_prefix: '', + _site_url: 'http://localhost:3000/', }); - describe('getURL with no CDN, no PREFIX for http://localhost:3000', () => { - testOptions({ - _cdn_prefix: '', - _root_url_path_prefix: '', - _site_url: 'http://localhost:3000', - }); + testCasesForOptions('getURL with no CDN, no PREFIX for http://localhost:3000', { + _cdn_prefix: '', + _root_url_path_prefix: '', + _site_url: 'http://localhost:3000', }); - describe('getURL with CDN, no PREFIX for http://localhost:3000/', () => { - testOptions({ - _cdn_prefix: 'https://cdn.com', - _root_url_path_prefix: '', - _site_url: 'http://localhost:3000/', - }); + testCasesForOptions('getURL with CDN, no PREFIX for http://localhost:3000/', { + _cdn_prefix: 'https://cdn.com', + _root_url_path_prefix: '', + _site_url: 'http://localhost:3000/', }); - describe('getURL with CDN, PREFIX for http://localhost:3000/', () => { - testOptions({ - _cdn_prefix: 'https://cdn.com', - _root_url_path_prefix: 'sub', - _site_url: 'http://localhost:3000/', - }); + testCasesForOptions('getURL with CDN, PREFIX for http://localhost:3000/', { + _cdn_prefix: 'https://cdn.com', + _root_url_path_prefix: 'sub', + _site_url: 'http://localhost:3000/', }); - describe('getURL with CDN, PREFIX for https://localhost:3000/', () => { - testOptions({ - _cdn_prefix: 'https://cdn.com', - _root_url_path_prefix: 'sub', - _site_url: 'https://localhost:3000/', - }); + testCasesForOptions('getURL with CDN, PREFIX for https://localhost:3000/', { + _cdn_prefix: 'https://cdn.com', + _root_url_path_prefix: 'sub', + _site_url: 'https://localhost:3000/', }); }); diff --git a/app/utils/server/functions/normalizeMessageFileUpload.js b/app/utils/server/functions/normalizeMessageFileUpload.js index 450396b0b765..948dc047c252 100644 --- a/app/utils/server/functions/normalizeMessageFileUpload.js +++ b/app/utils/server/functions/normalizeMessageFileUpload.js @@ -1,11 +1,11 @@ import { getURL } from '../../lib/getURL'; import { FileUpload } from '../../../file-upload/server'; -import { Uploads } from '../../../models/server'; +import { Uploads } from '../../../models/server/raw'; -export const normalizeMessageFileUpload = (message) => { +export const normalizeMessageFileUpload = async (message) => { if (message.file && !message.fileUpload) { const jwt = FileUpload.generateJWTToFileUrls({ rid: message.rid, userId: message.u._id, fileId: message.file._id }); - const file = Uploads.findOne({ _id: message.file._id }); + const file = await Uploads.findOne({ _id: message.file._id }); if (!file) { return message; } diff --git a/app/version-check/server/functions/checkVersionUpdate.js b/app/version-check/server/functions/checkVersionUpdate.js index 65cd246d663c..9933081a038c 100644 --- a/app/version-check/server/functions/checkVersionUpdate.js +++ b/app/version-check/server/functions/checkVersionUpdate.js @@ -42,7 +42,7 @@ export default () => { if (update.exists) { Settings.updateValueById('Update_LatestAvailableVersion', update.lastestVersion.version); - sendMessagesToAdmins({ + Promise.await(sendMessagesToAdmins({ msgs: ({ adminUser }) => [{ msg: `*${ TAPi18n.__('Update_your_RocketChat', adminUser.language) }*\n${ TAPi18n.__('New_version_available_(s)', update.lastestVersion.version, adminUser.language) }\n${ update.lastestVersion.infoUrl }` }], banners: [{ id: `versionUpdate-${ update.lastestVersion.version }`.replace(/\./g, '_'), @@ -52,11 +52,11 @@ export default () => { textArguments: [update.lastestVersion.version], link: update.lastestVersion.infoUrl, }], - }); + })); } if (alerts && alerts.length) { - sendMessagesToAdmins({ + Promise.await(sendMessagesToAdmins({ msgs: ({ adminUser }) => alerts .filter((alert) => !Users.bannerExistsById(adminUser._id, `alert-${ alert.id }`)) .map((alert) => ({ @@ -71,6 +71,6 @@ export default () => { modifiers: alert.modifiers, link: alert.infoUrl, })), - }); + })); } }; diff --git a/app/videobridge/server/methods/bbb.js b/app/videobridge/server/methods/bbb.js index 4721dc9842ce..d4c4db288e32 100644 --- a/app/videobridge/server/methods/bbb.js +++ b/app/videobridge/server/methods/bbb.js @@ -1,5 +1,6 @@ import { Meteor } from 'meteor/meteor'; import { HTTP } from 'meteor/http'; +import { check } from 'meteor/check'; import xml2js from 'xml2js'; import BigBlueButtonApi from '../../../bigbluebutton/server'; @@ -7,6 +8,7 @@ import { SystemLogger } from '../../../../server/lib/logger/system'; import { settings } from '../../../settings/server'; import { Rooms, Users } from '../../../models/server'; import { saveStreamingOptions } from '../../../channel-settings/server'; +import { canAccessRoom } from '../../../authorization/server'; import { API } from '../../../api/server'; const parser = new xml2js.Parser({ @@ -24,11 +26,27 @@ const getBBBAPI = () => { Meteor.methods({ bbbJoin({ rid }) { + check(rid, String); + if (!this.userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'bbbJoin' }); } - if (!Meteor.call('canAccessRoom', rid, this.userId)) { + if (!rid) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'bbbJoin' }); + } + + const user = Users.findOneById(this.userId); + if (!user) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'bbbJoin' }); + } + + const room = Rooms.findOneById(rid); + if (!room) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'bbbJoin' }); + } + + if (!canAccessRoom(room, user)) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'bbbJoin' }); } @@ -38,7 +56,6 @@ Meteor.methods({ const { api } = getBBBAPI(); const meetingID = settings.get('uniqueID') + rid; - const room = Rooms.findOneById(rid); const createUrl = api.urlFor('create', { name: room.t === 'd' ? 'Direct' : room.name, meetingID, @@ -56,8 +73,6 @@ Meteor.methods({ const doc = parseString(createResult.content); if (doc.response.returncode[0]) { - const user = Users.findOneById(this.userId); - const hookApi = api.urlFor('hooks/create', { meetingID, callbackURL: Meteor.absoluteUrl(`api/v1/videoconference.bbb.update/${ meetingID }`), @@ -90,11 +105,17 @@ Meteor.methods({ }, bbbEnd({ rid }) { + check(rid, String); + if (!this.userId) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'bbbEnd' }); } - if (!Meteor.call('canAccessRoom', rid, this.userId)) { + if (!rid) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'bbbEnd' }); + } + + if (!canAccessRoom({ _id: rid }, { _id: this.userId })) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'bbbEnd' }); } diff --git a/app/webdav/client/actionButton.js b/app/webdav/client/actionButton.js index 6b72d8785711..cbfd32da1c3f 100644 --- a/app/webdav/client/actionButton.js +++ b/app/webdav/client/actionButton.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { t, getURL } from '../../utils'; -import { WebdavAccounts } from '../../models'; +import { WebdavAccounts } from '../../models/client'; import { settings } from '../../settings'; import { MessageAction, modal } from '../../ui-utils'; import { messageArgs } from '../../ui-utils/client/lib/messageArgs'; diff --git a/app/webdav/client/selectWebdavAccount.js b/app/webdav/client/selectWebdavAccount.js index 5acb7f43f9c4..14f771822cc4 100644 --- a/app/webdav/client/selectWebdavAccount.js +++ b/app/webdav/client/selectWebdavAccount.js @@ -3,7 +3,7 @@ import { Template } from 'meteor/templating'; import { modal } from '../../ui-utils'; import { t } from '../../utils'; -import { WebdavAccounts } from '../../models'; +import { WebdavAccounts } from '../../models/client'; import { dispatchToastMessage } from '../../../client/lib/toast'; Template.selectWebdavAccount.helpers({ diff --git a/app/webdav/client/startup/messageBoxActions.js b/app/webdav/client/startup/messageBoxActions.js index a209259d4aaa..be63edecd599 100644 --- a/app/webdav/client/startup/messageBoxActions.js +++ b/app/webdav/client/startup/messageBoxActions.js @@ -4,7 +4,7 @@ import { Tracker } from 'meteor/tracker'; import { t } from '../../../utils'; import { settings } from '../../../settings'; import { messageBox, modal } from '../../../ui-utils'; -import { WebdavAccounts } from '../../../models'; +import { WebdavAccounts } from '../../../models/client'; messageBox.actions.add('WebDAV', 'Add Server', { id: 'add-webdav', diff --git a/app/webdav/server/methods/addWebdavAccount.js b/app/webdav/server/methods/addWebdavAccount.js index 3bfc6aef3495..fdaf715903fa 100644 --- a/app/webdav/server/methods/addWebdavAccount.js +++ b/app/webdav/server/methods/addWebdavAccount.js @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import { settings } from '../../../settings'; -import { WebdavAccounts } from '../../../models'; +import { settings } from '../../../settings/server'; +import { WebdavAccounts } from '../../../models/server/raw'; import { WebdavClientAdapter } from '../lib/webdavClientAdapter'; import { Notifications } from '../../../notifications/server'; @@ -24,7 +24,7 @@ Meteor.methods({ pass: String, })); - const duplicateAccount = WebdavAccounts.findOne({ user_id: userId, server_url: formData.serverURL, username: formData.username }); + const duplicateAccount = await WebdavAccounts.findOneByUserIdServerUrlAndUsername({ user_id: userId, server_url: formData.serverURL, username: formData.username }); if (duplicateAccount !== undefined) { throw new Meteor.Error('duplicated-account', { method: 'addWebdavAccount', @@ -49,7 +49,7 @@ Meteor.methods({ }; await client.stat('/'); - WebdavAccounts.insert(accountData); + await WebdavAccounts.insertOne(accountData); Notifications.notifyUser(userId, 'webdav', { type: 'changed', account: accountData, @@ -89,12 +89,14 @@ Meteor.methods({ }; await client.stat('/'); - WebdavAccounts.upsert({ + await WebdavAccounts.updateOne({ user_id: userId, server_url: data.serverURL, name: data.name, }, { $set: accountData, + }, { + upsert: true, }); Notifications.notifyUser(userId, 'webdav', { type: 'changed', diff --git a/app/webdav/server/methods/getFileFromWebdav.js b/app/webdav/server/methods/getFileFromWebdav.js index aeeda2a2636e..a63d72c04723 100644 --- a/app/webdav/server/methods/getFileFromWebdav.js +++ b/app/webdav/server/methods/getFileFromWebdav.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { settings } from '../../../settings'; import { getWebdavCredentials } from './getWebdavCredentials'; -import { WebdavAccounts } from '../../../models'; +import { WebdavAccounts } from '../../../models/server/raw'; import { WebdavClientAdapter } from '../lib/webdavClientAdapter'; Meteor.methods({ @@ -14,7 +14,7 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'WebDAV Integration Not Allowed', { method: 'getFileFromWebdav' }); } - const account = WebdavAccounts.findOne({ _id: accountId, user_id: Meteor.userId() }); + const account = await WebdavAccounts.findOneByIdAndUserId(accountId, Meteor.userId()); if (!account) { throw new Meteor.Error('error-invalid-account', 'Invalid WebDAV Account', { method: 'getFileFromWebdav' }); } diff --git a/app/webdav/server/methods/getWebdavFileList.js b/app/webdav/server/methods/getWebdavFileList.js index 52e3c6e3904a..e9b5e3526d7c 100644 --- a/app/webdav/server/methods/getWebdavFileList.js +++ b/app/webdav/server/methods/getWebdavFileList.js @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { settings } from '../../../settings'; import { getWebdavCredentials } from './getWebdavCredentials'; -import { WebdavAccounts } from '../../../models'; +import { WebdavAccounts } from '../../../models/server/raw'; import { WebdavClientAdapter } from '../lib/webdavClientAdapter'; Meteor.methods({ @@ -15,7 +15,7 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'WebDAV Integration Not Allowed', { method: 'getWebdavFileList' }); } - const account = WebdavAccounts.findOne({ _id: accountId, user_id: Meteor.userId() }); + const account = await WebdavAccounts.findOneByIdAndUserId(accountId, Meteor.userId()); if (!account) { throw new Meteor.Error('error-invalid-account', 'Invalid WebDAV Account', { method: 'getWebdavFileList' }); } diff --git a/app/webdav/server/methods/getWebdavFilePreview.js b/app/webdav/server/methods/getWebdavFilePreview.js index 2de21cae6fef..5940d44601b4 100644 --- a/app/webdav/server/methods/getWebdavFilePreview.js +++ b/app/webdav/server/methods/getWebdavFilePreview.js @@ -3,7 +3,7 @@ import { createClient } from 'webdav'; import { settings } from '../../../settings'; import { getWebdavCredentials } from './getWebdavCredentials'; -import { WebdavAccounts } from '../../../models'; +import { WebdavAccounts } from '../../../models/server/raw'; Meteor.methods({ async getWebdavFilePreview(accountId, path) { @@ -15,7 +15,7 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'WebDAV Integration Not Allowed', { method: 'getWebdavFilePreview' }); } - const account = WebdavAccounts.findOne({ _id: accountId, user_id: Meteor.userId() }); + const account = await WebdavAccounts.findOneByIdAndUserId(accountId, Meteor.userId()); if (!account) { throw new Meteor.Error('error-invalid-account', 'Invalid WebDAV Account', { method: 'getWebdavFilePreview' }); } diff --git a/app/webdav/server/methods/removeWebdavAccount.js b/app/webdav/server/methods/removeWebdavAccount.js index 46f8c7b515f7..dc6ba032cfc7 100644 --- a/app/webdav/server/methods/removeWebdavAccount.js +++ b/app/webdav/server/methods/removeWebdavAccount.js @@ -1,18 +1,18 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { WebdavAccounts } from '../../../models'; +import { WebdavAccounts } from '../../../models/server/raw'; import { Notifications } from '../../../notifications/server'; Meteor.methods({ - removeWebdavAccount(accountId) { + async removeWebdavAccount(accountId) { if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid User', { method: 'removeWebdavAccount' }); } check(accountId, String); - const removed = WebdavAccounts.removeByUserAndId(accountId, Meteor.userId()); + const removed = await WebdavAccounts.removeByUserAndId(accountId, Meteor.userId()); if (removed) { Notifications.notifyUser(Meteor.userId(), 'webdav', { type: 'removed', diff --git a/app/webdav/server/methods/uploadFileToWebdav.ts b/app/webdav/server/methods/uploadFileToWebdav.ts index 345550285e7f..1f794ea0ad48 100644 --- a/app/webdav/server/methods/uploadFileToWebdav.ts +++ b/app/webdav/server/methods/uploadFileToWebdav.ts @@ -3,7 +3,7 @@ import { Meteor } from 'meteor/meteor'; import { settings } from '../../../settings/server'; import { Logger } from '../../../logger/server'; import { getWebdavCredentials } from './getWebdavCredentials'; -import { WebdavAccounts } from '../../../models/server'; +import { WebdavAccounts } from '../../../models/server/raw'; import { WebdavClientAdapter } from '../lib/webdavClientAdapter'; const logger = new Logger('WebDAV_Upload'); @@ -18,7 +18,7 @@ Meteor.methods({ throw new Meteor.Error('error-not-allowed', 'WebDAV Integration Not Allowed', { method: 'uploadFileToWebdav' }); } - const account = WebdavAccounts.findOne({ _id: accountId }); + const account = await WebdavAccounts.findOneById(accountId); if (!account) { throw new Meteor.Error('error-invalid-account', 'Invalid WebDAV Account', { method: 'uploadFileToWebdav' }); } diff --git a/client/sidebar/sections/Omnichannel.js b/client/sidebar/sections/Omnichannel.js index 3a18ac5b6347..c829b44c1169 100644 --- a/client/sidebar/sections/Omnichannel.js +++ b/client/sidebar/sections/Omnichannel.js @@ -2,6 +2,7 @@ import { Sidebar } from '@rocket.chat/fuselage'; import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; import React, { memo } from 'react'; +import { hasPermission } from '../../../app/authorization/client'; import { useOmnichannelShowQueueLink, useOmnichannelAgentAvailable, @@ -49,7 +50,9 @@ const OmnichannelSection = (props) => { )} - + {hasPermission(['view-omnichannel-contact-center']) && ( + + )} ); diff --git a/client/views/omnichannel/departments/EditDepartment.js b/client/views/omnichannel/departments/EditDepartment.js index 24885d0f53a6..3fbb653e2569 100644 --- a/client/views/omnichannel/departments/EditDepartment.js +++ b/client/views/omnichannel/departments/EditDepartment.js @@ -1,4 +1,3 @@ -/* eslint-disable complexity */ import { FieldGroup, Field, @@ -33,11 +32,7 @@ import DepartmentsAgentsTable from './DepartmentsAgentsTable'; function EditDepartment({ data, id, title, reload, allowedToForwardData }) { const t = useTranslation(); - const agentsRoute = useRoute('omnichannel-departments'); - const eeForms = useSubscription(formsSubscription); - const initialAgents = useRef((data && data.agents) || []); - - const router = useRoute('omnichannel-departments'); + const departmentsRoute = useRoute('omnichannel-departments'); const { useEeNumberInput = () => {}, @@ -45,7 +40,9 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { useEeTextAreaInput = () => {}, useDepartmentForwarding = () => {}, useDepartmentBusinessHours = () => {}, - } = eeForms; + } = useSubscription(formsSubscription); + + const initialAgents = useRef((data && data.agents) || []); const MaxChats = useEeNumberInput(); const VisitorInactivity = useEeNumberInput(); @@ -57,29 +54,23 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { const { department } = data || { department: {} }; - const [tags, setTags] = useState((department && department.chatClosingTags) || []); - const [tagsText, setTagsText] = useState(); + const [[tags, tagsText], setTagsState] = useState(() => [department?.chatClosingTags ?? [], '']); const { values, handlers, hasUnsavedChanges } = useForm({ - name: (department && department.name) || '', - email: (department && department.email) || '', - description: (department && department.description) || '', - enabled: !!(department && department.enabled), - maxNumberSimultaneousChat: (department && department.maxNumberSimultaneousChat) || undefined, - showOnRegistration: !!(department && department.showOnRegistration), - showOnOfflineForm: !!(department && department.showOnOfflineForm), - abandonedRoomsCloseCustomMessage: - (department && department.abandonedRoomsCloseCustomMessage) || '', - requestTagBeforeClosingChat: (department && department.requestTagBeforeClosingChat) || false, - offlineMessageChannelName: (department && department.offlineMessageChannelName) || '', - visitorInactivityTimeoutInSeconds: - (department && department.visitorInactivityTimeoutInSeconds) || undefined, - waitingQueueMessage: (department && department.waitingQueueMessage) || '', + name: department?.name || '', + email: department?.email || '', + description: department?.description || '', + enabled: !!department?.enabled, + maxNumberSimultaneousChat: department?.maxNumberSimultaneousChat || undefined, + showOnRegistration: !!department?.showOnRegistration, + showOnOfflineForm: !!department?.showOnOfflineForm, + abandonedRoomsCloseCustomMessage: department?.abandonedRoomsCloseCustomMessage || '', + requestTagBeforeClosingChat: department?.requestTagBeforeClosingChat || false, + offlineMessageChannelName: department?.offlineMessageChannelName || '', + visitorInactivityTimeoutInSeconds: department?.visitorInactivityTimeoutInSeconds || undefined, + waitingQueueMessage: department?.waitingQueueMessage || '', departmentsAllowedToForward: - (allowedToForwardData && - allowedToForwardData.departments && - allowedToForwardData.departments.map((dep) => ({ label: dep.name, value: dep._id }))) || - [], + allowedToForwardData?.departments?.map((dep) => ({ label: dep.name, value: dep._id })) || [], }); const { handleName, @@ -119,20 +110,25 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { const { phase: roomsPhase, items: roomsItems, itemCount: roomsTotal } = useRecordList(RoomsList); const handleTagChipClick = (tag) => () => { - setTags((tags) => tags.filter((_tag) => _tag !== tag)); + setTagsState(([tags, tagsText]) => [tags.filter((_tag) => _tag !== tag), tagsText]); }; const handleTagTextSubmit = useMutableCallback(() => { - if (!tags.includes(tagsText)) { - setTags([...tags, tagsText]); - setTagsText(''); - } - }); + setTagsState((state) => { + const [tags, tagsText] = state; + + if (tags.includes(tagsText)) { + return state; + } - const handleTagTextChange = useMutableCallback((e) => { - setTagsText(e.target.value); + return [[...tags, tagsText], '']; + }); }); + const handleTagTextChange = (e) => { + setTagsState(([tags]) => [tags, e.target.value]); + }; + const saveDepartmentInfo = useMethod('livechat:saveDepartment'); const saveDepartmentAgentsInfoOnEdit = useEndpoint('POST', `livechat/department/${id}/agents`); @@ -197,8 +193,7 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { visitorInactivityTimeoutInSeconds, abandonedRoomsCloseCustomMessage, waitingQueueMessage, - departmentsAllowedToForward: - departmentsAllowedToForward && departmentsAllowedToForward.map((dep) => dep.value).join(), + departmentsAllowedToForward: departmentsAllowedToForward?.map((dep) => dep.value).join(), }; const agentListPayload = { @@ -227,14 +222,14 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { } dispatchToastMessage({ type: 'success', message: t('Saved') }); reload(); - agentsRoute.push({}); + departmentsRoute.push({}); } catch (error) { dispatchToastMessage({ type: 'error', message: error }); } }); const handleReturn = useMutableCallback(() => { - router.push({}); + departmentsRoute.push({}); }); const invalidForm = @@ -438,7 +433,7 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { {t('Conversation_closing_tags_description')} - {tags && tags.length > 0 && ( + {tags?.length > 0 && ( {tags.map((tag, i) => ( @@ -451,7 +446,7 @@ function EditDepartment({ data, id, title, reload, allowedToForwardData }) { )} {DepartmentBusinessHours && ( - + )} diff --git a/client/views/omnichannel/departments/EditDepartmentWithAllowedForwardData.js b/client/views/omnichannel/departments/EditDepartmentWithAllowedForwardData.js index ff9c26f4ef57..a217c4ec8aaa 100644 --- a/client/views/omnichannel/departments/EditDepartmentWithAllowedForwardData.js +++ b/client/views/omnichannel/departments/EditDepartmentWithAllowedForwardData.js @@ -1,4 +1,3 @@ -/* eslint-disable complexity */ import { Box } from '@rocket.chat/fuselage'; import React, { useMemo } from 'react'; diff --git a/client/views/omnichannel/departments/EditDepartmentWithData.js b/client/views/omnichannel/departments/EditDepartmentWithData.js index ccdc6ab6e589..d4bbc31c35ca 100644 --- a/client/views/omnichannel/departments/EditDepartmentWithData.js +++ b/client/views/omnichannel/departments/EditDepartmentWithData.js @@ -1,4 +1,3 @@ -/* eslint-disable complexity */ import { Box } from '@rocket.chat/fuselage'; import React from 'react'; diff --git a/client/views/omnichannel/directory/OmnichannelDirectoryPage.js b/client/views/omnichannel/directory/OmnichannelDirectoryPage.js index 4f19bbf53901..3a0cec01e44a 100644 --- a/client/views/omnichannel/directory/OmnichannelDirectoryPage.js +++ b/client/views/omnichannel/directory/OmnichannelDirectoryPage.js @@ -1,7 +1,9 @@ import { Tabs } from '@rocket.chat/fuselage'; import React, { useEffect, useCallback, useState } from 'react'; +import NotAuthorizedPage from '../../../components/NotAuthorizedPage'; import Page from '../../../components/Page'; +import { usePermission } from '../../../contexts/AuthorizationContext'; import { useCurrentRoute, useRoute, useRouteParameter } from '../../../contexts/RouterContext'; import { useTranslation } from '../../../contexts/TranslationContext'; import ContextualBar from './ContextualBar'; @@ -14,6 +16,7 @@ const OmnichannelDirectoryPage = () => { const [routeName] = useCurrentRoute(); const tab = useRouteParameter('page'); const directoryRoute = useRoute('omnichannel-directory'); + const canViewDirectory = usePermission('view-omnichannel-contact-center'); useEffect(() => { if (routeName !== 'omnichannel-directory') { @@ -32,6 +35,10 @@ const OmnichannelDirectoryPage = () => { const t = useTranslation(); + if (!canViewDirectory) { + return ; + } + return ( diff --git a/client/views/omnichannel/directory/chats/contextualBar/ChatInfo.js b/client/views/omnichannel/directory/chats/contextualBar/ChatInfo.js index 6ded84b119d2..5f1cb3015d9d 100644 --- a/client/views/omnichannel/directory/chats/contextualBar/ChatInfo.js +++ b/client/views/omnichannel/directory/chats/contextualBar/ChatInfo.js @@ -50,6 +50,7 @@ function ChatInfo({ id, route }) { priorityId, livechatData, source, + queuedAt, } = room || { room: { v: {} } }; const routePath = useRoute(route || 'omnichannel-directory'); @@ -58,6 +59,7 @@ function ChatInfo({ id, route }) { const hasGlobalEditRoomPermission = hasPermission('save-others-livechat-room-info'); const hasLocalEditRoomPermission = servedBy?._id === Meteor.userId(); const visitorId = v?._id; + const queueStartedAt = queuedAt || ts; const dispatchToastMessage = useToastMessageDispatch(); useEffect(() => { @@ -126,13 +128,13 @@ function ChatInfo({ id, route }) { {topic} )} - {ts && ( + {queueStartedAt && ( {servedBy ? ( - {moment(servedBy.ts).from(moment(ts), true)} + {moment(servedBy.ts).from(moment(queueStartedAt), true)} ) : ( - {moment(ts).fromNow(true)} + {moment(queueStartedAt).fromNow(true)} )} )} diff --git a/client/views/omnichannel/directory/chats/contextualBar/ChatInfoDirectory.js b/client/views/omnichannel/directory/chats/contextualBar/ChatInfoDirectory.js index 8e62089de68d..12823d186cc3 100644 --- a/client/views/omnichannel/directory/chats/contextualBar/ChatInfoDirectory.js +++ b/client/views/omnichannel/directory/chats/contextualBar/ChatInfoDirectory.js @@ -45,6 +45,7 @@ function ChatInfoDirectory({ id, route, room }) { responseBy, priorityId, livechatData, + queuedAt, } = room || { room: { v: {} } }; const routePath = useRoute(route || 'omnichannel-directory'); @@ -53,6 +54,7 @@ function ChatInfoDirectory({ id, route, room }) { const hasGlobalEditRoomPermission = hasPermission('save-others-livechat-room-info'); const hasLocalEditRoomPermission = servedBy?._id === Meteor.userId(); const visitorId = v?._id; + const queueStartedAt = queuedAt || ts; const dispatchToastMessage = useToastMessageDispatch(); useEffect(() => { @@ -120,13 +122,13 @@ function ChatInfoDirectory({ id, route, room }) { {topic} )} - {ts && ( + {queueStartedAt && ( {servedBy ? ( - {moment(servedBy.ts).from(moment(ts), true)} + {moment(servedBy.ts).from(moment(queueStartedAt), true)} ) : ( - {moment(ts).fromNow(true)} + {moment(queueStartedAt).fromNow(true)} )} )} diff --git a/definition/Federation.ts b/definition/Federation.ts new file mode 100644 index 000000000000..d961d8226429 --- /dev/null +++ b/definition/Federation.ts @@ -0,0 +1,5 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface IFederationServer extends IRocketChatRecord { + domain: string; +} diff --git a/definition/IAnalytic.ts b/definition/IAnalytic.ts new file mode 100644 index 000000000000..2a9b1e83c2e9 --- /dev/null +++ b/definition/IAnalytic.ts @@ -0,0 +1,33 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +interface IAnalyticsBase extends IRocketChatRecord { + type: 'messages' | 'users' | 'seat-request'; + date: number; +} + +export interface IAnalyticsMessages extends IAnalyticsBase { + type: 'messages'; + room: { + _id: string; + name?: string; + t: string; + usernames: string[]; + }; +} + +export interface IAnalyticsUsers extends IAnalyticsBase { + type: 'users'; + room: { + _id: string; + name?: string; + t: string; + usernames: string[]; + }; +} + +export interface IAnalyticsSeatRequest extends IAnalyticsBase { + type: 'seat-request'; + count: number; +} + +export type IAnalytic = IAnalyticsBase | IAnalyticsMessages | IAnalyticsUsers | IAnalyticsSeatRequest; diff --git a/definition/IAvatar.ts b/definition/IAvatar.ts new file mode 100644 index 000000000000..b8bd8b82bfbd --- /dev/null +++ b/definition/IAvatar.ts @@ -0,0 +1,13 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface IAvatar extends IRocketChatRecord { + name: string; + rid: string; + userId: string; + store: string; + complete: boolean; + uploading: boolean; + progress: number; + extension: string; + uploadedAt: Date; +} diff --git a/definition/ICredentialToken.ts b/definition/ICredentialToken.ts new file mode 100644 index 000000000000..ceaf44203cba --- /dev/null +++ b/definition/ICredentialToken.ts @@ -0,0 +1,10 @@ +export interface ICredentialToken { + _id: string; + + userInfo: { + username?: string; + attributes?: any; + profile?: Record; + }; + expireAt: Date; +} diff --git a/definition/ICustomSound.ts b/definition/ICustomSound.ts new file mode 100644 index 000000000000..0fd8c09e0851 --- /dev/null +++ b/definition/ICustomSound.ts @@ -0,0 +1,6 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface ICustomSound extends IRocketChatRecord { + name: string; + statusType: string; +} diff --git a/definition/ICustomUserStatus.ts b/definition/ICustomUserStatus.ts new file mode 100644 index 000000000000..56bb8874be3f --- /dev/null +++ b/definition/ICustomUserStatus.ts @@ -0,0 +1,6 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface ICustomUserStatus extends IRocketChatRecord { + name: string; + statusType: string; +} diff --git a/definition/IEmojiCustom.ts b/definition/IEmojiCustom.ts new file mode 100644 index 000000000000..a7d874f834ea --- /dev/null +++ b/definition/IEmojiCustom.ts @@ -0,0 +1,7 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface IEmojiCustom extends IRocketChatRecord { + name: string; + aliases: string; + extension: string; +} diff --git a/definition/IExportOperation.ts b/definition/IExportOperation.ts new file mode 100644 index 000000000000..9ccd591c4823 --- /dev/null +++ b/definition/IExportOperation.ts @@ -0,0 +1,16 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface IExportOperation extends IRocketChatRecord { + roomList?: string[]; + status: string; + fileList: string[]; + generatedFile?: string; + fileId: string; + userNameTable: string; + userData: string; + generatedUserFile: string; + generatedAvatar: string; + exportPath: string; + assetsPath: string; + createdAt: Date; +} diff --git a/definition/IIntegration.ts b/definition/IIntegration.ts index 5ad55faab937..fa851e5a1208 100644 --- a/definition/IIntegration.ts +++ b/definition/IIntegration.ts @@ -1,5 +1,9 @@ -export interface IIntegration { - _id: string; +import { IRocketChatRecord } from './IRocketChatRecord'; +import { IUser } from './IUser'; + +export interface IIntegration extends IRocketChatRecord { type: string; enabled: boolean; + channel: string; + _createdBy: IUser; } diff --git a/definition/IIntegrationHistory.ts b/definition/IIntegrationHistory.ts index 2149aff521be..04e1df6cbb03 100644 --- a/definition/IIntegrationHistory.ts +++ b/definition/IIntegrationHistory.ts @@ -1,5 +1,6 @@ -export interface IIntegrationHistory { - _id: string; +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface IIntegrationHistory extends IRocketChatRecord { type: string; step: string; integration: { @@ -8,7 +9,20 @@ export interface IIntegrationHistory { event: string; _createdAt: Date; _updatedAt: Date; - // "data" : + data?: { + user?: any; + room?: any; + }; ranPrepareScript: boolean; finished: boolean; + + triggerWord?: string; + prepareSentMessage?: string; + processSentMessage?: string; + url?: string; + httpCallData?: string; + httpError?: any; + httpResult?: string; + error?: any; + errorStack?: any; } diff --git a/definition/IInvite.ts b/definition/IInvite.ts new file mode 100644 index 000000000000..b02a02576257 --- /dev/null +++ b/definition/IInvite.ts @@ -0,0 +1,11 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface IInvite extends IRocketChatRecord { + days: number; + maxUses: number; + rid: string; + userId: string; + createdAt: Date; + expires: Date; + uses: number; +} diff --git a/definition/ILivechatBusinessHour.ts b/definition/ILivechatBusinessHour.ts index cdb18c9fd4be..ab2ec4f70315 100644 --- a/definition/ILivechatBusinessHour.ts +++ b/definition/ILivechatBusinessHour.ts @@ -21,6 +21,7 @@ export interface IBusinessHourWorkHour { start: IBusinessHourTime; finish: IBusinessHourTime; open: boolean; + code: unknown; } export interface IBusinessHourTimezone { diff --git a/definition/IOAuthApps.ts b/definition/IOAuthApps.ts new file mode 100644 index 000000000000..71e004a067bd --- /dev/null +++ b/definition/IOAuthApps.ts @@ -0,0 +1,14 @@ +export interface IOAuthApps { + _id: string; + + name: string; + active: boolean; + clientId: string; + clientSecret: string; + redirectUri: string; + _createdAt: Date; + _createdBy: { + _id: string; + username: string; + }; +} diff --git a/definition/IOEmbedCache.ts b/definition/IOEmbedCache.ts new file mode 100644 index 000000000000..b99341bddf63 --- /dev/null +++ b/definition/IOEmbedCache.ts @@ -0,0 +1,6 @@ +export interface IOEmbedCache { + _id: string; + + data: any; + updatedAt: Date; +} diff --git a/definition/IReport.ts b/definition/IReport.ts new file mode 100644 index 000000000000..61ee97bb9914 --- /dev/null +++ b/definition/IReport.ts @@ -0,0 +1,9 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; +import type { IMessage } from './IMessage/IMessage'; + +export interface IReport extends IRocketChatRecord { + message: IMessage; + description: string; + ts: Date; + userId: string; +} diff --git a/definition/IRoom.ts b/definition/IRoom.ts index 622844ef2783..8597a9035e82 100644 --- a/definition/IRoom.ts +++ b/definition/IRoom.ts @@ -61,6 +61,8 @@ export interface IRoom extends IRocketChatRecord { sysMes?: string[]; muted?: string[]; + + usernames?: string[]; } export interface ICreatedRoom extends IRoom { @@ -73,6 +75,8 @@ export interface IDirectMessageRoom extends Omit; } +export const isDirectMessageRoom = (room: Partial): room is IDirectMessageRoom => room.t === 'd'; + export enum OmnichannelSourceType { WIDGET = 'widget', EMAIL = 'email', @@ -120,6 +124,7 @@ export interface IOmnichannelRoom extends Omit room.t === 'l'; diff --git a/definition/IServerEvent.ts b/definition/IServerEvent.ts index edc831fb7c56..a0aab45e7a81 100644 --- a/definition/IServerEvent.ts +++ b/definition/IServerEvent.ts @@ -10,5 +10,5 @@ export interface IServerEvent { t: IServerEventType; ts: Date; ip: string; - u?: Partial; + u?: Partial>; } diff --git a/definition/ISession.ts b/definition/ISession.ts new file mode 100644 index 000000000000..93981f655cf7 --- /dev/null +++ b/definition/ISession.ts @@ -0,0 +1,29 @@ +export interface ISession { + _id: string; + + type: string; + mostImportantRole: string; + userId: string; + lastActivityAt: Date; + device: { + type: string; + name: string; + longVersion: string; + os: { + name: string; + version: string; + }; + version: string; + }; + year: number; + month: number; + day: number; + instanceId: string; + sessionId: string; + _updatedAt: Date; + createdAt: Date; + host: string; + ip: string; + loginAt: Date; + closedAt: Date; +} diff --git a/definition/ISetting.ts b/definition/ISetting.ts index 096d25bcae85..635506db1c2e 100644 --- a/definition/ISetting.ts +++ b/definition/ISetting.ts @@ -57,7 +57,7 @@ export interface ISettingBase { wizard?: { step: number; order: number; - }; + } | null; persistent?: boolean; // todo: remove readonly?: boolean; // todo: remove alert?: string; // todo: check if this is still used @@ -97,6 +97,7 @@ export interface ISettingCode extends ISettingBase { export interface ISettingAction extends ISettingBase { type: 'action'; + value: string; actionText?: string; } export interface ISettingAsset extends ISettingBase { @@ -104,6 +105,14 @@ export interface ISettingAsset extends ISettingBase { value: AssetValue; } +export interface ISettingDate extends ISettingBase { + type: 'date'; + value: Date; +} + +export const isDateSetting = (setting: ISetting): setting is ISettingDate => setting.type === 'date'; + + export const isSettingEnterprise = (setting: ISettingBase): setting is ISettingEnterprise => setting.enterprise === true; export const isSettingColor = (setting: ISettingBase): setting is ISettingColor => setting.type === 'color'; diff --git a/definition/ISmarshHistory.ts b/definition/ISmarshHistory.ts new file mode 100644 index 000000000000..43ba0d8f91a8 --- /dev/null +++ b/definition/ISmarshHistory.ts @@ -0,0 +1,6 @@ +export interface ISmarshHistory { + _id: string; + + lastRan: Date; + lastResult: string; +} diff --git a/definition/IStatistic.ts b/definition/IStatistic.ts new file mode 100644 index 000000000000..c94e92e5da5f --- /dev/null +++ b/definition/IStatistic.ts @@ -0,0 +1,24 @@ +export interface IStatistic { + _id: string; + + version: string; + instanceCount: number; + oplogEnabled: boolean; + totalUsers: number; + activeUsers: number; + nonActiveUsers: number; + onlineUsers: number; + awayUsers: number; + offlineUsers: number; + totalRooms: number; + totalChannels: number; + totalPrivateGroups: number; + totalDirect: number; + totalLivechat: number; + totalMessages: number; + totalChannelMessages: number; + totalPrivateGroupMessages: number; + totalDirectMessages: number; + totalLivechatMessages: number; + pushQueue: number; +} diff --git a/definition/IUpload.ts b/definition/IUpload.ts new file mode 100644 index 000000000000..1eba78e441da --- /dev/null +++ b/definition/IUpload.ts @@ -0,0 +1,12 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface IUpload extends IRocketChatRecord { + typeGroup?: string; + type?: string; + name: string; + aliases?: string; + extension?: string; + complete?: boolean; + uploading?: boolean; + progress?: number; +} diff --git a/definition/IUser.ts b/definition/IUser.ts index 732c684961ba..e40908cec83a 100644 --- a/definition/IUser.ts +++ b/definition/IUser.ts @@ -94,7 +94,8 @@ export interface IRole { mandatory2fa?: boolean; name: string; protected: boolean; - scope?: string; + // scope?: string; + scope?: 'Users' | 'Subscriptions'; _id: string; } @@ -143,6 +144,12 @@ export interface IUser extends IRocketChatRecord { ldap?: boolean; } +export interface IRegisterUser extends IUser { + username: string; + name: string; +} +export const isRegisterUser = (user: IUser): user is IRegisterUser => user.username !== undefined && user.name !== undefined; + export type IUserDataEvent = { id: unknown; } diff --git a/definition/IUserDataFile.ts b/definition/IUserDataFile.ts new file mode 100644 index 000000000000..8be01d1c88d2 --- /dev/null +++ b/definition/IUserDataFile.ts @@ -0,0 +1,13 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface IUserDataFile extends IRocketChatRecord { + name: string; + rid: string; + userId: string; + store: string; + complete: boolean; + uploading: boolean; + progress: number; + extension: string; + uploadedAt: Date; +} diff --git a/definition/IWebdavAccount.ts b/definition/IWebdavAccount.ts new file mode 100644 index 000000000000..264df6f57546 --- /dev/null +++ b/definition/IWebdavAccount.ts @@ -0,0 +1,9 @@ +import { IRocketChatRecord } from './IRocketChatRecord'; + +export interface IWebdavAccount extends IRocketChatRecord { + user_id: string; + server_url: string; + username: string; + password: string; + name: string; +} diff --git a/definition/externals/meteor/meteor.d.ts b/definition/externals/meteor/meteor.d.ts index f4e6c3a65c44..d2f9bc1c8300 100644 --- a/definition/externals/meteor/meteor.d.ts +++ b/definition/externals/meteor/meteor.d.ts @@ -1,4 +1,5 @@ /* eslint-disable @typescript-eslint/interface-name-prefix */ +import 'meteor/meteor'; declare module 'meteor/meteor' { namespace Meteor { diff --git a/ee/app/auditing/server/index.js b/ee/app/auditing/server/index.ts similarity index 79% rename from ee/app/auditing/server/index.js rename to ee/app/auditing/server/index.ts index a816e3a1bc2c..57ac621de7dd 100644 --- a/ee/app/auditing/server/index.js +++ b/ee/app/auditing/server/index.ts @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { onLicense } from '../../license/server'; -import { Permissions, Roles } from '../../../../app/models/server'; +import { Permissions, Roles } from '../../../../app/models/server/raw'; onLicense('auditing', () => { require('./methods'); @@ -14,8 +14,8 @@ onLicense('auditing', () => { ]; const defaultRoles = [ - { name: 'auditor', scope: 'Users' }, - { name: 'auditor-log', scope: 'Users' }, + { name: 'auditor', scope: 'Users' as const }, + { name: 'auditor-log', scope: 'Users' as const }, ]; permissions.forEach((permission) => { @@ -23,7 +23,7 @@ onLicense('auditing', () => { }); defaultRoles.forEach((role) => - Roles.createOrUpdate(role.name, role.scope, role.description), + Roles.createOrUpdate(role.name, role.scope), ); }); }); diff --git a/ee/app/authorization/server/resetEnterprisePermissions.js b/ee/app/authorization/server/resetEnterprisePermissions.js deleted file mode 100644 index cebc88ba83be..000000000000 --- a/ee/app/authorization/server/resetEnterprisePermissions.js +++ /dev/null @@ -1,6 +0,0 @@ -import { Permissions } from '../../../../app/models/server'; -import { guestPermissions } from '../lib/guestPermissions'; - -export const resetEnterprisePermissions = function() { - Permissions.update({ _id: { $nin: guestPermissions } }, { $pull: { roles: 'guest' } }, { multi: true }); -}; diff --git a/ee/app/authorization/server/resetEnterprisePermissions.ts b/ee/app/authorization/server/resetEnterprisePermissions.ts new file mode 100644 index 000000000000..2c7cd9def87e --- /dev/null +++ b/ee/app/authorization/server/resetEnterprisePermissions.ts @@ -0,0 +1,7 @@ + +import { Permissions } from '../../../../app/models/server/raw'; +import { guestPermissions } from '../lib/guestPermissions'; + +export const resetEnterprisePermissions = async function(): Promise { + await Permissions.update({ _id: { $nin: guestPermissions } }, { $pull: { roles: 'guest' } }, { multi: true }); +}; diff --git a/ee/app/canned-responses/server/permissions.js b/ee/app/canned-responses/server/permissions.ts similarity index 90% rename from ee/app/canned-responses/server/permissions.js rename to ee/app/canned-responses/server/permissions.ts index 26e838540df1..f32650dd09b9 100644 --- a/ee/app/canned-responses/server/permissions.js +++ b/ee/app/canned-responses/server/permissions.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; -import Permissions from '../../../../app/models/server/models/Permissions'; +import { Permissions } from '../../../../app/models/server/raw'; Meteor.startup(() => { Permissions.create('view-canned-responses', ['livechat-agent', 'livechat-monitor', 'livechat-manager', 'admin']); diff --git a/ee/app/engagement-dashboard/server/lib/messages.js b/ee/app/engagement-dashboard/server/lib/messages.js index e49443dc5829..f4208928fcbb 100644 --- a/ee/app/engagement-dashboard/server/lib/messages.js +++ b/ee/app/engagement-dashboard/server/lib/messages.js @@ -1,9 +1,7 @@ import moment from 'moment'; -import AnalyticsRaw from '../../../../../app/models/server/raw/Analytics'; import { roomTypes } from '../../../../../app/utils'; -import { Messages } from '../../../../../app/models/server/raw'; -import { Analytics } from '../../../../../app/models/server'; +import { Messages, Analytics } from '../../../../../app/models/server/raw'; import { convertDateToInt, diffBetweenDaysInclusive, convertIntToDate, getTotalOfWeekItems } from './date'; export const handleMessagesSent = (message, room) => { @@ -11,7 +9,7 @@ export const handleMessagesSent = (message, room) => { if (!roomTypesToShow.includes(room.t)) { return message; } - Promise.await(AnalyticsRaw.saveMessageSent({ + Promise.await(Analytics.saveMessageSent({ date: convertDateToInt(message.ts), room, })); @@ -23,7 +21,7 @@ export const handleMessagesDeleted = (message, room) => { if (!roomTypesToShow.includes(room.t)) { return; } - Promise.await(AnalyticsRaw.saveMessageDeleted({ + Promise.await(Analytics.saveMessageDeleted({ date: convertDateToInt(message.ts), room, })); @@ -31,7 +29,7 @@ export const handleMessagesDeleted = (message, room) => { }; export const fillFirstDaysOfMessagesIfNeeded = async (date) => { - const messagesFromAnalytics = await AnalyticsRaw.findByTypeBeforeDate({ + const messagesFromAnalytics = await Analytics.findByTypeBeforeDate({ type: 'messages', date: convertDateToInt(date), }).toArray(); @@ -41,10 +39,10 @@ export const fillFirstDaysOfMessagesIfNeeded = async (date) => { start: startOfPeriod, end: date, }); - messages.forEach((message) => Analytics.insert({ + await Promise.all(messages.map((message) => Analytics.insertOne({ ...message, date: parseInt(message.date), - })); + }))); } }; @@ -54,16 +52,16 @@ export const findWeeklyMessagesSentData = async ({ start, end }) => { const startOfLastWeek = moment(endOfLastWeek).clone().subtract(daysBetweenDates, 'days').toDate(); const today = convertDateToInt(end); const yesterday = convertDateToInt(moment(end).clone().subtract(1, 'days').toDate()); - const currentPeriodMessages = await AnalyticsRaw.getMessagesSentTotalByDate({ + const currentPeriodMessages = await Analytics.getMessagesSentTotalByDate({ start: convertDateToInt(start), end: convertDateToInt(end), options: { count: daysBetweenDates, sort: { _id: -1 } }, - }); - const lastPeriodMessages = await AnalyticsRaw.getMessagesSentTotalByDate({ + }).toArray(); + const lastPeriodMessages = await Analytics.getMessagesSentTotalByDate({ start: convertDateToInt(startOfLastWeek), end: convertDateToInt(endOfLastWeek), options: { count: daysBetweenDates, sort: { _id: -1 } }, - }); + }).toArray(); const yesterdayMessages = (currentPeriodMessages.find((item) => item._id === yesterday) || {}).messages || 0; const todayMessages = (currentPeriodMessages.find((item) => item._id === today) || {}).messages || 0; const currentPeriodTotalOfMessages = getTotalOfWeekItems(currentPeriodMessages, 'messages'); @@ -82,10 +80,10 @@ export const findWeeklyMessagesSentData = async ({ start, end }) => { }; export const findMessagesSentOrigin = async ({ start, end }) => { - const origins = await AnalyticsRaw.getMessagesOrigin({ + const origins = await Analytics.getMessagesOrigin({ start: convertDateToInt(start), end: convertDateToInt(end), - }); + }).toArray(); const roomTypesToShow = roomTypes.getTypesToShowOnDashboard(); const responseTypes = origins.map((origin) => origin.t); const missingTypes = roomTypesToShow.filter((type) => !responseTypes.includes(type)); @@ -96,10 +94,10 @@ export const findMessagesSentOrigin = async ({ start, end }) => { }; export const findTopFivePopularChannelsByMessageSentQuantity = async ({ start, end }) => { - const channels = await AnalyticsRaw.getMostPopularChannelsByMessagesSentQuantity({ + const channels = await Analytics.getMostPopularChannelsByMessagesSentQuantity({ start: convertDateToInt(start), end: convertDateToInt(end), options: { count: 5, sort: { messages: -1 } }, - }); + }).toArray(); return { channels }; }; diff --git a/ee/app/engagement-dashboard/server/lib/users.js b/ee/app/engagement-dashboard/server/lib/users.js index 69c72c007e06..b9d8738827c1 100644 --- a/ee/app/engagement-dashboard/server/lib/users.js +++ b/ee/app/engagement-dashboard/server/lib/users.js @@ -1,9 +1,6 @@ import moment from 'moment'; -import AnalyticsRaw from '../../../../../app/models/server/raw/Analytics'; -import Sessions from '../../../../../app/models/server/raw/Sessions'; -import { Users } from '../../../../../app/models/server/raw'; -import { Analytics } from '../../../../../app/models/server'; +import { Users, Analytics, Sessions } from '../../../../../app/models/server/raw'; import { convertDateToInt, diffBetweenDaysInclusive, getTotalOfWeekItems, convertIntToDate } from './date'; export const handleUserCreated = (user) => { @@ -11,7 +8,7 @@ export const handleUserCreated = (user) => { return; } - Promise.await(AnalyticsRaw.saveUserData({ + Promise.await(Analytics.saveUserData({ date: convertDateToInt(user.ts), user, })); @@ -19,7 +16,7 @@ export const handleUserCreated = (user) => { }; export const fillFirstDaysOfUsersIfNeeded = async (date) => { - const usersFromAnalytics = await AnalyticsRaw.findByTypeBeforeDate({ + const usersFromAnalytics = await Analytics.findByTypeBeforeDate({ type: 'users', date: convertDateToInt(date), }).toArray(); @@ -29,7 +26,7 @@ export const fillFirstDaysOfUsersIfNeeded = async (date) => { start: startOfPeriod, end: date, }); - users.forEach((user) => Analytics.insert({ + users.forEach((user) => Analytics.insertOne({ ...user, date: parseInt(user.date), })); @@ -42,16 +39,16 @@ export const findWeeklyUsersRegisteredData = async ({ start, end }) => { const startOfLastWeek = moment(endOfLastWeek).clone().subtract(daysBetweenDates, 'days').toDate(); const today = convertDateToInt(end); const yesterday = convertDateToInt(moment(end).clone().subtract(1, 'days').toDate()); - const currentPeriodUsers = await AnalyticsRaw.getTotalOfRegisteredUsersByDate({ + const currentPeriodUsers = await Analytics.getTotalOfRegisteredUsersByDate({ start: convertDateToInt(start), end: convertDateToInt(end), options: { count: daysBetweenDates, sort: { _id: -1 } }, - }); - const lastPeriodUsers = await AnalyticsRaw.getTotalOfRegisteredUsersByDate({ + }).toArray(); + const lastPeriodUsers = await Analytics.getTotalOfRegisteredUsersByDate({ start: convertDateToInt(startOfLastWeek), end: convertDateToInt(endOfLastWeek), options: { count: daysBetweenDates, sort: { _id: -1 } }, - }); + }).toArray(); const yesterdayUsers = (currentPeriodUsers.find((item) => item._id === yesterday) || {}).users || 0; const todayUsers = (currentPeriodUsers.find((item) => item._id === today) || {}).users || 0; const currentPeriodTotalUsers = getTotalOfWeekItems(currentPeriodUsers, 'users'); diff --git a/ee/app/livechat-enterprise/server/hooks/afterTakeInquiry.js b/ee/app/livechat-enterprise/server/hooks/afterTakeInquiry.js index 424f3840a834..60f407f28c2e 100644 --- a/ee/app/livechat-enterprise/server/hooks/afterTakeInquiry.js +++ b/ee/app/livechat-enterprise/server/hooks/afterTakeInquiry.js @@ -17,6 +17,6 @@ callbacks.add('livechat.afterTakeInquiry', async (inquiry) => { const { department } = inquiry; debouncedDispatchWaitingQueueStatus(department); - cbLogger.debug(`Statuses for queue ${ department || 'Public' } updated succesfully`); + cbLogger.debug(`Statuses for queue ${ department || 'Public' } updated successfully`); return inquiry; }, callbacks.priority.MEDIUM, 'livechat-after-take-inquiry'); diff --git a/ee/app/livechat-enterprise/server/hooks/beforeRoutingChat.js b/ee/app/livechat-enterprise/server/hooks/beforeRoutingChat.js index 61bbd6261be7..671251f7e035 100644 --- a/ee/app/livechat-enterprise/server/hooks/beforeRoutingChat.js +++ b/ee/app/livechat-enterprise/server/hooks/beforeRoutingChat.js @@ -31,11 +31,13 @@ callbacks.add('livechat.beforeRouteChat', async (inquiry, agent) => { saveQueueInquiry(inquiry); - const [inq] = await LivechatInquiry.getCurrentSortedQueueAsync({ _id, department }); - if (inq) { - dispatchInquiryPosition(inq); + if (settings.get('Omnichannel_calculate_dispatch_service_queue_statistics')) { + const [inq] = await LivechatInquiry.getCurrentSortedQueueAsync({ _id, department }); + if (inq) { + dispatchInquiryPosition(inq); + cbLogger.debug(`Callback success. Inquiry ${ _id } position has been notified`); + } } - cbLogger.debug(`Callback success. Inquiry ${ _id } position has been notified`); return LivechatInquiry.findOneById(_id); }, callbacks.priority.HIGH, 'livechat-before-routing-chat'); diff --git a/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts b/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts index f8ebf12e436d..b59b31f01215 100644 --- a/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts +++ b/ee/app/livechat-enterprise/server/hooks/onAgentAssignmentFailed.ts @@ -16,8 +16,7 @@ const handleOnAgentAssignmentFailed = async ({ inquiry, room, options }: { inqui const { _id: roomId } = room; const { _id: inquiryId } = inquiry; - LivechatInquiry.queueInquiry(inquiryId); - LivechatInquiry.removeDefaultAgentById(inquiryId); + LivechatInquiry.queueInquiryAndRemoveDefaultAgent(inquiryId); LivechatRooms.removeAgentByRoomId(roomId); Subscriptions.removeByRoomId(roomId); dispatchAgentDelegated(roomId, null); diff --git a/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js b/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js index eb52f3ed9752..2bf0ff792be7 100644 --- a/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js +++ b/ee/app/livechat-enterprise/server/hooks/onCloseLivechat.js @@ -1,7 +1,6 @@ import { callbacks } from '../../../../../app/callbacks'; import { settings } from '../../../../../app/settings/server'; import { debouncedDispatchWaitingQueueStatus } from '../lib/Helper'; -import { RoutingManager } from '../../../../../app/livechat/server/lib/RoutingManager'; import { LivechatEnterprise } from '../lib/LivechatEnterprise'; const onCloseLivechat = (room) => { @@ -12,10 +11,7 @@ const onCloseLivechat = (room) => { } const { departmentId } = room || {}; - if (!RoutingManager.getConfig().autoAssignAgent) { - debouncedDispatchWaitingQueueStatus(departmentId); - return room; - } + debouncedDispatchWaitingQueueStatus(departmentId); return room; }; diff --git a/ee/app/livechat-enterprise/server/lib/Helper.js b/ee/app/livechat-enterprise/server/lib/Helper.js index 59e3425657ba..c4f14c440547 100644 --- a/ee/app/livechat-enterprise/server/lib/Helper.js +++ b/ee/app/livechat-enterprise/server/lib/Helper.js @@ -95,8 +95,17 @@ export const dispatchInquiryPosition = async (inquiry, queueInfo) => { }; export const dispatchWaitingQueueStatus = async (department) => { + if (!settings.get('Livechat_waiting_queue') && !settings.get('Omnichannel_calculate_dispatch_service_queue_statistics')) { + return; + } + helperLogger.debug(`Updating statuses for queue ${ department || 'Public' }`); const queue = await LivechatInquiry.getCurrentSortedQueueAsync({ department }); + + if (!queue.length) { + return; + } + const queueInfo = await getQueueInfo(department); queue.forEach((inquiry) => { dispatchInquiryPosition(inquiry, queueInfo); @@ -126,14 +135,11 @@ export const processWaitingQueue = async (department) => { if (room && room.servedBy) { const { _id: rid, servedBy: { _id: agentId } } = room; - helperLogger.debug(`Inquiry ${ inquiry._id } taken succesfully by agent ${ agentId }. Notifying`); + helperLogger.debug(`Inquiry ${ inquiry._id } taken successfully by agent ${ agentId }. Notifying`); return setTimeout(() => { propagateAgentDelegated(rid, agentId); }, 1000); } - - const { departmentId } = room || {}; - await debouncedDispatchWaitingQueueStatus(departmentId); }; export const setPredictedVisitorAbandonmentTime = (room) => { @@ -254,6 +260,10 @@ export const getLivechatQueueInfo = async (room) => { return null; } + if (!settings.get('Omnichannel_calculate_dispatch_service_queue_statistics')) { + return null; + } + const { _id: rid, departmentId: department } = room; const inquiry = LivechatInquiry.findOneByRoomId(rid, { fields: { _id: 1, status: 1 } }); if (!inquiry) { diff --git a/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js b/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js index 0109eceb75fd..0646bced25c9 100644 --- a/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js +++ b/ee/app/livechat-enterprise/server/lib/LivechatEnterprise.js @@ -199,7 +199,8 @@ export const LivechatEnterprise = { }, }; -const RACE_TIMEOUT = 1000; +const DEFAULT_RACE_TIMEOUT = 5000; +let queueDelayTimeout = DEFAULT_RACE_TIMEOUT; const queueWorker = { running: false, @@ -242,9 +243,9 @@ const queueWorker = { } const queue = await this.nextQueue(); - queueLogger.debug(`Executing queue ${ queue || 'Public' } with timeout of ${ RACE_TIMEOUT }`); + queueLogger.debug(`Executing queue ${ queue || 'Public' } with timeout of ${ queueDelayTimeout }`); - setTimeout(this.checkQueue.bind(this, queue), RACE_TIMEOUT); + setTimeout(this.checkQueue.bind(this, queue), queueDelayTimeout); }, async checkQueue(queue) { @@ -292,3 +293,7 @@ settings.watch('Livechat_enabled', (enabled) => { omnichannelIsEnabled = enabled; omnichannelIsEnabled && RoutingManager.isMethodSet() ? shouldQueueStart() : queueWorker.stop(); }); + +settings.watch('Omnichannel_queue_delay_timeout', (timeout) => { + queueDelayTimeout = timeout < 1 ? DEFAULT_RACE_TIMEOUT : timeout * 1000; +}); diff --git a/ee/app/livechat-enterprise/server/permissions.js b/ee/app/livechat-enterprise/server/permissions.js deleted file mode 100644 index ef2b44ffd0b7..000000000000 --- a/ee/app/livechat-enterprise/server/permissions.js +++ /dev/null @@ -1,22 +0,0 @@ -import { Permissions, Roles } from '../../../../app/models/server'; - -export const createPermissions = () => { - if (!Permissions) { - return; - } - - const livechatMonitorRole = 'livechat-monitor'; - const livechatManagerRole = 'livechat-manager'; - const adminRole = 'admin'; - - const monitorRole = Roles.findOneById(livechatMonitorRole, { fields: { _id: 1 } }); - if (!monitorRole) { - Roles.createOrUpdate(livechatMonitorRole); - } - - Permissions.create('manage-livechat-units', [adminRole, livechatManagerRole]); - Permissions.create('manage-livechat-monitors', [adminRole, livechatManagerRole]); - Permissions.create('manage-livechat-tags', [adminRole, livechatManagerRole]); - Permissions.create('manage-livechat-priorities', [adminRole, livechatManagerRole]); - Permissions.create('manage-livechat-canned-responses', [adminRole, livechatManagerRole, livechatMonitorRole]); -}; diff --git a/ee/app/livechat-enterprise/server/permissions.ts b/ee/app/livechat-enterprise/server/permissions.ts new file mode 100644 index 000000000000..1b0ef93e0a68 --- /dev/null +++ b/ee/app/livechat-enterprise/server/permissions.ts @@ -0,0 +1,21 @@ + +import { Permissions, Roles } from '../../../../app/models/server/raw'; + +export const createPermissions = async (): Promise => { + const livechatMonitorRole = 'livechat-monitor'; + const livechatManagerRole = 'livechat-manager'; + const adminRole = 'admin'; + + const monitorRole = await Roles.findOneById(livechatMonitorRole, { fields: { _id: 1 } }); + if (!monitorRole) { + await Roles.createOrUpdate(livechatMonitorRole); + } + + await Promise.all([ + Permissions.create('manage-livechat-units', [adminRole, livechatManagerRole]), + Permissions.create('manage-livechat-monitors', [adminRole, livechatManagerRole]), + Permissions.create('manage-livechat-tags', [adminRole, livechatManagerRole]), + Permissions.create('manage-livechat-priorities', [adminRole, livechatManagerRole]), + Permissions.create('manage-livechat-canned-responses', [adminRole, livechatManagerRole, livechatMonitorRole]), + ]); +}; diff --git a/ee/app/livechat-enterprise/server/settings.ts b/ee/app/livechat-enterprise/server/settings.ts index d3dc3be24135..80c5f53e5ef2 100644 --- a/ee/app/livechat-enterprise/server/settings.ts +++ b/ee/app/livechat-enterprise/server/settings.ts @@ -113,6 +113,37 @@ export const createSettings = (): void => { ], }); + this.add('Omnichannel_calculate_dispatch_service_queue_statistics', true, { + type: 'boolean', + group: 'Omnichannel', + section: 'Queue_management', + i18nLabel: 'Omnichannel_calculate_dispatch_service_queue_statistics', + enableQuery: [{ _id: 'Livechat_waiting_queue', value: true }, omnichannelEnabledQuery], + enterprise: true, + invalidValue: false, + modules: [ + 'livechat-enterprise', + ], + }); + + this.add('Omnichannel_queue_delay_timeout', 5, { + type: 'int', + group: 'Omnichannel', + section: 'Queue_management', + i18nLabel: 'Queue_delay_timeout', + i18nDescription: 'Time_in_seconds', + enableQuery: [ + { _id: 'Livechat_waiting_queue', value: true }, + { _id: 'Livechat_Routing_Method', value: { $ne: 'Manual_Selection' } }, + omnichannelEnabledQuery, + ], + enterprise: true, + invalidValue: 5, + modules: [ + 'livechat-enterprise', + ], + }); + this.add('Livechat_number_most_recent_chats_estimate_wait_time', 100, { type: 'int', group: 'Omnichannel', diff --git a/ee/server/lib/ldap/Manager.ts b/ee/server/lib/ldap/Manager.ts index 7230dbdb6c6d..6b1c46b34eed 100644 --- a/ee/server/lib/ldap/Manager.ts +++ b/ee/server/lib/ldap/Manager.ts @@ -8,10 +8,10 @@ import type { IRole } from '../../../../definition/IRole'; import { IImportUser } from '../../../../definition/IImportUser'; import { ImporterAfterImportCallback } from '../../../../app/importer/server/definitions/IConversionCallbacks'; import { settings } from '../../../../app/settings/server'; -import { Roles, Rooms } from '../../../../app/models/server'; +import { Rooms } from '../../../../app/models/server'; import { Users as UsersRaw, - Roles as RolesRaw, + Roles, Subscriptions as SubscriptionsRaw, } from '../../../../app/models/server/raw'; import { LDAPDataConverter } from '../../../../server/lib/ldap/DataConverter'; @@ -191,7 +191,7 @@ export class LDAPEEManager extends LDAPManager { return; } - const roles = await RolesRaw.find({}, { + const roles = await Roles.find({}, { fields: { _updatedAt: 0, }, @@ -224,7 +224,7 @@ export class LDAPEEManager extends LDAPManager { logger.debug(`User role exists for mapping ${ ldapField } -> ${ roleName }`); if (await this.isUserInGroup(ldap, syncUserRolesBaseDN, syncUserRolesFilter, { dn, username }, ldapField)) { - if (Roles.addUserRoles(user._id, roleName)) { + if (await Roles.addUserRoles(user._id, roleName)) { this.broadcastRoleChange('added', roleName, user._id, username); } logger.debug(`Synced user group ${ roleName } from LDAP for ${ user.username }`); @@ -235,7 +235,7 @@ export class LDAPEEManager extends LDAPManager { continue; } - if (Roles.removeUserRoles(user._id, roleName)) { + if (await Roles.removeUserRoles(user._id, roleName)) { this.broadcastRoleChange('removed', roleName, user._id, username); } } diff --git a/ee/server/lib/oauth/Manager.ts b/ee/server/lib/oauth/Manager.ts index 55d571344246..b53b1de90db5 100644 --- a/ee/server/lib/oauth/Manager.ts +++ b/ee/server/lib/oauth/Manager.ts @@ -1,7 +1,8 @@ import { addUserRoles, removeUserFromRoles } from '../../../../app/authorization/server'; -import { Roles, Rooms } from '../../../../app/models/server'; +import { Rooms } from '../../../../app/models/server'; import { addUserToRoom, createRoom } from '../../../../app/lib/server/functions'; import { Logger } from '../../../../app/logger/server'; +import { Roles } from '../../../../app/models/server/raw'; export const logger = new Logger('OAuth'); @@ -60,7 +61,7 @@ export class OAuthEEManager { if (identity && roleClaimName) { // Adding roles if (identity[roleClaimName] && Array.isArray(identity[roleClaimName])) { - roles = identity[roleClaimName].filter((val: string) => val !== 'offline_access' && val !== 'uma_authorization' && Roles.findOneByIdOrName(val)); + roles = identity[roleClaimName].filter((val: string) => val !== 'offline_access' && val !== 'uma_authorization' && Promise.await(Roles.findOneByIdOrName(val))); } } diff --git a/ee/server/services/ddp-streamer/streams/index.ts b/ee/server/services/ddp-streamer/streams/index.ts index a34fd2757eec..8d058b1a87c2 100644 --- a/ee/server/services/ddp-streamer/streams/index.ts +++ b/ee/server/services/ddp-streamer/streams/index.ts @@ -14,10 +14,11 @@ const notifications = new NotificationsModule(Stream); getConnection() .then((db) => { + const Users = new UsersRaw(db.collection(Collections.User)); notifications.configure({ Rooms: new RoomsRaw(db.collection(Collections.Rooms)), - Subscriptions: new SubscriptionsRaw(db.collection(Collections.Subscriptions)), - Users: new UsersRaw(db.collection(Collections.User)), + Subscriptions: new SubscriptionsRaw(db.collection(Collections.Subscriptions), { Users }), + Users, Settings: new SettingsRaw(db.collection(Collections.Settings)), }); }); diff --git a/ee/server/services/stream-hub/StreamHub.ts b/ee/server/services/stream-hub/StreamHub.ts index b559e43a7f75..ebb4830ead12 100755 --- a/ee/server/services/stream-hub/StreamHub.ts +++ b/ee/server/services/stream-hub/StreamHub.ts @@ -31,13 +31,13 @@ export class StreamHub extends ServiceClass implements IServiceClass { const Rooms = new RoomsRaw(db.collection('rocketchat_room'), Trash); const Settings = new SettingsRaw(db.collection('rocketchat_settings'), Trash); const Users = new UsersRaw(UsersCol, Trash); - const UsersSessions = new UsersSessionsRaw(db.collection('usersSessions'), Trash); - const Subscriptions = new SubscriptionsRaw(db.collection('rocketchat_subscription'), Trash); + const UsersSessions = new UsersSessionsRaw(db.collection('usersSessions'), Trash, { preventSetUpdatedAt: true }); + const Subscriptions = new SubscriptionsRaw(db.collection('rocketchat_subscription'), { Users }, Trash); const LivechatInquiry = new LivechatInquiryRaw(db.collection('rocketchat_livechat_inquiry'), Trash); const LivechatDepartmentAgents = new LivechatDepartmentAgentsRaw(db.collection('rocketchat_livechat_department_agents'), Trash); const Messages = new MessagesRaw(db.collection('rocketchat_message'), Trash); const Permissions = new PermissionsRaw(db.collection('rocketchat_permissions'), Trash); - const Roles = new RolesRaw(db.collection('rocketchat_roles'), Trash, { Users, Subscriptions }); + const Roles = new RolesRaw(db.collection('rocketchat_roles'), { Users, Subscriptions }, Trash); const LoginServiceConfiguration = new LoginServiceConfigurationRaw(db.collection('meteor_accounts_loginServiceConfiguration'), Trash); const InstanceStatus = new InstanceStatusRaw(db.collection('instances'), Trash); const IntegrationHistory = new IntegrationHistoryRaw(db.collection('rocketchat_integration_history'), Trash); diff --git a/imports/message-read-receipt/server/api/methods/getReadReceipts.js b/imports/message-read-receipt/server/api/methods/getReadReceipts.js index f73448b92d3c..810c7db70d65 100644 --- a/imports/message-read-receipt/server/api/methods/getReadReceipts.js +++ b/imports/message-read-receipt/server/api/methods/getReadReceipts.js @@ -1,10 +1,11 @@ import { Meteor } from 'meteor/meteor'; -import { Messages } from '../../../../../app/models'; +import { Messages } from '../../../../../app/models/server'; +import { canAccessRoom } from '../../../../../app/authorization/server'; import { ReadReceipt } from '../../lib/ReadReceipt'; Meteor.methods({ - getReadReceipts({ messageId }) { + async getReadReceipts({ messageId }) { if (!Meteor.userId()) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getReadReceipts' }); } @@ -19,8 +20,7 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-message', 'Invalid message', { method: 'getReadReceipts' }); } - const room = Meteor.call('canAccessRoom', message.rid, Meteor.userId()); - if (!room) { + if (!canAccessRoom({ _id: message.rid }, { _id: Meteor.userId() })) { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'getReadReceipts' }); } diff --git a/imports/message-read-receipt/server/lib/ReadReceipt.js b/imports/message-read-receipt/server/lib/ReadReceipt.js index ab30a835763b..82310014d5e5 100644 --- a/imports/message-read-receipt/server/lib/ReadReceipt.js +++ b/imports/message-read-receipt/server/lib/ReadReceipt.js @@ -1,13 +1,12 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; -import { ReadReceipts, Subscriptions, Messages, Rooms, Users, LivechatVisitors } from '../../../../app/models'; -import { settings } from '../../../../app/settings'; -import { roomTypes } from '../../../../app/utils'; +import { Subscriptions, Messages, Rooms, Users, LivechatVisitors } from '../../../../app/models/server'; +import { ReadReceipts } from '../../../../app/models/server/raw'; +import { settings } from '../../../../app/settings/server'; +import { roomTypes } from '../../../../app/utils/server'; import { SystemLogger } from '../../../../server/lib/logger/system'; -const rawReadReceipts = ReadReceipts.model.rawCollection(); - // debounced function by roomId, so multiple calls within 2 seconds to same roomId runs only once const list = {}; const debounceByRoomId = function(fn) { @@ -85,17 +84,21 @@ export const ReadReceipt = { } try { - await rawReadReceipts.insertMany(receipts); + await ReadReceipts.insertMany(receipts); } catch (e) { SystemLogger.error('Error inserting read receipts per user'); } } }, - getReceipts(message) { - return ReadReceipts.findByMessageId(message._id).map((receipt) => ({ + async getReceipts(message) { + const receipts = await ReadReceipts.findByMessageId(message._id).toArray(); + + return receipts.map((receipt) => ({ ...receipt, - user: receipt.token ? LivechatVisitors.getVisitorByToken(receipt.token, { fields: { username: 1, name: 1 } }) : Users.findOneById(receipt.userId, { fields: { username: 1, name: 1 } }), + user: receipt.token + ? LivechatVisitors.getVisitorByToken(receipt.token, { fields: { username: 1, name: 1 } }) + : Users.findOneById(receipt.userId, { fields: { username: 1, name: 1 } }), })); }, }; diff --git a/package-lock.json b/package-lock.json index c679ed74f1db..fb1db4f80cf9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9916,6 +9916,15 @@ } } }, + "@types/node-rsa": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@types/node-rsa/-/node-rsa-1.1.1.tgz", + "integrity": "sha512-itzxtaBgk4OMbrCawVCvas934waMZWjW17v7EYgFVlfYS/cl0/P7KZdojWCq9SDJMI5cnLQLUP8ayhVCTY8TEg==", + "dev": true, + "requires": { + "@types/node": "*" + } + }, "@types/nodemailer": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.2.tgz", diff --git a/package.json b/package.json index 77fd4943a19b..c5865032264d 100644 --- a/package.json +++ b/package.json @@ -90,6 +90,7 @@ "@types/moment-timezone": "^0.5.30", "@types/mongodb": "^3.6.19", "@types/node": "^12.20.10", + "@types/node-rsa": "^1.1.1", "@types/nodemailer": "^6.4.2", "@types/parseurl": "^1.3.1", "@types/psl": "^1.1.0", diff --git a/packages/rocketchat-i18n/i18n/en.i18n.json b/packages/rocketchat-i18n/i18n/en.i18n.json index d021ceebcd5a..3d317ebc4c72 100644 --- a/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/packages/rocketchat-i18n/i18n/en.i18n.json @@ -3178,6 +3178,8 @@ "Omnichannel": "Omnichannel", "Omnichannel_Directory": "Omnichannel Directory", "Omnichannel_appearance": "Omnichannel Appearance", + "Omnichannel_calculate_dispatch_service_queue_statistics": "Calculate and dispatch Omnichannel waiting queue statistics", + "Omnichannel_calculate_dispatch_service_queue_statistics_Description": "Processing and dispatching waiting queue statistics such as position and estimated waiting time. If *Livechat channel* is not in use, it is recommended to disable this setting and prevent the server from doing unnecessary processes.", "Omnichannel_Contact_Center": "Omnichannel Contact Center", "Omnichannel_contact_manager_routing": "Assign new conversations to the contact manager", "Omnichannel_contact_manager_routing_Description": "This setting allocates a chat to the assigned Contact Manager, as long as the Contact Manager is online when the chat starts", @@ -3405,6 +3407,7 @@ "Query_description": "Additional conditions for determining which users to send the email to. Unsubscribed users are automatically removed from the query. It must be a valid JSON. Example: \"{\"createdAt\":{\"$gt\":{\"$date\": \"2015-01-01T00:00:00.000Z\"}}}\"", "Query_is_not_valid_JSON": "Query is not valid JSON", "Queue": "Queue", + "Queue_delay_timeout": "Queue processing delay timeout", "Queue_Time": "Queue Time", "Queue_management": "Queue Management", "quote": "quote", @@ -4530,6 +4533,8 @@ "Videos": "Videos", "View_All": "View All Members", "View_channels": "View Channels", + "view-omnichannel-contact-center": "View Omnichannel Contact Center", + "view-omnichannel-contact-center_description": "Permission to view and interact with the Omnichannel Contact Center", "View_Logs": "View Logs", "View_mode": "View Mode", "View_original": "View Original", @@ -4730,4 +4735,4 @@ "Your_temporary_password_is_password": "Your temporary password is [password].", "Your_TOTP_has_been_reset": "Your Two Factor TOTP has been reset.", "Your_workspace_is_ready": "Your workspace is ready to use ๐ŸŽ‰" -} \ No newline at end of file +} diff --git a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index cbedd37797ae..f34ce0b737cd 100644 --- a/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -3171,6 +3171,8 @@ "Omnichannel": "Omnichannel", "Omnichannel_Directory": "Diretรณrio Omnichannel", "Omnichannel_appearance": "Aparรชncia do Omnichannel", + "Omnichannel_calculate_dispatch_service_queue_statistics": "Calcular e propagar dados estatรญsticos da fila de espera Omnichannel", + "Omnichannel_calculate_dispatch_service_queue_statistics_Description": "Proessamento de dados estatรญsticos da fila de espera Omnichannel como as posiรงรฃo e tempo estimado de espera na fila. Se o *canal Livechat* nรฃo estiver em uso, รฉ recomendรกvel desabilitar essa configuraรงรฃo e evitar que o servidor execute processos desnecessรกrios.", "Omnichannel_Contact_Center": "Central de Contatos Omnichannel", "Omnichannel_contact_manager_routing": "Atribuir novas conversas para o Gerente do Contato", "Omnichannel_contact_manager_routing_Description": "Aloca novos chats para o Gerente do Contato(quando atribuรญdo), desde que o Gerente do Contato esteja online quando a conversa รฉ iniciado", @@ -3397,6 +3399,7 @@ "Query_description": "Condiรงรตes adicionais para determinar para quais usuรกrios enviar e-mail. Usuรกrios nรฃo inscritos sรฃo automaticamente removidos a partir da consulta. Deve ser um JSON vรกlido. Exemplo: `{\"createdAt\": {\"$gt\": {\"$date\": \"2015-01-01T00: 00: 00.000Z\"}}}`", "Query_is_not_valid_JSON": "Query nรฃo รฉ um JSON vรกlido", "Queue": "Fila", + "Queue_delay_timeout": "Tempo limite de atraso para processamento da fila", "Queue_Time": "Tempo na fila", "Queue_management": "Gerenciamento de Fila", "quote": "citaรงรฃo", @@ -4522,6 +4525,8 @@ "Videos": "Vรญdeos", "View_All": "Ver Todos Membros", "View_channels": "Ver Canais", + "view-omnichannel-contact-center": "Ver o Centro de Contatos do Omnichannel", + "view-omnichannel-contact-center_description": "Permissรฃo para ver e interagir com o Centro de Contatos do Omnichannel", "View_Logs": "Ver Logs", "View_mode": "Modo de visualizaรงรฃo", "View_original": "Visualizar original", @@ -4722,4 +4727,4 @@ "Your_temporary_password_is_password": "Sua senha temporรกria รฉ [password].", "Your_TOTP_has_been_reset": "Seu TOTP de dois fatores foi redefinido.", "Your_workspace_is_ready": "O seu espaรงo de trabalho estรก pronto para usar ๐ŸŽ‰" -} \ No newline at end of file +} diff --git a/server/cron/statistics.js b/server/cron/statistics.js index 7c51e51963cf..8f268bf0c1a5 100644 --- a/server/cron/statistics.js +++ b/server/cron/statistics.js @@ -5,8 +5,8 @@ import { getWorkspaceAccessToken } from '../../app/cloud/server'; import { statistics } from '../../app/statistics'; import { settings } from '../../app/settings/server'; -function generateStatistics(logger) { - const cronStatistics = statistics.save(); +async function generateStatistics(logger) { + const cronStatistics = await statistics.save(); cronStatistics.host = Meteor.absoluteUrl(); diff --git a/server/database/readSecondaryPreferred.ts b/server/database/readSecondaryPreferred.ts index b23e538a319c..e29e90b4dc92 100644 --- a/server/database/readSecondaryPreferred.ts +++ b/server/database/readSecondaryPreferred.ts @@ -1,6 +1,6 @@ -import { ReadPreference, Db } from 'mongodb'; +import { ReadPreference, Db, ReadPreferenceOrMode } from 'mongodb'; -export function readSecondaryPreferred(db: Db, tags: any[] = []): ReadPreference | string { +export function readSecondaryPreferred(db: Db, tags: any[] = []): ReadPreferenceOrMode { const { readPreferenceTags, readPreference } = db.options; if (tags.length) { diff --git a/server/features/EmailInbox/EmailInbox.ts b/server/features/EmailInbox/EmailInbox.ts index f483e546a4ab..4e8f4aac8607 100644 --- a/server/features/EmailInbox/EmailInbox.ts +++ b/server/features/EmailInbox/EmailInbox.ts @@ -55,7 +55,7 @@ export async function configureEmailInboxes(): Promise { } try { - await EmailMessageHistory.insertOne({ _id: email.messageId, email: emailInboxRecord.email }); + await EmailMessageHistory.create({ _id: email.messageId, email: emailInboxRecord.email }); onEmailReceived(email, emailInboxRecord.email, emailInboxRecord.department); } catch (e: any) { // In case the email message history has been received by other instance.. diff --git a/server/features/EmailInbox/EmailInbox_Outgoing.ts b/server/features/EmailInbox/EmailInbox_Outgoing.ts index f9cf4e1d2140..271729d20a62 100644 --- a/server/features/EmailInbox/EmailInbox_Outgoing.ts +++ b/server/features/EmailInbox/EmailInbox_Outgoing.ts @@ -8,7 +8,8 @@ import { IEmailInbox } from '../../../definition/IEmailInbox'; import { IUser } from '../../../definition/IUser'; import { FileUpload } from '../../../app/file-upload/server'; import { slashCommands } from '../../../app/utils/server'; -import { Messages, Rooms, Uploads, Users } from '../../../app/models/server'; +import { Messages, Rooms, Users } from '../../../app/models/server'; +import { Uploads } from '../../../app/models/server/raw'; import { Inbox, inboxes } from './EmailInbox'; import { sendMessage } from '../../../app/lib/server/functions/sendMessage'; import { settings } from '../../../app/settings/server'; @@ -81,7 +82,11 @@ slashCommands.add('sendEmailAttachment', (command: any, params: string) => { }); } - const file = Uploads.findOneById(message.file._id); + const file = Promise.await(Uploads.findOneById(message.file._id)); + + if (!file) { + return; + } FileUpload.getBuffer(file, (_err?: Error, buffer?: Buffer) => { !_err && buffer && sendEmail(inbox, { diff --git a/server/lib/channelExport.ts b/server/lib/channelExport.ts index 720304cc9247..d8a046522482 100644 --- a/server/lib/channelExport.ts +++ b/server/lib/channelExport.ts @@ -156,9 +156,9 @@ export const sendFile = async (data: ExportFile, user: IUser): Promise => await exportMessages(); - fullFileList.forEach((attachmentData: any) => { - copyFile(attachmentData, assetsPath); - }); + for await (const attachmentData of fullFileList) { + await copyFile(attachmentData, assetsPath); + } const exportFile = `${ baseDir }-export.zip`; await makeZipFile(exportPath, exportFile); diff --git a/server/lib/migrations.ts b/server/lib/migrations.ts index f83db8d418dd..a7be121af9d8 100644 --- a/server/lib/migrations.ts +++ b/server/lib/migrations.ts @@ -135,7 +135,7 @@ function migrate(direction: 'up' | 'down', migration: IMigration): void { log.startup(`Running ${ direction }() on version ${ migration.version }${ migration.name ? `(${ migration.name })` : '' }`); - migration[direction]?.(migration); + Promise.await(migration[direction]?.(migration)); } diff --git a/server/lib/pushConfig.js b/server/lib/pushConfig.ts similarity index 83% rename from server/lib/pushConfig.js rename to server/lib/pushConfig.ts index bb70e738c492..150617458b5f 100644 --- a/server/lib/pushConfig.js +++ b/server/lib/pushConfig.ts @@ -2,13 +2,13 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import { getWorkspaceAccessToken } from '../../app/cloud/server'; -import { hasRole } from '../../app/authorization'; -import { settings } from '../../app/settings'; +import { hasRole } from '../../app/authorization/server'; +import { settings } from '../../app/settings/server'; import { appTokensCollection, Push } from '../../app/push/server'; Meteor.methods({ - push_test() { + 'push_test'() { const user = Meteor.user(); if (!user) { @@ -71,17 +71,25 @@ Meteor.methods({ }, }); -function configurePush() { - if (!settings.get('Push_enable')) { +settings.watch('Push_enable', async function(enabled) { + if (!enabled) { return; } - const gateways = settings.get('Push_enable_gateway') && settings.get('Register_Server') && settings.get('Cloud_Service_Agree_PrivacyTerms') - ? settings.get('Push_gateway').split('\n') + ? settings.get('Push_gateway').split('\n') : undefined; - let apn; - let gcm; + let apn: { + apiKey?: string; + passphrase: string; + key: string; + cert: string; + gateway?: string; + } | undefined; + let gcm: { + apiKey: string; + projectNumber: string; + } | undefined; if (!gateways) { gcm = { @@ -120,9 +128,7 @@ function configurePush() { gateways, uniqueId: settings.get('uniqueID'), getAuthorization() { - return `Bearer ${ getWorkspaceAccessToken() }`; + return `Bearer ${ Promise.await(getWorkspaceAccessToken()) }`; }, }); -} - -Meteor.startup(configurePush); +}); diff --git a/server/lib/sendMessagesToAdmins.js b/server/lib/sendMessagesToAdmins.js index 5eedbf14aeae..08af72647e96 100644 --- a/server/lib/sendMessagesToAdmins.js +++ b/server/lib/sendMessagesToAdmins.js @@ -1,47 +1,45 @@ + import { Meteor } from 'meteor/meteor'; import { SystemLogger } from './logger/system'; -import { Roles, Users } from '../../app/models/server'; +import { Users } from '../../app/models/server'; +import { Roles } from '../../app/models/server/raw'; -export function sendMessagesToAdmins({ - fromId = 'rocket.cat', - checkFrom = true, - msgs = [], - banners = [], -}) { - const fromUser = checkFrom ? Users.findOneById(fromId, { fields: { _id: 1 } }) : true; +export async function sendMessagesToAdmins(message) { + const fromUser = message.checkFrom ? Users.findOneById(message.fromId, { fields: { _id: 1 } }) : true; + const users = await Roles.findUsersInRole('admin'); - Roles.findUsersInRole('admin').forEach((adminUser) => { + users.forEach((adminUser) => { if (fromUser) { try { - Meteor.runAsUser(fromId, () => Meteor.call('createDirectMessage', adminUser.username)); + Meteor.runAsUser(message.fromId, () => Meteor.call('createDirectMessage', adminUser.username)); - const rid = [adminUser._id, fromId].sort().join(''); + const rid = [adminUser._id, message.fromId].sort().join(''); - if (typeof msgs === 'function') { - msgs = msgs({ adminUser }); + if (typeof message.msgs === 'function') { + message.msgs = message.msgs({ adminUser }); } - if (!Array.isArray(msgs)) { - msgs = [msgs]; + if (!Array.isArray(message.msgs)) { + message.msgs = [message.msgs]; } - if (typeof banners === 'function') { - banners = banners({ adminUser }); + if (typeof message.banners === 'function') { + message.banners = message.banners({ adminUser }); } - if (!Array.isArray(banners)) { - banners = Array.from(banners); + if (!Array.isArray(message.banners)) { + message.banners = [message.banners]; } - Meteor.runAsUser(fromId, () => { - msgs.forEach((msg) => Meteor.call('sendMessage', Object.assign({ rid }, msg))); + Meteor.runAsUser(message.fromId, () => { + message.msgs.forEach((msg) => Meteor.call('sendMessage', Object.assign({ rid }, msg))); }); } catch (e) { SystemLogger.error(e); } } - banners.forEach((banner) => Users.addBannerById(adminUser._id, banner)); + message.banners.forEach((banner) => Users.addBannerById(adminUser._id, banner)); }); } diff --git a/server/methods/OEmbedCacheCleanup.js b/server/methods/OEmbedCacheCleanup.js index 168cc4aba10b..d2a8d651467b 100644 --- a/server/methods/OEmbedCacheCleanup.js +++ b/server/methods/OEmbedCacheCleanup.js @@ -1,11 +1,11 @@ import { Meteor } from 'meteor/meteor'; -import { OEmbedCache } from '../../app/models'; +import { OEmbedCache } from '../../app/models/server/raw'; import { settings } from '../../app/settings'; import { hasRole } from '../../app/authorization'; Meteor.methods({ - OEmbedCacheCleanup() { + async OEmbedCacheCleanup() { if (Meteor.userId() && !hasRole(Meteor.userId(), 'admin')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'OEmbedCacheCleanup', @@ -15,7 +15,7 @@ Meteor.methods({ const date = new Date(); const expirationDays = settings.get('API_EmbedCacheExpirationDays'); date.setDate(date.getDate() - expirationDays); - OEmbedCache.removeAfterDate(date); + await OEmbedCache.removeAfterDate(date); return { message: 'cache_cleared', }; diff --git a/server/methods/afterVerifyEmail.js b/server/methods/afterVerifyEmail.js deleted file mode 100644 index 5e1d65004d5b..000000000000 --- a/server/methods/afterVerifyEmail.js +++ /dev/null @@ -1,29 +0,0 @@ -import { Meteor } from 'meteor/meteor'; - -import { Users, Roles } from '../../app/models'; - -Meteor.methods({ - afterVerifyEmail() { - const userId = Meteor.userId(); - - if (!userId) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'afterVerifyEmail', - }); - } - - const user = Users.findOneById(userId); - if (user && user.emails && Array.isArray(user.emails)) { - const verifiedEmail = user.emails.find((email) => email.verified); - const rolesToChangeTo = { anonymous: ['user'] }; - const rolesThatNeedChanges = user.roles.filter((role) => rolesToChangeTo[role]); - - if (rolesThatNeedChanges.length && verifiedEmail) { - rolesThatNeedChanges.forEach((role) => { - Roles.addUserRoles(user._id, rolesToChangeTo[role]); - Roles.removeUserRoles(user._id, role); - }); - } - } - }, -}); diff --git a/server/methods/afterVerifyEmail.ts b/server/methods/afterVerifyEmail.ts new file mode 100644 index 000000000000..6f6a621fc315 --- /dev/null +++ b/server/methods/afterVerifyEmail.ts @@ -0,0 +1,39 @@ +import { Meteor } from 'meteor/meteor'; + +import { Users } from '../../app/models/server'; +import { Roles } from '../../app/models/server/raw'; +import { IUser } from '../../definition/IUser'; + +const rolesToChangeTo: Map = new Map([ + ['anonymous', ['user']], +]); + +Meteor.methods({ + async afterVerifyEmail() { + const userId = Meteor.userId(); + + if (!userId) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'afterVerifyEmail', + }); + } + + const user = Users.findOneById(userId) as IUser; + if (user && user.emails && Array.isArray(user.emails)) { + const verifiedEmail = user.emails.find((email) => email.verified); + + const rolesThatNeedChanges = user.roles.filter((role) => rolesToChangeTo.has(role)); + + + if (verifiedEmail) { + await Promise.all(rolesThatNeedChanges.map(async (role) => { + const rolesToAdd = rolesToChangeTo.get(role); + if (rolesToAdd) { + await Roles.addUserRoles(userId, rolesToAdd); + } + await Roles.removeUserRoles(user._id, [role]); + })); + } + } + }, +}); diff --git a/server/methods/browseChannels.js b/server/methods/browseChannels.js index 03bb78922c7c..9fc942a1657b 100644 --- a/server/methods/browseChannels.js +++ b/server/methods/browseChannels.js @@ -145,7 +145,7 @@ const getTeams = (user, searchTerm, sort, pagination) => { }; }; -const getUsers = (user, text, workspace, sort, pagination) => { +const getUsers = async (user, text, workspace, sort, pagination) => { if (!user || !hasPermission(user._id, 'view-outside-room') || !hasPermission(user._id, 'view-d-room')) { return; } @@ -183,7 +183,7 @@ const getUsers = (user, text, workspace, sort, pagination) => { // Try to find federated users, when applicable if (isFederationEnabled() && workspace === 'external' && text.indexOf('@') !== -1) { - const users = federationSearchUsers(text); + const users = await federationSearchUsers(text); for (const user of users) { if (results.find((e) => e._id === user._id)) { continue; } @@ -208,7 +208,7 @@ const getUsers = (user, text, workspace, sort, pagination) => { }; Meteor.methods({ - browseChannels({ text = '', workspace = '', type = 'channels', sortBy = 'name', sortDirection = 'asc', page, offset, limit = 10 }) { + async browseChannels({ text = '', workspace = '', type = 'channels', sortBy = 'name', sortDirection = 'asc', page, offset, limit = 10 }) { const searchTerm = s.trim(escapeRegExp(text)); if (!['channels', 'users', 'teams'].includes(type) || !['asc', 'desc'].includes(sortDirection) || ((!page && page !== 0) && (!offset && offset !== 0))) { diff --git a/server/methods/canAccessRoom.js b/server/methods/canAccessRoom.js index 7aa3e637a867..4b1f7b28098c 100644 --- a/server/methods/canAccessRoom.js +++ b/server/methods/canAccessRoom.js @@ -5,54 +5,57 @@ import { Users, Rooms } from '../../app/models/server'; import { canAccessRoom } from '../../app/authorization/server'; import { settings } from '../../app/settings/server'; -Meteor.methods({ - canAccessRoom(rid, userId, extraData) { - check(rid, String); - check(userId, Match.Maybe(String)); - - let user; +if (['yes', 'true'].includes(String(process.env.ALLOW_CANACCESSROOM_METHOD).toLowerCase())) { + console.warn('Method canAccessRoom is deprecated and will be removed after version 5.0'); + Meteor.methods({ + canAccessRoom(rid, userId, extraData) { + check(rid, String); + check(userId, Match.Maybe(String)); + + let user; + + if (userId) { + user = Users.findOneById(userId, { + fields: { + username: 1, + }, + }); - if (userId) { - user = Users.findOneById(userId, { - fields: { - username: 1, - }, - }); + if (!user || !user.username) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'canAccessRoom', + }); + } + } - if (!user || !user.username) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { + if (!rid) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'canAccessRoom', }); } - } - if (!rid) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { - method: 'canAccessRoom', - }); - } + const room = Rooms.findOneById(rid); - const room = Rooms.findOneById(rid); + if (!room) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { + method: 'canAccessRoom', + }); + } - if (!room) { - throw new Meteor.Error('error-invalid-room', 'Invalid room', { - method: 'canAccessRoom', - }); - } + if (canAccessRoom.call(this, room, user, extraData)) { + if (user) { + room.username = user.username; + } + return room; + } - if (canAccessRoom.call(this, room, user, extraData)) { - if (user) { - room.username = user.username; + if (!userId && settings.get('Accounts_AllowAnonymousRead') === false) { + throw new Meteor.Error('error-invalid-user', 'Invalid user', { + method: 'canAccessRoom', + }); } - return room; - } - - if (!userId && settings.get('Accounts_AllowAnonymousRead') === false) { - throw new Meteor.Error('error-invalid-user', 'Invalid user', { - method: 'canAccessRoom', - }); - } - - return false; - }, -}); + + return false; + }, + }); +} diff --git a/server/methods/createDirectMessage.js b/server/methods/createDirectMessage.js index 151298a89c72..0a729ce72380 100644 --- a/server/methods/createDirectMessage.js +++ b/server/methods/createDirectMessage.js @@ -36,7 +36,7 @@ export function createDirectMessage(usernames, userId, excludeSelf = false) { // If the username does have an `@`, but does not exist locally, we create it first if (!to && username.indexOf('@') !== -1) { - to = addUser(username); + to = Promise.await(addUser(username)); } if (!to) { diff --git a/server/methods/deleteFileMessage.js b/server/methods/deleteFileMessage.js index 18353d1a45c8..a35a407f9211 100644 --- a/server/methods/deleteFileMessage.js +++ b/server/methods/deleteFileMessage.js @@ -5,7 +5,7 @@ import { FileUpload } from '../../app/file-upload'; import { Messages } from '../../app/models'; Meteor.methods({ - deleteFileMessage(fileID) { + async deleteFileMessage(fileID) { check(fileID, String); const msg = Messages.getMessageByFileId(fileID); diff --git a/server/methods/deleteUser.js b/server/methods/deleteUser.js index f47da58045e4..20ea77dd30b1 100644 --- a/server/methods/deleteUser.js +++ b/server/methods/deleteUser.js @@ -7,7 +7,7 @@ import { callbacks } from '../../app/callbacks/server'; import { deleteUser } from '../../app/lib/server'; Meteor.methods({ - deleteUser(userId, confirmRelinquish = false) { + async deleteUser(userId, confirmRelinquish = false) { check(userId, String); if (!Meteor.userId()) { @@ -46,7 +46,7 @@ Meteor.methods({ }); } - deleteUser(userId, confirmRelinquish); + await deleteUser(userId, confirmRelinquish); callbacks.run('afterDeleteUser', user); diff --git a/server/methods/getUsersOfRoom.js b/server/methods/getUsersOfRoom.js index d17d91013cb6..7a97eb194ee3 100644 --- a/server/methods/getUsersOfRoom.js +++ b/server/methods/getUsersOfRoom.js @@ -1,7 +1,7 @@ import { Meteor } from 'meteor/meteor'; -import { Subscriptions } from '../../app/models/server'; -import { hasPermission } from '../../app/authorization/server'; +import { Subscriptions, Rooms } from '../../app/models/server'; +import { canAccessRoom, hasPermission } from '../../app/authorization/server'; import { findUsersOfRoom } from '../lib/findUsersOfRoom'; Meteor.methods({ @@ -11,11 +11,19 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'getUsersOfRoom' }); } - const room = Meteor.call('canAccessRoom', rid, userId); - if (!room) { + if (!rid) { throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'getUsersOfRoom' }); } + const room = Rooms.findOneById(rid, { fields: { broadcast: 1 } }); + if (!room) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getUsersOfRoom' }); + } + + if (!canAccessRoom(room, { _id: userId })) { + throw new Meteor.Error('not-authorized', 'Not Authorized', { method: 'getUsersOfRoom' }); + } + if (room.broadcast && !hasPermission(userId, 'view-broadcast-member-list', rid)) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'getUsersOfRoom' }); } diff --git a/server/methods/loadHistory.js b/server/methods/loadHistory.js index 9302a8209f22..55096be4df9f 100644 --- a/server/methods/loadHistory.js +++ b/server/methods/loadHistory.js @@ -1,9 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { Subscriptions } from '../../app/models'; -import { hasPermission } from '../../app/authorization'; -import { settings } from '../../app/settings'; +import { Subscriptions, Rooms } from '../../app/models/server'; +import { canAccessRoom, hasPermission } from '../../app/authorization/server'; +import { settings } from '../../app/settings/server'; import { loadMessageHistory } from '../../app/lib/server'; Meteor.methods({ @@ -17,7 +17,15 @@ Meteor.methods({ } const fromId = Meteor.userId(); - const room = Meteor.call('canAccessRoom', rid, fromId); + + const room = Rooms.findOneById(rid, { fields: { t: 1 } }); + if (!room) { + return false; + } + + if (!canAccessRoom(room, { _id: fromId })) { + return false; + } if (!room) { return false; diff --git a/server/methods/loadMissedMessages.js b/server/methods/loadMissedMessages.js index bad55e3f5bf3..1c94d33c3cad 100644 --- a/server/methods/loadMissedMessages.js +++ b/server/methods/loadMissedMessages.js @@ -1,8 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { Messages } from '../../app/models'; -import { settings } from '../../app/settings'; +import { canAccessRoom } from '../../app/authorization/server'; +import { Messages } from '../../app/models/server'; +import { settings } from '../../app/settings/server'; Meteor.methods({ loadMissedMessages(rid, start) { @@ -10,7 +11,12 @@ Meteor.methods({ check(start, Date); const fromId = Meteor.userId(); - if (!Meteor.call('canAccessRoom', rid, fromId)) { + + if (!rid) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'getUsersOfRoom' }); + } + + if (!canAccessRoom({ _id: rid }, { _id: fromId })) { return false; } diff --git a/server/methods/loadNextMessages.js b/server/methods/loadNextMessages.js index 7136447873e4..bc7589eae118 100644 --- a/server/methods/loadNextMessages.js +++ b/server/methods/loadNextMessages.js @@ -1,8 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { Messages } from '../../app/models'; -import { settings } from '../../app/settings'; +import { canAccessRoom } from '../../app/authorization/server'; +import { Messages } from '../../app/models/server'; +import { settings } from '../../app/settings/server'; import { normalizeMessagesForUser } from '../../app/utils/server/lib/normalizeMessagesForUser'; Meteor.methods({ @@ -16,9 +17,13 @@ Meteor.methods({ }); } + if (!rid) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'loadNextMessages' }); + } + const fromId = Meteor.userId(); - if (!Meteor.call('canAccessRoom', rid, fromId)) { + if (!canAccessRoom({ _id: rid }, { _id: fromId })) { return false; } diff --git a/server/methods/loadSurroundingMessages.js b/server/methods/loadSurroundingMessages.js index 180f9f4b3e16..c784c8350909 100644 --- a/server/methods/loadSurroundingMessages.js +++ b/server/methods/loadSurroundingMessages.js @@ -1,8 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { Messages } from '../../app/models'; -import { settings } from '../../app/settings'; +import { canAccessRoom } from '../../app/authorization/server'; +import { Messages } from '../../app/models/server'; +import { settings } from '../../app/settings/server'; import { normalizeMessagesForUser } from '../../app/utils/server/lib/normalizeMessagesForUser'; Meteor.methods({ @@ -28,7 +29,7 @@ Meteor.methods({ return false; } - if (!Meteor.call('canAccessRoom', message.rid, fromId)) { + if (!canAccessRoom({ _id: message.rid }, { _id: fromId })) { return false; } diff --git a/server/methods/messageSearch.js b/server/methods/messageSearch.js index d03bf00cb8af..72ed6a304eea 100644 --- a/server/methods/messageSearch.js +++ b/server/methods/messageSearch.js @@ -2,9 +2,10 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { escapeRegExp } from '@rocket.chat/string-helpers'; +import { canAccessRoom } from '../../app/authorization/server'; import { Subscriptions } from '../../app/models/server'; import { Messages } from '../../app/models/server/raw'; -import { settings } from '../../app/settings'; +import { settings } from '../../app/settings/server'; import { readSecondaryPreferred } from '../database/readSecondaryPreferred'; Meteor.methods({ @@ -30,8 +31,8 @@ Meteor.methods({ // Don't process anything else if the user can't access the room if (rid) { - if (!Meteor.call('canAccessRoom', rid, currentUserId)) { - return result; + if (!canAccessRoom({ _id: rid }, { _id: currentUserId })) { + return false; } } else if (settings.get('Search.defaultProvider.GlobalSearchEnabled') !== true) { return result; diff --git a/server/methods/registerUser.js b/server/methods/registerUser.js index cef1d1b8398b..802f271efc40 100644 --- a/server/methods/registerUser.js +++ b/server/methods/registerUser.js @@ -9,7 +9,7 @@ import { validateEmailDomain, passwordPolicy } from '../../app/lib'; import { validateInviteToken } from '../../app/invites/server/functions/validateInviteToken'; Meteor.methods({ - registerUser(formData) { + async registerUser(formData) { const AllowAnonymousRead = settings.get('Accounts_AllowAnonymousRead'); const AllowAnonymousWrite = settings.get('Accounts_AllowAnonymousWrite'); const manuallyApproveNewUsers = settings.get('Accounts_ManuallyApproveNewUsers'); @@ -45,7 +45,7 @@ Meteor.methods({ } try { - validateInviteToken(formData.secretURL); + await validateInviteToken(formData.secretURL); } catch (e) { throw new Meteor.Error('error-user-registration-secret', 'User registration is only allowed via Secret URL', { method: 'registerUser' }); } diff --git a/server/methods/removeRoomOwner.js b/server/methods/removeRoomOwner.ts similarity index 86% rename from server/methods/removeRoomOwner.js rename to server/methods/removeRoomOwner.ts index 9ae598883c6b..adb33a7b29b5 100644 --- a/server/methods/removeRoomOwner.js +++ b/server/methods/removeRoomOwner.ts @@ -1,14 +1,14 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { hasPermission, getUsersInRole } from '../../app/authorization'; -import { Users, Subscriptions, Messages } from '../../app/models'; -import { settings } from '../../app/settings'; +import { hasPermission, getUsersInRole } from '../../app/authorization/server'; +import { Users, Subscriptions, Messages } from '../../app/models/server'; +import { settings } from '../../app/settings/server'; import { api } from '../sdk/api'; import { Team } from '../sdk'; Meteor.methods({ - removeRoomOwner(rid, userId) { + async removeRoomOwner(rid, userId) { check(rid, String); check(userId, String); @@ -45,7 +45,7 @@ Meteor.methods({ }); } - const numOwners = getUsersInRole('owner', rid).count(); + const numOwners = await (await getUsersInRole('owner', rid)).count(); if (numOwners === 1) { throw new Meteor.Error('error-remove-last-owner', 'This is the last owner. Please set a new owner before removing this one.', { @@ -65,9 +65,9 @@ Meteor.methods({ role: 'owner', }); - const team = Promise.await(Team.getOneByMainRoomId(rid)); + const team = await Team.getOneByMainRoomId(rid); if (team) { - Promise.await(Team.removeRolesFromMember(team._id, userId, ['owner'])); + await Team.removeRolesFromMember(team._id, userId, ['owner']); } if (settings.get('UI_DisplayRoles')) { diff --git a/server/methods/removeUserFromRoom.js b/server/methods/removeUserFromRoom.ts similarity index 89% rename from server/methods/removeUserFromRoom.js rename to server/methods/removeUserFromRoom.ts index 70576c73ced4..d4b8ac0f9a01 100644 --- a/server/methods/removeUserFromRoom.js +++ b/server/methods/removeUserFromRoom.ts @@ -1,14 +1,14 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; -import { hasPermission, hasRole, getUsersInRole, removeUserFromRoles } from '../../app/authorization'; -import { Users, Subscriptions, Rooms, Messages } from '../../app/models'; -import { callbacks } from '../../app/callbacks'; +import { hasPermission, hasRole, getUsersInRole, removeUserFromRoles } from '../../app/authorization/server'; +import { Users, Subscriptions, Rooms, Messages } from '../../app/models/server'; +import { callbacks } from '../../app/callbacks/server'; import { roomTypes, RoomMemberActions } from '../../app/utils/server'; import { Team } from '../sdk'; Meteor.methods({ - removeUserFromRoom(data) { + async removeUserFromRoom(data) { check(data, Match.ObjectIncluding({ rid: String, username: String, @@ -48,7 +48,7 @@ Meteor.methods({ } if (hasRole(removedUser._id, 'owner', room._id)) { - const numOwners = getUsersInRole('owner', room._id).fetch().length; + const numOwners = await (await getUsersInRole('owner', room._id)).count(); if (numOwners === 1) { throw new Meteor.Error('error-you-are-last-owner', 'You are the last owner. Please set new owner before leaving the room.', { @@ -74,7 +74,7 @@ Meteor.methods({ if (room.teamId && room.teamMain) { // if a user is kicked from the main team room, delete the team membership - Promise.await(Team.removeMember(room.teamId, removedUser._id)); + await Team.removeMember(room.teamId, removedUser._id); } Meteor.defer(function() { diff --git a/server/methods/reportMessage.js b/server/methods/reportMessage.js index 9ff33ef8426d..ea2f42daa1e5 100644 --- a/server/methods/reportMessage.js +++ b/server/methods/reportMessage.js @@ -1,10 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { Messages, Reports } from '../../app/models'; +import { Messages } from '../../app/models/server'; +import { Reports } from '../../app/models/server/raw'; Meteor.methods({ - reportMessage(messageId, description) { + async reportMessage(messageId, description) { check(messageId, String); check(description, String); @@ -27,6 +28,8 @@ Meteor.methods({ }); } - return Reports.createWithMessageDescriptionAndUserId(message, description, Meteor.userId()); + await Reports.createWithMessageDescriptionAndUserId(message, description, Meteor.userId()); + + return true; }, }); diff --git a/server/methods/requestDataDownload.js b/server/methods/requestDataDownload.js index f388b4831bd0..a49654f4807a 100644 --- a/server/methods/requestDataDownload.js +++ b/server/methods/requestDataDownload.js @@ -4,8 +4,8 @@ import path from 'path'; import mkdirp from 'mkdirp'; import { Meteor } from 'meteor/meteor'; -import { ExportOperations, UserDataFiles } from '../../app/models'; -import { settings } from '../../app/settings'; +import { ExportOperations, UserDataFiles } from '../../app/models/server/raw'; +import { settings } from '../../app/settings/server'; import { DataExport } from '../../app/user-data-download/server/DataExport'; let tempFolder = '/tmp/userData'; @@ -16,13 +16,13 @@ if (settings.get('UserData_FileSystemPath') != null) { } Meteor.methods({ - requestDataDownload({ fullExport = false }) { + async requestDataDownload({ fullExport = false }) { const currentUserData = Meteor.user(); const userId = currentUserData._id; - const lastOperation = ExportOperations.findLastOperationByUser(userId, fullExport); + const lastOperation = await ExportOperations.findLastOperationByUser(userId, fullExport); const requestDay = lastOperation ? lastOperation.createdAt : new Date(); - const pendingOperationsBeforeMyRequestCount = ExportOperations.findAllPendingBeforeMyRequest(requestDay).count(); + const pendingOperationsBeforeMyRequestCount = await ExportOperations.findAllPendingBeforeMyRequest(requestDay).count(); if (lastOperation) { const yesterday = new Date(); @@ -30,7 +30,7 @@ Meteor.methods({ if (lastOperation.createdAt > yesterday) { if (lastOperation.status === 'completed') { - const file = lastOperation.fileId ? UserDataFiles.findOneById(lastOperation.fileId) : UserDataFiles.findLastFileByUser(userId); + const file = lastOperation.fileId ? await UserDataFiles.findOneById(lastOperation.fileId) : await UserDataFiles.findLastFileByUser(userId); if (file) { return { requested: false, @@ -64,7 +64,7 @@ Meteor.methods({ userData: currentUserData, }; - const id = ExportOperations.create(exportOperation); + const id = await ExportOperations.create(exportOperation); exportOperation._id = id; const folderName = path.join(tempFolder, id); @@ -81,7 +81,7 @@ Meteor.methods({ exportOperation.assetsPath = assetsFolder; exportOperation.status = 'pending'; - ExportOperations.updateOperation(exportOperation); + await ExportOperations.updateOperation(exportOperation); return { requested: true, diff --git a/server/modules/watchers/publishFields.ts b/server/modules/watchers/publishFields.ts index 10427adf4853..0e6aa92ab555 100644 --- a/server/modules/watchers/publishFields.ts +++ b/server/modules/watchers/publishFields.ts @@ -100,6 +100,7 @@ export const roomFields = { metrics: 1, ts: 1, waitingResponse: 1, + queuedAt: 1, // fields used by DMs usernames: 1, diff --git a/server/modules/watchers/watchers.module.ts b/server/modules/watchers/watchers.module.ts index b95a6b535bad..9ba8b2ada991 100644 --- a/server/modules/watchers/watchers.module.ts +++ b/server/modules/watchers/watchers.module.ts @@ -364,13 +364,13 @@ export function initWatchers(models: IModelsParam, broadcast: BroadcastCallback, } }); - watch(Integrations, async ({ clientAction, id, data }) => { + watch(Integrations, async ({ clientAction, id, data: eventData }) => { if (clientAction === 'removed') { broadcast('watch.integrations', { clientAction, id, data: { _id: id } }); return; } - data = data ?? await Integrations.findOneById(id); + const data = eventData ?? await Integrations.findOneById(id); if (!data) { return; } diff --git a/server/publications/messages.js b/server/publications/messages.js index 6b2f1af649c3..8ff0aa462211 100644 --- a/server/publications/messages.js +++ b/server/publications/messages.js @@ -1,7 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; -import { Messages } from '../../app/models'; +import { canAccessRoom } from '../../app/authorization/server'; +import { Messages } from '../../app/models/server'; Meteor.methods({ 'messages/get'(rid, { lastUpdate, latestDate = new Date(), oldestDate, inclusive = false, count = 20, unreads = false }) { @@ -15,7 +16,11 @@ Meteor.methods({ }); } - if (!Meteor.call('canAccessRoom', rid, fromId)) { + if (!rid) { + throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'messages/get' }); + } + + if (!canAccessRoom({ _id: rid }, { _id: fromId })) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'messages/get', }); diff --git a/server/publications/room/index.js b/server/publications/room/index.js index b31c33610e26..1756623ba73f 100644 --- a/server/publications/room/index.js +++ b/server/publications/room/index.js @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; import _ from 'underscore'; -import { roomTypes } from '../../../app/utils'; -import { hasPermission } from '../../../app/authorization'; -import { Rooms } from '../../../app/models'; -import { settings } from '../../../app/settings'; +import { roomTypes } from '../../../app/utils/server'; +import { canAccessRoom, hasPermission } from '../../../app/authorization/server'; +import { Rooms } from '../../../app/models/server'; +import { settings } from '../../../app/settings/server'; import { roomFields } from '../../modules/watchers/publishFields'; const roomMap = (record) => { @@ -51,7 +51,7 @@ Meteor.methods({ throw new Meteor.Error('error-invalid-room', 'Invalid room', { method: 'getRoomByTypeAndName' }); } - if (!Meteor.call('canAccessRoom', room._id, userId)) { + if (!canAccessRoom(room, { _id: userId })) { throw new Meteor.Error('error-no-permission', 'No permission', { method: 'getRoomByTypeAndName' }); } diff --git a/server/publications/settings/index.js b/server/publications/settings/index.ts similarity index 55% rename from server/publications/settings/index.js rename to server/publications/settings/index.ts index 3e9db1beab85..2c54b6fc0eff 100644 --- a/server/publications/settings/index.js +++ b/server/publications/settings/index.ts @@ -1,19 +1,20 @@ import { Meteor } from 'meteor/meteor'; -import { Settings } from '../../../app/models/server'; import { hasPermission, hasAtLeastOnePermission } from '../../../app/authorization/server'; import { getSettingPermissionId } from '../../../app/authorization/lib'; import { SettingsEvents } from '../../../app/settings/server'; +import { Settings } from '../../../app/models/server/raw'; +import { ISetting } from '../../../definition/ISetting'; Meteor.methods({ - 'public-settings/get'(updatedAt) { + async 'public-settings/get'(updatedAt) { if (updatedAt instanceof Date) { - const records = Settings.findNotHiddenPublicUpdatedAfter(updatedAt).fetch(); + const records = await Settings.findNotHiddenPublicUpdatedAfter(updatedAt).toArray(); SettingsEvents.emit('fetch-settings', records); return { update: records, - remove: Settings.trashFindDeletedAfter(updatedAt, { + remove: await Settings.trashFindDeletedAfter(updatedAt, { hidden: { $ne: true, }, @@ -23,16 +24,16 @@ Meteor.methods({ _id: 1, _deletedAt: 1, }, - }).fetch(), + }).toArray(), }; } - const publicSettings = Settings.findNotHiddenPublic().fetch(); + const publicSettings = await Settings.findNotHiddenPublic().toArray() as ISetting[]; SettingsEvents.emit('fetch-settings', publicSettings); return publicSettings; }, - 'private-settings/get'(updatedAfter) { + async 'private-settings/get'(updatedAfter) { const uid = Meteor.userId(); if (!uid) { @@ -46,13 +47,13 @@ Meteor.methods({ return []; } - const bypass = (settings) => settings; + const bypass = (settings: T): T => settings; - const applyFilter = (fn, args) => fn(args); + const applyFilter = (fn: Function, args: any[]): any => fn(args); - const getAuthorizedSettingsFiltered = (settings) => settings.filter((record) => hasPermission(uid, getSettingPermissionId(record._id))); + const getAuthorizedSettingsFiltered = (settings: ISetting[]): ISetting[] => settings.filter((record) => hasPermission(uid, getSettingPermissionId(record._id))); - const getAuthorizedSettings = (updatedAfter, privilegedSetting) => applyFilter(privilegedSetting ? bypass : getAuthorizedSettingsFiltered, Settings.findNotHidden(updatedAfter && { updatedAfter }).fetch()); + const getAuthorizedSettings = async (updatedAfter: Date, privilegedSetting: boolean): Promise => applyFilter(privilegedSetting ? bypass : getAuthorizedSettingsFiltered, await Settings.findNotHidden(updatedAfter && { updatedAfter }).toArray()); if (!(updatedAfter instanceof Date)) { // this does not only imply an unfiltered setting range, it also identifies the caller's context: @@ -62,8 +63,8 @@ Meteor.methods({ } return { - update: getAuthorizedSettings(updatedAfter, privilegedSetting), - remove: Settings.trashFindDeletedAfter(updatedAfter, { + update: await getAuthorizedSettings(updatedAfter, privilegedSetting), + remove: await Settings.trashFindDeletedAfter(updatedAfter, { hidden: { $ne: true, }, @@ -72,7 +73,7 @@ Meteor.methods({ _id: 1, _deletedAt: 1, }, - }).fetch(), + }).toArray(), }; }, }); diff --git a/server/routes/avatar/room.js b/server/routes/avatar/room.js index 7f359f31f79f..000395d74aa9 100644 --- a/server/routes/avatar/room.js +++ b/server/routes/avatar/room.js @@ -7,17 +7,18 @@ import { setCacheAndDispositionHeaders, } from './utils'; import { FileUpload } from '../../../app/file-upload'; -import { Rooms, Avatars } from '../../../app/models/server'; +import { Rooms } from '../../../app/models/server'; +import { Avatars } from '../../../app/models/server/raw'; import { roomTypes } from '../../../app/utils'; -const getRoomAvatar = (roomId) => { +const getRoomAvatar = async (roomId) => { const room = Rooms.findOneById(roomId, { fields: { t: 1, prid: 1, name: 1, fname: 1 } }); if (!room) { return {}; } - const file = Avatars.findOneByRoomId(room._id); + const file = await Avatars.findOneByRoomId(room._id); // if it is a discussion that doesn't have it's own avatar, returns the parent's room avatar if (room.prid && !file) { @@ -27,10 +28,10 @@ const getRoomAvatar = (roomId) => { return { room, file }; }; -export const roomAvatar = Meteor.bindEnvironment(function(req, res/* , next*/) { +export const roomAvatar = Meteor.bindEnvironment(async function(req, res/* , next*/) { const roomId = decodeURIComponent(req.url.substr(1).replace(/\?.*$/, '')); - const { room, file } = getRoomAvatar(roomId); + const { room, file } = await getRoomAvatar(roomId); if (!room) { res.writeHead(404); res.end(); diff --git a/server/routes/avatar/user.js b/server/routes/avatar/user.js index ac93faca2563..2b93d20b4e7f 100644 --- a/server/routes/avatar/user.js +++ b/server/routes/avatar/user.js @@ -8,11 +8,12 @@ import { } from './utils'; import { FileUpload } from '../../../app/file-upload'; import { settings } from '../../../app/settings/server'; -import { Users, Avatars } from '../../../app/models/server'; +import { Users } from '../../../app/models/server'; +import { Avatars } from '../../../app/models/server/raw'; // request /avatar/@name forces returning the svg -export const userAvatar = Meteor.bindEnvironment(function(req, res) { +export const userAvatar = Meteor.bindEnvironment(async function(req, res) { const requestUsername = decodeURIComponent(req.url.substr(1).replace(/\?.*$/, '')); if (!requestUsername) { @@ -34,7 +35,7 @@ export const userAvatar = Meteor.bindEnvironment(function(req, res) { const reqModifiedHeader = req.headers['if-modified-since']; - const file = Avatars.findOneByName(requestUsername); + const file = await Avatars.findOneByName(requestUsername); if (file) { res.setHeader('Content-Security-Policy', 'default-src \'none\''); diff --git a/server/sdk/types/ITeamService.ts b/server/sdk/types/ITeamService.ts index ad4a790ab6fe..50396cd20552 100644 --- a/server/sdk/types/ITeamService.ts +++ b/server/sdk/types/ITeamService.ts @@ -70,6 +70,7 @@ export interface ITeamService { members(uid: string, teamId: string, canSeeAll: boolean, options?: IPaginationOptions, queryOptions?: FilterQuery): Promise>; addMembers(uid: string, teamId: string, members: Array): Promise; updateMember(teamId: string, members: ITeamMemberParams): Promise; + removeMember(teamId: string, userId: string): Promise; removeMembers(uid: string, teamId: string, members: Array): Promise; getInfoByName(teamName: string): Promise | null>; getInfoById(teamId: string): Promise | null>; @@ -90,4 +91,5 @@ export interface ITeamService { insertMemberOnTeams(userId: string, teamIds: Array): Promise; removeMemberFromTeams(userId: string, teamIds: Array): Promise; removeAllMembersFromTeam(teamId: string): Promise; + removeRolesFromMember(teamId: string, userId: string, roles: Array): Promise; } diff --git a/server/services/analytics/service.ts b/server/services/analytics/service.ts index b2d4b8dee9e0..ab69003a733e 100644 --- a/server/services/analytics/service.ts +++ b/server/services/analytics/service.ts @@ -1,8 +1,9 @@ -import { Db } from 'mongodb'; +import type { Db } from 'mongodb'; import { ServiceClass } from '../../sdk/types/ServiceClass'; import { IAnalyticsService } from '../../sdk/types/IAnalyticsService'; import { AnalyticsRaw } from '../../../app/models/server/raw/Analytics'; +import { IAnalyticsSeatRequest } from '../../../definition/IAnalytic'; export class AnalyticsService extends ServiceClass implements IAnalyticsService { protected name = 'analytics'; @@ -19,8 +20,8 @@ export class AnalyticsService extends ServiceClass implements IAnalyticsService } async getSeatRequestCount(): Promise { - const result = await this.Analytics.findOne({ type: 'seat-request' }); - return result ? result.count : 0; + const result = await this.Analytics.findOne({ type: 'seat-request' }, {}); + return result?.count ? result.count : 0; } async resetSeatRequestCount(): Promise { diff --git a/server/services/authorization/service.ts b/server/services/authorization/service.ts index f5c6b0a1fa43..728d5a49c591 100644 --- a/server/services/authorization/service.ts +++ b/server/services/authorization/service.ts @@ -45,9 +45,16 @@ export class Authorization extends ServiceClass implements IAuthorization { this.Permissions = db.collection('rocketchat_permissions'); this.Users = new UsersRaw(db.collection('users')); - this.Roles = new RolesRaw(db.collection('rocketchat_roles')); - Subscriptions = new SubscriptionsRaw(db.collection('rocketchat_subscription')); + Subscriptions = new SubscriptionsRaw(db.collection('rocketchat_subscription'), { + Users: this.Users, + }); + + this.Roles = new RolesRaw(db.collection('rocketchat_roles'), { + Users: this.Users, + Subscriptions, + }); + Settings = new SettingsRaw(db.collection('rocketchat_settings')); Rooms = new RoomsRaw(db.collection('rocketchat_room')); TeamMembers = new TeamMemberRaw(db.collection('rocketchat_team_member')); @@ -98,7 +105,7 @@ export class Authorization extends ServiceClass implements IAuthorization { } private getPublicRoles = mem(async (): Promise => { - const roles = await this.Roles.find>({ scope: 'Users', description: { $exists: 1, $ne: '' } }, { projection: { _id: 1 } }).toArray(); + const roles = await this.Roles.find>({ scope: 'Users', description: { $exists: true, $ne: '' } }, { projection: { _id: 1 } }).toArray(); return roles.map(({ _id }) => _id); }, { maxAge: 10000 }); diff --git a/server/services/nps/notification.ts b/server/services/nps/notification.ts index 72dd87c80397..af4832781087 100644 --- a/server/services/nps/notification.ts +++ b/server/services/nps/notification.ts @@ -39,9 +39,9 @@ export const getBannerForAdmins = Meteor.bindEnvironment((expireAt: Date): Omit< }); export const notifyAdmins = Meteor.bindEnvironment((expireAt: Date) => { - sendMessagesToAdmins({ + Promise.await(sendMessagesToAdmins({ msgs: ({ adminUser }: { adminUser: any }): any => ({ msg: TAPi18n.__('NPS_survey_is_scheduled_to-run-at__date__for_all_users', { date: moment(expireAt).format('YYYY-MM-DD'), lng: adminUser.language }), }), - }); + })); }); diff --git a/server/services/team/service.ts b/server/services/team/service.ts index 0bc5106439a7..d76e8bab32a9 100644 --- a/server/services/team/service.ts +++ b/server/services/team/service.ts @@ -59,10 +59,12 @@ export class TeamService extends ServiceClass implements ITeamService { super(); this.RoomsModel = new RoomsRaw(db.collection('rocketchat_room')); - this.SubscriptionsModel = new SubscriptionsRaw(db.collection('rocketchat_subscription')); + this.Users = new UsersRaw(db.collection('users')); + this.SubscriptionsModel = new SubscriptionsRaw(db.collection('rocketchat_subscription'), { + Users: this.Users, + }); this.TeamModel = new TeamRaw(db.collection('rocketchat_team')); this.TeamMembersModel = new TeamMemberRaw(db.collection('rocketchat_team_member')); - this.Users = new UsersRaw(db.collection('users')); this.MessagesModel = new MessagesRaw(db.collection('rocketchat_message')); } diff --git a/server/startup/initialData.js b/server/startup/initialData.js index 7db252c92b03..668426a7d3ca 100644 --- a/server/startup/initialData.js +++ b/server/startup/initialData.js @@ -1,15 +1,15 @@ import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; -import _ from 'underscore'; import { RocketChatFile } from '../../app/file'; -import { FileUpload } from '../../app/file-upload'; -import { addUserRoles, getUsersInRole } from '../../app/authorization'; -import { Users, Settings, Rooms } from '../../app/models'; -import { settings } from '../../app/settings'; -import { checkUsernameAvailability, addUserToDefaultChannels } from '../../app/lib'; - -Meteor.startup(function() { +import { FileUpload } from '../../app/file-upload/server'; +import { addUserRoles, getUsersInRole } from '../../app/authorization/server'; +import { Users, Rooms } from '../../app/models/server'; +import { settings } from '../../app/settings/server'; +import { checkUsernameAvailability, addUserToDefaultChannels } from '../../app/lib/server'; +import { Settings } from '../../app/models/server/raw'; + +Meteor.startup(async function() { if (settings.get('Show_Setup_Wizard') === 'pending' && !Rooms.findOneById('GENERAL')) { Rooms.createWithIdTypeAndName('GENERAL', 'c', 'general', { default: true, @@ -48,7 +48,7 @@ Meteor.startup(function() { } if (process.env.ADMIN_PASS) { - if (_.isEmpty(getUsersInRole('admin').fetch())) { + if (await (await getUsersInRole('admin')).count() === 0) { console.log('Inserting admin user:'.green); const adminUser = { name: 'Administrator', @@ -134,16 +134,16 @@ Meteor.startup(function() { } } - if (_.isEmpty(getUsersInRole('admin').fetch())) { + if (await (await getUsersInRole('admin')).count() === 0) { const oldestUser = Users.getOldest({ _id: 1, username: 1, name: 1 }); if (oldestUser) { - addUserRoles(oldestUser._id, 'admin'); + addUserRoles(oldestUser._id, ['admin']); console.log(`No admins are found. Set ${ oldestUser.username || oldestUser.name } as admin for being the oldest user`); } } - if (!_.isEmpty(getUsersInRole('admin').fetch())) { + if (await (await getUsersInRole('admin')).count() !== 0) { if (settings.get('Show_Setup_Wizard') === 'pending') { console.log('Setting Setup Wizard to "in_progress" because, at least, one admin was found'); Settings.updateValueById('Show_Setup_Wizard', 'in_progress'); @@ -189,7 +189,7 @@ Meteor.startup(function() { Accounts.setPassword(adminUser._id, adminUser._id); - addUserRoles(adminUser._id, 'admin'); + addUserRoles(adminUser._id, ['admin']); if (settings.get('Show_Setup_Wizard') === 'pending') { Settings.updateValueById('Show_Setup_Wizard', 'in_progress'); diff --git a/server/startup/instance.js b/server/startup/instance.js index 5dcebcb2be5b..c4aa1ebada0b 100644 --- a/server/startup/instance.js +++ b/server/startup/instance.js @@ -5,10 +5,6 @@ import { InstanceStatus } from 'meteor/konecty:multiple-instances-status'; import { startStreamBroadcast } from '../stream/streamBroadcast'; -export function getInstances() { - InstanceStatus.find().fetch(); -} - Meteor.startup(function() { const instance = { host: process.env.INSTANCE_IP ? String(process.env.INSTANCE_IP).trim() : 'localhost', diff --git a/server/startup/migrations/index.ts b/server/startup/migrations/index.ts index dfeec238d17a..ef9a9a912cd8 100644 --- a/server/startup/migrations/index.ts +++ b/server/startup/migrations/index.ts @@ -68,4 +68,5 @@ import './v241'; import './v242'; import './v243'; import './v244'; +import './v245'; import './xrun'; diff --git a/server/startup/migrations/v174.js b/server/startup/migrations/v174.ts similarity index 53% rename from server/startup/migrations/v174.js rename to server/startup/migrations/v174.ts index a03aeb430a15..f9b063903599 100644 --- a/server/startup/migrations/v174.js +++ b/server/startup/migrations/v174.ts @@ -1,5 +1,5 @@ +import { Permissions } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Permissions } from '../../../app/models/server'; const appRolePermissions = [ 'api-bypass-rate-limit', @@ -18,9 +18,9 @@ const appRolePermissions = [ addMigration({ version: 174, up() { - Permissions.update({ _id: { $in: appRolePermissions } }, { $addToSet: { roles: 'app' } }, { multi: true }); + return Permissions.update({ _id: { $in: appRolePermissions } }, { $addToSet: { roles: 'app' } }, { multi: true }); }, down() { - Permissions.update({ _id: { $in: appRolePermissions } }, { $pull: { roles: 'app' } }, { multi: true }); + return Permissions.update({ _id: { $in: appRolePermissions } }, { $pull: { roles: 'app' } }, { multi: true }); }, }); diff --git a/server/startup/migrations/v175.js b/server/startup/migrations/v175.js index 0b12049c66d2..0d931a02e4c1 100644 --- a/server/startup/migrations/v175.js +++ b/server/startup/migrations/v175.js @@ -1,18 +1,17 @@ import { addMigration } from '../../lib/migrations'; import { theme } from '../../../app/theme/server/server'; -import { Settings } from '../../../app/models'; +import { Settings } from '../../../app/models/server/raw'; addMigration({ version: 175, up() { - Object.entries(theme.variables) + Promise.await(Promise.all(Object.entries(theme.variables) .filter(([, value]) => value.type === 'color') - .forEach(([key, { editor }]) => { - Settings.update({ _id: `theme-color-${ key }` }, { - $set: { - packageEditor: editor, - }, - }); - }); + .map(([key, { editor }]) => Settings.update({ _id: `theme-color-${ key }` }, { + $set: { + packageEditor: editor, + }, + })), + )); }, }); diff --git a/server/startup/migrations/v176.js b/server/startup/migrations/v176.js deleted file mode 100644 index 4484febcb5a6..000000000000 --- a/server/startup/migrations/v176.js +++ /dev/null @@ -1,12 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { - Settings, -} from '../../../app/models'; - - -addMigration({ - version: 176, - up() { - Settings.remove({ _id: 'Livechat', type: 'group' }); - }, -}); diff --git a/server/startup/migrations/v176.ts b/server/startup/migrations/v176.ts new file mode 100644 index 000000000000..5dcbf7bc8980 --- /dev/null +++ b/server/startup/migrations/v176.ts @@ -0,0 +1,9 @@ +import { Settings } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + +addMigration({ + version: 176, + up() { + return Settings.deleteOne({ _id: 'Livechat', type: 'group' }); + }, +}); diff --git a/server/startup/migrations/v177.js b/server/startup/migrations/v177.ts similarity index 72% rename from server/startup/migrations/v177.js rename to server/startup/migrations/v177.ts index 3acca9065d7b..51b62d3c6bf2 100644 --- a/server/startup/migrations/v177.js +++ b/server/startup/migrations/v177.ts @@ -1,16 +1,18 @@ +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; addMigration({ version: 177, up() { // Disable auto opt in for existent installations - Settings.upsert({ + Settings.update({ _id: 'Accounts_TwoFactorAuthentication_By_Email_Auto_Opt_In', }, { $set: { value: false, }, + }, { + upsert: true, }); }, }); diff --git a/server/startup/migrations/v178.js b/server/startup/migrations/v178.js deleted file mode 100644 index 2ee62cfd8166..000000000000 --- a/server/startup/migrations/v178.js +++ /dev/null @@ -1,12 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { - Settings, -} from '../../../app/models'; - - -addMigration({ - version: 178, - up() { - Settings.remove({ _id: 'Livechat_enable_inquiry_fetch_by_stream' }); - }, -}); diff --git a/server/startup/migrations/v178.ts b/server/startup/migrations/v178.ts new file mode 100644 index 000000000000..503c3d66d1cd --- /dev/null +++ b/server/startup/migrations/v178.ts @@ -0,0 +1,9 @@ +import { Settings } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + +addMigration({ + version: 178, + up() { + return Settings.removeById('Livechat_enable_inquiry_fetch_by_stream'); + }, +}); diff --git a/server/startup/migrations/v182.js b/server/startup/migrations/v182.js index fde3cc07062c..74a840c88a9a 100644 --- a/server/startup/migrations/v182.js +++ b/server/startup/migrations/v182.js @@ -1,9 +1,9 @@ import { addMigration } from '../../lib/migrations'; -import { Analytics } from '../../../app/models/server'; +import { Analytics } from '../../../app/models/server/raw'; addMigration({ version: 182, up() { - Analytics.remove({}); + Analytics.deleteMany({}); }, }); diff --git a/server/startup/migrations/v183.js b/server/startup/migrations/v183.js index e031f63f028c..525148b0f8a8 100644 --- a/server/startup/migrations/v183.js +++ b/server/startup/migrations/v183.js @@ -2,7 +2,8 @@ import { Meteor } from 'meteor/meteor'; import { Random } from 'meteor/random'; import { addMigration } from '../../lib/migrations'; -import { Rooms, Messages, Subscriptions, Uploads, Settings, Users } from '../../../app/models/server'; +import { Rooms, Messages, Subscriptions, Users } from '../../../app/models/server'; +import { Settings, Uploads } from '../../../app/models/server/raw'; const unifyRooms = (room) => { // verify if other DM already exists @@ -59,8 +60,8 @@ const fixSelfDMs = () => { }, { multi: true }); // Fix error of upload permission check using Meteor.userId() - Meteor.runAsUser(room.uids[0], () => { - Uploads.update({ rid: room._id }, { + Meteor.runAsUser(room.uids[0], async () => { + await Uploads.update({ rid: room._id }, { $set: { rid: correctId, }, @@ -90,11 +91,11 @@ const fixDiscussions = () => { }); }; -const fixUserSearch = () => { - const setting = Settings.findOneById('Accounts_SearchFields', { fields: { value: 1 } }); +const fixUserSearch = async () => { + const setting = await Settings.findOneById('Accounts_SearchFields', { projection: { value: 1 } }); const value = setting?.value?.trim(); if (value === '' || value === 'username, name') { - Settings.updateValueById('Accounts_SearchFields', 'username, name, bio'); + await Settings.updateValueById('Accounts_SearchFields', 'username, name, bio'); } Users.tryDropIndex('name_text_username_text_bio_text'); @@ -105,6 +106,6 @@ addMigration({ up() { fixDiscussions(); fixSelfDMs(); - fixUserSearch(); + return fixUserSearch(); }, }); diff --git a/server/startup/migrations/v184.js b/server/startup/migrations/v184.ts similarity index 71% rename from server/startup/migrations/v184.js rename to server/startup/migrations/v184.ts index 84112ae971a9..4542313f029f 100644 --- a/server/startup/migrations/v184.js +++ b/server/startup/migrations/v184.ts @@ -1,16 +1,18 @@ +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; addMigration({ version: 184, up() { // Set SAML signature validation type to 'Either' - Settings.upsert({ + Settings.update({ _id: 'SAML_Custom_Default_signature_validation_type', }, { $set: { value: 'Either', }, + }, { + upsert: true, }); }, }); diff --git a/server/startup/migrations/v185.js b/server/startup/migrations/v185.js deleted file mode 100644 index de080b1ebab4..000000000000 --- a/server/startup/migrations/v185.js +++ /dev/null @@ -1,19 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { - Settings, -} from '../../../app/models/server'; - -addMigration({ - version: 185, - up() { - const setting = Settings.findOne({ _id: 'Message_SetNameToAliasEnabled' }); - if (setting && setting.value) { - Settings.update({ _id: 'UI_Use_Real_Name' }, { - $set: { - value: true, - }, - }); - } - Settings.remove({ _id: 'Message_SetNameToAliasEnabled' }); - }, -}); diff --git a/server/startup/migrations/v185.ts b/server/startup/migrations/v185.ts new file mode 100644 index 000000000000..c9d79130aea8 --- /dev/null +++ b/server/startup/migrations/v185.ts @@ -0,0 +1,18 @@ +import { Settings } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + + +addMigration({ + version: 185, + async up() { + const setting = await Settings.findOne({ _id: 'Message_SetNameToAliasEnabled' }); + if (setting && setting.value) { + await Settings.update({ _id: 'UI_Use_Real_Name' }, { + $set: { + value: true, + }, + }); + } + return Settings.removeById('Message_SetNameToAliasEnabled'); + }, +}); diff --git a/server/startup/migrations/v187.js b/server/startup/migrations/v187.js index 349e4142dd54..f3f958a248dc 100644 --- a/server/startup/migrations/v187.js +++ b/server/startup/migrations/v187.js @@ -1,8 +1,7 @@ import { Mongo } from 'meteor/mongo'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; -import { NotificationQueue } from '../../../app/models/server/raw'; +import { NotificationQueue, Settings } from '../../../app/models/server/raw'; function convertNotification(notification) { try { @@ -55,14 +54,14 @@ async function migrateNotifications() { addMigration({ version: 187, - up() { - Settings.remove({ _id: 'Push_send_interval' }); - Settings.remove({ _id: 'Push_send_batch_size' }); - Settings.remove({ _id: 'Push_debug' }); - Settings.remove({ _id: 'Notifications_Always_Notify_Mobile' }); + async up() { + await Settings.removeById('Push_send_interval'); + await Settings.removeById('Push_send_batch_size'); + await Settings.removeById('Push_debug'); + await Settings.removeById('Notifications_Always_Notify_Mobile'); try { - Promise.await(migrateNotifications()); + await migrateNotifications(); } catch (err) { // Ignore if the collection does not exist if (!err.code || err.code !== 26) { diff --git a/server/startup/migrations/v188.js b/server/startup/migrations/v188.ts similarity index 51% rename from server/startup/migrations/v188.js rename to server/startup/migrations/v188.ts index b63ca803a96e..50bc75990d03 100644 --- a/server/startup/migrations/v188.js +++ b/server/startup/migrations/v188.ts @@ -1,5 +1,6 @@ +import { Permissions } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Permissions } from '../../../app/models/server'; + const newRolePermissions = [ 'view-d-room', @@ -11,6 +12,6 @@ const roleName = 'guest'; addMigration({ version: 188, up() { - Permissions.update({ _id: { $in: newRolePermissions } }, { $addToSet: { roles: roleName } }, { multi: true }); + return Permissions.update({ _id: { $in: newRolePermissions } }, { $addToSet: { roles: roleName } }, { multi: true }); }, }); diff --git a/server/startup/migrations/v189.js b/server/startup/migrations/v189.js deleted file mode 100644 index 9e90f77531d2..000000000000 --- a/server/startup/migrations/v189.js +++ /dev/null @@ -1,11 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; - -addMigration({ - version: 189, - up() { - Settings.remove({ _id: 'Livechat_Knowledge_Enabled' }); - Settings.remove({ _id: 'Livechat_Knowledge_Apiai_Key' }); - Settings.remove({ _id: 'Livechat_Knowledge_Apiai_Language' }); - }, -}); diff --git a/server/startup/migrations/v189.ts b/server/startup/migrations/v189.ts new file mode 100644 index 000000000000..310bad4bc224 --- /dev/null +++ b/server/startup/migrations/v189.ts @@ -0,0 +1,11 @@ +import { Settings } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + +addMigration({ + version: 189, + async up() { + await Settings.removeById('Livechat_Knowledge_Enabled'); + await Settings.removeById('Livechat_Knowledge_Apiai_Key'); + await Settings.removeById('Livechat_Knowledge_Apiai_Language'); + }, +}); diff --git a/server/startup/migrations/v190.js b/server/startup/migrations/v190.js index a49a8e348283..52e07bc9ab62 100644 --- a/server/startup/migrations/v190.js +++ b/server/startup/migrations/v190.js @@ -5,7 +5,7 @@ addMigration({ version: 190, up() { // Remove unused settings - Promise.await(Settings.col.deleteOne({ _id: 'Accounts_Default_User_Preferences_desktopNotificationDuration' })); + Promise.await(Settings.removeById('Accounts_Default_User_Preferences_desktopNotificationDuration')); Promise.await(Subscriptions.col.updateMany({ desktopNotificationDuration: { $exists: true, diff --git a/server/startup/migrations/v191.js b/server/startup/migrations/v191.js deleted file mode 100644 index b2d7ed2b81df..000000000000 --- a/server/startup/migrations/v191.js +++ /dev/null @@ -1,9 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; - -addMigration({ - version: 191, - up() { - Settings.remove({ _id: /theme-color-status/ }, { multi: true }); - }, -}); diff --git a/server/startup/migrations/v191.ts b/server/startup/migrations/v191.ts new file mode 100644 index 000000000000..f30c1699899f --- /dev/null +++ b/server/startup/migrations/v191.ts @@ -0,0 +1,9 @@ +import { Settings } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + +addMigration({ + version: 191, + async up() { + return Settings.deleteMany({ _id: /theme-color-status/ }); + }, +}); diff --git a/server/startup/migrations/v194.js b/server/startup/migrations/v194.js index 3129b37e5068..d7e20745beb8 100644 --- a/server/startup/migrations/v194.js +++ b/server/startup/migrations/v194.js @@ -1,12 +1,12 @@ import { - Settings, Users, } from '../../../app/models/server'; +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -function updateFieldMap() { +async function updateFieldMap() { const _id = 'SAML_Custom_Default_user_data_fieldmap'; - const setting = Settings.findOne({ _id }); + const setting = await Settings.findOne({ _id }); if (!setting || !setting.value) { return; } @@ -19,7 +19,7 @@ function updateFieldMap() { if (setting.value === '{"username":"username", "email":"email", "cn": "name"}') { // include de eppn identifier if it was used const value = `{"username":"username", "email":"email", "name": "cn"${ usedEppn ? ', "__identifier__": "eppn"' : '' }}`; - Settings.update({ _id }, { + await Settings.update({ _id }, { $set: { value, }, @@ -90,7 +90,7 @@ function updateIdentifierLocation() { function setOldLogoutResponseTemplate() { // For existing users, use a template compatible with the old SAML implementation instead of the default - Settings.upsert({ + return Settings.update({ _id: 'SAML_Custom_Default_LogoutResponse_template', }, { $set: { @@ -99,14 +99,16 @@ function setOldLogoutResponseTemplate() { `, }, + }, { + upsert: true, }); } addMigration({ version: 194, - up() { - updateFieldMap(); - updateIdentifierLocation(); - setOldLogoutResponseTemplate(); + async up() { + await updateFieldMap(); + await updateIdentifierLocation(); + await setOldLogoutResponseTemplate(); }, }); diff --git a/server/startup/migrations/v195.js b/server/startup/migrations/v195.ts similarity index 61% rename from server/startup/migrations/v195.js rename to server/startup/migrations/v195.ts index 5334f31bb2aa..89221f519bb4 100644 --- a/server/startup/migrations/v195.js +++ b/server/startup/migrations/v195.ts @@ -1,16 +1,14 @@ import moment from 'moment-timezone'; -import { ObjectId } from 'mongodb'; import { Mongo } from 'meteor/mongo'; import { addMigration } from '../../lib/migrations'; -import { Permissions, Settings } from '../../../app/models/server'; -import { LivechatBusinessHours } from '../../../app/models/server/raw'; -import { LivechatBusinessHourTypes } from '../../../definition/ILivechatBusinessHour'; +import { LivechatBusinessHours, Permissions, Settings } from '../../../app/models/server/raw'; +import { ILivechatBusinessHour, IBusinessHourWorkHour, LivechatBusinessHourTypes } from '../../../definition/ILivechatBusinessHour'; -const migrateCollection = () => { - const LivechatOfficeHour = new Mongo.Collection('rocketchat_livechat_office_hour'); +const migrateCollection = async (): Promise => { + const LivechatOfficeHour = new Mongo.Collection('rocketchat_livechat_office_hour'); const days = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']; - const officeHours = []; + const officeHours: IBusinessHourWorkHour[] = []; days.forEach((day) => { const officeHour = LivechatOfficeHour.findOne({ day }); if (officeHour) { @@ -22,15 +20,15 @@ const migrateCollection = () => { return; } - const businessHour = { + const businessHour: Omit = { name: '', active: true, type: LivechatBusinessHourTypes.DEFAULT, ts: new Date(), - workHours: officeHours.map((officeHour) => ({ + workHours: officeHours.map((officeHour): IBusinessHourWorkHour => ({ day: officeHour.day, start: { - time: officeHour.start, + time: officeHour.start as any, utc: { dayOfWeek: moment(`${ officeHour.day }:${ officeHour.start }`, 'dddd:HH:mm').utc().format('dddd'), time: moment(`${ officeHour.day }:${ officeHour.start }`, 'dddd:HH:mm').utc().format('HH:mm'), @@ -41,7 +39,7 @@ const migrateCollection = () => { }, }, finish: { - time: officeHour.finish, + time: officeHour.finish as any, utc: { dayOfWeek: moment(`${ officeHour.day }:${ officeHour.finish }`, 'dddd:HH:mm').utc().format('dddd'), time: moment(`${ officeHour.day }:${ officeHour.finish }`, 'dddd:HH:mm').utc().format('HH:mm'), @@ -56,11 +54,10 @@ const migrateCollection = () => { })), timezone: { name: moment.tz.guess(), - utc: moment().utcOffset() / 60, + utc: String(moment().utcOffset() / 60), }, }; - if (LivechatBusinessHours.find({ type: LivechatBusinessHourTypes.DEFAULT }).count() === 0) { - businessHour._id = new ObjectId().toHexString(); + if (await LivechatBusinessHours.find({ type: LivechatBusinessHourTypes.DEFAULT }).count() === 0) { LivechatBusinessHours.insertOne(businessHour); } else { LivechatBusinessHours.update({ type: LivechatBusinessHourTypes.DEFAULT }, { $set: { ...businessHour } }); @@ -77,14 +74,14 @@ const migrateCollection = () => { addMigration({ version: 195, - up() { - Settings.remove({ _id: 'Livechat_enable_office_hours' }); - Settings.remove({ _id: 'Livechat_allow_online_agents_outside_office_hours' }); - const permission = Permissions.findOneById('view-livechat-officeHours'); + async up() { + await Settings.removeById('Livechat_enable_office_hours'); + await Settings.removeById('Livechat_allow_online_agents_outside_office_hours'); + const permission = await Permissions.findOneById('view-livechat-officeHours'); if (permission) { - Permissions.upsert({ _id: 'view-livechat-business-hours' }, { $set: { roles: permission.roles } }); - Permissions.remove({ _id: 'view-livechat-officeHours' }); + await Permissions.update({ _id: 'view-livechat-business-hours' }, { $set: { roles: permission.roles } }, { upsert: true }); + await Permissions.deleteOne({ _id: 'view-livechat-officeHours' }); } - Promise.await(migrateCollection()); + await migrateCollection(); }, }); diff --git a/server/startup/migrations/v198.js b/server/startup/migrations/v198.ts similarity index 54% rename from server/startup/migrations/v198.js rename to server/startup/migrations/v198.ts index e2e2bf16fdb5..1a979fe3e948 100644 --- a/server/startup/migrations/v198.js +++ b/server/startup/migrations/v198.ts @@ -1,44 +1,50 @@ -import { Settings } from '../../../app/models/server'; +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; addMigration({ version: 198, - up: () => { - const discussion = Settings.findOneById('RetentionPolicy_DoNotExcludeDiscussion'); - const thread = Settings.findOneById('RetentionPolicy_DoNotExcludeThreads'); - const pinned = Settings.findOneById('RetentionPolicy_ExcludePinned'); + up: async () => { + const discussion = await Settings.findOneById('RetentionPolicy_DoNotExcludeDiscussion'); + const thread = await Settings.findOneById('RetentionPolicy_DoNotExcludeThreads'); + const pinned = await Settings.findOneById('RetentionPolicy_ExcludePinned'); if (discussion) { - Settings.upsert({ + await Settings.update({ _id: 'RetentionPolicy_DoNotPruneDiscussion', }, { $set: { value: discussion.value, }, + }, { + upsert: true, }); } if (thread) { - Settings.upsert({ + await Settings.update({ _id: 'RetentionPolicy_DoNotPruneThreads', }, { $set: { value: thread.value, }, + }, { + upsert: true, }); } if (pinned) { - Settings.upsert({ + await Settings.update({ _id: 'RetentionPolicy_DoNotPrunePinned', }, { $set: { value: pinned.value, }, + }, { + upsert: true, }); } - Settings.remove({ + return Settings.deleteMany({ _id: { $in: ['RetentionPolicy_DoNotExcludeDiscussion', 'RetentionPolicy_DoNotExcludeThreads', 'RetentionPolicy_ExcludePinned'] }, }); }, diff --git a/server/startup/migrations/v201.js b/server/startup/migrations/v201.js index 0fb5ae254412..0a74087cf773 100644 --- a/server/startup/migrations/v201.js +++ b/server/startup/migrations/v201.js @@ -1,17 +1,17 @@ import { Meteor } from 'meteor/meteor'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import { Settings } from '../../../app/models/server'; +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; import { sendMessagesToAdmins } from '../../lib/sendMessagesToAdmins'; addMigration({ version: 201, - up: () => { - const pushEnabled = Settings.findOneById('Push_enable'); - const pushGatewayEnabled = Settings.findOneById('Push_enable_gateway'); - const registerServer = Settings.findOneById('Register_Server'); - const cloudAgreement = Settings.findOneById('Cloud_Service_Agree_PrivacyTerms'); + up: async () => { + const pushEnabled = await Settings.findOneById('Push_enable'); + const pushGatewayEnabled = await Settings.findOneById('Push_enable_gateway'); + const registerServer = await Settings.findOneById('Register_Server'); + const cloudAgreement = await Settings.findOneById('Cloud_Service_Agree_PrivacyTerms'); if (!pushEnabled?.value) { return; @@ -24,12 +24,14 @@ addMigration({ } // if push gateway is enabled but server is not registered or cloud terms not agreed, disable gateway and alert admin - Settings.upsert({ + Settings.update({ _id: 'Push_enable_gateway', }, { $set: { value: false, }, + }, { + update: true, }); const id = 'push-gateway-disabled'; diff --git a/server/startup/migrations/v202.js b/server/startup/migrations/v202.js index 02362a2d165e..7cb0e457f2cf 100644 --- a/server/startup/migrations/v202.js +++ b/server/startup/migrations/v202.js @@ -1,15 +1,15 @@ import { addMigration } from '../../lib/migrations'; -import Uploads from '../../../app/models/server/models/Uploads'; +import { Uploads } from '../../../app/models/server/raw'; addMigration({ version: 202, - up() { - Promise.await(Uploads.model.rawCollection().updateMany({ + async up() { + await Uploads.updateMany({ type: 'audio/mp3', }, { $set: { type: 'audio/mpeg', }, - })); + }); }, }); diff --git a/server/startup/migrations/v203.js b/server/startup/migrations/v203.js deleted file mode 100644 index ed70284f278d..000000000000 --- a/server/startup/migrations/v203.js +++ /dev/null @@ -1,10 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { Avatars } from '../../../app/models/server'; - -addMigration({ - version: 203, - up() { - Avatars.tryDropIndex({ name: 1 }); - Avatars.tryEnsureIndex({ name: 1 }, { sparse: true }); - }, -}); diff --git a/server/startup/migrations/v203.ts b/server/startup/migrations/v203.ts new file mode 100644 index 000000000000..d5594ece72bb --- /dev/null +++ b/server/startup/migrations/v203.ts @@ -0,0 +1,10 @@ +import { addMigration } from '../../lib/migrations'; +import { Avatars } from '../../../app/models/server/raw'; + +addMigration({ + version: 203, + async up() { + await Avatars.col.dropIndex('name_1'); + await Avatars.col.createIndex({ name: 1 }, { sparse: true }); + }, +}); diff --git a/server/startup/migrations/v205.js b/server/startup/migrations/v205.js index 061075e17ece..2feed878dcbc 100644 --- a/server/startup/migrations/v205.js +++ b/server/startup/migrations/v205.js @@ -1,16 +1,18 @@ +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; addMigration({ version: 205, up() { // Disable this new enforcement setting for existent installations. - Settings.upsert({ + Settings.update({ _id: 'Accounts_TwoFactorAuthentication_Enforce_Password_Fallback', }, { $set: { value: false, }, + }, { + upsert: true, }); }, }); diff --git a/server/startup/migrations/v207.js b/server/startup/migrations/v207.js deleted file mode 100644 index 556f66394632..000000000000 --- a/server/startup/migrations/v207.js +++ /dev/null @@ -1,9 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models'; - -addMigration({ - version: 207, - up() { - Settings.removeById('theme-color-tertiary-background-color'); - }, -}); diff --git a/server/startup/migrations/v207.ts b/server/startup/migrations/v207.ts new file mode 100644 index 000000000000..cac3bd875c1c --- /dev/null +++ b/server/startup/migrations/v207.ts @@ -0,0 +1,10 @@ +import { Settings } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + + +addMigration({ + version: 207, + up() { + return Settings.removeById('theme-color-tertiary-background-color'); + }, +}); diff --git a/server/startup/migrations/v208.js b/server/startup/migrations/v208.js index 9a604c6e4a88..ced4c5f2d5b3 100644 --- a/server/startup/migrations/v208.js +++ b/server/startup/migrations/v208.js @@ -1,41 +1,31 @@ -import Future from 'fibers/future'; - import { addMigration } from '../../lib/migrations'; import { Users, Sessions } from '../../../app/models/server/raw'; -async function migrateSessions(fut) { +async function migrateSessions() { const cursor = Users.find({ roles: 'anonymous' }, { projection: { _id: 1 } }); if (!cursor) { return; } - const users = await cursor.toArray(); if (users.length === 0) { - fut.return(); return; } const userIds = users.map(({ _id }) => _id); - Sessions.update({ + await Sessions.updateMany({ userId: { $in: userIds }, }, { $set: { roles: ['anonymous'], }, - }, { - multi: true, }); - - fut.return(); } addMigration({ version: 208, up() { - const fut = new Future(); - migrateSessions(fut); - fut.wait(); + Promise.await(migrateSessions()); }, }); diff --git a/server/startup/migrations/v214.js b/server/startup/migrations/v214.js deleted file mode 100644 index 787b8acbeb74..000000000000 --- a/server/startup/migrations/v214.js +++ /dev/null @@ -1,11 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { Permissions } from '../../../app/models/server'; - -const roleName = 'admin'; - -addMigration({ - version: 214, - up() { - Permissions.update({ _id: 'toggle-room-e2e-encryption' }, { $addToSet: { roles: roleName } }); - }, -}); diff --git a/server/startup/migrations/v214.ts b/server/startup/migrations/v214.ts new file mode 100644 index 000000000000..ec0b5491e433 --- /dev/null +++ b/server/startup/migrations/v214.ts @@ -0,0 +1,12 @@ +import { Permissions } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + + +const roleName = 'admin'; + +addMigration({ + version: 214, + up() { + return Permissions.update({ _id: 'toggle-room-e2e-encryption' }, { $addToSet: { roles: roleName } }); + }, +}); diff --git a/server/startup/migrations/v215.js b/server/startup/migrations/v215.ts similarity index 52% rename from server/startup/migrations/v215.js rename to server/startup/migrations/v215.ts index e3d71a070a72..bd6f08358254 100644 --- a/server/startup/migrations/v215.js +++ b/server/startup/migrations/v215.ts @@ -1,14 +1,14 @@ +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; const removed = ['advocacy', 'industry', 'publicRelations', 'healthcarePharmaceutical', 'helpCenter']; addMigration({ version: 215, - up() { - const current = Settings.findOneById('Industry'); - if (removed.includes(current.value)) { - Settings.update({ + async up() { + const current = await Settings.findOneById('Industry'); + if (current && typeof current.value === 'string' && removed.includes(current.value)) { + await Settings.update({ _id: 'Industry', }, { $set: { diff --git a/server/startup/migrations/v216.js b/server/startup/migrations/v216.js index d004e5a32c7e..4a3225d6b251 100644 --- a/server/startup/migrations/v216.js +++ b/server/startup/migrations/v216.js @@ -1,42 +1,43 @@ +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models'; addMigration({ version: 216, - up() { - Settings.find({ _id: /Accounts_OAuth_Custom/, i18nLabel: 'Accounts_OAuth_Custom_Enable' }).forEach(function(customOauth) { + async up() { + return Promise.all((await Settings.find({ _id: /Accounts_OAuth_Custom/, i18nLabel: 'Accounts_OAuth_Custom_Enable' }).toArray()).map(async function(customOauth) { const parts = customOauth._id.split('-'); const name = parts[1]; const id = `Accounts_OAuth_Custom-${ name }-key_field`; - if (!Settings.findOne({ _id: id })) { - Settings.insert({ - _id: id, - type: 'select', - group: 'OAuth', - section: `Custom OAuth: ${ name }`, - i18nLabel: 'Accounts_OAuth_Custom_Key_Field', - persistent: true, - values: [ - { - key: 'username', - i18nLabel: 'Username', - }, - { - key: 'email', - i18nLabel: 'Email', - }, - ], - packageValue: 'username', - valueSource: 'packageValue', - ts: new Date(), - hidden: false, - blocked: false, - sorter: 103, - i18nDescription: `Accounts_OAuth_Custom-${ name }-key_field_Description`, - createdAt: new Date(), - value: 'username', - }); + if (await Settings.findOne({ _id: id })) { + return; } - }); + return Settings.insert({ + _id: id, + type: 'select', + group: 'OAuth', + section: `Custom OAuth: ${ name }`, + i18nLabel: 'Accounts_OAuth_Custom_Key_Field', + persistent: true, + values: [ + { + key: 'username', + i18nLabel: 'Username', + }, + { + key: 'email', + i18nLabel: 'Email', + }, + ], + packageValue: 'username', + valueSource: 'packageValue', + ts: new Date(), + hidden: false, + blocked: false, + sorter: 103, + i18nDescription: `Accounts_OAuth_Custom-${ name }-key_field_Description`, + createdAt: new Date(), + value: 'username', + }); + })); }, }); diff --git a/server/startup/migrations/v217.js b/server/startup/migrations/v217.js deleted file mode 100644 index 3c82aeb5931c..000000000000 --- a/server/startup/migrations/v217.js +++ /dev/null @@ -1,12 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { Permissions } from '../../../app/models'; - -addMigration({ - version: 217, - up() { - const oldPermission = Permissions.findOne('view-livechat-queue'); - if (oldPermission) { - Permissions.update({ _id: 'view-livechat-queue' }, { $addToSet: { roles: 'livechat-agent' } }); - } - }, -}); diff --git a/server/startup/migrations/v217.ts b/server/startup/migrations/v217.ts new file mode 100644 index 000000000000..3ca79fccc2d5 --- /dev/null +++ b/server/startup/migrations/v217.ts @@ -0,0 +1,13 @@ +import { Permissions } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + + +addMigration({ + version: 217, + async up() { + const oldPermission = await Permissions.findOne('view-livechat-queue'); + if (oldPermission) { + return Permissions.update({ _id: 'view-livechat-queue' }, { $addToSet: { roles: 'livechat-agent' } }); + } + }, +}); diff --git a/server/startup/migrations/v218.js b/server/startup/migrations/v218.js deleted file mode 100644 index 01b2668a7cda..000000000000 --- a/server/startup/migrations/v218.js +++ /dev/null @@ -1,9 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { Statistics } from '../../../app/models/server'; - -addMigration({ - version: 218, - up() { - Statistics.tryDropIndex({ createdAt: 1 }); - }, -}); diff --git a/server/startup/migrations/v218.ts b/server/startup/migrations/v218.ts new file mode 100644 index 000000000000..c17991620cb2 --- /dev/null +++ b/server/startup/migrations/v218.ts @@ -0,0 +1,10 @@ +import { addMigration } from '../../lib/migrations'; +import { Statistics } from '../../../app/models/server/raw'; + +addMigration({ + version: 218, + up() { + // TODO test if dropIndex do not raise exceptions. + Statistics.col.dropIndex('createdAt_1'); + }, +}); diff --git a/server/startup/migrations/v219.js b/server/startup/migrations/v219.ts similarity index 70% rename from server/startup/migrations/v219.js rename to server/startup/migrations/v219.ts index bd16d7c18b89..cd2d5eacc06f 100644 --- a/server/startup/migrations/v219.js +++ b/server/startup/migrations/v219.ts @@ -1,16 +1,17 @@ + +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; addMigration({ version: 219, - up() { + async up() { const SettingIds = { old: 'Livechat_auto_close_abandoned_rooms', new: 'Livechat_abandoned_rooms_action', }; - const oldSetting = Settings.findOne({ _id: SettingIds.old }); + const oldSetting = await Settings.findOne({ _id: SettingIds.old }); if (!oldSetting) { return; } @@ -27,8 +28,6 @@ addMigration({ }, }); - Settings.remove({ - _id: SettingIds.old, - }); + return Settings.removeById(SettingIds.old); }, }); diff --git a/server/startup/migrations/v220.js b/server/startup/migrations/v220.ts similarity index 99% rename from server/startup/migrations/v220.js rename to server/startup/migrations/v220.ts index 59795ce4eff9..cdaa4e6c07e6 100644 --- a/server/startup/migrations/v220.js +++ b/server/startup/migrations/v220.ts @@ -1,10 +1,10 @@ +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; addMigration({ version: 220, - up() { - Settings.update({ + async up() { + await Settings.update({ _id: 'Organization_Type', }, { $set: { @@ -29,7 +29,7 @@ addMigration({ }, }); - Settings.update({ + await Settings.update({ _id: 'Industry', }, { $set: { @@ -142,7 +142,7 @@ addMigration({ }, }); - Settings.update({ + await Settings.update({ _id: 'Country', }, { values: [ diff --git a/server/startup/migrations/v223.js b/server/startup/migrations/v223.js deleted file mode 100644 index 60c60180ea70..000000000000 --- a/server/startup/migrations/v223.js +++ /dev/null @@ -1,11 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { Permissions } from '../../../app/models/server'; - -const roleName = 'user'; - -addMigration({ - version: 223, - up() { - Permissions.update({ _id: 'message-impersonate' }, { $addToSet: { roles: roleName } }); - }, -}); diff --git a/server/startup/migrations/v223.ts b/server/startup/migrations/v223.ts new file mode 100644 index 000000000000..ef357c554277 --- /dev/null +++ b/server/startup/migrations/v223.ts @@ -0,0 +1,11 @@ +import { Permissions } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + +const roleName = 'user'; + +addMigration({ + version: 223, + up() { + return Permissions.update({ _id: 'message-impersonate' }, { $addToSet: { roles: roleName } }); + }, +}); diff --git a/server/startup/migrations/v224.js b/server/startup/migrations/v224.js deleted file mode 100644 index 5e69367f5609..000000000000 --- a/server/startup/migrations/v224.js +++ /dev/null @@ -1,11 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { Permissions } from '../../../app/models/server'; - -const roleName = 'app'; - -addMigration({ - version: 224, - up() { - Permissions.update({ _id: 'message-impersonate' }, { $addToSet: { roles: roleName } }); - }, -}); diff --git a/server/startup/migrations/v224.ts b/server/startup/migrations/v224.ts new file mode 100644 index 000000000000..be266c225bd0 --- /dev/null +++ b/server/startup/migrations/v224.ts @@ -0,0 +1,9 @@ +import { Permissions } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + +addMigration({ + version: 224, + up() { + return Permissions.update({ _id: 'message-impersonate' }, { $addToSet: { roles: 'app' } }); + }, +}); diff --git a/server/startup/migrations/v225.js b/server/startup/migrations/v225.ts similarity index 70% rename from server/startup/migrations/v225.js rename to server/startup/migrations/v225.ts index bee7349b8b64..62fabfcde620 100644 --- a/server/startup/migrations/v225.js +++ b/server/startup/migrations/v225.ts @@ -1,30 +1,35 @@ import { addMigration } from '../../lib/migrations'; -import { Settings, Users } from '../../../app/models/server'; +import { Users } from '../../../app/models/server'; +import { Settings } from '../../../app/models/server/raw'; addMigration({ version: 225, - up() { - const hideAvatarsSetting = Settings.findOneById('Accounts_Default_User_Preferences_hideAvatars'); - const hideAvatarsSidebarSetting = Settings.findOneById('Accounts_Default_User_Preferences_sidebarHideAvatar'); + async up() { + const hideAvatarsSetting = await Settings.findOneById('Accounts_Default_User_Preferences_hideAvatars'); + const hideAvatarsSidebarSetting = await Settings.findOneById('Accounts_Default_User_Preferences_sidebarHideAvatar'); Settings.removeById('Accounts_Default_User_Preferences_sidebarShowDiscussion'); Settings.removeById('Accounts_Default_User_Preferences_sidebarHideAvatar'); - Settings.upsert({ + Settings.update({ _id: 'Accounts_Default_User_Preferences_sidebarDisplayAvatar', }, { $set: { - value: !hideAvatarsSidebarSetting.value, + value: !hideAvatarsSidebarSetting?.value, }, + }, { + upsert: true, }); - Settings.removeById('Accounts_Default_User_Preferences_hideAvatars'); - Settings.upsert({ + await Settings.removeById('Accounts_Default_User_Preferences_hideAvatars'); + Settings.update({ _id: 'Accounts_Default_User_Preferences_displayAvatars', }, { $set: { - value: !hideAvatarsSetting.value, + value: !hideAvatarsSetting?.value, }, + }, { + upsert: true, }); Users.update({ 'settings.preferences.hideAvatars': true }, { diff --git a/server/startup/migrations/v226.js b/server/startup/migrations/v226.js deleted file mode 100644 index da53274e5ec4..000000000000 --- a/server/startup/migrations/v226.js +++ /dev/null @@ -1,9 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; - -addMigration({ - version: 226, - up() { - Settings.removeById('Apps_Game_Center_enabled'); - }, -}); diff --git a/server/startup/migrations/v226.ts b/server/startup/migrations/v226.ts new file mode 100644 index 000000000000..c94cc0b0d1e8 --- /dev/null +++ b/server/startup/migrations/v226.ts @@ -0,0 +1,9 @@ +import { Settings } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + +addMigration({ + version: 226, + up() { + return Settings.removeById('Apps_Game_Center_enabled'); + }, +}); diff --git a/server/startup/migrations/v228.js b/server/startup/migrations/v228.js deleted file mode 100644 index 9ac69ef146cc..000000000000 --- a/server/startup/migrations/v228.js +++ /dev/null @@ -1,11 +0,0 @@ -import { addMigration } from '../../lib/migrations'; -import { Permissions } from '../../../app/models'; - -addMigration({ - version: 228, - up() { - if (Permissions) { - Permissions.update({ _id: 'manage-livechat-canned-responses' }, { $addToSet: { roles: 'livechat-monitor' } }); - } - }, -}); diff --git a/server/startup/migrations/v228.ts b/server/startup/migrations/v228.ts new file mode 100644 index 000000000000..4d49253980f3 --- /dev/null +++ b/server/startup/migrations/v228.ts @@ -0,0 +1,9 @@ +import { Permissions } from '../../../app/models/server/raw'; +import { addMigration } from '../../lib/migrations'; + +addMigration({ + version: 228, + up() { + return Permissions.update({ _id: 'manage-livechat-canned-responses' }, { $addToSet: { roles: 'livechat-monitor' } }); + }, +}); diff --git a/server/startup/migrations/v229.js b/server/startup/migrations/v229.ts similarity index 62% rename from server/startup/migrations/v229.js rename to server/startup/migrations/v229.ts index 87c38d2a68f6..71c00c907909 100644 --- a/server/startup/migrations/v229.js +++ b/server/startup/migrations/v229.ts @@ -1,15 +1,15 @@ -import { Settings } from '../../../app/models/server'; +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; addMigration({ version: 229, - up() { - const oldNamesValidationSetting = Settings.findOneById( + async up() { + const oldNamesValidationSetting = await Settings.findOneById( 'UTF8_Names_Validation', ); const oldNamesValidationSettingValue = oldNamesValidationSetting?.value || '[0-9a-zA-Z-_.]+'; - Settings.upsert( + Settings.update( { _id: 'UTF8_User_Names_Validation', }, @@ -18,9 +18,12 @@ addMigration({ value: oldNamesValidationSettingValue, }, }, + { + upsert: true, + }, ); - Settings.upsert( + Settings.update( { _id: 'UTF8_Channel_Names_Validation', }, @@ -28,9 +31,11 @@ addMigration({ $set: { value: oldNamesValidationSettingValue, }, + }, { + upsert: true, }, ); - Settings.removeById('UTF8_Names_Validation'); + return Settings.removeById('UTF8_Names_Validation'); }, }); diff --git a/server/startup/migrations/v230.ts b/server/startup/migrations/v230.ts index 22c48c7762ff..c3103291bd00 100644 --- a/server/startup/migrations/v230.ts +++ b/server/startup/migrations/v230.ts @@ -1,12 +1,12 @@ +import { Permissions } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Permissions } from '../../../app/models/server'; - -const roleName = 'app'; addMigration({ version: 230, up() { - Permissions.update({ _id: 'start-discussion' }, { $addToSet: { roles: roleName } }); - Permissions.update({ _id: 'start-discussion-other-user' }, { $addToSet: { roles: roleName } }); + return Promise.all([ + Permissions.addRole('start-discussion', 'app'), + Permissions.addRole('start-discussion-other-user', 'app'), + ]); }, }); diff --git a/server/startup/migrations/v231.ts b/server/startup/migrations/v231.ts index 67a897b6e3f4..70dbd39bd854 100644 --- a/server/startup/migrations/v231.ts +++ b/server/startup/migrations/v231.ts @@ -5,12 +5,12 @@ import { addMigration } from '../../lib/migrations'; import { BannerPlatform } from '../../../definition/IBanner'; import { Banner } from '../../sdk'; import { settings } from '../../../app/settings/server'; -import { Settings } from '../../../app/models/server'; import { isEnterprise } from '../../../ee/app/license/server'; +import { Settings } from '../../../app/models/server/raw'; addMigration({ version: 231, - up() { + async up() { const LDAPEnabled = settings.get('LDAP_Enable'); const SAMLEnabled = settings.get('SAML_Custom_Default'); @@ -18,7 +18,7 @@ addMigration({ _id: { $in: [/^Accounts_OAuth_(Custom-)?([^-_]+)$/, 'Accounts_OAuth_GitHub_Enterprise'] }, value: true, }; - const CustomOauthEnabled = !!Settings.findOne(query); + const CustomOauthEnabled = !! await Settings.findOne(query); const isAuthServiceEnabled = LDAPEnabled || SAMLEnabled || CustomOauthEnabled; diff --git a/server/startup/migrations/v233.ts b/server/startup/migrations/v233.ts index 9d9068a588d8..bf51873d9e19 100644 --- a/server/startup/migrations/v233.ts +++ b/server/startup/migrations/v233.ts @@ -1,10 +1,10 @@ +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; addMigration({ version: 233, up() { - Settings.remove({ _id: { $in: [ + return Settings.deleteMany({ _id: { $in: [ 'Log_Package', 'Log_File', ] } }); diff --git a/server/startup/migrations/v234.ts b/server/startup/migrations/v234.ts index 4d8dbe2db412..8909a16e21c1 100644 --- a/server/startup/migrations/v234.ts +++ b/server/startup/migrations/v234.ts @@ -1,10 +1,10 @@ +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; addMigration({ version: 234, up() { - Settings.remove({ + return Settings.deleteMany({ _id: { $in: [ 'GoogleVision_Enable', diff --git a/server/startup/migrations/v235.ts b/server/startup/migrations/v235.ts index fe2308356e50..1b6fb0af8e1a 100644 --- a/server/startup/migrations/v235.ts +++ b/server/startup/migrations/v235.ts @@ -1,10 +1,11 @@ import { addMigration } from '../../lib/migrations'; -import { Settings, Subscriptions, Users } from '../../../app/models/server'; +import { Subscriptions, Users } from '../../../app/models/server'; +import { Settings } from '../../../app/models/server/raw'; addMigration({ version: 235, - up() { - Settings.removeById('Accounts_Default_User_Preferences_audioNotifications'); + async up() { + await Settings.removeById('Accounts_Default_User_Preferences_audioNotifications'); // delete field from subscriptions Subscriptions.update({ diff --git a/server/startup/migrations/v236.ts b/server/startup/migrations/v236.ts index c1f839896400..2f0ea88c17f2 100644 --- a/server/startup/migrations/v236.ts +++ b/server/startup/migrations/v236.ts @@ -1,13 +1,13 @@ +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; addMigration({ version: 236, - up() { - Settings.removeById('Canned Responses'); - Settings.removeById('Canned_Responses'); + async up() { + await Settings.removeById('Canned Responses'); + await Settings.removeById('Canned_Responses'); - Settings.upsert( + await Settings.update( { _id: 'Canned_Responses_Enable', }, @@ -16,6 +16,9 @@ addMigration({ group: 'Omnichannel', }, }, + { + upsert: true, + }, ); }, }); diff --git a/server/startup/migrations/v237.ts b/server/startup/migrations/v237.ts index f53d8d0eb195..155ae0b29a3e 100644 --- a/server/startup/migrations/v237.ts +++ b/server/startup/migrations/v237.ts @@ -1,7 +1,7 @@ import { addMigration } from '../../lib/migrations'; import { settings } from '../../../app/settings/server'; -import { Settings } from '../../../app/models/server'; import { isEnterprise } from '../../../ee/app/license/server'; +import { Settings } from '../../../app/models/server/raw'; function copySettingValue(newName: string, oldName: string): void { const value = settings.get(oldName); @@ -9,12 +9,12 @@ function copySettingValue(newName: string, oldName: string): void { return; } - Settings.upsert({ _id: newName }, { $set: { value } }); + Settings.update({ _id: newName }, { $set: { value } }, { upsert: true }); } addMigration({ version: 237, - up() { + async up() { const isEE = isEnterprise(); // Override AD defaults with the previously configured values @@ -23,7 +23,9 @@ addMigration({ // If we're sure the server is AD, then select it - otherwise keep it as generic ldap const useAdDefaults = settings.get('LDAP_User_Search_Field') === 'sAMAccountName'; - Settings.upsert({ _id: 'LDAP_Server_Type' }, { $set: { value: useAdDefaults ? 'ad' : '' } }); + Settings.update({ _id: 'LDAP_Server_Type' }, { $set: { value: useAdDefaults ? 'ad' : '' } }, { + upsert: true, + }); // The setting to use the field map also determined if the user data was updated on login or not copySettingValue('LDAP_Update_Data_On_Login', 'LDAP_Sync_User_Data'); @@ -44,14 +46,14 @@ addMigration({ } if (fieldMap[key] === 'name') { - Settings.upsert({ _id: 'LDAP_Name_Field' }, { $set: { value: key } }); - Settings.upsert({ _id: 'LDAP_AD_Name_Field' }, { $set: { value: key } }); + Settings.update({ _id: 'LDAP_Name_Field' }, { $set: { value: key } }, { upsert: true }); + Settings.update({ _id: 'LDAP_AD_Name_Field' }, { $set: { value: key } }, { upsert: true }); continue; } if (fieldMap[key] === 'email') { - Settings.upsert({ _id: 'LDAP_Email_Field' }, { $set: { value: key } }); - Settings.upsert({ _id: 'LDAP_AD_Email_Field' }, { $set: { value: key } }); + Settings.update({ _id: 'LDAP_Email_Field' }, { $set: { value: key } }, { upsert: true }); + Settings.update({ _id: 'LDAP_AD_Email_Field' }, { $set: { value: key } }, { upsert: true }); continue; } @@ -60,10 +62,10 @@ addMigration({ if (isEE) { const newJson = JSON.stringify(newObject); - Settings.upsert({ _id: 'LDAP_CustomFieldMap' }, { $set: { value: newJson } }); + Settings.update({ _id: 'LDAP_CustomFieldMap' }, { $set: { value: newJson } }, { upsert: true }); const syncCustomFields = Object.keys(newObject).length > 0 && settings.get('LDAP_Sync_User_Data'); - Settings.upsert({ _id: 'LDAP_Sync_Custom_Fields' }, { $set: { value: syncCustomFields } }); + Settings.update({ _id: 'LDAP_Sync_Custom_Fields' }, { $set: { value: syncCustomFields } }, { upsert: true }); } } @@ -80,7 +82,7 @@ addMigration({ copySettingValue('LDAP_Sync_User_Data_Channels_Filter', 'LDAP_Sync_User_Data_Groups_Filter'); copySettingValue('LDAP_Sync_User_Data_Channels_BaseDN', 'LDAP_Sync_User_Data_Groups_BaseDN'); - Settings.remove({ + await Settings.deleteMany({ _id: { $in: [ 'LDAP_Sync_Now', diff --git a/server/startup/migrations/v240.ts b/server/startup/migrations/v240.ts index 4ce8f2c7d0ac..296040d649d7 100644 --- a/server/startup/migrations/v240.ts +++ b/server/startup/migrations/v240.ts @@ -1,9 +1,9 @@ +import { Settings } from '../../../app/models/server/raw'; import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; addMigration({ version: 240, up() { - Settings.removeById('Support_Cordova_App'); + return Settings.removeById('Support_Cordova_App'); }, }); diff --git a/server/startup/migrations/v242.ts b/server/startup/migrations/v242.ts index 86419e276328..b94f3f6439d5 100644 --- a/server/startup/migrations/v242.ts +++ b/server/startup/migrations/v242.ts @@ -1,5 +1,6 @@ import { addMigration } from '../../lib/migrations'; -import { LivechatInquiry, Settings } from '../../../app/models/server'; +import { LivechatInquiry } from '../../../app/models/server'; +import { Settings } from '../../../app/models/server/raw'; function removeQueueTimeoutFromInquiries(): void { LivechatInquiry.update({ @@ -8,23 +9,23 @@ function removeQueueTimeoutFromInquiries(): void { }, { $unset: { estimatedInactivityCloseTimeAt: 1 } }, { multi: true }); } -function removeSetting(): void { - const oldSetting = Settings.findOneById('Livechat_max_queue_wait_time_action'); +async function removeSetting(): Promise { + const oldSetting = await Settings.findOneById('Livechat_max_queue_wait_time_action'); if (!oldSetting) { return; } const currentAction = oldSetting.value; if (currentAction === 'Nothing') { - Settings.upsert({ _id: 'Livechat_max_queue_wait_time' }, { $set: { value: -1 } }); + await Settings.update({ _id: 'Livechat_max_queue_wait_time' }, { $set: { value: -1 } }, { upsert: true }); } - Settings.removeById('Livechat_max_queue_wait_time_action'); + await Settings.removeById('Livechat_max_queue_wait_time_action'); } addMigration({ version: 242, up() { removeQueueTimeoutFromInquiries(); - removeSetting(); + return removeSetting(); }, }); diff --git a/server/startup/migrations/v243.ts b/server/startup/migrations/v243.ts index 89d3f9f03e70..71a2e7d97c75 100644 --- a/server/startup/migrations/v243.ts +++ b/server/startup/migrations/v243.ts @@ -1,19 +1,22 @@ import { addMigration } from '../../lib/migrations'; -import { Settings, Users } from '../../../app/models/server'; +import { Users } from '../../../app/models/server'; +import { Settings } from '../../../app/models/server/raw'; addMigration({ version: 243, - up() { - const mobileNotificationsSetting = Settings.findOneById('Accounts_Default_User_Preferences_mobileNotifications'); + async up() { + const mobileNotificationsSetting = await Settings.findOneById('Accounts_Default_User_Preferences_mobileNotifications'); - Settings.removeById('Accounts_Default_User_Preferences_mobileNotifications'); + await Settings.removeById('Accounts_Default_User_Preferences_mobileNotifications'); if (mobileNotificationsSetting && mobileNotificationsSetting.value) { - Settings.upsert({ + Settings.update({ _id: 'Accounts_Default_User_Preferences_pushNotifications', }, { $set: { value: mobileNotificationsSetting.value, }, + }, { + upsert: true, }); } diff --git a/server/startup/migrations/v244.ts b/server/startup/migrations/v244.ts index ce16525c6991..d5c39dffc20c 100644 --- a/server/startup/migrations/v244.ts +++ b/server/startup/migrations/v244.ts @@ -1,27 +1,9 @@ -import { settings, settingsRegistry } from '../../../app/settings/server'; +import { upsertPermissions } from '../../../app/authorization/server/functions/upsertPermissions'; import { addMigration } from '../../lib/migrations'; addMigration({ version: 244, up() { - const customOauthServices = settings.getByRegexp(/Accounts_OAuth_Custom-[^-]+$/mi); - const serviceNames = customOauthServices.map(([key]) => key.replace('Accounts_OAuth_Custom-', '')); - - serviceNames.forEach((serviceName) => { - settingsRegistry.add(`Accounts_OAuth_Custom-${ serviceName }-roles_to_sync`, '', { - type: 'string', - group: 'OAuth', - section: `Custom OAuth: ${ serviceName }`, - i18nLabel: 'Accounts_OAuth_Custom_Roles_To_Sync', - i18nDescription: 'Accounts_OAuth_Custom_Roles_To_Sync_Description', - enterprise: true, - enableQuery: { - _id: `Accounts_OAuth_Custom-${ serviceName }-merge_roles`, - value: true, - }, - invalidValue: '', - modules: ['oauth-enterprise'], - }); - }); + return upsertPermissions(); }, }); diff --git a/server/startup/migrations/v245.ts b/server/startup/migrations/v245.ts new file mode 100644 index 000000000000..ce16525c6991 --- /dev/null +++ b/server/startup/migrations/v245.ts @@ -0,0 +1,27 @@ +import { settings, settingsRegistry } from '../../../app/settings/server'; +import { addMigration } from '../../lib/migrations'; + +addMigration({ + version: 244, + up() { + const customOauthServices = settings.getByRegexp(/Accounts_OAuth_Custom-[^-]+$/mi); + const serviceNames = customOauthServices.map(([key]) => key.replace('Accounts_OAuth_Custom-', '')); + + serviceNames.forEach((serviceName) => { + settingsRegistry.add(`Accounts_OAuth_Custom-${ serviceName }-roles_to_sync`, '', { + type: 'string', + group: 'OAuth', + section: `Custom OAuth: ${ serviceName }`, + i18nLabel: 'Accounts_OAuth_Custom_Roles_To_Sync', + i18nDescription: 'Accounts_OAuth_Custom_Roles_To_Sync_Description', + enterprise: true, + enableQuery: { + _id: `Accounts_OAuth_Custom-${ serviceName }-merge_roles`, + value: true, + }, + invalidValue: '', + modules: ['oauth-enterprise'], + }); + }); + }, +}); diff --git a/server/startup/presence.js b/server/startup/presence.js index 73fc47b2725c..b476d431ed6b 100644 --- a/server/startup/presence.js +++ b/server/startup/presence.js @@ -1,8 +1,7 @@ import { Meteor } from 'meteor/meteor'; import { UserPresence } from 'meteor/konecty:user-presence'; -import InstanceStatusModel from '../../app/models/server/models/InstanceStatus'; -import UsersSessionsModel from '../../app/models/server/models/UsersSessions'; +import { InstanceStatus, UsersSessions } from '../../app/models/server/raw'; import { isPresenceMonitorEnabled } from '../lib/isPresenceMonitorEnabled'; Meteor.startup(function() { @@ -14,16 +13,8 @@ Meteor.startup(function() { // UserPresenceMonitor.start(); // Remove lost connections - const ids = InstanceStatusModel.find({}, { fields: { _id: 1 } }).fetch().map((id) => id._id); + const ids = Promise.await(InstanceStatus.find({}, { projection: { _id: 1 } }).toArray()) + .map((id) => id._id); - const update = { - $pull: { - connections: { - instanceId: { - $nin: ids, - }, - }, - }, - }; - UsersSessionsModel.update({}, update, { multi: true }); + Promise.await(UsersSessions.clearConnectionsFromInstanceId(ids)); }); diff --git a/server/stream/streamBroadcast.js b/server/stream/streamBroadcast.js index 4c94ccc59ab4..c866885b101b 100644 --- a/server/stream/streamBroadcast.js +++ b/server/stream/streamBroadcast.js @@ -5,11 +5,11 @@ import { check } from 'meteor/check'; import { DDP } from 'meteor/ddp'; import { Logger } from '../lib/logger/Logger'; -import { hasPermission } from '../../app/authorization'; +import { hasPermission } from '../../app/authorization/server'; import { settings } from '../../app/settings/server'; -import { isDocker, getURL } from '../../app/utils'; +import { isDocker, getURL } from '../../app/utils/server'; import { Users } from '../../app/models/server'; -import InstanceStatusModel from '../../app/models/server/models/InstanceStatus'; +import { InstanceStatus as InstanceStatusRaw } from '../../app/models/server/raw'; import { StreamerCentral } from '../modules/streamer/streamer.module'; import { isPresenceMonitorEnabled } from '../lib/isPresenceMonitorEnabled'; @@ -61,7 +61,7 @@ function startMatrixBroadcast() { } matrixBroadCastActions = { - added(record) { + added: Meteor.bindEnvironment((record) => { cache.set(record._id, record); const subPath = getURL('', { cdn: false, full: false }); @@ -100,7 +100,7 @@ function startMatrixBroadcast() { connections[instance].onReconnect = function() { return authorizeConnection(instance); }; - }, + }), removed(id) { const record = cache.get(id); @@ -129,19 +129,15 @@ function startMatrixBroadcast() { }, }; - const query = { + InstanceStatusRaw.find({ 'extraInformation.port': { $exists: true, }, - }; - - const options = { + }, { sort: { _createdAt: -1, }, - }; - - InstanceStatusModel.find(query, options).fetch().forEach(matrixBroadCastActions.added); + }).forEach(matrixBroadCastActions.added); } diff --git a/tests/end-to-end/api/05-chat.js b/tests/end-to-end/api/05-chat.js index d63bd228aa73..84941df7bae7 100644 --- a/tests/end-to-end/api/05-chat.js +++ b/tests/end-to-end/api/05-chat.js @@ -750,7 +750,7 @@ describe('[Chat]', function() { .to.have.string('