diff --git a/apps/meteor/app/api/server/v1/users.ts b/apps/meteor/app/api/server/v1/users.ts index 944f33dce8d6..957695853997 100644 --- a/apps/meteor/app/api/server/v1/users.ts +++ b/apps/meteor/app/api/server/v1/users.ts @@ -978,10 +978,16 @@ API.v1.addRoute( post() { check( this.bodyParams, - Match.ObjectIncluding({ - status: Match.Maybe(String), - message: Match.Maybe(String), - }), + Match.OneOf( + Match.ObjectIncluding({ + status: Match.Maybe(String), + message: String, + }), + Match.ObjectIncluding({ + status: String, + message: Match.Maybe(String), + }), + ), ); if (!settings.get('Accounts_AllowUserStatusMessageChange')) { diff --git a/apps/meteor/app/apps/server/communication/rest.js b/apps/meteor/app/apps/server/communication/rest.js index 632a57ea24f7..79276a15a8b1 100644 --- a/apps/meteor/app/apps/server/communication/rest.js +++ b/apps/meteor/app/apps/server/communication/rest.js @@ -70,13 +70,13 @@ export class AppsRestApi { '', { authRequired: true, permissionsRequired: ['manage-apps'] }, { - get() { + async get() { const baseUrl = orchestrator.getMarketplaceUrl(); // Gets the Apps from the marketplace if (this.queryParams.marketplace) { const headers = getDefaultHeaders(); - const token = getWorkspaceAccessToken(); + const token = await getWorkspaceAccessToken(); if (token) { headers.Authorization = `Bearer ${token}`; } @@ -100,7 +100,7 @@ export class AppsRestApi { if (this.queryParams.categories) { const headers = getDefaultHeaders(); - const token = getWorkspaceAccessToken(); + const token = await getWorkspaceAccessToken(); if (token) { headers.Authorization = `Bearer ${token}`; } @@ -183,8 +183,8 @@ export class AppsRestApi { const headers = getDefaultHeaders(); try { - const downloadToken = getWorkspaceAccessToken(true, 'marketplace:download', false); - const marketplaceToken = getWorkspaceAccessToken(); + const downloadToken = await getWorkspaceAccessToken(true, 'marketplace:download', false); + const marketplaceToken = await getWorkspaceAccessToken(); const [downloadResponse, marketplaceResponse] = await Promise.all([ fetch(`${baseUrl}/v2/apps/${this.bodyParams.appId}/download/${this.bodyParams.version}?token=${downloadToken}`, { @@ -318,11 +318,11 @@ export class AppsRestApi { 'bundles/:id/apps', { authRequired: true, permissionsRequired: ['manage-apps'] }, { - get() { + async get() { const baseUrl = orchestrator.getMarketplaceUrl(); const headers = {}; - const token = getWorkspaceAccessToken(); + const token = await getWorkspaceAccessToken(); if (token) { headers.Authorization = `Bearer ${token}`; } @@ -347,16 +347,48 @@ export class AppsRestApi { }, ); + this.api.addRoute( + 'featured', + { authRequired: true }, + { + async get() { + const baseUrl = orchestrator.getMarketplaceUrl(); + + const headers = getDefaultHeaders(); + const token = await getWorkspaceAccessToken(); + if (token) { + headers.Authorization = `Bearer ${token}`; + } + + let result; + try { + result = HTTP.get(`${baseUrl}/v1/apps/featured`, { + headers, + }); + } catch (e) { + return handleError('Unable to access Marketplace. Does the server has access to the internet?', e); + } + + if (!result || result.statusCode !== 200) { + orchestrator.getRocketChatLogger().error('Error getting the Featured Apps from the Marketplace:', result.data); + return API.v1.failure(); + } + + return API.v1.success(result.data); + }, + }, + ); + this.api.addRoute( ':id', { authRequired: true, permissionsRequired: ['manage-apps'] }, { - get() { + async get() { if (this.queryParams.marketplace && this.queryParams.version) { const baseUrl = orchestrator.getMarketplaceUrl(); const headers = {}; // DO NOT ATTACH THE FRAMEWORK/ENGINE VERSION HERE. - const token = getWorkspaceAccessToken(); + const token = await getWorkspaceAccessToken(); if (token) { headers.Authorization = `Bearer ${token}`; } @@ -382,7 +414,7 @@ export class AppsRestApi { const baseUrl = orchestrator.getMarketplaceUrl(); const headers = getDefaultHeaders(); - const token = getWorkspaceAccessToken(); + const token = await getWorkspaceAccessToken(); if (token) { headers.Authorization = `Bearer ${token}`; } @@ -434,7 +466,7 @@ export class AppsRestApi { const baseUrl = orchestrator.getMarketplaceUrl(); const headers = getDefaultHeaders(); - const token = getWorkspaceAccessToken(true, 'marketplace:download', false); + const token = await getWorkspaceAccessToken(true, 'marketplace:download', false); try { const response = await fetch( @@ -532,11 +564,11 @@ export class AppsRestApi { ':id/versions', { authRequired: true, permissionsRequired: ['manage-apps'] }, { - get() { + async get() { const baseUrl = orchestrator.getMarketplaceUrl(); const headers = {}; // DO NOT ATTACH THE FRAMEWORK/ENGINE VERSION HERE. - const token = getWorkspaceAccessToken(); + const token = await getWorkspaceAccessToken(); if (token) { headers.Authorization = `Bearer ${token}`; } @@ -564,16 +596,16 @@ export class AppsRestApi { ':id/sync', { authRequired: true, permissionsRequired: ['manage-apps'] }, { - post() { + async post() { const baseUrl = orchestrator.getMarketplaceUrl(); const headers = getDefaultHeaders(); - const token = getWorkspaceAccessToken(); + const token = await getWorkspaceAccessToken(); if (token) { headers.Authorization = `Bearer ${token}`; } - const workspaceIdSetting = Promise.await(Settings.findOneById('Cloud_Workspace_Id')); + const workspaceIdSetting = await Settings.findOneById('Cloud_Workspace_Id'); let result; try { @@ -590,7 +622,7 @@ export class AppsRestApi { return API.v1.failure(); } - Promise.await(Apps.updateAppsMarketplaceInfo([result.data])); + await Apps.updateAppsMarketplaceInfo([result.data]); return API.v1.success({ app: result.data }); }, diff --git a/apps/meteor/app/assets/server/assets.ts b/apps/meteor/app/assets/server/assets.ts index 1262f85041df..d37e67470b10 100644 --- a/apps/meteor/app/assets/server/assets.ts +++ b/apps/meteor/app/assets/server/assets.ts @@ -9,13 +9,13 @@ import sizeOf from 'image-size'; import sharp from 'sharp'; import { NextHandleFunction } from 'connect'; import { IRocketChatAssets, IRocketChatAsset } from '@rocket.chat/core-typings'; +import { Settings } from '@rocket.chat/models'; import { settings, settingsRegistry } from '../../settings/server'; import { getURL } from '../../utils/lib/getURL'; import { getExtension } from '../../utils/lib/mimeTypes'; import { hasPermission } from '../../authorization/server'; import { RocketChatFile } from '../../file'; -import { Settings } from '../../models/server'; const RocketChatAssetsInstance = new RocketChatFile.GridFS({ name: 'assets', @@ -347,7 +347,7 @@ function addAssetToSetting(asset: string, value: IRocketChatAsset): void { if (typeof currentValue === 'object' && currentValue.defaultUrl !== getAssetByKey(asset).defaultUrl) { currentValue.defaultUrl = getAssetByKey(asset).defaultUrl; - Settings.updateValueById(key, currentValue); + Promise.await(Settings.updateValueById(key, currentValue)); } } diff --git a/apps/meteor/app/authentication/server/startup/index.js b/apps/meteor/app/authentication/server/startup/index.js index b508d87dced7..536117e232d0 100644 --- a/apps/meteor/app/authentication/server/startup/index.js +++ b/apps/meteor/app/authentication/server/startup/index.js @@ -4,12 +4,12 @@ import { Accounts } from 'meteor/accounts-base'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import _ from 'underscore'; import { escapeRegExp, escapeHTML } from '@rocket.chat/string-helpers'; -import { Roles, Users as UsersRaw } from '@rocket.chat/models'; +import { Roles, Settings, Users as UsersRaw } from '@rocket.chat/models'; import * as Mailer from '../../../mailer/server/api'; import { settings } from '../../../settings/server'; import { callbacks } from '../../../../lib/callbacks'; -import { Settings, Users } from '../../../models/server'; +import { Users } from '../../../models/server'; import { addUserRoles } from '../../../../server/lib/roles/addUserRoles'; import { getAvatarSuggestionForUser } from '../../../lib/server/functions/getAvatarSuggestionForUser'; import { parseCSV } from '../../../../lib/utils/parseCSV'; @@ -295,7 +295,7 @@ Accounts.insertUserDoc = _.wrap(Accounts.insertUserDoc, function (insertUserDoc, if (!roles.includes('admin') && !hasAdmin) { roles.push('admin'); if (settings.get('Show_Setup_Wizard') === 'pending') { - Settings.updateValueById('Show_Setup_Wizard', 'in_progress'); + Promise.await(Settings.updateValueById('Show_Setup_Wizard', 'in_progress')); } } diff --git a/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts b/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts index 712795441ece..edc18eaf52f6 100644 --- a/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts +++ b/apps/meteor/app/cloud/server/functions/buildRegistrationData.ts @@ -6,12 +6,12 @@ import { Users } from '../../../models/server'; import { statistics } from '../../../statistics/server'; import { LICENSE_VERSION } from '../license'; -type WorkspaceRegistrationData = { +type WorkspaceRegistrationData = { uniqueId: string; workspaceId: SettingValue; address: SettingValue; contactName: string; - contactEmail: string; + contactEmail: T; seats: number; allowMarketing: SettingValue; accountName: SettingValue; @@ -33,7 +33,7 @@ type WorkspaceRegistrationData = { npsEnabled: SettingValue; }; -export async function buildWorkspaceRegistrationData(contactEmail: string): Promise { +export async function buildWorkspaceRegistrationData(contactEmail: T): Promise> { const stats = (await Statistics.findLast()) || (await statistics.get()); const address = settings.get('Site_Url'); diff --git a/apps/meteor/app/cloud/server/functions/connectWorkspace.js b/apps/meteor/app/cloud/server/functions/connectWorkspace.ts similarity index 85% rename from apps/meteor/app/cloud/server/functions/connectWorkspace.js rename to apps/meteor/app/cloud/server/functions/connectWorkspace.ts index 9c2a424c3221..a0d76a402eff 100644 --- a/apps/meteor/app/cloud/server/functions/connectWorkspace.js +++ b/apps/meteor/app/cloud/server/functions/connectWorkspace.ts @@ -1,16 +1,16 @@ import { HTTP } from 'meteor/http'; +import { Settings } from '@rocket.chat/models'; import { getRedirectUri } from './getRedirectUri'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; -import { Settings } from '../../../models/server'; import { settings } from '../../../settings/server'; import { saveRegistrationData } from './saveRegistrationData'; import { SystemLogger } from '../../../../server/lib/logger/system'; -export function connectWorkspace(token) { +export async function connectWorkspace(token: string) { const { connectToCloud } = retrieveRegistrationStatus(); if (!connectToCloud) { - Settings.updateValueById('Register_Server', true); + await Settings.updateValueById('Register_Server', true); } // shouldn't get here due to checking this on the method @@ -36,7 +36,7 @@ export function connectWorkspace(token) { }, data: regInfo, }); - } catch (e) { + } catch (e: any) { if (e.response && e.response.data && e.response.data.error) { SystemLogger.error(`Failed to register with Rocket.Chat Cloud. Error: ${e.response.data.error}`); } else { @@ -52,7 +52,7 @@ export function connectWorkspace(token) { return false; } - Promise.await(saveRegistrationData(data)); + await saveRegistrationData(data); return true; } diff --git a/apps/meteor/app/cloud/server/functions/disconnectWorkspace.js b/apps/meteor/app/cloud/server/functions/disconnectWorkspace.ts similarity index 55% rename from apps/meteor/app/cloud/server/functions/disconnectWorkspace.js rename to apps/meteor/app/cloud/server/functions/disconnectWorkspace.ts index c1ac36729aaa..0c1a9743b104 100644 --- a/apps/meteor/app/cloud/server/functions/disconnectWorkspace.js +++ b/apps/meteor/app/cloud/server/functions/disconnectWorkspace.ts @@ -1,13 +1,14 @@ +import { Settings } from '@rocket.chat/models'; + import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; -import { Settings } from '../../../models/server'; -export function disconnectWorkspace() { +export async function disconnectWorkspace() { const { connectToCloud } = retrieveRegistrationStatus(); if (!connectToCloud) { return true; } - Settings.updateValueById('Register_Server', false); + await Settings.updateValueById('Register_Server', false); return true; } diff --git a/apps/meteor/app/cloud/server/functions/getOAuthAuthorizationUrl.js b/apps/meteor/app/cloud/server/functions/getOAuthAuthorizationUrl.ts similarity index 57% rename from apps/meteor/app/cloud/server/functions/getOAuthAuthorizationUrl.js rename to apps/meteor/app/cloud/server/functions/getOAuthAuthorizationUrl.ts index 54b2df8a490c..ffae4d1e5a15 100644 --- a/apps/meteor/app/cloud/server/functions/getOAuthAuthorizationUrl.js +++ b/apps/meteor/app/cloud/server/functions/getOAuthAuthorizationUrl.ts @@ -1,20 +1,20 @@ import { Random } from 'meteor/random'; +import { Settings } from '@rocket.chat/models'; import { getRedirectUri } from './getRedirectUri'; -import { Settings } from '../../../models/server'; import { settings } from '../../../settings/server'; import { userScopes } from '../oauthScopes'; -export function getOAuthAuthorizationUrl() { +export async function getOAuthAuthorizationUrl() { const state = Random.id(); - Settings.updateValueById('Cloud_Workspace_Registration_State', state); + await Settings.updateValueById('Cloud_Workspace_Registration_State', state); const cloudUrl = settings.get('Cloud_Url'); - const client_id = settings.get('Cloud_Workspace_Client_Id'); + const clientId = settings.get('Cloud_Workspace_Client_Id'); const redirectUri = getRedirectUri(); const scope = userScopes.join(' '); - return `${cloudUrl}/authorize?response_type=code&client_id=${client_id}&redirect_uri=${redirectUri}&scope=${scope}&state=${state}`; + return `${cloudUrl}/authorize?response_type=code&client_id=${clientId}&redirect_uri=${redirectUri}&scope=${scope}&state=${state}`; } diff --git a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.js b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts similarity index 52% rename from apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.js rename to apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts index b1538161cafb..cea3a6192a2c 100644 --- a/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.js +++ b/apps/meteor/app/cloud/server/functions/getWorkspaceAccessToken.ts @@ -1,6 +1,7 @@ +import { Settings } from '@rocket.chat/models'; + import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; import { getWorkspaceAccessTokenWithScope } from './getWorkspaceAccessTokenWithScope'; -import { Settings } from '../../../models/server'; import { settings } from '../../../settings/server'; /** @@ -9,25 +10,31 @@ import { settings } from '../../../settings/server'; * @param {boolean} save * @returns string */ -export function getWorkspaceAccessToken(forceNew = false, scope = '', save = true) { +export async function getWorkspaceAccessToken(forceNew = false, scope = '', save = true) { const { connectToCloud, workspaceRegistered } = retrieveRegistrationStatus(); if (!connectToCloud || !workspaceRegistered) { return ''; } - const expires = Settings.findOneById('Cloud_Workspace_Access_Token_Expires_At'); + const expires = await Settings.findOneById('Cloud_Workspace_Access_Token_Expires_At'); + + if (expires === null) { + throw new Error('Cloud_Workspace_Access_Token_Expires_At is not set'); + } const now = new Date(); - if (now < expires.value && !forceNew) { + if (expires.value && now < expires.value && !forceNew) { return settings.get('Cloud_Workspace_Access_Token'); } const accessToken = getWorkspaceAccessTokenWithScope(scope); if (save) { - Settings.updateValueById('Cloud_Workspace_Access_Token', accessToken.token); - Settings.updateValueById('Cloud_Workspace_Access_Token_Expires_At', accessToken.expiresAt); + await Promise.all([ + Settings.updateValueById('Cloud_Workspace_Access_Token', accessToken.token), + Settings.updateValueById('Cloud_Workspace_Access_Token_Expires_At', accessToken.expiresAt), + ]); } return accessToken.token; diff --git a/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.js b/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts similarity index 72% rename from apps/meteor/app/cloud/server/functions/getWorkspaceLicense.js rename to apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts index 6e2e8300d2e5..23cf5e3b5015 100644 --- a/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.js +++ b/apps/meteor/app/cloud/server/functions/getWorkspaceLicense.ts @@ -1,14 +1,14 @@ import { HTTP } from 'meteor/http'; +import { Settings } from '@rocket.chat/models'; import { getWorkspaceAccessToken } from './getWorkspaceAccessToken'; import { settings } from '../../../settings/server'; -import { Settings } from '../../../models/server'; import { callbacks } from '../../../../lib/callbacks'; import { LICENSE_VERSION } from '../license'; import { SystemLogger } from '../../../../server/lib/logger/system'; -export function getWorkspaceLicense() { - const token = getWorkspaceAccessToken(); +export async function getWorkspaceLicense() { + const token = await getWorkspaceAccessToken(); if (!token) { return { updated: false, license: '' }; @@ -21,7 +21,7 @@ export function getWorkspaceLicense() { Authorization: `Bearer ${token}`, }, }); - } catch (e) { + } catch (e: any) { if (e.response && e.response.data && e.response.data.error) { SystemLogger.error(`Failed to update license from Rocket.Chat Cloud. Error: ${e.response.data.error}`); } else { @@ -32,13 +32,17 @@ export function getWorkspaceLicense() { } const remoteLicense = licenseResult.data; - const currentLicense = settings.get('Cloud_Workspace_License'); + const currentLicense = await Settings.findOne('Cloud_Workspace_License'); + + if (!currentLicense || !currentLicense._updatedAt) { + throw new Error('Failed to retrieve current license'); + } if (remoteLicense.updatedAt <= currentLicense._updatedAt) { return { updated: false, license: '' }; } - Settings.updateValueById('Cloud_Workspace_License', remoteLicense.license); + await Settings.updateValueById('Cloud_Workspace_License', remoteLicense.license); callbacks.run('workspaceLicenseChanged', remoteLicense.license); diff --git a/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.js b/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts similarity index 81% rename from apps/meteor/app/cloud/server/functions/startRegisterWorkspace.js rename to apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts index 9633a3835159..3b20e89ac974 100644 --- a/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.js +++ b/apps/meteor/app/cloud/server/functions/startRegisterWorkspace.ts @@ -1,9 +1,9 @@ import { HTTP } from 'meteor/http'; +import { Settings } from '@rocket.chat/models'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; import { syncWorkspace } from './syncWorkspace'; import { settings } from '../../../settings/server'; -import { Settings } from '../../../models/server'; import { buildWorkspaceRegistrationData } from './buildRegistrationData'; import { SystemLogger } from '../../../../server/lib/logger/system'; @@ -15,9 +15,9 @@ export async function startRegisterWorkspace(resend = false) { return true; } - Settings.updateValueById('Register_Server', true); + await Settings.updateValueById('Register_Server', true); - const regInfo = await buildWorkspaceRegistrationData(); + const regInfo = await buildWorkspaceRegistrationData(undefined); const cloudUrl = settings.get('Cloud_Url'); @@ -26,7 +26,7 @@ export async function startRegisterWorkspace(resend = false) { result = HTTP.post(`${cloudUrl}/api/v2/register/workspace?resend=${resend}`, { data: regInfo, }); - } catch (e) { + } catch (e: any) { if (e.response && e.response.data && e.response.data.error) { SystemLogger.error(`Failed to register with Rocket.Chat Cloud. ErrorCode: ${e.response.data.error}`); } else { @@ -39,7 +39,7 @@ export async function startRegisterWorkspace(resend = false) { return false; } - Settings.updateValueById('Cloud_Workspace_Id', data.id); + await Settings.updateValueById('Cloud_Workspace_Id', data.id); return true; } diff --git a/apps/meteor/app/cloud/server/functions/syncWorkspace.js b/apps/meteor/app/cloud/server/functions/syncWorkspace.ts similarity index 81% rename from apps/meteor/app/cloud/server/functions/syncWorkspace.js rename to apps/meteor/app/cloud/server/functions/syncWorkspace.ts index ad8ec96faa49..b4de1a8fd664 100644 --- a/apps/meteor/app/cloud/server/functions/syncWorkspace.js +++ b/apps/meteor/app/cloud/server/functions/syncWorkspace.ts @@ -1,10 +1,10 @@ import { HTTP } from 'meteor/http'; +import { Settings } from '@rocket.chat/models'; import { buildWorkspaceRegistrationData } from './buildRegistrationData'; import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; import { getWorkspaceAccessToken } from './getWorkspaceAccessToken'; import { getWorkspaceLicense } from './getWorkspaceLicense'; -import { Settings } from '../../../models/server'; import { settings } from '../../../settings/server'; import { getAndCreateNpsSurvey } from '../../../../server/services/nps/getAndCreateNpsSurvey'; import { NPS, Banner } from '../../../../server/sdk'; @@ -16,14 +16,14 @@ export async function syncWorkspace(reconnectCheck = false) { return false; } - const info = await buildWorkspaceRegistrationData(); + const info = await buildWorkspaceRegistrationData(undefined); const workspaceUrl = settings.get('Cloud_Workspace_Registration_Client_Uri'); let result; try { - const headers = {}; - const token = getWorkspaceAccessToken(true); + const headers: Record = {}; + const token = await getWorkspaceAccessToken(true); if (token) { headers.Authorization = `Bearer ${token}`; @@ -36,8 +36,8 @@ export async function syncWorkspace(reconnectCheck = false) { headers, }); - getWorkspaceLicense(); - } catch (e) { + await getWorkspaceLicense(); + } catch (e: any) { if (e.response && e.response.data && e.response.data.error) { SystemLogger.error(`Failed to sync with Rocket.Chat Cloud. Error: ${e.response.data.error}`); } else { @@ -53,11 +53,11 @@ export async function syncWorkspace(reconnectCheck = false) { } if (data.publicKey) { - Settings.updateValueById('Cloud_Workspace_PublicKey', data.publicKey); + await Settings.updateValueById('Cloud_Workspace_PublicKey', data.publicKey); } if (data.trial?.trialId) { - Settings.updateValueById('Cloud_Workspace_Had_Trial', true); + await Settings.updateValueById('Cloud_Workspace_Had_Trial', true); } if (data.nps) { @@ -69,6 +69,10 @@ export async function syncWorkspace(reconnectCheck = false) { npsId, startAt, expireAt: new Date(expireAt), + createdBy: { + _id: 'rocket.cat', + username: 'rocket.cat', + }, }); const now = new Date(); diff --git a/apps/meteor/app/cloud/server/functions/unregisterWorkspace.js b/apps/meteor/app/cloud/server/functions/unregisterWorkspace.js deleted file mode 100644 index 37895e53e248..000000000000 --- a/apps/meteor/app/cloud/server/functions/unregisterWorkspace.js +++ /dev/null @@ -1,19 +0,0 @@ -import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; -import { Settings } from '../../../models/server'; - -export function unregisterWorkspace() { - const { workspaceRegistered } = retrieveRegistrationStatus(); - if (!workspaceRegistered) { - return true; - } - - Settings.updateValueById('Cloud_Workspace_Id', null); - Settings.updateValueById('Cloud_Workspace_Name', null); - Settings.updateValueById('Cloud_Workspace_Client_Id', null); - Settings.updateValueById('Cloud_Workspace_Client_Secret', null); - Settings.updateValueById('Cloud_Workspace_Client_Secret_Expires_At', null); - Settings.updateValueById('Cloud_Workspace_PublicKey', null); - Settings.updateValueById('Cloud_Workspace_Registration_Client_Uri', null); - - return true; -} diff --git a/apps/meteor/app/cloud/server/functions/unregisterWorkspace.ts b/apps/meteor/app/cloud/server/functions/unregisterWorkspace.ts new file mode 100644 index 000000000000..e4023b6f66f7 --- /dev/null +++ b/apps/meteor/app/cloud/server/functions/unregisterWorkspace.ts @@ -0,0 +1,22 @@ +import { Settings } from '@rocket.chat/models'; + +import { retrieveRegistrationStatus } from './retrieveRegistrationStatus'; + +export async function unregisterWorkspace() { + const { workspaceRegistered } = retrieveRegistrationStatus(); + if (!workspaceRegistered) { + return true; + } + + await Promise.all([ + Settings.updateValueById('Cloud_Workspace_Id', null), + Settings.updateValueById('Cloud_Workspace_Name', null), + Settings.updateValueById('Cloud_Workspace_Client_Id', null), + Settings.updateValueById('Cloud_Workspace_Client_Secret', null), + Settings.updateValueById('Cloud_Workspace_Client_Secret_Expires_At', null), + Settings.updateValueById('Cloud_Workspace_PublicKey', null), + Settings.updateValueById('Cloud_Workspace_Registration_Client_Uri', null), + ]); + + return true; +} diff --git a/apps/meteor/app/cloud/server/index.js b/apps/meteor/app/cloud/server/index.js index 55ae7bbc04c1..50dc3361a22c 100644 --- a/apps/meteor/app/cloud/server/index.js +++ b/apps/meteor/app/cloud/server/index.js @@ -46,7 +46,7 @@ Meteor.startup(function () { try { SystemLogger.info('REG_TOKEN Provided. Attempting to register'); - if (!connectWorkspace(process.env.REG_TOKEN)) { + if (!Promise.await(connectWorkspace(process.env.REG_TOKEN))) { throw new Error("Couldn't register with token. Please make sure token is valid or hasn't already been used"); } diff --git a/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.js b/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.js index 73b7b475f892..8fd8700b4f43 100644 --- a/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.js +++ b/apps/meteor/app/error-handler/server/lib/RocketChat.ErrorHandler.js @@ -1,7 +1,8 @@ import { Meteor } from 'meteor/meteor'; +import { Settings } from '@rocket.chat/models'; import { settings } from '../../../settings/server'; -import { Users, Rooms, Settings } from '../../../models/server'; +import { Users, Rooms } from '../../../models/server'; import { sendMessage } from '../../../lib'; class ErrorHandler { diff --git a/apps/meteor/app/file-upload/server/lib/FileUpload.js b/apps/meteor/app/file-upload/server/lib/FileUpload.js index 7d7afd4c8dc0..dc99688c5642 100644 --- a/apps/meteor/app/file-upload/server/lib/FileUpload.js +++ b/apps/meteor/app/file-upload/server/lib/FileUpload.js @@ -12,12 +12,11 @@ import { Match } from 'meteor/check'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; import filesize from 'filesize'; import { AppsEngineException } from '@rocket.chat/apps-engine/definition/exceptions'; -import { Avatars, UserDataFiles, Uploads } from '@rocket.chat/models'; +import { Avatars, UserDataFiles, Uploads, Settings } from '@rocket.chat/models'; import { settings } from '../../../settings/server'; import Users from '../../../models/server/models/Users'; import Rooms from '../../../models/server/models/Rooms'; -import Settings from '../../../models/server/models/Settings'; import { mime } from '../../../utils/lib/mimeTypes'; import { hasPermission } from '../../../authorization/server/functions/hasPermission'; import { canAccessRoom } from '../../../authorization/server/functions/canAccessRoom'; @@ -32,11 +31,11 @@ import { roomCoordinator } from '../../../../server/lib/rooms/roomCoordinator'; const cookie = new Cookies(); let maxFileSize = 0; -settings.watch('FileUpload_MaxFileSize', function (value) { +settings.watch('FileUpload_MaxFileSize', async function (value) { try { maxFileSize = parseInt(value); } catch (e) { - maxFileSize = Settings.findOneById('FileUpload_MaxFileSize').packageValue; + maxFileSize = await Settings.findOneById('FileUpload_MaxFileSize').packageValue; } }); diff --git a/apps/meteor/app/importer-csv/server/importer.js b/apps/meteor/app/importer-csv/server/importer.js index 1d2ce78e38dd..61ba6824a8c6 100644 --- a/apps/meteor/app/importer-csv/server/importer.js +++ b/apps/meteor/app/importer-csv/server/importer.js @@ -1,7 +1,8 @@ +import { Settings } from '@rocket.chat/models'; import { Random } from 'meteor/random'; import { Base, ProgressStep, ImporterWebsocket } from '../../importer/server'; -import { Users, Settings as SettingsRaw } from '../../models/server'; +import { Users } from '../../models/server'; export class CsvImporter extends Base { constructor(info, importRecord) { @@ -121,7 +122,7 @@ export class CsvImporter extends Base { }); } - SettingsRaw.incrementValueById('CSV_Importer_Count', usersCount); + Promise.await(Settings.incrementValueById('CSV_Importer_Count', usersCount)); super.updateRecord({ 'count.users': usersCount }); return increaseProgressCount(); } diff --git a/apps/meteor/app/importer-hipchat-enterprise/server/importer.js b/apps/meteor/app/importer-hipchat-enterprise/server/importer.js index 0e1fed9db759..86a59e058a69 100644 --- a/apps/meteor/app/importer-hipchat-enterprise/server/importer.js +++ b/apps/meteor/app/importer-hipchat-enterprise/server/importer.js @@ -3,9 +3,9 @@ import path from 'path'; import fs from 'fs'; import { Meteor } from 'meteor/meteor'; +import { Settings } from '@rocket.chat/models'; import { Base, ProgressStep } from '../../importer/server'; -import { Settings as SettingsRaw } from '../../models/server'; export class HipChatEnterpriseImporter extends Base { constructor(info, importRecord) { @@ -53,7 +53,7 @@ export class HipChatEnterpriseImporter extends Base { this.converter.addUser(newUser); } - SettingsRaw.incrementValueById('Hipchat_Enterprise_Importer_Count', count); + await Settings.incrementValueById('Hipchat_Enterprise_Importer_Count', count); super.updateRecord({ 'count.users': count }); super.addCountToTotal(count); } diff --git a/apps/meteor/app/importer-slack-users/server/importer.js b/apps/meteor/app/importer-slack-users/server/importer.js index da35a8d1bfc4..83e221c836ab 100644 --- a/apps/meteor/app/importer-slack-users/server/importer.js +++ b/apps/meteor/app/importer-slack-users/server/importer.js @@ -1,10 +1,11 @@ import { Meteor } from 'meteor/meteor'; import { Accounts } from 'meteor/accounts-base'; import { Random } from 'meteor/random'; +import { Settings } from '@rocket.chat/models'; import { RawImports, Base, ProgressStep, Selection, SelectionUser } from '../../importer/server'; import { RocketChatFile } from '../../file'; -import { Users, Settings as SettingsRaw } from '../../models/server'; +import { Users } from '../../models/server'; export class SlackUsersImporter extends Base { constructor(info, importRecord) { @@ -166,7 +167,7 @@ export class SlackUsersImporter extends Base { }); } - SettingsRaw.incrementValueById('Slack_Users_Importer_Count', this.users.users.length); + Settings.incrementValueById('Slack_Users_Importer_Count', this.users.users.length); super.updateProgress(ProgressStep.FINISHING); super.updateProgress(ProgressStep.DONE); } catch (e) { diff --git a/apps/meteor/app/importer-slack/server/importer.js b/apps/meteor/app/importer-slack/server/importer.js index 8a9caa1775fe..99661ca06dad 100644 --- a/apps/meteor/app/importer-slack/server/importer.js +++ b/apps/meteor/app/importer-slack/server/importer.js @@ -1,7 +1,8 @@ import _ from 'underscore'; +import { Settings } from '@rocket.chat/models'; import { Base, ProgressStep, ImporterWebsocket } from '../../importer/server'; -import { Messages, ImportData, Settings as SettingsRaw } from '../../models/server'; +import { Messages, ImportData } from '../../models/server'; import { settings } from '../../settings/server'; import { MentionsParser } from '../../mentions/lib/MentionsParser'; import { getUserAvatarURL } from '../../utils/lib/getUserAvatarURL'; @@ -155,7 +156,7 @@ export class SlackImporter extends Base { } this.converter.addUser(newUser); - SettingsRaw.incrementValueById('Slack_Importer_Count'); + Promise.all(Settings.incrementValueById('Slack_Importer_Count')); } } diff --git a/apps/meteor/app/importer/server/classes/ImporterBase.js b/apps/meteor/app/importer/server/classes/ImporterBase.js index 73ebfdd161a6..1190b8b90515 100644 --- a/apps/meteor/app/importer/server/classes/ImporterBase.js +++ b/apps/meteor/app/importer/server/classes/ImporterBase.js @@ -2,6 +2,7 @@ import http from 'http'; import fs from 'fs'; import https from 'https'; +import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import AdmZip from 'adm-zip'; import getFileType from 'file-type'; @@ -11,7 +12,7 @@ import { ImporterWebsocket } from './ImporterWebsocket'; import { ProgressStep } from '../../lib/ImporterProgressStep'; import { ImporterInfo } from '../../lib/ImporterInfo'; import { RawImports } from '../models/RawImports'; -import { Settings, Imports, ImportData } from '../../../models/server'; +import { Imports, ImportData } from '../../../models/server'; import { Logger } from '../../../logger'; import { ImportDataConverter } from './ImportDataConverter'; import { t } from '../../../utils/server'; @@ -250,29 +251,29 @@ export class Base { switch (step) { case ProgressStep.IMPORTING_STARTED: - this.oldSettings.Accounts_AllowedDomainsList = Settings.findOneById('Accounts_AllowedDomainsList').value; - Settings.updateValueById('Accounts_AllowedDomainsList', ''); + this.oldSettings.Accounts_AllowedDomainsList = Promise.await(Settings.findOneById('Accounts_AllowedDomainsList')).value; + Promise.await(Settings.updateValueById('Accounts_AllowedDomainsList', '')); - this.oldSettings.Accounts_AllowUsernameChange = Settings.findOneById('Accounts_AllowUsernameChange').value; - Settings.updateValueById('Accounts_AllowUsernameChange', true); + this.oldSettings.Accounts_AllowUsernameChange = Promise.await(Settings.findOneById('Accounts_AllowUsernameChange')).value; + Promise.await(Settings.updateValueById('Accounts_AllowUsernameChange', true)); - this.oldSettings.FileUpload_MaxFileSize = Settings.findOneById('FileUpload_MaxFileSize').value; - Settings.updateValueById('FileUpload_MaxFileSize', -1); + this.oldSettings.FileUpload_MaxFileSize = Promise.await(Settings.findOneById('FileUpload_MaxFileSize')).value; + Promise.await(Settings.updateValueById('FileUpload_MaxFileSize', -1)); - this.oldSettings.FileUpload_MediaTypeWhiteList = Settings.findOneById('FileUpload_MediaTypeWhiteList').value; - Settings.updateValueById('FileUpload_MediaTypeWhiteList', '*'); + this.oldSettings.FileUpload_MediaTypeWhiteList = Promise.await(Settings.findOneById('FileUpload_MediaTypeWhiteList')).value; + Promise.await(Settings.updateValueById('FileUpload_MediaTypeWhiteList', '*')); - this.oldSettings.FileUpload_MediaTypeBlackList = Settings.findOneById('FileUpload_MediaTypeBlackList').value; - Settings.updateValueById('FileUpload_MediaTypeBlackList', ''); + this.oldSettings.FileUpload_MediaTypeBlackList = Promise.await(Settings.findOneById('FileUpload_MediaTypeBlackList')).value; + Promise.await(Settings.updateValueById('FileUpload_MediaTypeBlackList', '')); break; case ProgressStep.DONE: case ProgressStep.ERROR: case ProgressStep.CANCELLED: - Settings.updateValueById('Accounts_AllowedDomainsList', this.oldSettings.Accounts_AllowedDomainsList); - Settings.updateValueById('Accounts_AllowUsernameChange', this.oldSettings.Accounts_AllowUsernameChange); - Settings.updateValueById('FileUpload_MaxFileSize', this.oldSettings.FileUpload_MaxFileSize); - Settings.updateValueById('FileUpload_MediaTypeWhiteList', this.oldSettings.FileUpload_MediaTypeWhiteList); - Settings.updateValueById('FileUpload_MediaTypeBlackList', this.oldSettings.FileUpload_MediaTypeBlackList); + Promise.await(Settings.updateValueById('Accounts_AllowedDomainsList', this.oldSettings.Accounts_AllowedDomainsList)); + Promise.await(Settings.updateValueById('Accounts_AllowUsernameChange', this.oldSettings.Accounts_AllowUsernameChange)); + Promise.await(Settings.updateValueById('FileUpload_MaxFileSize', this.oldSettings.FileUpload_MaxFileSize)); + Promise.await(Settings.updateValueById('FileUpload_MediaTypeWhiteList', this.oldSettings.FileUpload_MediaTypeWhiteList)); + Promise.await(Settings.updateValueById('FileUpload_MediaTypeBlackList', this.oldSettings.FileUpload_MediaTypeBlackList)); break; } diff --git a/apps/meteor/app/irc/server/irc-bridge/index.js b/apps/meteor/app/irc/server/irc-bridge/index.js index 04054f027bc8..855f68acdded 100644 --- a/apps/meteor/app/irc/server/irc-bridge/index.js +++ b/apps/meteor/app/irc/server/irc-bridge/index.js @@ -2,12 +2,12 @@ import { Meteor } from 'meteor/meteor'; import Queue from 'queue-fifo'; import moment from 'moment'; import _ from 'underscore'; +import { Settings } from '@rocket.chat/models'; import * as peerCommandHandlers from './peerHandlers'; import * as localCommandHandlers from './localHandlers'; import { callbacks } from '../../../../lib/callbacks'; import * as servers from '../servers'; -import { Settings } from '../../../models/server'; import { Logger } from '../../../logger/server'; const logger = new Logger('IRC Bridge'); @@ -57,7 +57,7 @@ class Bridge { removed = false; this.loggedInUsers = []; - const lastPing = Settings.findOneById('IRC_Bridge_Last_Ping'); + const lastPing = Promise.await(Settings.findOneById('IRC_Bridge_Last_Ping')); if (lastPing) { if (Math.abs(moment(lastPing.value).diff()) < 1000 * 30) { this.log('Not trying to connect.'); diff --git a/apps/meteor/app/irc/server/methods/resetIrcConnection.js b/apps/meteor/app/irc/server/methods/resetIrcConnection.ts similarity index 75% rename from apps/meteor/app/irc/server/methods/resetIrcConnection.js rename to apps/meteor/app/irc/server/methods/resetIrcConnection.ts index 9c033c27017f..c72f894b64b0 100644 --- a/apps/meteor/app/irc/server/methods/resetIrcConnection.js +++ b/apps/meteor/app/irc/server/methods/resetIrcConnection.ts @@ -1,27 +1,33 @@ +import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { Settings } from '../../../models/server'; import { settings } from '../../../settings/server'; import Bridge from '../irc-bridge'; Meteor.methods({ resetIrcConnection() { const ircEnabled = Boolean(settings.get('IRC_Enabled')); - Settings.upsert( + Settings.updateOne( { _id: 'IRC_Bridge_Last_Ping' }, { $set: { value: new Date(0), }, }, + { + upsert: true, + }, ); - Settings.upsert( + Settings.updateOne( { _id: 'IRC_Bridge_Reset_Time' }, { $set: { value: new Date(), }, }, + { + upsert: true, + }, ); if (!ircEnabled) { @@ -47,7 +53,7 @@ Meteor.methods({ peer: settings.get('IRC_Peer_Password'), }, }; - + // TODO: is this the best way to do this? is this really necessary? Meteor.ircBridge = new Bridge(config); Meteor.ircBridge.init(); }), @@ -60,3 +66,10 @@ Meteor.methods({ }; }, }); + +declare module 'meteor/meteor' { + // eslint-disable-next-line @typescript-eslint/no-namespace + export namespace Meteor { + export let ircBridge: Bridge; + } +} diff --git a/apps/meteor/app/lib/server/methods/sendInvitationEmail.js b/apps/meteor/app/lib/server/methods/sendInvitationEmail.ts similarity index 71% rename from apps/meteor/app/lib/server/methods/sendInvitationEmail.js rename to apps/meteor/app/lib/server/methods/sendInvitationEmail.ts index fe33f73fe323..23f575a96c02 100644 --- a/apps/meteor/app/lib/server/methods/sendInvitationEmail.js +++ b/apps/meteor/app/lib/server/methods/sendInvitationEmail.ts @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; import { check } from 'meteor/check'; +import { Settings } from '@rocket.chat/models'; import * as Mailer from '../../../mailer'; -import { hasPermission } from '../../../authorization'; +import { hasPermission } from '../../../authorization/server'; import { settings } from '../../../settings/server'; -import { Settings as SettingsRaw } from '../../../models/server'; let html = ''; Meteor.startup(() => { @@ -14,14 +14,15 @@ Meteor.startup(() => { }); Meteor.methods({ - sendInvitationEmail(emails) { + async sendInvitationEmail(emails) { check(emails, [String]); - if (!Meteor.userId()) { + const uid = Meteor.userId(); + if (!uid) { throw new Meteor.Error('error-invalid-user', 'Invalid user', { method: 'sendInvitationEmail', }); } - if (!hasPermission(Meteor.userId(), 'bulk-register-user')) { + if (!hasPermission(uid, 'bulk-register-user')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'sendInvitationEmail', }); @@ -34,7 +35,13 @@ Meteor.methods({ }); } - const subject = settings.get('Invitation_Subject'); + const subject = settings.get('Invitation_Subject'); + + if (!subject) { + throw new Meteor.Error('error-email-send-failed', 'No subject', { + method: 'sendInvitationEmail', + }); + } return validEmails.filter((email) => { try { @@ -48,7 +55,7 @@ Meteor.methods({ }, }); - SettingsRaw.incrementValueById('Invitation_Email_Count'); + Settings.incrementValueById('Invitation_Email_Count'); return mailerResult; } catch ({ message }) { throw new Meteor.Error('error-email-send-failed', `Error trying to send email: ${message}`, { diff --git a/apps/meteor/app/livechat/imports/server/rest/upload.js b/apps/meteor/app/livechat/imports/server/rest/upload.js index 5057c5e7e006..079daec7be22 100644 --- a/apps/meteor/app/livechat/imports/server/rest/upload.js +++ b/apps/meteor/app/livechat/imports/server/rest/upload.js @@ -1,21 +1,21 @@ import { Meteor } from 'meteor/meteor'; import filesize from 'filesize'; -import { LivechatVisitors } from '@rocket.chat/models'; +import { LivechatVisitors, Settings } from '@rocket.chat/models'; import { settings } from '../../../../settings/server'; -import { Settings, LivechatRooms } from '../../../../models/server'; +import { LivechatRooms } from '../../../../models/server'; import { fileUploadIsValidContentType } from '../../../../utils/server'; -import { FileUpload } from '../../../../file-upload'; +import { FileUpload } from '../../../../file-upload/server'; import { API } from '../../../../api/server'; import { getUploadFormData } from '../../../../api/server/lib/getUploadFormData'; let maxFileSize; -settings.watch('FileUpload_MaxFileSize', function (value) { +settings.watch('FileUpload_MaxFileSize', async function (value) { try { maxFileSize = parseInt(value); } catch (e) { - maxFileSize = Settings.findOneById('FileUpload_MaxFileSize').packageValue; + maxFileSize = await Settings.findOneById('FileUpload_MaxFileSize').packageValue; } }); diff --git a/apps/meteor/app/livechat/server/api/v1/videoCall.js b/apps/meteor/app/livechat/server/api/v1/videoCall.js index ca13d3278bcd..5365da0aab53 100644 --- a/apps/meteor/app/livechat/server/api/v1/videoCall.js +++ b/apps/meteor/app/livechat/server/api/v1/videoCall.js @@ -1,8 +1,9 @@ import { Meteor } from 'meteor/meteor'; import { Match, check } from 'meteor/check'; import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import { Settings } from '@rocket.chat/models'; -import { Messages, Rooms, Settings } from '../../../../models'; +import { Messages, Rooms } from '../../../../models'; import { settings as rcSettings } from '../../../../settings/server'; import { API } from '../../../../api/server'; import { settings } from '../lib/livechat'; @@ -16,7 +17,7 @@ API.v1.addRoute( 'livechat/webrtc.call', { authRequired: true }, { - get() { + async get() { try { check(this.queryParams, { rid: Match.Maybe(String), @@ -40,7 +41,7 @@ API.v1.addRoute( throw new Meteor.Error('webRTC calling not enabled'); } - const config = Promise.await(settings()); + const config = await settings(); if (!config.theme || !config.theme.actionLinks || !config.theme.actionLinks.webrtc) { throw new Meteor.Error('invalid-livechat-config'); } @@ -48,19 +49,17 @@ API.v1.addRoute( let { callStatus } = room; if (!callStatus || callStatus === 'ended' || callStatus === 'declined') { - Settings.incrementValueById('WebRTC_Calls_Count'); + await Settings.incrementValueById('WebRTC_Calls_Count'); callStatus = 'ringing'; - Promise.await(Rooms.setCallStatusAndCallStartTime(room._id, callStatus)); - Promise.await( - Messages.createWithTypeRoomIdMessageAndUser( - 'livechat_webrtc_video_call', - room._id, - TAPi18n.__('Join_my_room_to_start_the_video_call'), - this.user, - { - actionLinks: config.theme.actionLinks.webrtc, - }, - ), + await Rooms.setCallStatusAndCallStartTime(room._id, callStatus); + await Messages.createWithTypeRoomIdMessageAndUser( + 'livechat_webrtc_video_call', + room._id, + TAPi18n.__('Join_my_room_to_start_the_video_call'), + this.user, + { + actionLinks: config.theme.actionLinks.webrtc, + }, ); } const videoCall = { diff --git a/apps/meteor/app/livechat/server/lib/Livechat.js b/apps/meteor/app/livechat/server/lib/Livechat.js index 79c0e5c86bbd..b237c5ea746e 100644 --- a/apps/meteor/app/livechat/server/lib/Livechat.js +++ b/apps/meteor/app/livechat/server/lib/Livechat.js @@ -9,7 +9,7 @@ import _ from 'underscore'; import s from 'underscore.string'; import moment from 'moment-timezone'; import UAParser from 'ua-parser-js'; -import { Users as UsersRaw, LivechatVisitors } from '@rocket.chat/models'; +import { Users as UsersRaw, LivechatVisitors, Settings } from '@rocket.chat/models'; import { QueueManager } from './QueueManager'; import { RoutingManager } from './RoutingManager'; @@ -22,7 +22,6 @@ import { LivechatRooms, Messages, Subscriptions, - Settings, Rooms, LivechatDepartmentAgents, LivechatDepartment, @@ -538,39 +537,41 @@ export const Livechat = { getInitSettings() { const rcSettings = {}; - Settings.findNotHiddenPublic([ - 'Livechat_title', - 'Livechat_title_color', - 'Livechat_enable_message_character_limit', - 'Livechat_message_character_limit', - 'Message_MaxAllowedSize', - 'Livechat_enabled', - 'Livechat_registration_form', - 'Livechat_allow_switching_departments', - 'Livechat_offline_title', - 'Livechat_offline_title_color', - 'Livechat_offline_message', - 'Livechat_offline_success_message', - 'Livechat_offline_form_unavailable', - 'Livechat_display_offline_form', - 'Omnichannel_call_provider', - 'Language', - 'Livechat_enable_transcript', - 'Livechat_transcript_message', - 'Livechat_fileupload_enabled', - 'FileUpload_Enabled', - 'Livechat_conversation_finished_message', - 'Livechat_conversation_finished_text', - 'Livechat_name_field_registration_form', - 'Livechat_email_field_registration_form', - 'Livechat_registration_form_message', - 'Livechat_force_accept_data_processing_consent', - 'Livechat_data_processing_consent_text', - 'Livechat_show_agent_info', - 'Livechat_clear_local_storage_when_chat_ended', - ]).forEach((setting) => { - rcSettings[setting._id] = setting.value; - }); + Promise.await( + Settings.findNotHiddenPublic([ + 'Livechat_title', + 'Livechat_title_color', + 'Livechat_enable_message_character_limit', + 'Livechat_message_character_limit', + 'Message_MaxAllowedSize', + 'Livechat_enabled', + 'Livechat_registration_form', + 'Livechat_allow_switching_departments', + 'Livechat_offline_title', + 'Livechat_offline_title_color', + 'Livechat_offline_message', + 'Livechat_offline_success_message', + 'Livechat_offline_form_unavailable', + 'Livechat_display_offline_form', + 'Omnichannel_call_provider', + 'Language', + 'Livechat_enable_transcript', + 'Livechat_transcript_message', + 'Livechat_fileupload_enabled', + 'FileUpload_Enabled', + 'Livechat_conversation_finished_message', + 'Livechat_conversation_finished_text', + 'Livechat_name_field_registration_form', + 'Livechat_email_field_registration_form', + 'Livechat_registration_form_message', + 'Livechat_force_accept_data_processing_consent', + 'Livechat_data_processing_consent_text', + 'Livechat_show_agent_info', + 'Livechat_clear_local_storage_when_chat_ended', + ]).forEach((setting) => { + rcSettings[setting._id] = setting.value; + }), + ); rcSettings.Livechat_history_monitor_type = settings.get('Livechat_history_monitor_type'); diff --git a/apps/meteor/app/livechat/server/methods/facebook.js b/apps/meteor/app/livechat/server/methods/facebook.js index 86cd7132cc1a..40c7f24e52f6 100644 --- a/apps/meteor/app/livechat/server/methods/facebook.js +++ b/apps/meteor/app/livechat/server/methods/facebook.js @@ -1,10 +1,10 @@ import { Meteor } from 'meteor/meteor'; +import { Settings } from '@rocket.chat/models'; import { hasPermission } from '../../../authorization'; import { SystemLogger } from '../../../../server/lib/logger/system'; import { settings } from '../../../settings/server'; import OmniChannel from '../lib/OmniChannel'; -import { Settings } from '../../../models/server'; Meteor.methods({ 'livechat:facebook'(options) { diff --git a/apps/meteor/app/livechat/server/methods/saveAppearance.js b/apps/meteor/app/livechat/server/methods/saveAppearance.ts similarity index 72% rename from apps/meteor/app/livechat/server/methods/saveAppearance.js rename to apps/meteor/app/livechat/server/methods/saveAppearance.ts index 1189a06dc6aa..e1ee2b991374 100644 --- a/apps/meteor/app/livechat/server/methods/saveAppearance.js +++ b/apps/meteor/app/livechat/server/methods/saveAppearance.ts @@ -1,11 +1,12 @@ import { Meteor } from 'meteor/meteor'; +import { Settings } from '@rocket.chat/models'; -import { hasPermission } from '../../../authorization'; -import { Settings } from '../../../models/server'; +import { hasPermission } from '../../../authorization/server'; Meteor.methods({ - 'livechat:saveAppearance'(settings) { - if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) { + async 'livechat:saveAppearance'(settings: { _id: string; value: any }[]) { + const uid = Meteor.userId(); + if (!uid || !hasPermission(uid, 'view-livechat-manager')) { throw new Meteor.Error('error-not-allowed', 'Not allowed', { method: 'livechat:saveAppearance', }); @@ -39,8 +40,10 @@ Meteor.methods({ throw new Meteor.Error('invalid-setting'); } - settings.forEach((setting) => { - Settings.updateValueById(setting._id, setting.value); - }); + await Promise.all( + settings.map((setting) => { + return Settings.updateValueById(setting._id, setting.value); + }), + ); }, }); diff --git a/apps/meteor/app/livechat/server/methods/saveIntegration.js b/apps/meteor/app/livechat/server/methods/saveIntegration.js deleted file mode 100644 index 539ff204094a..000000000000 --- a/apps/meteor/app/livechat/server/methods/saveIntegration.js +++ /dev/null @@ -1,55 +0,0 @@ -import { Meteor } from 'meteor/meteor'; -import s from 'underscore.string'; - -import { hasPermission } from '../../../authorization'; -import { Settings } from '../../../models/server'; - -Meteor.methods({ - 'livechat:saveIntegration'(values) { - if (!Meteor.userId() || !hasPermission(Meteor.userId(), 'view-livechat-manager')) { - throw new Meteor.Error('error-not-allowed', 'Not allowed', { - method: 'livechat:saveIntegration', - }); - } - - if (typeof values.Livechat_webhookUrl !== 'undefined') { - Settings.updateValueById('Livechat_webhookUrl', s.trim(values.Livechat_webhookUrl)); - } - - if (typeof values.Livechat_secret_token !== 'undefined') { - Settings.updateValueById('Livechat_secret_token', s.trim(values.Livechat_secret_token)); - } - - if (typeof values.Livechat_webhook_on_start !== 'undefined') { - Settings.updateValueById('Livechat_webhook_on_start', !!values.Livechat_webhook_on_start); - } - - if (typeof values.Livechat_webhook_on_close !== 'undefined') { - Settings.updateValueById('Livechat_webhook_on_close', !!values.Livechat_webhook_on_close); - } - - if (typeof values.Livechat_webhook_on_chat_taken !== 'undefined') { - Settings.updateValueById('Livechat_webhook_on_chat_taken', !!values.Livechat_webhook_on_chat_taken); - } - - if (typeof values.Livechat_webhook_on_chat_queued !== 'undefined') { - Settings.updateValueById('Livechat_webhook_on_chat_queued', !!values.Livechat_webhook_on_chat_queued); - } - - if (typeof values.Livechat_webhook_on_forward !== 'undefined') { - Settings.updateValueById('Livechat_webhook_on_forward', !!values.Livechat_webhook_on_forward); - } - - if (typeof values.Livechat_webhook_on_offline_msg !== 'undefined') { - Settings.updateValueById('Livechat_webhook_on_offline_msg', !!values.Livechat_webhook_on_offline_msg); - } - - if (typeof values.Livechat_webhook_on_visitor_message !== 'undefined') { - Settings.updateValueById('Livechat_webhook_on_visitor_message', !!values.Livechat_webhook_on_visitor_message); - } - - if (typeof values.Livechat_webhook_on_agent_message !== 'undefined') { - Settings.updateValueById('Livechat_webhook_on_agent_message', !!values.Livechat_webhook_on_agent_message); - } - }, -}); diff --git a/apps/meteor/app/livechat/server/methods/saveIntegration.ts b/apps/meteor/app/livechat/server/methods/saveIntegration.ts new file mode 100644 index 000000000000..7422ba362526 --- /dev/null +++ b/apps/meteor/app/livechat/server/methods/saveIntegration.ts @@ -0,0 +1,56 @@ +import { Settings } from '@rocket.chat/models'; +import { Meteor } from 'meteor/meteor'; +import s from 'underscore.string'; + +import { hasPermission } from '../../../authorization/server'; + +Meteor.methods({ + async 'livechat:saveIntegration'(values) { + const uid = Meteor.userId(); + if (!uid || !hasPermission(uid, 'view-livechat-manager')) { + throw new Meteor.Error('error-not-allowed', 'Not allowed', { + method: 'livechat:saveIntegration', + }); + } + + if (typeof values.Livechat_webhookUrl !== 'undefined') { + await Settings.updateValueById('Livechat_webhookUrl', s.trim(values.Livechat_webhookUrl)); + } + + if (typeof values.Livechat_secret_token !== 'undefined') { + await Settings.updateValueById('Livechat_secret_token', s.trim(values.Livechat_secret_token)); + } + + if (typeof values.Livechat_webhook_on_start !== 'undefined') { + await Settings.updateValueById('Livechat_webhook_on_start', !!values.Livechat_webhook_on_start); + } + + if (typeof values.Livechat_webhook_on_close !== 'undefined') { + await Settings.updateValueById('Livechat_webhook_on_close', !!values.Livechat_webhook_on_close); + } + + if (typeof values.Livechat_webhook_on_chat_taken !== 'undefined') { + await Settings.updateValueById('Livechat_webhook_on_chat_taken', !!values.Livechat_webhook_on_chat_taken); + } + + if (typeof values.Livechat_webhook_on_chat_queued !== 'undefined') { + await Settings.updateValueById('Livechat_webhook_on_chat_queued', !!values.Livechat_webhook_on_chat_queued); + } + + if (typeof values.Livechat_webhook_on_forward !== 'undefined') { + await Settings.updateValueById('Livechat_webhook_on_forward', !!values.Livechat_webhook_on_forward); + } + + if (typeof values.Livechat_webhook_on_offline_msg !== 'undefined') { + await Settings.updateValueById('Livechat_webhook_on_offline_msg', !!values.Livechat_webhook_on_offline_msg); + } + + if (typeof values.Livechat_webhook_on_visitor_message !== 'undefined') { + await Settings.updateValueById('Livechat_webhook_on_visitor_message', !!values.Livechat_webhook_on_visitor_message); + } + + if (typeof values.Livechat_webhook_on_agent_message !== 'undefined') { + await Settings.updateValueById('Livechat_webhook_on_agent_message', !!values.Livechat_webhook_on_agent_message); + } + }, +}); diff --git a/apps/meteor/app/mailer/server/api.ts b/apps/meteor/app/mailer/server/api.ts index 75b5f7e11770..6f9bb6e7d667 100644 --- a/apps/meteor/app/mailer/server/api.ts +++ b/apps/meteor/app/mailer/server/api.ts @@ -7,9 +7,9 @@ import juice from 'juice'; import stripHtml from 'string-strip-html'; import { escapeHTML } from '@rocket.chat/string-helpers'; import type { ISetting } from '@rocket.chat/core-typings'; +import { Settings } from '@rocket.chat/models'; import { settings } from '../../settings/server'; -import { Settings as SettingsRaw } from '../../models/server'; import { replaceVariables } from './replaceVariables'; import { Apps } from '../../apps/server'; import { validateEmail } from '../../../lib/emailValidator'; @@ -165,7 +165,7 @@ export const sendNoWrap = ({ html = undefined; } - SettingsRaw.incrementValueById('Triggered_Emails_Count'); + Settings.incrementValueById('Triggered_Emails_Count'); const email = { to, from, replyTo, subject, html, text, headers }; diff --git a/apps/meteor/app/models/server/index.ts b/apps/meteor/app/models/server/index.ts index c378f5ad6714..4620f9d4caab 100644 --- a/apps/meteor/app/models/server/index.ts +++ b/apps/meteor/app/models/server/index.ts @@ -2,7 +2,6 @@ import { Base } from './models/_Base'; import { BaseDb } from './models/_BaseDb'; import Messages from './models/Messages'; import Rooms from './models/Rooms'; -import Settings from './models/Settings'; import Subscriptions from './models/Subscriptions'; import Users from './models/Users'; import Imports from './models/Imports'; @@ -25,7 +24,6 @@ export { BaseDb, Messages, Rooms, - Settings, Subscriptions, Users, Imports, diff --git a/apps/meteor/app/models/server/models/Settings.js b/apps/meteor/app/models/server/models/Settings.js deleted file mode 100644 index 490b4b2822eb..000000000000 --- a/apps/meteor/app/models/server/models/Settings.js +++ /dev/null @@ -1,280 +0,0 @@ -import { Base } from './_Base'; - -export class Settings extends Base { - constructor(...args) { - super(...args); - - this.tryEnsureIndex({ blocked: 1 }, { sparse: 1 }); - this.tryEnsureIndex({ hidden: 1 }, { sparse: 1 }); - } - - // FIND - findById(_id) { - const query = { _id }; - - return this.find(query); - } - - findOneNotHiddenById(_id) { - const query = { - _id, - hidden: { $ne: true }, - }; - - return this.findOne(query); - } - - findByIds(_id = []) { - _id = [].concat(_id); - - const query = { - _id: { - $in: _id, - }, - }; - - return this.find(query); - } - - findPublic(options) { - const query = { public: true }; - - return this.find(query, options); - } - - findNotHiddenPublic(ids = []) { - const filter = { - 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, - }, - }); - } - - findNotHiddenPublicUpdatedAfter(updatedAt) { - const filter = { - hidden: { $ne: true }, - public: true, - _updatedAt: { - $gt: updatedAt, - }, - }; - - return this.find(filter, { - fields: { - _id: 1, - value: 1, - editor: 1, - enterprise: 1, - invalidValue: 1, - modules: 1, - requiredOnWizard: 1, - }, - }); - } - - findNotHiddenPrivate() { - return this.find({ - hidden: { $ne: true }, - public: { $ne: true }, - }); - } - - findNotHidden({ updatedAfter, ...options } = {}) { - const query = { - hidden: { $ne: true }, - }; - - if (updatedAfter) { - query._updatedAt = { $gt: updatedAfter }; - } - - return this.find(query, options); - } - - findNotHiddenUpdatedAfter(updatedAt) { - return this.find({ - hidden: { $ne: true }, - _updatedAt: { - $gt: updatedAt, - }, - }); - } - - findSetupWizardSettings() { - return this.find({ wizard: { $exists: true, $ne: null } }); - } - - findEnterpriseSettings() { - return this.find({ enterprise: true }); - } - - // UPDATE - updateValueById(_id, value) { - const query = { - blocked: { $ne: true }, - value: { $ne: value }, - _id, - }; - - const update = { - $set: { - value, - }, - }; - - return this.update(query, update); - } - - incrementValueById(_id, value = 1) { - const query = { - blocked: { $ne: true }, - _id, - }; - - const update = { - $inc: { - value, - }, - }; - - return this.update(query, update); - } - - updateValueAndEditorById(_id, value, editor) { - const query = { - blocked: { $ne: true }, - value: { $ne: value }, - _id, - }; - - const update = { - $set: { - value, - editor, - }, - }; - - return this.update(query, update); - } - - updateValueNotHiddenById(_id, value) { - const query = { - _id, - hidden: { $ne: true }, - blocked: { $ne: true }, - }; - - const update = { - $set: { - value, - }, - }; - - return this.update(query, update); - } - - updateOptionsById(_id, options) { - const query = { - blocked: { $ne: true }, - _id, - }; - - const update = { $set: options }; - - return this.update(query, update); - } - - addOptionValueById(_id, option = {}) { - const query = { - blocked: { $ne: true }, - _id, - }; - - const { key, i18nLabel } = option; - const update = { - $addToSet: { - values: { - key, - i18nLabel, - }, - }, - }; - - return this.update(query, update); - } - - removeOptionValueByIdAndKey(_id, key) { - const query = { - blocked: { $ne: true }, - _id, - }; - - const update = { - $pull: { - values: { key }, - }, - }; - - return this.update(query, update); - } - - // INSERT - createWithIdAndValue(_id, value) { - const record = { - _id, - value, - _createdAt: new Date(), - }; - - return this.insert(record); - } - - // REMOVE - removeById(_id) { - const query = { - blocked: { $ne: true }, - _id, - }; - - return this.remove(query); - } - - // RENAME SETTING - renameSetting(oldId, newId) { - const oldSetting = this.findById(oldId).fetch()[0]; - if (oldSetting) { - this.removeById(oldSetting._id); - // there has been some problem with upsert() when changing the complete doc, so decide explicitly for insert or update - let newSetting = this.findById(newId).fetch()[0]; - if (newSetting) { - this.updateValueById(newId, oldSetting.value); - } else { - newSetting = oldSetting; - newSetting._id = newId; - delete newSetting.$loki; - this.insert(newSetting); - } - } - } - - insert(record, ...args) { - return super.insert({ createdAt: new Date(), ...record }, ...args); - } -} - -export default new Settings('settings', true); diff --git a/apps/meteor/app/settings/server/CachedSettings.ts b/apps/meteor/app/settings/server/CachedSettings.ts index c86f207f4b09..d9657378151a 100644 --- a/apps/meteor/app/settings/server/CachedSettings.ts +++ b/apps/meteor/app/settings/server/CachedSettings.ts @@ -16,151 +16,52 @@ export interface ICachedSettings { /* * @description: The settings object as ready */ - initilized(): void; + initialized(): void; - /* - * returns if the setting is defined - * @param _id - The setting id - * @returns {boolean} - */ has(_id: ISetting['_id']): boolean; - /* - * Gets the current Object of the setting - * @param _id - The setting id - * @returns {ISetting} - The current Object of the setting - */ getSetting(_id: ISetting['_id']): ISetting | undefined; - /* - * Gets the current value of the setting - * @remarks - * - In development mode if you are trying to get the value of a setting that is not defined, it will give an warning, in theory it makes sense, there no reason to do that - * @param _id - The setting id - * @returns {SettingValue} - The current value of the setting - */ get(_id: ISetting['_id']): T; - /* - * Gets the current value of the setting - * @remarks - * - In development mode if you are trying to get the value of a setting that is not defined, it will give an warning, in theory it makes sense, there no reason to do that - * @param _id - The setting id - * @returns {SettingValue} - The current value of the setting - * - */ - /* @deprecated */ getByRegexp(_id: RegExp): [string, T][]; - /* - * Get the current value of the settings, and keep track of changes - * @remarks - * - This callback is debounced - * - The callback is not fire until the settings got initialized - * @param _ids - Array of setting id - * @param callback - The callback to run - * @returns {() => void} - A function that can be used to cancel the observe - */ watchMultiple(_id: ISetting['_id'][], callback: (settings: T[]) => void): () => void; - /* - * Get the current value of the setting, and keep track of changes - * @remarks - * - This callback is debounced - * - The callback is not fire until the settings got initialized - * @param _id - The setting id - * @param callback - The callback to run - * @returns {() => void} - A function that can be used to cancel the observe - */ watch(_id: ISetting['_id'], cb: (args: T) => void, config?: OverCustomSettingsConfig): () => void; - /* - * Get the current value of the setting, or wait until the initialized - * @remarks - * - This is a one time run - * - This callback is debounced - * - The callback is not fire until the settings got initialized - * @param _id - The setting id - * @param callback - The callback to run - * @returns {() => void} - A function that can be used to cancel the observe - */ watchOnce( _id: ISetting['_id'], cb: (args: T) => void, config?: OverCustomSettingsConfig, ): () => void; - /* - * Observes the given setting by id and keep track of changes - * @remarks - * - This callback is debounced - * - The callback is not fire until the setting is changed - * - The callback is not fire until all the settings get initialized - * @param _id - The setting id - * @param callback - The callback to run - * @returns {() => void} - A function that can be used to cancel the observe - */ change( _id: ISetting['_id'], callback: (args: T) => void, config?: OverCustomSettingsConfig, ): () => void; - /* - * Observes multiple settings and keep track of changes - * @remarks - * - This callback is debounced - * - The callback is not fire until the setting is changed - * - The callback is not fire until all the settings get initialized - * @param _ids - Array of setting id - * @param callback - The callback to run - * @returns {() => void} - A function that can be used to cancel the observe - */ changeMultiple( _ids: ISetting['_id'][], callback: (settings: T[]) => void, config?: OverCustomSettingsConfig, ): () => void; - /* - * Observes the setting and fires only if there is a change. Runs only once - * @remarks - * - This is a one time run - * - This callback is debounced - * - The callback is not fire until the setting is changed - * - The callback is not fire until all the settings get initialized - * @param _id - The setting id - * @param callback - The callback to run - * @returns {() => void} - A function that can be used to cancel the observe - */ changeOnce( _id: ISetting['_id'], callback: (args: T) => void, config?: OverCustomSettingsConfig, ): () => void; - /* - * Sets the value of the setting - * @remarks - * - if the value set is the same as the current value, the change will not be fired - * - if the value is set before the initialization, the emit will be queued and will be fired after initialization - * @param _id - The setting id - * @param value - The value to set - * @returns {void} - */ set(record: ISetting): void; getConfig(config?: OverCustomSettingsConfig): SettingsConfig; - /* @deprecated */ watchByRegex(regex: RegExp, cb: (...args: [string, SettingValue]) => void, config?: OverCustomSettingsConfig): () => void; - /* @deprecated */ changeByRegex(regex: RegExp, callback: (...args: [string, SettingValue]) => void, config?: OverCustomSettingsConfig): () => void; - /* - * @description: Wait until the settings get ready then run the callback - */ onReady(cb: () => void): void; } @@ -188,16 +89,19 @@ export class CachedSettings store = new Map(); - initilized(): void { + /** + * The settings object as ready + */ + initialized(): void { if (this.ready) { return; } this.ready = true; this.emit('ready'); - SystemLogger.debug('Settings initalized'); + SystemLogger.debug('Settings initialized'); } - /* + /** * returns if the setting is defined * @param _id - The setting id * @returns {boolean} @@ -209,6 +113,11 @@ export class CachedSettings return this.store.has(_id); } + /** + * Gets the current Object of the setting + * @param _id - The setting id + * @returns {ISetting} - The current Object of the setting + */ public getSetting(_id: ISetting['_id']): ISetting | undefined { if (!this.ready && warn) { SystemLogger.warn(`Settings not initialized yet. getting: ${_id}`); @@ -216,11 +125,10 @@ export class CachedSettings return this.store.get(_id); } - /* + /** * Gets the current value of the setting - * @remarks - * - In development mode if you are trying to get the value of a setting that is not defined, it will give an warning, in theory it makes sense, there no reason to do that - * - The setting's value will be cached in memory so it won't call the DB every time you fetch a particular setting + * - In development mode if you are trying to get the value of a setting that is not defined, it will give an warning, in theory it makes sense, there no reason to do that + * - The setting's value will be cached in memory so it won't call the DB every time you fetch a particular setting * @param _id - The setting id * @returns {SettingValue} - The current value of the setting */ @@ -231,15 +139,13 @@ export class CachedSettings return this.store.get(_id)?.value as T; } - /* + /** * Gets the current value of the setting - * @remarks - * - In development mode if you are trying to get the value of a setting that is not defined, it will give an warning, in theory it makes sense, there no reason to do that + * - In development mode if you are trying to get the value of a setting that is not defined, it will give an warning, in theory it makes sense, there no reason to do that + * @deprecated * @param _id - The setting id * @returns {SettingValue} - The current value of the setting - * */ - /* @deprecated */ public getByRegexp(_id: RegExp): [string, T][] { if (!this.ready && warn) { SystemLogger.warn(`Settings not initialized yet. getting: ${_id}`); @@ -248,11 +154,10 @@ export class CachedSettings return [...this.store.entries()].filter(([key]) => _id.test(key)).map(([key, setting]) => [key, setting.value]) as [string, T][]; } - /* + /** * Get the current value of the settings, and keep track of changes - * @remarks - * - This callback is debounced - * - The callback is not fire until the settings got initialized + * - This callback is debounced + * - The callback is not fire until the settings got initialized * @param _ids - Array of setting id * @param callback - The callback to run * @returns {() => void} - A function that can be used to cancel the observe @@ -286,11 +191,10 @@ export class CachedSettings }; } - /* + /** * Get the current value of the setting, and keep track of changes - * @remarks - * - This callback is debounced - * - The callback is not fire until the settings got initialized + * - This callback is debounced + * - The callback is not fire until the settings got initialized * @param _id - The setting id * @param callback - The callback to run * @returns {() => void} - A function that can be used to cancel the observe @@ -317,12 +221,11 @@ export class CachedSettings return this.change(_id, cb, config); } - /* + /** * Get the current value of the setting, or wait until the initialized - * @remarks - * - This is a one time run - * - This callback is debounced - * - The callback is not fire until the settings got initialized + * - This is a one time run + * - This callback is debounced + * - The callback is not fire until the settings got initialized * @param _id - The setting id * @param callback - The callback to run * @returns {() => void} - A function that can be used to cancel the observe @@ -339,12 +242,11 @@ export class CachedSettings return this.changeOnce(_id, cb, config); } - /* + /** * Observes the given setting by id and keep track of changes - * @remarks - * - This callback is debounced - * - The callback is not fire until the setting is changed - * - The callback is not fire until all the settings get initialized + * - This callback is debounced + * - The callback is not fire until the setting is changed + * - The callback is not fire until all the settings get initialized * @param _id - The setting id * @param callback - The callback to run * @returns {() => void} - A function that can be used to cancel the observe @@ -358,12 +260,11 @@ export class CachedSettings return this.on(_id, _.debounce(callback, debounce) as any); } - /* + /** * Observes multiple settings and keep track of changes - * @remarks - * - This callback is debounced - * - The callback is not fire until the setting is changed - * - The callback is not fire until all the settings get initialized + * - This callback is debounced + * - The callback is not fire until the setting is changed + * - The callback is not fire until all the settings get initialized * @param _ids - Array of setting id * @param callback - The callback to run * @returns {() => void} - A function that can be used to cancel the observe @@ -387,13 +288,12 @@ export class CachedSettings }; } - /* + /** * Observes the setting and fires only if there is a change. Runs only once - * @remarks - * - This is a one time run - * - This callback is debounced - * - The callback is not fire until the setting is changed - * - The callback is not fire until all the settings get initialized + * - This is a one time run + * - This callback is debounced + * - The callback is not fire until the setting is changed + * - The callback is not fire until all the settings get initialized * @param _id - The setting id * @param callback - The callback to run * @returns {() => void} - A function that can be used to cancel the observe @@ -407,11 +307,10 @@ export class CachedSettings return this.once(_id, _.debounce(callback, debounce) as any); } - /* + /** * Sets the value of the setting - * @remarks - * - if the value set is the same as the current value, the change will not be fired - * - if the value is set before the initialization, the emit will be queued and will be fired after initialization + * - if the value set is the same as the current value, the change will not be fired + * - if the value is set before the initialization, the emit will be queued and will be fired after initialization * @param _id - The setting id * @param value - The value to set * @returns {void} @@ -438,7 +337,7 @@ export class CachedSettings ...config, }); - /* @deprecated */ + /** @deprecated */ public watchByRegex(regex: RegExp, cb: (...args: [string, SettingValue]) => void, config?: OverCustomSettingsConfig): () => void { if (!this.ready) { const cancel = new Set<() => void>(); @@ -461,7 +360,7 @@ export class CachedSettings return this.changeByRegex(regex, cb, config); } - /* @deprecated */ + /** @deprecated */ public changeByRegex(regex: RegExp, callback: (...args: [string, SettingValue]) => void, config?: OverCustomSettingsConfig): () => void { const store: Map void> = new Map(); return this.on('*', ([_id, value]) => { @@ -475,6 +374,9 @@ export class CachedSettings }); } + /** + * Wait until the settings get ready then run the callback + */ public onReady(cb: () => void): void { if (this.ready) { return cb(); diff --git a/apps/meteor/app/settings/server/SettingsRegistry.ts b/apps/meteor/app/settings/server/SettingsRegistry.ts index 0c5c00ff2c10..71ae504a68b3 100644 --- a/apps/meteor/app/settings/server/SettingsRegistry.ts +++ b/apps/meteor/app/settings/server/SettingsRegistry.ts @@ -1,8 +1,9 @@ import { Emitter } from '@rocket.chat/emitter'; import { isEqual } from 'underscore'; import { ISetting, ISettingGroup, isSettingEnterprise, SettingValue } from '@rocket.chat/core-typings'; +import { Settings } from '@rocket.chat/models'; +import type { ISettingsModel } from '@rocket.chat/model-typings'; -import type SettingsModel from '../../models/server/models/Settings'; import { SystemLogger } from '../../../server/lib/logger/system'; import { overwriteSetting } from './functions/overwriteSetting'; import { overrideSetting } from './functions/overrideSetting'; @@ -83,13 +84,13 @@ const compareSettings = compareSettingsIgnoringKeys([ ]); export class SettingsRegistry { - private model: typeof SettingsModel; + private model: ISettingsModel; private store: ICachedSettings; private _sorter: { [key: string]: number } = {}; - constructor({ store, model }: { store: ICachedSettings; model: typeof SettingsModel }) { + constructor({ store, model }: { store: ICachedSettings; model: typeof Settings }) { this.store = store; this.model = model; } @@ -97,7 +98,7 @@ export class SettingsRegistry { /* * Add a setting */ - add(_id: string, value: SettingValue, { sorter, section, group, ...options }: ISettingAddOptions = {}): void { + async add(_id: string, value: SettingValue, { sorter, section, group, ...options }: ISettingAddOptions = {}): Promise { if (!_id || value == null) { throw new Error('Invalid arguments'); } @@ -156,7 +157,7 @@ export class SettingsRegistry { const overwrittenKeys = Object.keys(settingFromCodeOverwritten); const removedKeys = Object.keys(settingStored).filter((key) => !['_updatedAt'].includes(key) && !overwrittenKeys.includes(key)); - this.model.upsert( + await this.model.updateOne( { _id }, { $set: { ...settingOverwrittenProps }, @@ -164,6 +165,7 @@ export class SettingsRegistry { $unset: removedKeys.reduce((unset, key) => ({ ...unset, [key]: 1 }), {}), }), }, + { upsert: true }, ); return; @@ -171,7 +173,7 @@ export class SettingsRegistry { if (settingStored && isOverwritten) { if (settingStored.value !== settingFromCodeOverwritten.value) { - this.model.upsert({ _id }, settingProps); + await this.model.updateOne({ _id }, settingProps, { upsert: true }); } return; } @@ -189,7 +191,7 @@ export class SettingsRegistry { const setting = isOverwritten ? settingFromCodeOverwritten : settingOverwrittenDefault; - this.model.insert(setting); // no need to emit unless we remove the oplog + await this.model.insertOne(setting); // no need to emit unless we remove the oplog this.store.set(setting); } @@ -197,10 +199,10 @@ export class SettingsRegistry { /* * Add a setting group */ - addGroup(_id: string, cb: addGroupCallback): void; + async addGroup(_id: string, cb: addGroupCallback): Promise; // eslint-disable-next-line no-dupe-class-members - addGroup(_id: string, groupOptions: ISettingAddGroupOptions | addGroupCallback = {}, cb?: addGroupCallback): void { + async addGroup(_id: string, groupOptions: ISettingAddGroupOptions | addGroupCallback = {}, cb?: addGroupCallback): Promise { if (!_id || (groupOptions instanceof Function && cb)) { throw new Error('Invalid arguments'); } @@ -214,7 +216,7 @@ export class SettingsRegistry { if (!this.store.has(_id)) { options.ts = new Date(); - this.model.insert(options); + await this.model.insertOne(options as ISetting); this.store.set(options as ISetting); } diff --git a/apps/meteor/app/settings/server/functions/settings.mocks.ts b/apps/meteor/app/settings/server/functions/settings.mocks.ts index e85f3f8a2bd2..59c47715d801 100644 --- a/apps/meteor/app/settings/server/functions/settings.mocks.ts +++ b/apps/meteor/app/settings/server/functions/settings.mocks.ts @@ -34,14 +34,14 @@ class SettingsClass { return [...this.data.values()].find((data) => Object.entries(query).every(([key, value]) => this.checkQueryMatch(key, data, value))); } - insert(doc: any): void { + insertOne(doc: any): void { this.data.set(doc._id, doc); // eslint-disable-next-line @typescript-eslint/no-var-requires this.settings.set(doc); this.insertCalls++; } - upsert(query: any, update: any): void { + updateOne(query: any, update: any): void { const existent = this.findOne(query); const data = { ...existent, ...query, ...update, ...update.$set }; @@ -71,4 +71,4 @@ class SettingsClass { export const Settings = new SettingsClass(); -mock('../../../models/server/models/Settings', Settings); +mock('@rocket.chat/models', { Settings }); diff --git a/apps/meteor/app/settings/server/index.ts b/apps/meteor/app/settings/server/index.ts index 765516219eaa..05be8d6aed4c 100644 --- a/apps/meteor/app/settings/server/index.ts +++ b/apps/meteor/app/settings/server/index.ts @@ -1,13 +1,23 @@ -import SettingsModel from '../../models/server/models/Settings'; +import { Settings } from '@rocket.chat/models'; + import { SettingsRegistry } from './SettingsRegistry'; import { initializeSettings } from './startup'; import { settings } from './cached'; import './applyMiddlewares'; +import { use } from './Middleware'; export { SettingsEvents } from './SettingsRegistry'; export { settings }; -export const settingsRegistry = new SettingsRegistry({ store: settings, model: SettingsModel }); +export const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings }); + +settingsRegistry.add = use(settingsRegistry.add, (context, next) => { + return Promise.await(next(...context)) as any; +}); + +settingsRegistry.addGroup = use(settingsRegistry.addGroup, (context, next) => { + return Promise.await(next(...context)) as any; +}); -initializeSettings({ SettingsModel, settings }); +Promise.await(initializeSettings({ model: Settings, settings })); diff --git a/apps/meteor/app/settings/server/raw.js b/apps/meteor/app/settings/server/raw.js deleted file mode 100644 index d458f7a5561b..000000000000 --- a/apps/meteor/app/settings/server/raw.js +++ /dev/null @@ -1,31 +0,0 @@ -import Settings from '../../models/server/models/Settings'; - -const cache = new Map(); - -export const setValue = (_id, value) => cache.set(_id, value); - -const setFromDB = async (_id) => { - const setting = Settings.findOneById(_id, { fields: { value: 1 } }); - if (!setting) { - return; - } - - setValue(_id, setting.value); - - return setting.value; -}; - -export const getValue = async (_id) => { - if (!cache.has(_id)) { - return setFromDB(_id); - } - - return cache.get(_id); -}; - -export const updateValue = (id, fields) => { - if (typeof fields.value === 'undefined') { - return; - } - setValue(id, fields.value); -}; diff --git a/apps/meteor/app/settings/server/raw.ts b/apps/meteor/app/settings/server/raw.ts new file mode 100644 index 000000000000..a32e3b77030e --- /dev/null +++ b/apps/meteor/app/settings/server/raw.ts @@ -0,0 +1,32 @@ +import { SettingValue } from '@rocket.chat/core-typings'; +import { Settings } from '@rocket.chat/models'; + +const cache = new Map(); + +export const setValue = (_id: string, value: SettingValue) => cache.set(_id, value); + +const setFromDB = async (_id: string) => { + const setting = await Settings.findOneById(_id, { projection: { value: 1 } }); + if (!setting) { + return; + } + + setValue(_id, setting.value); + + return setting.value; +}; + +export const getValue = async (_id: string) => { + if (!cache.has(_id)) { + return setFromDB(_id); + } + + return cache.get(_id); +}; + +export const updateValue = (id: string, fields: T) => { + if (typeof fields.value === 'undefined') { + return; + } + setValue(id, fields.value); +}; diff --git a/apps/meteor/app/settings/server/startup.ts b/apps/meteor/app/settings/server/startup.ts index 9c43be76437a..2fcc1e754507 100644 --- a/apps/meteor/app/settings/server/startup.ts +++ b/apps/meteor/app/settings/server/startup.ts @@ -1,13 +1,13 @@ import type { ISetting } from '@rocket.chat/core-typings'; +import type { Settings } from '@rocket.chat/models'; -import { Settings } from '../../models/server/models/Settings'; import { ICachedSettings } from './CachedSettings'; // eslint-disable-next-line @typescript-eslint/naming-convention -export function initializeSettings({ SettingsModel, settings }: { SettingsModel: Settings; settings: ICachedSettings }): void { - SettingsModel.find().forEach((record: ISetting) => { +export async function initializeSettings({ model, settings }: { model: typeof Settings; settings: ICachedSettings }): Promise { + await model.find().forEach((record: ISetting) => { settings.set(record); }); - settings.initilized(); + settings.initialized(); } diff --git a/apps/meteor/app/statistics/server/functions/updateStatsCounter.ts b/apps/meteor/app/statistics/server/functions/updateStatsCounter.ts index 4641b040f819..f46612772021 100644 --- a/apps/meteor/app/statistics/server/functions/updateStatsCounter.ts +++ b/apps/meteor/app/statistics/server/functions/updateStatsCounter.ts @@ -1,4 +1,5 @@ -import { Settings } from '../../../models/server'; +import { Settings } from '@rocket.chat/models'; + import telemetryEvent from '../lib/telemetryEvents'; type updateCounterDataType = { settingsId: string }; diff --git a/apps/meteor/app/statistics/server/lib/statistics.ts b/apps/meteor/app/statistics/server/lib/statistics.ts index 5f1bf083f87b..8d0918e90449 100644 --- a/apps/meteor/app/statistics/server/lib/statistics.ts +++ b/apps/meteor/app/statistics/server/lib/statistics.ts @@ -21,9 +21,10 @@ import { Messages as MessagesRaw, Roles as RolesRaw, InstanceStatus, + Settings, } from '@rocket.chat/models'; -import { Settings, Users, Rooms, Subscriptions, Messages } from '../../../models/server'; +import { Users, Rooms, Subscriptions, Messages } from '../../../models/server'; import { settings } from '../../../settings/server'; import { Info, getMongoInfo } from '../../../utils/server'; import { getControl } from '../../../../server/lib/migrations'; @@ -67,18 +68,21 @@ export const statistics = { // Setup Wizard statistics.wizard = {}; - wizardFields.forEach((field) => { - const record = Settings.findOne(field); - if (record) { - const wizardField = field.replace(/_/g, '').replace(field[0], field[0].toLowerCase()); - statistics.wizard[wizardField] = record.value; - } - }); + await Promise.all( + wizardFields.map(async (field) => { + const record = await Settings.findOne(field); + if (record) { + const wizardField = field.replace(/_/g, '').replace(field[0], field[0].toLowerCase()); + statistics.wizard[wizardField] = record.value; + } + }), + ); // Version + const uniqueID = await Settings.findOne('uniqueID'); statistics.uniqueId = settings.get('uniqueID'); - if (Settings.findOne('uniqueID')) { - statistics.installedAt = Settings.findOne('uniqueID').createdAt; + if (uniqueID) { + statistics.installedAt = String(uniqueID.createdAt); } if (Info) { @@ -453,6 +457,8 @@ export const statistics = { statsPms.push(Analytics.resetSeatRequestCount()); + // TODO: Is that the best way to do this? maybe we should use a promise.all() + statistics.dashboardCount = settings.get('Engagement_Dashboard_Load_Count'); statistics.messageAuditApply = settings.get('Message_Auditing_Apply_Count'); statistics.messageAuditLoad = settings.get('Message_Auditing_Panel_Load_Count'); @@ -483,22 +489,22 @@ export const statistics = { statistics.matrixBridgeEnabled = settings.get('Federation_Matrix_enabled'); statistics.uncaughtExceptionsCount = settings.get('Uncaught_Exceptions_Count'); - const defaultHomeTitle = Settings.findOneById('Layout_Home_Title').packageValue; + const defaultHomeTitle = (await Settings.findOneById('Layout_Home_Title'))?.packageValue; statistics.homeTitleChanged = settings.get('Layout_Home_Title') !== defaultHomeTitle; - const defaultHomeBody = Settings.findOneById('Layout_Home_Body').packageValue; + const defaultHomeBody = (await Settings.findOneById('Layout_Home_Body'))?.packageValue; statistics.homeBodyChanged = settings.get('Layout_Home_Body') !== defaultHomeBody; - const defaultCustomCSS = Settings.findOneById('theme-custom-css').packageValue; + const defaultCustomCSS = (await Settings.findOneById('theme-custom-css'))?.packageValue; statistics.customCSSChanged = settings.get('theme-custom-css') !== defaultCustomCSS; - const defaultOnLogoutCustomScript = Settings.findOneById('Custom_Script_On_Logout').packageValue; + const defaultOnLogoutCustomScript = (await Settings.findOneById('Custom_Script_On_Logout'))?.packageValue; statistics.onLogoutCustomScriptChanged = settings.get('Custom_Script_On_Logout') !== defaultOnLogoutCustomScript; - const defaultLoggedOutCustomScript = Settings.findOneById('Custom_Script_Logged_Out').packageValue; + const defaultLoggedOutCustomScript = (await Settings.findOneById('Custom_Script_Logged_Out'))?.packageValue; statistics.loggedOutCustomScriptChanged = settings.get('Custom_Script_Logged_Out') !== defaultLoggedOutCustomScript; - const defaultLoggedInCustomScript = Settings.findOneById('Custom_Script_Logged_In').packageValue; + const defaultLoggedInCustomScript = (await Settings.findOneById('Custom_Script_Logged_In'))?.packageValue; statistics.loggedInCustomScriptChanged = settings.get('Custom_Script_Logged_In') !== defaultLoggedInCustomScript; await Promise.all(statsPms).catch(log); diff --git a/apps/meteor/app/theme/server/server.js b/apps/meteor/app/theme/server/server.js index 07990e9c75bd..1897fe731d2d 100644 --- a/apps/meteor/app/theme/server/server.js +++ b/apps/meteor/app/theme/server/server.js @@ -5,11 +5,11 @@ import less from 'less'; import Autoprefixer from 'less-plugin-autoprefixer'; import { WebApp } from 'meteor/webapp'; import { Meteor } from 'meteor/meteor'; +import { Settings } from '@rocket.chat/models'; import { settings, settingsRegistry } from '../../settings/server'; import { Logger } from '../../logger'; import { addStyle } from '../../ui-master/server/inject'; -import { Settings } from '../../models/server'; const logger = new Logger('rocketchat:theme'); diff --git a/apps/meteor/app/ui-master/server/index.js b/apps/meteor/app/ui-master/server/index.js index cf837de93795..4905b5d7663d 100644 --- a/apps/meteor/app/ui-master/server/index.js +++ b/apps/meteor/app/ui-master/server/index.js @@ -2,11 +2,12 @@ import { Meteor } from 'meteor/meteor'; import { Inject } from 'meteor/meteorhacks:inject-initial'; import { Tracker } from 'meteor/tracker'; import _ from 'underscore'; +import { Settings } from '@rocket.chat/models'; import { escapeHTML } from '@rocket.chat/string-helpers'; -import { Settings } from '../../models/server'; import { settings } from '../../settings/server'; import { applyHeadInjections, headInjections, injectIntoBody, injectIntoHead } from './inject'; + import './scripts'; export * from './inject'; @@ -124,16 +125,11 @@ Meteor.startup(() => { }); const renderDynamicCssList = _.debounce( - Meteor.bindEnvironment(() => { + Meteor.bindEnvironment(async () => { // const variables = RocketChat.models.Settings.findOne({_id:'theme-custom-variables'}, {fields: { value: 1}}); - const colors = Settings.find({ _id: /theme-color-rc/i }, { fields: { value: 1, editor: 1 } }) - .fetch() - .filter((color) => color && color.value); - - if (!colors) { - return; - } + const colors = await Settings.find({ _id: /theme-color-rc/i }, { projection: { value: 1, editor: 1 } }).toArray(); const css = colors + .filter((color) => color && color.value) .map(({ _id, value, editor }) => { if (editor === 'expression') { return `--${_id.replace('theme-color-', '')}: var(--${value});`; diff --git a/apps/meteor/app/version-check/server/functions/checkVersionUpdate.js b/apps/meteor/app/version-check/server/functions/checkVersionUpdate.js deleted file mode 100644 index 8a3191a9863f..000000000000 --- a/apps/meteor/app/version-check/server/functions/checkVersionUpdate.js +++ /dev/null @@ -1,93 +0,0 @@ -import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; -import semver from 'semver'; - -import getNewUpdates from './getNewUpdates'; -import { settings } from '../../../settings/server'; -import { Info } from '../../../utils'; -import { Users, Settings } from '../../../models/server'; -import logger from '../logger'; -import { sendMessagesToAdmins } from '../../../../server/lib/sendMessagesToAdmins'; -// import getNewUpdates from '../sampleUpdateData'; - -export default () => { - logger.info('Checking for version updates'); - - const { versions, alerts } = getNewUpdates(); - - const update = { - exists: false, - lastestVersion: null, - security: false, - }; - - const lastCheckedVersion = settings.get('Update_LatestAvailableVersion'); - versions.forEach((version) => { - if (semver.lte(version.version, lastCheckedVersion)) { - return; - } - - if (semver.lte(version.version, Info.version)) { - return; - } - - update.exists = true; - update.lastestVersion = version; - - if (version.security === true) { - update.security = true; - } - }); - - if (update.exists) { - Settings.updateValueById('Update_LatestAvailableVersion', update.lastestVersion.version); - - 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, '_'), - priority: 10, - title: 'Update_your_RocketChat', - text: 'New_version_available_(s)', - textArguments: [update.lastestVersion.version], - link: update.lastestVersion.infoUrl, - }, - ], - }), - ); - } - - if (alerts && alerts.length) { - Promise.await( - sendMessagesToAdmins({ - msgs: ({ adminUser }) => - alerts - .filter((alert) => !Users.bannerExistsById(adminUser._id, `alert-${alert.id}`)) - .map((alert) => ({ - msg: `*${TAPi18n.__('Rocket_Chat_Alert', adminUser.language)}:*\n\n*${TAPi18n.__( - alert.title, - adminUser.language, - )}*\n${TAPi18n.__(alert.text, ...(alert.textArguments || []), adminUser.language)}\n${alert.infoUrl}`, - })), - banners: alerts.map((alert) => ({ - id: `alert-${alert.id}`.replace(/\./g, '_'), - priority: 10, - title: alert.title, - text: alert.text, - textArguments: alert.textArguments, - modifiers: alert.modifiers, - link: alert.infoUrl, - })), - }), - ); - } -}; diff --git a/apps/meteor/app/version-check/server/functions/checkVersionUpdate.ts b/apps/meteor/app/version-check/server/functions/checkVersionUpdate.ts new file mode 100644 index 000000000000..6577cd412922 --- /dev/null +++ b/apps/meteor/app/version-check/server/functions/checkVersionUpdate.ts @@ -0,0 +1,90 @@ +import { TAPi18n } from 'meteor/rocketchat:tap-i18n'; +import semver from 'semver'; +import { Settings } from '@rocket.chat/models'; + +import { getNewUpdates } from './getNewUpdates'; +import { settings } from '../../../settings/server'; +import { Info } from '../../../utils/server'; +import { Users } from '../../../models/server'; +import logger from '../logger'; +import { sendMessagesToAdmins } from '../../../../server/lib/sendMessagesToAdmins'; +// import getNewUpdates from '../sampleUpdateData'; + +export const checkVersionUpdate = async () => { + logger.info('Checking for version updates'); + + const { versions, alerts } = await getNewUpdates(); + + const lastCheckedVersion = settings.get('Update_LatestAvailableVersion'); + + for await (const version of versions) { + if (!lastCheckedVersion) { + break; + } + if (semver.lte(version.version, lastCheckedVersion)) { + continue; + } + + if (semver.lte(version.version, Info.version)) { + continue; + } + + await Settings.updateValueById('Update_LatestAvailableVersion', version.version); + + await sendMessagesToAdmins({ + msgs: ({ adminUser }) => [ + { + msg: `*${TAPi18n.__('Update_your_RocketChat', { ...(adminUser.language && { lng: adminUser.language }) })}*\n${TAPi18n.__( + 'New_version_available_(s)', + { + postProcess: 'sprintf', + sprintf: [version.version], + }, + )}\n${version.infoUrl}`, + }, + ], + banners: [ + { + id: `versionUpdate-${version.version}`.replace(/\./g, '_'), + priority: 10, + title: 'Update_your_RocketChat', + text: 'New_version_available_(s)', + textArguments: [version.version], + link: version.infoUrl, + modifiers: [], + }, + ], + }); + break; + } + + if (alerts && alerts.length) { + await sendMessagesToAdmins({ + msgs: ({ adminUser }) => + alerts + .filter((alert) => !Users.bannerExistsById(adminUser._id, `alert-${alert.id}`)) + .map((alert) => ({ + msg: `*${TAPi18n.__('Rocket_Chat_Alert', { ...(adminUser.language && { lng: adminUser.language }) })}:*\n\n*${TAPi18n.__( + alert.title, + { ...(adminUser.language && { lng: adminUser.language }) }, + )}*\n${TAPi18n.__(alert.text, { + ...(adminUser.language && { lng: adminUser.language }), + ...(Array.isArray(alert.textArguments) && { + postProcess: 'sprintf', + sprintf: alert.textArguments, + }), + ...((!Array.isArray(alert.textArguments) && alert.textArguments) || {}), // bien dormido + })}\n${alert.infoUrl}`, + })), + banners: alerts.map((alert) => ({ + id: `alert-${alert.id}`.replace(/\./g, '_'), + priority: 10, + title: alert.title, + text: alert.text, + textArguments: alert.textArguments, + modifiers: alert.modifiers, + link: alert.infoUrl, + })), + }); + } +}; diff --git a/apps/meteor/app/version-check/server/functions/getNewUpdates.js b/apps/meteor/app/version-check/server/functions/getNewUpdates.js deleted file mode 100644 index f5e8e7c0c691..000000000000 --- a/apps/meteor/app/version-check/server/functions/getNewUpdates.js +++ /dev/null @@ -1,72 +0,0 @@ -import os from 'os'; - -import { HTTP } from 'meteor/http'; -import { check, Match } from 'meteor/check'; - -import { Settings } from '../../../models/server'; -import { Info } from '../../../utils'; -import { getWorkspaceAccessToken } from '../../../cloud/server'; - -export default () => { - try { - const uniqueID = Settings.findOne('uniqueID'); - - const params = { - uniqueId: uniqueID.value, - installedAt: uniqueID.createdAt.toJSON(), - version: Info.version, - osType: os.type(), - osPlatform: os.platform(), - osArch: os.arch(), - osRelease: os.release(), - nodeVersion: process.version, - deployMethod: process.env.DEPLOY_METHOD || 'tar', - deployPlatform: process.env.DEPLOY_PLATFORM || 'selfinstall', - }; - - const headers = {}; - const token = getWorkspaceAccessToken(); - if (token) { - headers.Authorization = `Bearer ${token}`; - } - - const { data } = HTTP.get('https://releases.rocket.chat/updates/check', { - params, - headers, - }); - - check( - data, - Match.ObjectIncluding({ - versions: [ - Match.ObjectIncluding({ - version: Match.Optional(String), - security: Match.Optional(Boolean), - infoUrl: Match.Optional(String), - }), - ], - alerts: Match.Optional([ - Match.ObjectIncluding({ - id: Match.Optional(String), - title: Match.Optional(String), - text: Match.Optional(String), - textArguments: Match.Optional([Match.Any]), - modifiers: Match.Optional([String]), - infoUrl: Match.Optional(String), - }), - ]), - }), - ); - - return data; - } catch (error) { - // There's no need to log this error - // as it's pointless and the user - // can't do anything about it anyways - - return { - versions: [], - alerts: [], - }; - } -}; diff --git a/apps/meteor/app/version-check/server/functions/getNewUpdates.ts b/apps/meteor/app/version-check/server/functions/getNewUpdates.ts new file mode 100644 index 000000000000..44bf58c3e7cb --- /dev/null +++ b/apps/meteor/app/version-check/server/functions/getNewUpdates.ts @@ -0,0 +1,93 @@ +import os from 'os'; + +import { Settings } from '@rocket.chat/models'; +import { HTTP } from 'meteor/http'; +import { check, Match } from 'meteor/check'; + +import { Info } from '../../../utils/server'; +import { getWorkspaceAccessToken } from '../../../cloud/server'; + +export const getNewUpdates = async () => { + try { + const uniqueID = await Settings.findOne('uniqueID'); + + if (!uniqueID) { + throw new Error('uniqueID not found'); + } + + const params = { + uniqueId: String(uniqueID.value), + installedAt: uniqueID.createdAt.toJSON(), + version: Info.version, + osType: os.type(), + osPlatform: os.platform(), + osArch: os.arch(), + osRelease: os.release(), + nodeVersion: process.version, + deployMethod: process.env.DEPLOY_METHOD || 'tar', + deployPlatform: process.env.DEPLOY_PLATFORM || 'selfinstall', + }; + + const token = await getWorkspaceAccessToken(); + const headers = { + ...(token && { Authorization: `Bearer ${token}` }), + }; + + const { data } = HTTP.get('https://releases.rocket.chat/updates/check', { + params, + headers, + }); + + check( + data, + Match.ObjectIncluding({ + versions: [ + Match.ObjectIncluding({ + version: String, + security: Match.Optional(Boolean), + infoUrl: String, + }), + ], + alerts: [ + Match.Optional([ + Match.ObjectIncluding({ + id: String, + title: String, + text: String, + textArguments: [Match.Any], + modifiers: [String], + infoUrl: String, + }), + ]), + ], + }), + ); + + return data as { + versions: { + version: string; + security: boolean; + infoUrl: string; + }[]; + + alerts: { + id: string; + priority: number; + title: string; + text: string; + textArguments?: string[]; + modifiers: string[]; + infoUrl: string; + }[]; + }; + } catch (error) { + // There's no need to log this error + // as it's pointless and the user + // can't do anything about it anyways + + return { + versions: [], + alerts: [], + }; + } +}; diff --git a/apps/meteor/app/version-check/server/index.ts b/apps/meteor/app/version-check/server/index.ts index 9400ea1c6b94..4f3513858774 100644 --- a/apps/meteor/app/version-check/server/index.ts +++ b/apps/meteor/app/version-check/server/index.ts @@ -2,7 +2,7 @@ import { Meteor } from 'meteor/meteor'; import { SyncedCron } from 'meteor/littledata:synced-cron'; import { settings } from '../../settings/server'; -import checkVersionUpdate from './functions/checkVersionUpdate'; +import { checkVersionUpdate } from './functions/checkVersionUpdate'; import './methods/banner_dismiss'; import './addSettings'; @@ -17,7 +17,7 @@ const addVersionCheckJob = Meteor.bindEnvironment(() => { name: jobName, schedule: (parser: { text: (time: string) => string }) => parser.text('at 2:00 am'), job() { - checkVersionUpdate(); + Promise.await(checkVersionUpdate()); }, }); }); @@ -25,7 +25,7 @@ const addVersionCheckJob = Meteor.bindEnvironment(() => { Meteor.startup(() => { Meteor.defer(() => { if (settings.get('Register_Server') && settings.get('Update_EnableChecker')) { - checkVersionUpdate(); + Promise.await(checkVersionUpdate()); } }); }); diff --git a/apps/meteor/app/voip/server/startup.ts b/apps/meteor/app/voip/server/startup.ts index 06703d944d77..7a0dc60f1885 100644 --- a/apps/meteor/app/voip/server/startup.ts +++ b/apps/meteor/app/voip/server/startup.ts @@ -1,10 +1,6 @@ -import debounce from 'lodash.debounce'; - import { settings } from '../../settings/server'; import { Voip } from '../../../server/sdk'; -const debouncedRefresh = debounce(Voip.refresh, 1000); - settings.watch('VoIP_Enabled', (value: boolean) => { return value ? Voip.init() : Voip.stop(); }); @@ -14,6 +10,8 @@ settings.changeMultiple( (_values) => { // Here, if 4 settings are changed at once, we're getting 4 diff callbacks. The good part is that all callbacks are fired almost instantly // So to avoid stopping/starting voip too often, we debounce the call and restart 1 second after the last setting has reached us. - return debouncedRefresh(); + if (settings.get('VoIP_Enabled')) { + Voip.refresh(); + } }, ); diff --git a/apps/meteor/client/hooks/useEndpointData.ts b/apps/meteor/client/hooks/useEndpointData.ts index f29eb2c161bf..8cea0c0b7013 100644 --- a/apps/meteor/client/hooks/useEndpointData.ts +++ b/apps/meteor/client/hooks/useEndpointData.ts @@ -3,8 +3,21 @@ import type { MatchPathPattern, OperationParams, OperationResult, PathFor } from import { useToastMessageDispatch, useEndpoint } from '@rocket.chat/ui-contexts'; import { useCallback, useEffect } from 'react'; +import { getConfig } from '../lib/utils/getConfig'; import { AsyncState, useAsyncState } from './useAsyncState'; +const log = (name: string): Console['log'] => + process.env.NODE_ENV !== 'production' || getConfig('debug') === 'true' + ? (...args): void => console.log(name, ...args) + : (): void => undefined; + +const deprecationWarning = log('useEndpointData is deprecated, use @tanstack/react-query instead'); + +/** + * use @tanstack/react-query with useEndpoint instead + * @deprecated + */ + export const useEndpointData = >( endpoint: TPath, params?: OperationParams<'GET', MatchPathPattern>, @@ -14,6 +27,7 @@ export const useEndpointData = >( ): AsyncState>>> & { reload: () => void; } => { + deprecationWarning({ endpoint, params, initialValue }); const { resolve, reject, reset, ...state } = useAsyncState(initialValue); const dispatchToastMessage = useToastMessageDispatch(); const getData = useEndpoint('GET', endpoint); diff --git a/apps/meteor/client/providers/DeviceProvider/DeviceProvider.tsx b/apps/meteor/client/providers/DeviceProvider/DeviceProvider.tsx index e695691f19df..8ab12e850b32 100644 --- a/apps/meteor/client/providers/DeviceProvider/DeviceProvider.tsx +++ b/apps/meteor/client/providers/DeviceProvider/DeviceProvider.tsx @@ -1,5 +1,6 @@ -import { DeviceContext, Device, IExperimentalHTMLAudioElement } from '@rocket.chat/ui-contexts'; -import React, { ReactElement, ReactNode, useEffect, useState } from 'react'; +import { useMutableCallback } from '@rocket.chat/fuselage-hooks'; +import { DeviceContext, Device, IExperimentalHTMLAudioElement, DeviceContextValue } from '@rocket.chat/ui-contexts'; +import React, { ReactElement, ReactNode, useEffect, useState, useMemo } from 'react'; import { isSetSinkIdAvailable } from './lib/isSetSinkIdAvailable'; @@ -8,6 +9,7 @@ type DeviceProviderProps = { }; export const DeviceProvider = ({ children }: DeviceProviderProps): ReactElement => { + const [enabled] = useState(typeof isSecureContext && isSecureContext); const [availableAudioOutputDevices, setAvailableAudioOutputDevices] = useState([]); const [availableAudioInputDevices, setAvailableAudioInputDevices] = useState([]); const [selectedAudioOutputDevice, setSelectedAudioOutputDevice] = useState({ @@ -21,23 +23,32 @@ export const DeviceProvider = ({ children }: DeviceProviderProps): ReactElement type: 'audio', }); - const setAudioOutputDevice = ({ - outputDevice, - HTMLAudioElement, - }: { - outputDevice: Device; - HTMLAudioElement: IExperimentalHTMLAudioElement; - }): void => { - if (!isSetSinkIdAvailable()) { - throw new Error('setSinkId is not available in this browser'); + const setAudioInputDevice = (device: Device): void => { + if (!isSecureContext) { + throw new Error('Device Changes are not available on insecure contexts'); } - setSelectedAudioOutputDevice(outputDevice); - HTMLAudioElement.setSinkId(outputDevice.id); + setSelectedAudioInputDevice(device); }; + const setAudioOutputDevice = useMutableCallback( + ({ outputDevice, HTMLAudioElement }: { outputDevice: Device; HTMLAudioElement: IExperimentalHTMLAudioElement }): void => { + if (!isSetSinkIdAvailable()) { + throw new Error('setSinkId is not available in this browser'); + } + if (!enabled) { + throw new Error('Device Changes are not available on insecure contexts'); + } + setSelectedAudioOutputDevice(outputDevice); + HTMLAudioElement.setSinkId(outputDevice.id); + }, + ); + useEffect(() => { + if (!enabled) { + return; + } const setMediaDevices = (): void => { - navigator.mediaDevices.enumerateDevices().then((devices) => { + navigator.mediaDevices?.enumerateDevices().then((devices) => { const audioInput: Device[] = []; const audioOutput: Device[] = []; devices.forEach((device) => { @@ -57,21 +68,37 @@ export const DeviceProvider = ({ children }: DeviceProviderProps): ReactElement }); }; - navigator.mediaDevices.addEventListener('devicechange', setMediaDevices); + navigator.mediaDevices?.addEventListener('devicechange', setMediaDevices); setMediaDevices(); return (): void => { - navigator.mediaDevices.removeEventListener('devicechange', setMediaDevices); + navigator.mediaDevices?.removeEventListener('devicechange', setMediaDevices); }; - }, []); + }, [enabled]); - const contextValue = { - availableAudioOutputDevices, + const contextValue = useMemo((): DeviceContextValue => { + if (!enabled) { + return { + enabled, + }; + } + + return { + enabled, + availableAudioOutputDevices, + availableAudioInputDevices, + selectedAudioOutputDevice, + selectedAudioInputDevice, + setAudioOutputDevice, + setAudioInputDevice, + }; + }, [ availableAudioInputDevices, - selectedAudioOutputDevice, + availableAudioOutputDevices, + enabled, selectedAudioInputDevice, + selectedAudioOutputDevice, setAudioOutputDevice, - setAudioInputDevice: setSelectedAudioInputDevice, - }; + ]); return {children}; }; diff --git a/apps/meteor/definition/externals/meteor/rocketchat-tap-i18n.d.ts b/apps/meteor/definition/externals/meteor/rocketchat-tap-i18n.d.ts index 91fd9097c4c9..bb1c393a29ba 100644 --- a/apps/meteor/definition/externals/meteor/rocketchat-tap-i18n.d.ts +++ b/apps/meteor/definition/externals/meteor/rocketchat-tap-i18n.d.ts @@ -9,7 +9,14 @@ declare module 'meteor/rocketchat:tap-i18n' { lng?: string; } & { [replacements: string]: boolean | number | string | string[]; - }, + } & ( + | { + postProcess: 'sprintf'; + sprintf: (boolean | number | string)[]; + } + | {} + ), + lang?: string, ): string; function getLanguages(): { [language: string]: { diff --git a/apps/meteor/ee/app/canned-responses/server/index.js b/apps/meteor/ee/app/canned-responses/server/index.js index b85fa231adab..fdcd77ec8321 100644 --- a/apps/meteor/ee/app/canned-responses/server/index.js +++ b/apps/meteor/ee/app/canned-responses/server/index.js @@ -1,5 +1,3 @@ -import { Meteor } from 'meteor/meteor'; - import { onLicense } from '../../license/server'; onLicense('canned-responses', () => { @@ -11,7 +9,5 @@ onLicense('canned-responses', () => { require('./methods/saveCannedResponse'); require('./methods/removeCannedResponse'); - Meteor.startup(function () { - createSettings(); - }); + createSettings(); }); diff --git a/apps/meteor/ee/app/license/server/getSeatsRequestLink.ts b/apps/meteor/ee/app/license/server/getSeatsRequestLink.ts index 0237eb094d22..b911f0113729 100644 --- a/apps/meteor/ee/app/license/server/getSeatsRequestLink.ts +++ b/apps/meteor/ee/app/license/server/getSeatsRequestLink.ts @@ -1,15 +1,16 @@ import type { ISetting } from '@rocket.chat/core-typings'; +import { Settings } from '@rocket.chat/models'; -import { Settings, Users } from '../../../../app/models/server'; +import { Users } from '../../../../app/models/server'; type WizardSettings = Array; const url = 'https://go.rocket.chat/i/seats-cap-upgrade'; -export const getSeatsRequestLink = (): string => { - const workspaceId: ISetting | undefined = Settings.findOneById('Cloud_Workspace_Id'); +export const getSeatsRequestLink = async (): Promise => { + const workspaceId = await Settings.findOneById('Cloud_Workspace_Id'); const activeUsers = Users.getActiveLocalUserCount(); - const wizardSettings: WizardSettings = Settings.findSetupWizardSettings().fetch(); + const wizardSettings: WizardSettings = await Settings.findSetupWizardSettings().toArray(); const newUrl = new URL(url); diff --git a/apps/meteor/ee/app/license/server/settings.js b/apps/meteor/ee/app/license/server/settings.js index f966c0bf5ee0..27867ee7fccf 100644 --- a/apps/meteor/ee/app/license/server/settings.js +++ b/apps/meteor/ee/app/license/server/settings.js @@ -1,7 +1,7 @@ +import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; import { settings, settingsRegistry } from '../../../../app/settings/server'; -import { Settings } from '../../../../app/models/server'; import { addLicense } from './license'; Meteor.startup(function () { diff --git a/apps/meteor/ee/app/livechat-enterprise/server/settings.ts b/apps/meteor/ee/app/livechat-enterprise/server/settings.ts index 1a68a16a6e6f..a217874a553c 100644 --- a/apps/meteor/ee/app/livechat-enterprise/server/settings.ts +++ b/apps/meteor/ee/app/livechat-enterprise/server/settings.ts @@ -1,10 +1,11 @@ +import { Settings } from '@rocket.chat/models'; + import { settingsRegistry } from '../../../../app/settings/server'; -import { Settings } from '../../../../app/models/server'; const omnichannelEnabledQuery = { _id: 'Livechat_enabled', value: true }; const businessHoursEnabled = { _id: 'Livechat_enable_business_hours', value: true }; -export const createSettings = (): void => { +export const createSettings = async (): Promise => { settingsRegistry.add('Livechat_abandoned_rooms_action', 'none', { type: 'select', group: 'Omnichannel', @@ -206,11 +207,11 @@ export const createSettings = (): void => { enableQuery: omnichannelEnabledQuery, }); - Settings.addOptionValueById('Livechat_Routing_Method', { + await Settings.addOptionValueById('Livechat_Routing_Method', { key: 'Load_Balancing', i18nLabel: 'Load_Balancing', }); - Settings.addOptionValueById('Livechat_Routing_Method', { + await Settings.addOptionValueById('Livechat_Routing_Method', { key: 'Load_Rotation', i18nLabel: 'Load_Rotation', }); diff --git a/apps/meteor/ee/app/settings/server/settings.ts b/apps/meteor/ee/app/settings/server/settings.ts index a4da8d0ed4e9..7f0558e2c8d2 100644 --- a/apps/meteor/ee/app/settings/server/settings.ts +++ b/apps/meteor/ee/app/settings/server/settings.ts @@ -1,8 +1,8 @@ import { Meteor } from 'meteor/meteor'; import { ISetting, SettingValue } from '@rocket.chat/core-typings'; +import { Settings } from '@rocket.chat/models'; import { isEnterprise, hasLicense, onValidateLicenses } from '../../license/server/license'; -import SettingsModel from '../../../../app/models/server/models/Settings'; import { use } from '../../../../app/settings/server/Middleware'; import { settings, SettingsEvents } from '../../../../app/settings/server'; @@ -50,7 +50,7 @@ SettingsEvents.on('fetch-settings', (settings: Array): void => { }); function updateSettings(): void { - const enterpriseSettings = SettingsModel.findEnterpriseSettings(); + const enterpriseSettings = Promise.await(Settings.findEnterpriseSettings()); enterpriseSettings.forEach((record: ISetting) => settings.set(record)); } diff --git a/apps/meteor/ee/client/voip/modals/DeviceSettingsModal.tsx b/apps/meteor/ee/client/voip/modals/DeviceSettingsModal.tsx index d29f238e5987..74b5ae009bb6 100644 --- a/apps/meteor/ee/client/voip/modals/DeviceSettingsModal.tsx +++ b/apps/meteor/ee/client/voip/modals/DeviceSettingsModal.tsx @@ -1,5 +1,12 @@ import { Modal, Field, Select, ButtonGroup, Button, SelectOption, Box } from '@rocket.chat/fuselage'; -import { useTranslation, useAvailableDevices, useToastMessageDispatch, useSetModal, useSelectedDevices } from '@rocket.chat/ui-contexts'; +import { + useTranslation, + useAvailableDevices, + useToastMessageDispatch, + useSetModal, + useSelectedDevices, + useIsDeviceManagementEnabled, +} from '@rocket.chat/ui-contexts'; import React, { ReactElement, useState } from 'react'; import { useForm, Controller, SubmitHandler } from 'react-hook-form'; @@ -14,6 +21,7 @@ type FieldValues = { const DeviceSettingsModal = (): ReactElement => { const setModal = useSetModal(); const onCancel = (): void => setModal(); + const isDeviceManagementEnabled = useIsDeviceManagementEnabled(); const t = useTranslation(); const dispatchToastMessage = useToastMessageDispatch(); const selectedAudioDevices = useSelectedDevices(); @@ -60,6 +68,11 @@ const DeviceSettingsModal = (): ReactElement => { )} + {!isDeviceManagementEnabled && ( + + {t('Device_Changes_Not_Available_Insecure_Context')} + + )} {t('Microphone')} diff --git a/apps/meteor/ee/server/api/licenses.ts b/apps/meteor/ee/server/api/licenses.ts index f7a5d75dbfb5..5e3f8eb25d74 100644 --- a/apps/meteor/ee/server/api/licenses.ts +++ b/apps/meteor/ee/server/api/licenses.ts @@ -1,7 +1,8 @@ import { check } from 'meteor/check'; +import { Settings } from '@rocket.chat/models'; import { getLicenses, validateFormat, flatModules, getMaxActiveUsers, isEnterprise } from '../../app/license/server/license'; -import { Settings, Users } from '../../../app/models/server'; +import { Users } from '../../../app/models/server'; import { API } from '../../../app/api/server/api'; import { hasPermission } from '../../../app/authorization/server'; import { ILicense } from '../../app/license/definitions/ILicense'; @@ -35,7 +36,7 @@ API.v1.addRoute( 'licenses.add', { authRequired: true }, { - post() { + async post() { check(this.bodyParams, { license: String, }); @@ -49,7 +50,7 @@ API.v1.addRoute( return API.v1.failure('Invalid license'); } - Settings.updateValueById('Enterprise_License', license); + await Settings.updateValueById('Enterprise_License', license); return API.v1.success(); }, diff --git a/apps/meteor/ee/server/requestSeatsRoute.ts b/apps/meteor/ee/server/requestSeatsRoute.ts index a7e08e5e0ade..8c9e09f21eb6 100644 --- a/apps/meteor/ee/server/requestSeatsRoute.ts +++ b/apps/meteor/ee/server/requestSeatsRoute.ts @@ -10,7 +10,7 @@ Meteor.startup(() => { WebApp.connectHandlers.use( '/requestSeats/', Meteor.bindEnvironment((_: IncomingMessage, res: ServerResponse) => { - const url = getSeatsRequestLink(); + const url = Promise.await(getSeatsRequestLink()); Analytics.saveSeatRequest(); res.writeHead(302, { Location: url }); diff --git a/apps/meteor/ee/server/startup/upsell.ts b/apps/meteor/ee/server/startup/upsell.ts index f979febe3918..8f9dfaf1ea25 100644 --- a/apps/meteor/ee/server/startup/upsell.ts +++ b/apps/meteor/ee/server/startup/upsell.ts @@ -1,6 +1,6 @@ import { Meteor } from 'meteor/meteor'; +import { Settings } from '@rocket.chat/models'; -import { Settings } from '../../../app/models/server'; import { onValidateLicenses, getLicenses } from '../../app/license/server/license'; const handleHadTrial = (): void => { diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json index 0703bddef7c4..2ddc00b7bffb 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/en.i18n.json @@ -1496,6 +1496,7 @@ "Desktop_Notifications_Not_Enabled": "Desktop Notifications are Not Enabled", "Details": "Details", "Device_Changes_Not_Available": "Device changes not available in this browser. For guaranteed availability, please use Rocket.Chat's official desktop app.", + "Device_Changes_Not_Available_Insecure_Context": "Device changes are only available on secure contexts (e.g. https://)", "Device_Management": "Device management", "Device_ID": "Device ID", "Device_Info": "Device Info", diff --git a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json index eea1859311e5..438f05af6fef 100644 --- a/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json +++ b/apps/meteor/packages/rocketchat-i18n/i18n/pt-BR.i18n.json @@ -1439,6 +1439,7 @@ "Desktop_Notifications_Not_Enabled": "Notificações da área de trabalho estão desabilitadas", "Details": "Detalhes", "Device_Changes_Not_Available": "Mudanças de dispositivo não estão disponíveis neste navegador, para disponíbilidade garantida, use o aplicativo desktop oficial do Rocket.Chat.", + "Device_Changes_Not_Available_Insecure_Context": "Mudanças de dispositivo somente estão disponíveis em contextos seguros. (https://)", "Different_Style_For_User_Mentions": "Estilo diferente para as menções do usuário", "Direct_Message": "Mensagem direta", "Direct_message_creation_description": "Você está prestes a criar uma conversa com vários usuários. Adicione os usuários com quem gostaria de conversar, todos no mesmo local, utilizando mensagens diretas.", diff --git a/apps/meteor/server/cron/federation.ts b/apps/meteor/server/cron/federation.ts index f35bec41756c..bedebb434fed 100644 --- a/apps/meteor/server/cron/federation.ts +++ b/apps/meteor/server/cron/federation.ts @@ -17,7 +17,7 @@ function updateSetting(id: string, value: SettingValue | null): void { Settings.updateValueById(id, value); } } else { - Settings.updateValueById(id, undefined); + Settings.updateValueById(id, null); } } diff --git a/apps/meteor/server/cron/statistics.js b/apps/meteor/server/cron/statistics.js index 1c8a382cd367..2910a158bb2f 100644 --- a/apps/meteor/server/cron/statistics.js +++ b/apps/meteor/server/cron/statistics.js @@ -16,7 +16,7 @@ async function generateStatistics(logger) { try { const headers = {}; - const token = getWorkspaceAccessToken(); + const token = await getWorkspaceAccessToken(); if (token) { headers.Authorization = `Bearer ${token}`; diff --git a/apps/meteor/server/lib/sendMessagesToAdmins.ts b/apps/meteor/server/lib/sendMessagesToAdmins.ts index 321e9c6a44e8..c26adaa88880 100644 --- a/apps/meteor/server/lib/sendMessagesToAdmins.ts +++ b/apps/meteor/server/lib/sendMessagesToAdmins.ts @@ -33,7 +33,7 @@ export async function sendMessagesToAdmins({ }: { fromId?: string; checkFrom?: boolean; - msgs?: Partial[] | Function; + msgs?: Partial[] | (({ adminUser }: { adminUser: IUser }) => Partial[]); banners?: Banner[] | Function; }): Promise { const fromUser = checkFrom ? await Users.findOneById(fromId, { projection: { _id: 1 } }) : true; diff --git a/apps/meteor/server/methods/getSetupWizardParameters.js b/apps/meteor/server/methods/getSetupWizardParameters.ts similarity index 66% rename from apps/meteor/server/methods/getSetupWizardParameters.js rename to apps/meteor/server/methods/getSetupWizardParameters.ts index a26ab8f329b5..56fae089374c 100644 --- a/apps/meteor/server/methods/getSetupWizardParameters.js +++ b/apps/meteor/server/methods/getSetupWizardParameters.ts @@ -1,11 +1,11 @@ +import { Settings } from '@rocket.chat/models'; import { Meteor } from 'meteor/meteor'; -import { Settings } from '../../app/models/server'; import { settings } from '../../app/settings/server'; Meteor.methods({ - getSetupWizardParameters() { - const setupWizardSettings = Settings.findSetupWizardSettings().fetch(); + async getSetupWizardParameters() { + const setupWizardSettings = await Settings.findSetupWizardSettings().toArray(); const serverAlreadyRegistered = !!settings.get('Cloud_Workspace_Client_Id') || process.env.DEPLOY_PLATFORM === 'rocket-cloud'; return { diff --git a/apps/meteor/server/models/raw/PushToken.ts b/apps/meteor/server/models/raw/PushToken.ts index 22feb733fc86..c58e6cab2597 100644 --- a/apps/meteor/server/models/raw/PushToken.ts +++ b/apps/meteor/server/models/raw/PushToken.ts @@ -23,4 +23,10 @@ export class PushTokenRaw extends BaseRaw implements IPushTokenModel authToken: { $nin: tokens }, }); } + + removeAllByUserId(userId: string): Promise { + return this.deleteMany({ + userId, + }); + } } diff --git a/apps/meteor/server/models/raw/Settings.ts b/apps/meteor/server/models/raw/Settings.ts index eaccab8e772d..3a5d150c0158 100644 --- a/apps/meteor/server/models/raw/Settings.ts +++ b/apps/meteor/server/models/raw/Settings.ts @@ -50,7 +50,10 @@ export class SettingsRaw extends BaseRaw implements ISettingsModel { return this.find(query); } - updateValueById(_id: string, value: T): Promise { + updateValueById( + _id: string, + value: (ISetting['value'] extends undefined ? never : ISetting['value']) | null, + ): Promise { const query = { blocked: { $ne: true }, value: { $ne: value }, @@ -207,4 +210,8 @@ export class SettingsRaw extends BaseRaw implements ISettingsModel { }, }); } + + findEnterpriseSettings(): FindCursor { + return this.find({ enterprise: true }); + } } diff --git a/apps/meteor/server/services/meteor/service.ts b/apps/meteor/server/services/meteor/service.ts index e14a5a721abb..41165ee95544 100644 --- a/apps/meteor/server/services/meteor/service.ts +++ b/apps/meteor/server/services/meteor/service.ts @@ -104,19 +104,20 @@ if (disableOplog) { // Re-implement meteor's reactivity that uses observe to disconnect sessions when the token // associated was removed processOnChange = (diff: Record, id: string): void => { + if (!diff || !('services.resume.loginTokens' in diff)) { + return; + } const loginTokens: undefined | { hashedToken: string }[] = diff['services.resume.loginTokens']; - if (loginTokens) { - const tokens = loginTokens.map(({ hashedToken }) => hashedToken); - - const cbs = userCallbacks.get(id); - if (cbs) { - [...cbs] - .filter(({ hashedToken }) => !tokens.includes(hashedToken)) - .forEach((item) => { - item.callbacks.removed(id); - cbs.delete(item); - }); - } + const tokens = loginTokens?.map(({ hashedToken }) => hashedToken); + + const cbs = userCallbacks.get(id); + if (cbs) { + [...cbs] + .filter(({ hashedToken }) => tokens === undefined || !tokens.includes(hashedToken)) + .forEach((item) => { + item.callbacks.removed(id); + cbs.delete(item); + }); } }; } diff --git a/apps/meteor/server/services/nps/getAndCreateNpsSurvey.ts b/apps/meteor/server/services/nps/getAndCreateNpsSurvey.ts index a4de08f90e22..5c7efb8d8021 100644 --- a/apps/meteor/server/services/nps/getAndCreateNpsSurvey.ts +++ b/apps/meteor/server/services/nps/getAndCreateNpsSurvey.ts @@ -18,7 +18,7 @@ type NpsSurveyData = { }; export const getAndCreateNpsSurvey = Meteor.bindEnvironment(async function getNpsSurvey(npsId: string) { - const token = getWorkspaceAccessToken(); + const token = await getWorkspaceAccessToken(); if (!token) { return false; } diff --git a/apps/meteor/server/services/nps/sendNpsResults.ts b/apps/meteor/server/services/nps/sendNpsResults.ts index 62751c691a0b..b7c585bf7deb 100644 --- a/apps/meteor/server/services/nps/sendNpsResults.ts +++ b/apps/meteor/server/services/nps/sendNpsResults.ts @@ -12,7 +12,7 @@ type NPSResultPayload = { }; export const sendNpsResults = Meteor.bindEnvironment(function sendNpsResults(npsId: string, data: NPSResultPayload) { - const token = getWorkspaceAccessToken(); + const token = Promise.await(getWorkspaceAccessToken()); if (!token) { return false; } diff --git a/apps/meteor/server/services/push/service.ts b/apps/meteor/server/services/push/service.ts index 399becf928b1..c62c9f099904 100644 --- a/apps/meteor/server/services/push/service.ts +++ b/apps/meteor/server/services/push/service.ts @@ -10,14 +10,18 @@ export class PushService extends ServiceClassInternal implements IPushService { super(); this.onEvent('watch.users', async ({ id, diff }) => { - if (diff && 'services.resume.loginTokens' in diff) { - const tokens = diff['services.resume.loginTokens'].map(({ hashedToken }: { hashedToken: string }) => hashedToken); - this.cleanUpUserTokens(id, tokens); + if (!diff || !('services.resume.loginTokens' in diff)) { + return; + } + if (diff['services.resume.loginTokens'] === undefined) { + await PushToken.removeAllByUserId(id); + return; + } + const loginTokens = Array.isArray(diff['services.resume.loginTokens']) ? diff['services.resume.loginTokens'] : []; + const tokens = loginTokens.map(({ hashedToken }: { hashedToken: string }) => hashedToken); + if (tokens.length > 0) { + await PushToken.removeByUserIdExceptTokens(id, tokens); } }); } - - private async cleanUpUserTokens(userId: string, tokens: string[]): Promise { - await PushToken.removeByUserIdExceptTokens(userId, tokens); - } } diff --git a/apps/meteor/server/services/voip/connector/asterisk/CommandHandler.ts b/apps/meteor/server/services/voip/connector/asterisk/CommandHandler.ts index c2081d638191..28a9b3147542 100644 --- a/apps/meteor/server/services/voip/connector/asterisk/CommandHandler.ts +++ b/apps/meteor/server/services/voip/connector/asterisk/CommandHandler.ts @@ -15,7 +15,7 @@ */ import { Db } from 'mongodb'; import type { IVoipConnectorResult, IManagementServerConnectionStatus } from '@rocket.chat/core-typings'; -import { IManagementConfigData, ServerType } from '@rocket.chat/core-typings'; +import { IManagementConfigData } from '@rocket.chat/core-typings'; import { Commands } from './Commands'; import { IConnection } from './IConnection'; @@ -24,7 +24,7 @@ import { Command, CommandType } from './Command'; import { AMIConnection } from './ami/AMIConnection'; import { CommandFactory } from './ami/CommandFactory'; import { WebsocketConnection } from '../websocket/WebsocketConnection'; -import { getServerConfigDataFromSettings } from '../../lib/Helper'; +import { getManagementServerConfig } from '../../lib/Helper'; const version = 'Asterisk Connector 1.0'; @@ -45,12 +45,9 @@ export class CommandHandler { async initConnection(commandType: CommandType): Promise { // Initialize available connections - // const connection = new AMIConnection(); const connection = new AMIConnection(); - let config: any = null; - if (commandType === CommandType.AMI) { - config = getServerConfigDataFromSettings(ServerType.MANAGEMENT); - } + + const config = commandType === CommandType.AMI ? getManagementServerConfig() : undefined; if (!config) { this.logger.warn('Management server configuration not found'); return; @@ -64,6 +61,12 @@ export class CommandHandler { this.connections.get(commandType)?.closeConnection(); this.connections.delete(commandType); } + + if (!config.host) { + this.logger.error('Invalid host'); + return; + } + try { await connection.connect( config.host, @@ -73,10 +76,14 @@ export class CommandHandler { ); this.connections.set(commandType, connection); this.continuousMonitor = CommandFactory.getCommandObject(Commands.event_stream, this.db); - this.continuousMonitor.connection = this.connections.get(this.continuousMonitor.type) as IConnection; + const continuousMonitor = this.connections.get(this.continuousMonitor.type); + if (!continuousMonitor) { + throw new Error(`No connection for ${this.continuousMonitor.type}`); + } + this.continuousMonitor.connection = continuousMonitor; this.continuousMonitor.initMonitor({}); - } catch (error: any) { - this.logger.error({ msg: 'Management server connection error', error }); + } catch (err: unknown) { + this.logger.error({ msg: 'Management server connection error', err }); } } diff --git a/apps/meteor/server/services/voip/lib/Helper.ts b/apps/meteor/server/services/voip/lib/Helper.ts index af50b3e27e16..4de70c7527cd 100644 --- a/apps/meteor/server/services/voip/lib/Helper.ts +++ b/apps/meteor/server/services/voip/lib/Helper.ts @@ -2,31 +2,33 @@ import { ServerType, IVoipManagementServerConfig, IVoipCallServerConfig } from ' import { settings } from '../../../../app/settings/server/cached'; +export function getManagementServerConfig(): IVoipManagementServerConfig { + return { + type: ServerType.MANAGEMENT, + host: settings.get('VoIP_Management_Server_Host'), + name: settings.get('VoIP_Management_Server_Name'), + configData: { + port: Number(settings.get('VoIP_Management_Server_Port')), + username: settings.get('VoIP_Management_Server_Username'), + password: settings.get('VoIP_Management_Server_Password'), + }, + }; +} + export function getServerConfigDataFromSettings(type: ServerType): IVoipCallServerConfig | IVoipManagementServerConfig { switch (type) { case ServerType.CALL_SERVER: { - const serverConfig: IVoipCallServerConfig = { + return { type: ServerType.CALL_SERVER, name: settings.get('VoIP_Server_Name'), configData: { websocketPath: settings.get('VoIP_Server_Websocket_Path'), }, }; - return serverConfig; } case ServerType.MANAGEMENT: { - const serverConfig: IVoipManagementServerConfig = { - type: ServerType.MANAGEMENT, - host: settings.get('VoIP_Management_Server_Host'), - name: settings.get('VoIP_Management_Server_Name'), - configData: { - port: Number(settings.get('VoIP_Management_Server_Port')), - username: settings.get('VoIP_Management_Server_Username'), - password: settings.get('VoIP_Management_Server_Password'), - }, - }; - return serverConfig; + return getManagementServerConfig(); } } } diff --git a/apps/meteor/server/startup/migrations/v246.ts b/apps/meteor/server/startup/migrations/v246.ts index 826b5c12579e..f7a2d05994cb 100644 --- a/apps/meteor/server/startup/migrations/v246.ts +++ b/apps/meteor/server/startup/migrations/v246.ts @@ -1,5 +1,6 @@ +import { Settings } from '@rocket.chat/models'; + import { addMigration } from '../../lib/migrations'; -import { Settings } from '../../../app/models/server'; import { settings } from '../../../app/settings/server'; addMigration({ @@ -7,11 +8,12 @@ addMigration({ up() { const livechatVideoCallEnabled = settings.get('Livechat_videocall_enabled'); if (livechatVideoCallEnabled) { - Settings.upsert( + Settings.updateOne( { _id: 'Omnichannel_call_provider' }, { $set: { value: 'Jitsi' }, }, + { upsert: true }, ); } Settings.removeById('Livechat_videocall_enabled'); @@ -20,11 +22,12 @@ addMigration({ const webRTCEnableDirect = settings.get('WebRTC_Enable_Direct'); const webRTCEnablePrivate = settings.get('WebRTC_Enable_Private'); if (webRTCEnableChannel || webRTCEnableDirect || webRTCEnablePrivate) { - Settings.upsert( + Settings.updateOne( { _id: 'WebRTC_Enabled' }, { $set: { value: true }, }, + { upsert: true }, ); } }, diff --git a/apps/meteor/tests/e2e/channel-management.spec.ts b/apps/meteor/tests/e2e/channel-management.spec.ts index 42a9aacb74b2..86b0b4bd9080 100644 --- a/apps/meteor/tests/e2e/channel-management.spec.ts +++ b/apps/meteor/tests/e2e/channel-management.spec.ts @@ -8,8 +8,8 @@ test.describe.serial('channel-management', () => { let poHomeChannel: HomeChannel; let targetChannel: string; - test.beforeAll(async ({ browser }) => { - targetChannel = await createTargetChannel(browser); + test.beforeAll(async ({ api }) => { + targetChannel = await createTargetChannel(api); }); test.beforeEach(async ({ page }) => { diff --git a/apps/meteor/tests/e2e/emojis.spec.ts b/apps/meteor/tests/e2e/emojis.spec.ts index 64fc11861800..41b713e9ed40 100644 --- a/apps/meteor/tests/e2e/emojis.spec.ts +++ b/apps/meteor/tests/e2e/emojis.spec.ts @@ -8,8 +8,8 @@ test.describe.serial('emoji', () => { let poHomeChannel: HomeChannel; let targetChannel: string; - test.beforeAll(async ({ browser }) => { - targetChannel = await createTargetChannel(browser); + test.beforeAll(async ({ api }) => { + targetChannel = await createTargetChannel(api); }); test.beforeEach(async ({ page }) => { diff --git a/apps/meteor/tests/e2e/file-upload.spec.ts b/apps/meteor/tests/e2e/file-upload.spec.ts index ba19ba0e4a94..5d4b439af84f 100644 --- a/apps/meteor/tests/e2e/file-upload.spec.ts +++ b/apps/meteor/tests/e2e/file-upload.spec.ts @@ -8,8 +8,8 @@ test.describe.serial('file-upload', () => { let poHomeChannel: HomeChannel; let targetChannel: string; - test.beforeAll(async ({ browser }) => { - targetChannel = await createTargetChannel(browser); + test.beforeAll(async ({ api }) => { + targetChannel = await createTargetChannel(api); }); test.beforeEach(async ({ page }) => { diff --git a/apps/meteor/tests/e2e/message-actions.spec.ts b/apps/meteor/tests/e2e/message-actions.spec.ts index de88828b8128..d0a68c624524 100644 --- a/apps/meteor/tests/e2e/message-actions.spec.ts +++ b/apps/meteor/tests/e2e/message-actions.spec.ts @@ -8,8 +8,8 @@ test.describe.serial('message-actions', () => { let poHomeChannel: HomeChannel; let targetChannel: string; - test.beforeAll(async ({ browser }) => { - targetChannel = await createTargetChannel(browser); + test.beforeAll(async ({ api }) => { + targetChannel = await createTargetChannel(api); }); test.beforeEach(async ({ page }) => { diff --git a/apps/meteor/tests/e2e/messaging.spec.ts b/apps/meteor/tests/e2e/messaging.spec.ts index 9449bf0f1da5..2b4f25b89b64 100644 --- a/apps/meteor/tests/e2e/messaging.spec.ts +++ b/apps/meteor/tests/e2e/messaging.spec.ts @@ -18,8 +18,8 @@ test.describe.serial('Messaging', () => { let poHomeChannel: HomeChannel; let targetChannel: string; - test.beforeAll(async ({ browser }) => { - targetChannel = await createTargetChannel(browser); + test.beforeAll(async ({ api }) => { + targetChannel = await createTargetChannel(api); }); test.beforeEach(async ({ page }) => { diff --git a/apps/meteor/tests/e2e/permissions.spec.ts b/apps/meteor/tests/e2e/permissions.spec.ts index c05273a34619..6ca8e020bd1a 100644 --- a/apps/meteor/tests/e2e/permissions.spec.ts +++ b/apps/meteor/tests/e2e/permissions.spec.ts @@ -1,5 +1,4 @@ import { test, expect } from './utils/test'; -import { BASE_API_URL, ADMIN_CREDENTIALS } from './config/constants'; import { HomeChannel } from './page-objects'; import { createTargetChannel } from './utils'; @@ -7,18 +6,10 @@ test.use({ storageState: 'user2-session.json' }); test.describe.serial('permissions', () => { let poHomeChannel: HomeChannel; - let apiSessionHeaders: { 'X-Auth-Token': string; 'X-User-Id': string }; let targetChannel: string; - test.beforeAll(async ({ request }) => { - const response = await request.post(`${BASE_API_URL}/login`, { data: ADMIN_CREDENTIALS }); - const { userId, authToken } = (await response.json()).data; - - apiSessionHeaders = { 'X-Auth-Token': authToken, 'X-User-Id': userId }; - }); - - test.beforeAll(async ({ browser }) => { - targetChannel = await createTargetChannel(browser); + test.beforeAll(async ({ api }) => { + targetChannel = await createTargetChannel(api); }); test.beforeEach(async ({ page }) => { @@ -28,13 +19,10 @@ test.describe.serial('permissions', () => { }); test.describe.serial('Edit message', () => { - test.beforeAll(async ({ request }) => { - const response = await request.post(`${BASE_API_URL}/settings/Message_AllowEditing`, { - headers: apiSessionHeaders, - data: { value: false }, - }); + test.beforeAll(async ({ api }) => { + const statusCode = (await api.post('/settings/Message_AllowEditing', { value: false })).status(); - expect(response.status()).toBe(200); + expect(statusCode).toBe(200); }); test('expect option(edit) not be visible', async () => { @@ -45,24 +33,18 @@ test.describe.serial('permissions', () => { await expect(poHomeChannel.content.btnOptionEditMessage).toBeHidden(); }); - test.afterAll(async ({ request }) => { - const response = await request.post(`${BASE_API_URL}/settings/Message_AllowEditing`, { - headers: apiSessionHeaders, - data: { value: true }, - }); + test.afterAll(async ({ api }) => { + const statusCode = (await api.post('/settings/Message_AllowEditing', { value: true })).status(); - expect(response.status()).toBe(200); + expect(statusCode).toBe(200); }); }); test.describe.serial('Delete message', () => { - test.beforeAll(async ({ request }) => { - const response = await request.post(`${BASE_API_URL}/settings/Message_AllowDeleting`, { - headers: apiSessionHeaders, - data: { value: false }, - }); + test.beforeAll(async ({ api }) => { + const statusCode = (await api.post('/settings/Message_AllowDeleting', { value: false })).status(); - expect(response.status()).toBe(200); + expect(statusCode).toBe(200); }); test('expect option(delete) not be visible', async () => { @@ -73,26 +55,20 @@ test.describe.serial('permissions', () => { await expect(poHomeChannel.content.btnOptionDeleteMessage).toBeHidden(); }); - test.afterAll(async ({ request }) => { - const response = await request.post(`${BASE_API_URL}/settings/Message_AllowDeleting`, { - headers: apiSessionHeaders, - data: { value: true }, - }); + test.afterAll(async ({ api }) => { + const statusCode = (await api.post('/settings/Message_AllowDeleting', { value: true })).status(); - expect(response.status()).toBe(200); + expect(statusCode).toBe(200); }); }); test.describe.serial('Pin message', () => { test.use({ storageState: 'admin-session.json' }); - test.beforeAll(async ({ request }) => { - const response = await request.post(`${BASE_API_URL}/settings/Message_AllowPinning`, { - headers: apiSessionHeaders, - data: { value: false }, - }); + test.beforeAll(async ({ api }) => { + const statusCode = (await api.post('/settings/Message_AllowPinning', { value: false })).status(); - expect(response.status()).toBe(200); + expect(statusCode).toBe(200); }); test('expect option(pin) not be visible', async () => { @@ -103,26 +79,20 @@ test.describe.serial('permissions', () => { await expect(poHomeChannel.content.btnOptionPinMessage).toBeHidden(); }); - test.afterAll(async ({ request }) => { - const response = await request.post(`${BASE_API_URL}/settings/Message_AllowPinning`, { - headers: apiSessionHeaders, - data: { value: true }, - }); + test.afterAll(async ({ api }) => { + const statusCode = (await api.post('/settings/Message_AllowPinning', { value: true })).status(); - expect(response.status()).toBe(200); + expect(statusCode).toBe(200); }); }); // FIXME: Wrong behavior in Rocket.chat, currently it shows the button // and after a click a "not allowed" alert pops up test.describe.skip('Star message', () => { - test.beforeAll(async ({ request }) => { - const response = await request.post(`${BASE_API_URL}/settings/Message_AllowStarring`, { - headers: apiSessionHeaders, - data: { value: false }, - }); + test.beforeAll(async ({ api }) => { + const statusCode = (await api.post('/settings/Message_AllowStarring', { value: false })).status(); - expect(response.status()).toBe(200); + expect(statusCode).toBe(200); }); test('expect option(star) not be visible', async () => { @@ -133,24 +103,18 @@ test.describe.serial('permissions', () => { await expect(poHomeChannel.content.btnOptionStarMessage).toBeHidden(); }); - test.afterAll(async ({ request }) => { - const response = await request.post(`${BASE_API_URL}/settings/Message_AllowStarring`, { - headers: apiSessionHeaders, - data: { value: true }, - }); + test.afterAll(async ({ api }) => { + const statusCode = (await api.post('/settings/Message_AllowStarring', { value: true })).status(); - expect(response.status()).toBe(200); + expect(statusCode).toBe(200); }); }); test.describe.serial('Upload file', () => { - test.beforeAll(async ({ request }) => { - const response = await request.post(`${BASE_API_URL}/settings/FileUpload_Enabled`, { - headers: apiSessionHeaders, - data: { value: false }, - }); + test.beforeAll(async ({ api }) => { + const statusCode = (await api.post('/settings/FileUpload_Enabled', { value: false })).status(); - expect(response.status()).toBe(200); + expect(statusCode).toBe(200); }); test('expect option(upload file) not be visible', async () => { @@ -160,24 +124,18 @@ test.describe.serial('permissions', () => { await expect(poHomeChannel.content.btnOptionFileUpload).toBeHidden(); }); - test.afterAll(async ({ request }) => { - const response = await request.post(`${BASE_API_URL}/settings/FileUpload_Enabled`, { - headers: apiSessionHeaders, - data: { value: true }, - }); + test.afterAll(async ({ api }) => { + const statusCode = (await api.post('/settings/FileUpload_Enabled', { value: true })).status(); - expect(response.status()).toBe(200); + expect(statusCode).toBe(200); }); }); test.describe.serial('Upload audio', () => { - test.beforeAll(async ({ request }) => { - const response = await request.post(`${BASE_API_URL}/settings/Message_AudioRecorderEnabled`, { - headers: apiSessionHeaders, - data: { value: false }, - }); + test.beforeAll(async ({ api }) => { + const statusCode = (await api.post('/settings/Message_AudioRecorderEnabled', { value: false })).status(); - expect(response.status()).toBe(200); + expect(statusCode).toBe(200); }); test('expect option(upload audio) not be visible', async () => { @@ -187,24 +145,18 @@ test.describe.serial('permissions', () => { await expect(poHomeChannel.content.btnRecordAudio).toBeHidden(); }); - test.afterAll(async ({ request }) => { - const response = await request.post(`${BASE_API_URL}/settings/Message_AudioRecorderEnabled`, { - headers: apiSessionHeaders, - data: { value: true }, - }); + test.afterAll(async ({ api }) => { + const statusCode = (await api.post('/settings/Message_AudioRecorderEnabled', { value: true })).status(); - expect(response.status()).toBe(200); + expect(statusCode).toBe(200); }); }); test.describe.serial('Upload video', () => { - test.beforeAll(async ({ request }) => { - const response = await request.post(`${BASE_API_URL}/settings/Message_VideoRecorderEnabled`, { - headers: apiSessionHeaders, - data: { value: false }, - }); + test.beforeAll(async ({ api }) => { + const statusCode = (await api.post('/settings/Message_VideoRecorderEnabled', { value: false })).status(); - expect(response.status()).toBe(200); + expect(statusCode).toBe(200); }); test('expect option(upload video) not be visible', async () => { @@ -214,31 +166,20 @@ test.describe.serial('permissions', () => { await expect(poHomeChannel.content.btnVideoMessage).toBeHidden(); }); - test.afterAll(async ({ request }) => { - const response = await request.post(`${BASE_API_URL}/settings/Message_VideoRecorderEnabled`, { - headers: apiSessionHeaders, - data: { value: true }, - }); + test.afterAll(async ({ api }) => { + const statusCode = (await api.post('/settings/Message_VideoRecorderEnabled', { value: true })).status(); - expect(response.status()).toBe(200); + expect(statusCode).toBe(200); }); }); - // FIXME: test.describe.skip('Filter words', () => { - test.beforeAll(async ({ request }) => { - const response1 = await request.post(`${BASE_API_URL}/settings/Message_AllowBadWordsFilter`, { - headers: apiSessionHeaders, - data: { value: false }, - }); - - const response2 = await request.post(`${BASE_API_URL}/settings/Message_BadWordsFilterList`, { - headers: apiSessionHeaders, - data: { value: 'badword' }, - }); - - expect(response1.status()).toBe(200); - expect(response2.status()).toBe(200); + test.beforeAll(async ({ api }) => { + const statusCode1 = (await api.post('/settings/Message_AllowBadWordsFilter', { value: true })).status(); + const statusCode2 = (await api.post('/settings/Message_BadWordsFilterList', { value: 'badword' })).status(); + + expect(statusCode1).toBe(200); + expect(statusCode2).toBe(200); }); test('expect badword be censored', async () => { @@ -248,13 +189,10 @@ test.describe.serial('permissions', () => { await expect(poHomeChannel.content.lastUserMessage).toContainText('*'.repeat(7)); }); - test.afterAll(async ({ request }) => { - const response = await request.post(`${BASE_API_URL}/settings/Message_AllowBadWordsFilter`, { - headers: apiSessionHeaders, - data: { value: true }, - }); + test.afterAll(async ({ api }) => { + const statusCode = (await api.post('/settings/Message_AllowBadWordsFilter', { value: false })).status(); - expect(response.status()).toBe(200); + expect(statusCode).toBe(200); }); }); }); diff --git a/apps/meteor/tests/e2e/utils/create-target-channel.ts b/apps/meteor/tests/e2e/utils/create-target-channel.ts index 1c6fa876ebfd..0918305dcb43 100644 --- a/apps/meteor/tests/e2e/utils/create-target-channel.ts +++ b/apps/meteor/tests/e2e/utils/create-target-channel.ts @@ -1,19 +1,15 @@ -import { Browser } from '@playwright/test'; import faker from '@faker-js/faker'; -import { HomeChannel } from '../page-objects/home-channel'; +import { BaseTest } from './test'; /** * createTargetChannel: * - Usefull to create a target channel for message related tests */ -export async function createTargetChannel(browser: Browser): Promise { - const page = await browser.newPage({ storageState: 'admin-session.json' }); +export async function createTargetChannel(api: BaseTest['api']): Promise { const name = faker.datatype.uuid(); - await page.goto('/home'); - await new HomeChannel(page).sidenav.createPublicChannel(name); - await page.close(); + await api.post('/channels.create', { name }); return name; } diff --git a/apps/meteor/tests/e2e/utils/test.ts b/apps/meteor/tests/e2e/utils/test.ts index 8809ce925996..1da94570dc80 100644 --- a/apps/meteor/tests/e2e/utils/test.ts +++ b/apps/meteor/tests/e2e/utils/test.ts @@ -2,11 +2,25 @@ import * as fs from 'fs'; import * as path from 'path'; import { v4 as uuid } from 'uuid'; -import { test as baseTest } from '@playwright/test'; +import { APIResponse, test as baseTest } from '@playwright/test'; + +import { BASE_API_URL, ADMIN_CREDENTIALS } from '../config/constants'; const PATH_NYC_OUTPUT = path.join(process.cwd(), '.nyc_output'); + +export type AnyObj = { [key: string]: any }; + +export type BaseTest = { + api: { + get(uri: string): Promise; + post(uri: string, data: AnyObj): Promise; + put(uri: string, data: AnyObj): Promise; + delete(uri: string): Promise; + }; +}; + // @ts-ignore -export const test = baseTest.extend({ +export const test = baseTest.extend({ context: async ({ context }, use) => { if (!process.env.E2E_COVERAGE) { await use(context); @@ -36,6 +50,31 @@ export const test = baseTest.extend({ }), ); }, + + api: async ({ request }, use) => { + const resp = await request.post(`${BASE_API_URL}/login`, { data: ADMIN_CREDENTIALS }); + const json = await resp.json(); + + const headers = { + 'X-Auth-Token': json.data.authToken, + 'X-User-Id': json.data.userId, + }; + + await use({ + get(uri: string) { + return request.get(BASE_API_URL + uri, { headers }); + }, + post(uri: string, data: AnyObj) { + return request.post(BASE_API_URL + uri, { headers, data }); + }, + put(uri: string, data: AnyObj) { + return request.put(BASE_API_URL + uri, { headers, data }); + }, + delete(uri: string) { + return request.delete(BASE_API_URL + uri, { headers }); + }, + }); + }, }); export const { expect } = test; diff --git a/apps/meteor/tests/end-to-end/api/01-users.js b/apps/meteor/tests/end-to-end/api/01-users.js index d7a559ed9cf5..ed30f8a0ce3f 100644 --- a/apps/meteor/tests/end-to-end/api/01-users.js +++ b/apps/meteor/tests/end-to-end/api/01-users.js @@ -92,8 +92,8 @@ describe('[Users]', function () { before((done) => clearCustomFields(done)); after((done) => clearCustomFields(done)); - it('should create a new user', (done) => { - request + it('should create a new user', async () => { + await request .post(api('users.create')) .set(credentials) .send({ @@ -120,8 +120,16 @@ describe('[Users]', function () { targetUser._id = res.body.user._id; targetUser.username = res.body.user.username; + }); + + await request + .post(api('login')) + .send({ + user: apiUsername, + password, }) - .end(done); + .expect('Content-Type', 'application/json') + .expect(200); }); it('should create a new user with custom fields', (done) => { @@ -3270,6 +3278,19 @@ describe('[Users]', function () { await updateSetting('Accounts_AllowInvisibleStatusOption', true); }); + it('should return an error when the payload is missing all supported fields', (done) => { + request + .post(api('users.setStatus')) + .set(credentials) + .send({}) + .expect('Content-Type', 'application/json') + .expect(400) + .expect((res) => { + expect(res.body).to.have.property('success', false); + expect(res.body.error).to.be.equal('Match error: Failed Match.OneOf, Match.Maybe or Match.Optional validation'); + }) + .end(done); + }); }); describe('[/users.removeOtherTokens]', () => { diff --git a/apps/meteor/tests/unit/app/settings/server/functions/settings.tests.ts b/apps/meteor/tests/unit/app/settings/server/functions/settings.tests.ts index 507a3d8e3927..d2f2fd04630e 100644 --- a/apps/meteor/tests/unit/app/settings/server/functions/settings.tests.ts +++ b/apps/meteor/tests/unit/app/settings/server/functions/settings.tests.ts @@ -11,12 +11,12 @@ describe('Settings', () => { process.env = {}; }); - it('should not insert the same setting twice', () => { + it('should not insert the same setting twice', async () => { const settings = new CachedSettings(); Settings.settings = settings; - settings.initilized(); + settings.initialized(); const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings as any }); - settingsRegistry.addGroup('group', function () { + await settingsRegistry.addGroup('group', function () { this.section('section', function () { this.add('my_setting', true, { type: 'boolean', @@ -44,7 +44,7 @@ describe('Settings', () => { autocomplete: true, }); - settingsRegistry.addGroup('group', function () { + await settingsRegistry.addGroup('group', function () { this.section('section', function () { this.add('my_setting', true, { type: 'boolean', @@ -58,7 +58,7 @@ describe('Settings', () => { expect(Settings.findOne({ _id: 'my_setting' }).value).to.be.equal(true); - settingsRegistry.addGroup('group', function () { + await settingsRegistry.addGroup('group', function () { this.section('section', function () { this.add('my_setting2', false, { type: 'boolean', @@ -74,15 +74,15 @@ describe('Settings', () => { expect(Settings.findOne({ _id: 'my_setting2' }).value).to.be.equal(false); }); - it('should respect override via environment as int', () => { + it('should respect override via environment as int', async () => { const settings = new CachedSettings(); Settings.settings = settings; - settings.initilized(); + settings.initialized(); const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings as any }); process.env.OVERWRITE_SETTING_my_setting = '1'; - settingsRegistry.addGroup('group', function () { + await settingsRegistry.addGroup('group', function () { this.section('section', function () { this.add('my_setting', 0, { type: 'int', @@ -114,7 +114,7 @@ describe('Settings', () => { process.env.OVERWRITE_SETTING_my_setting = '2'; - settingsRegistry.addGroup('group', function () { + await settingsRegistry.addGroup('group', function () { this.section('section', function () { this.add('my_setting', 0, { type: 'int', @@ -132,14 +132,14 @@ describe('Settings', () => { }); }); - it('should respect override via environment as boolean', () => { + it('should respect override via environment as boolean', async () => { process.env.OVERWRITE_SETTING_my_setting_bool = 'true'; const settings = new CachedSettings(); Settings.settings = settings; - settings.initilized(); + settings.initialized(); const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings as any }); - settingsRegistry.addGroup('group', function () { + await settingsRegistry.addGroup('group', function () { this.section('section', function () { this.add('my_setting_bool', false, { type: 'boolean', @@ -171,7 +171,7 @@ describe('Settings', () => { process.env.OVERWRITE_SETTING_my_setting_bool = 'false'; - settingsRegistry.addGroup('group', function () { + await settingsRegistry.addGroup('group', function () { this.section('section', function () { this.add('my_setting_bool', true, { type: 'boolean', @@ -189,14 +189,14 @@ describe('Settings', () => { }); }); - it('should respect override via environment as string', () => { + it('should respect override via environment as string', async () => { process.env.OVERWRITE_SETTING_my_setting_str = 'hey'; const settings = new CachedSettings(); Settings.settings = settings; - settings.initilized(); + settings.initialized(); const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings as any }); - settingsRegistry.addGroup('group', function () { + await settingsRegistry.addGroup('group', function () { this.section('section', function () { this.add('my_setting_str', '', { type: 'string', @@ -228,7 +228,7 @@ describe('Settings', () => { process.env.OVERWRITE_SETTING_my_setting_str = 'hey ho'; - settingsRegistry.addGroup('group', function () { + await settingsRegistry.addGroup('group', function () { this.section('section', function () { this.add('my_setting_str', 'hey', { type: 'string', @@ -247,14 +247,14 @@ describe('Settings', () => { }); }); - it('should respect initial value via environment', () => { + it('should respect initial value via environment', async () => { process.env.my_setting = '1'; const settings = new CachedSettings(); Settings.settings = settings; - settings.initilized(); + settings.initialized(); const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings as any }); - settingsRegistry.addGroup('group', function () { + await settingsRegistry.addGroup('group', function () { this.section('section', function () { this.add('my_setting', 0, { type: 'int', @@ -284,7 +284,7 @@ describe('Settings', () => { expect(Settings.upsertCalls).to.be.equal(0); expect(Settings.findOne({ _id: 'my_setting' })).to.include(expectedSetting); - settingsRegistry.addGroup('group', function () { + await settingsRegistry.addGroup('group', function () { this.section('section', function () { this.add('my_setting', 0, { type: 'int', @@ -298,33 +298,35 @@ describe('Settings', () => { expect(Settings.findOne({ _id: 'my_setting' })).to.include({ ...expectedSetting }); }); - it('should call `settings.get` callback on setting added', (done) => { - const settings = new CachedSettings(); - Settings.settings = settings; - settings.initilized(); - const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings as any }); - - const spiedCallback1 = spy(); - const spiedCallback2 = spy(); - - settingsRegistry.addGroup('group', function () { - this.section('section', function () { - this.add('setting_callback', 'value1', { - type: 'string', + it('should call `settings.get` callback on setting added', async () => { + return new Promise(async (resolve) => { + const settings = new CachedSettings(); + Settings.settings = settings; + settings.initialized(); + const settingsRegistry = new SettingsRegistry({ store: settings, model: Settings as any }); + + const spiedCallback1 = spy(); + const spiedCallback2 = spy(); + + await settingsRegistry.addGroup('group', function () { + this.section('section', function () { + this.add('setting_callback', 'value1', { + type: 'string', + }); }); }); - }); - settings.watch('setting_callback', spiedCallback1, { debounce: 10 }); - settings.watchByRegex(/setting_callback/, spiedCallback2, { debounce: 10 }); + settings.watch('setting_callback', spiedCallback1, { debounce: 10 }); + settings.watchByRegex(/setting_callback/, spiedCallback2, { debounce: 10 }); - setTimeout(() => { - expect(spiedCallback1).to.have.been.called.exactly(1); - expect(spiedCallback2).to.have.been.called.exactly(1); - expect(spiedCallback1).to.have.been.called.always.with('value1'); - expect(spiedCallback2).to.have.been.called.always.with('setting_callback', 'value1'); - done(); - }, settings.getConfig({ debounce: 10 }).debounce); + setTimeout(() => { + expect(spiedCallback1).to.have.been.called.exactly(1); + expect(spiedCallback2).to.have.been.called.exactly(1); + expect(spiedCallback1).to.have.been.called.always.with('value1'); + expect(spiedCallback2).to.have.been.called.always.with('setting_callback', 'value1'); + resolve(); + }, settings.getConfig({ debounce: 10 }).debounce); + }); }); it('should call `settings.watch` callback on setting changed registering before initialized', (done) => { @@ -337,7 +339,7 @@ describe('Settings', () => { settings.watch('setting_callback', spiedCallback1, { debounce: 1 }); settings.watchByRegex(/setting_callback/gi, spiedCallback2, { debounce: 1 }); - settings.initilized(); + settings.initialized(); settingsRegistry.addGroup('group', function () { this.section('section', function () { this.add('setting_callback', 'value2', { diff --git a/apps/meteor/tests/unit/app/settings/server/raw.tests.js b/apps/meteor/tests/unit/app/settings/server/raw.tests.js deleted file mode 100644 index 4aa6fe69f193..000000000000 --- a/apps/meteor/tests/unit/app/settings/server/raw.tests.js +++ /dev/null @@ -1,45 +0,0 @@ -import { expect, spy } from 'chai'; -import rewire from 'rewire'; - -describe('Raw Settings', () => { - let rawModule; - const cache = new Map(); - - before('rewire deps', () => { - const spied = spy(async (id) => { - if (id === '1') { - return 'some-setting-value'; - } - return null; - }); - - rawModule = rewire('../../../../../app/settings/server/raw'); - rawModule.__set__('setFromDB', spied); - rawModule.__set__('cache', cache); - }); - - it('should get the value from database when it isnt in cache', async () => { - const setting = await rawModule.getValue('1'); - - expect(setting).to.be.equal('some-setting-value'); - }); - - it('should get the value from cache when its available', async () => { - cache.set('2', 'supeer-setting'); - const setting = await rawModule.getValue('2'); - - expect(setting).to.be.equal('supeer-setting'); - }); - - it('should update the value in cache', async () => { - await rawModule.updateValue('2', { value: 'not-super-setting' }); - - expect(cache.get('2')).to.be.equal('not-super-setting'); - }); - - it('should not update the setting if the new value is undefined', async () => { - await rawModule.updateValue('2', {}); - - expect(cache.get('2')).to.be.equal('not-super-setting'); - }); -}); diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000000..afcd3f3b7b1e --- /dev/null +++ b/codecov.yml @@ -0,0 +1,8 @@ +coverage: + status: + project: + default: + target: auto + threshold: 1% +comment: + layout: 'reach, diff, flags' diff --git a/docker-compose-ci.yml b/docker-compose-ci.yml index 2303f5d0ca31..a067bf45b305 100644 --- a/docker-compose-ci.yml +++ b/docker-compose-ci.yml @@ -8,6 +8,7 @@ services: image: ghcr.io/${LOWERCASE_REPOSITORY}/rocket.chat:${RC_DOCKER_TAG} environment: - TEST_MODE=true + - EXIT_UNHANDLEDPROMISEREJECTION=true - "MONGO_URL=${MONGO_URL}" - "MONGO_OPLOG_URL=${MONGO_OPLOG_URL}" - "TRANSPORTER=${TRANSPORTER}" diff --git a/packages/core-typings/src/FeaturedApps.ts b/packages/core-typings/src/FeaturedApps.ts new file mode 100644 index 000000000000..efe32300abef --- /dev/null +++ b/packages/core-typings/src/FeaturedApps.ts @@ -0,0 +1,11 @@ +import type { App } from './Apps'; + +export type FeaturedAppsSections = { + sections: FeaturedAppsSection[]; +}; + +export type FeaturedAppsSection = { + i18nLabel: string; + slug: string; + apps: App[]; +}; diff --git a/packages/core-typings/src/ISetting.ts b/packages/core-typings/src/ISetting.ts index bd7f5d461e74..99b7e2cc27cd 100644 --- a/packages/core-typings/src/ISetting.ts +++ b/packages/core-typings/src/ISetting.ts @@ -12,7 +12,7 @@ export enum SettingEditor { type AssetValue = { defaultUrl?: string }; export type SettingValueMultiSelect = (string | number)[]; export type SettingValueRoomPick = Array<{ _id: string; name: string }> | string; -export type SettingValue = string | boolean | number | SettingValueMultiSelect | Date | AssetValue | undefined; +export type SettingValue = string | boolean | number | SettingValueMultiSelect | Date | AssetValue | undefined | null; export interface ISettingSelectOption { key: string | number; diff --git a/packages/core-typings/src/index.ts b/packages/core-typings/src/index.ts index 696b276dae12..6c9c8e5bcdda 100644 --- a/packages/core-typings/src/index.ts +++ b/packages/core-typings/src/index.ts @@ -1,4 +1,5 @@ export * from './Apps'; +export * from './FeaturedApps'; export * from './IRoom'; export * from './UIKit'; export * from './IMessage'; diff --git a/packages/model-typings/src/models/IPushTokenModel.ts b/packages/model-typings/src/models/IPushTokenModel.ts index 148b0d28a548..6295c3e6ea65 100644 --- a/packages/model-typings/src/models/IPushTokenModel.ts +++ b/packages/model-typings/src/models/IPushTokenModel.ts @@ -5,4 +5,6 @@ import type { IBaseModel } from './IBaseModel'; export interface IPushTokenModel extends IBaseModel { removeByUserIdExceptTokens(userId: string, tokens: string[]): Promise; + + removeAllByUserId(userId: string): Promise; } diff --git a/packages/model-typings/src/models/ISettingsModel.ts b/packages/model-typings/src/models/ISettingsModel.ts index 03079670fe10..ea7ab2d813fb 100644 --- a/packages/model-typings/src/models/ISettingsModel.ts +++ b/packages/model-typings/src/models/ISettingsModel.ts @@ -12,7 +12,10 @@ export interface ISettingsModel extends IBaseModel { findByIds(_id?: string[] | string): FindCursor; - updateValueById(_id: string, value: T): Promise; + updateValueById( + _id: string, + value: (ISetting['value'] extends undefined ? never : ISetting['value']) | null, + ): Promise; incrementValueById(_id: ISetting['_id'], value?: number): Promise; @@ -42,6 +45,8 @@ export interface ISettingsModel extends IBaseModel { findSetupWizardSettings(): FindCursor; + findEnterpriseSettings(): FindCursor; + addOptionValueById(_id: ISetting['_id'], option: ISettingSelectOption): Promise; findNotHiddenPublicUpdatedAfter(updatedAt: Date): FindCursor; diff --git a/packages/rest-typings/src/apps/index.ts b/packages/rest-typings/src/apps/index.ts index 313ba8dac422..6b7e5b8e043d 100644 --- a/packages/rest-typings/src/apps/index.ts +++ b/packages/rest-typings/src/apps/index.ts @@ -4,7 +4,7 @@ import type { IExternalComponent } from '@rocket.chat/apps-engine/definition/ext import type { IPermission } from '@rocket.chat/apps-engine/definition/permissions/IPermission'; import type { ISetting } from '@rocket.chat/apps-engine/definition/settings'; import type { IUIActionButton } from '@rocket.chat/apps-engine/definition/ui'; -import type { AppScreenshot, App } from '@rocket.chat/core-typings'; +import type { AppScreenshot, App, FeaturedAppsSections } from '@rocket.chat/core-typings'; export type AppsEndpoints = { '/apps/externalComponents': { @@ -96,6 +96,12 @@ export type AppsEndpoints = { }; }; + '/apps/featured': { + GET: () => { + sections: FeaturedAppsSections; + }; + }; + '/apps': { GET: | ((params: { buildExternalUrl: 'true'; purchaseType?: 'buy' | 'subscription'; appId?: string; details?: 'true' | 'false' }) => { diff --git a/packages/ui-contexts/src/DeviceContext.ts b/packages/ui-contexts/src/DeviceContext.ts index 545bb4353375..483f03707f95 100644 --- a/packages/ui-contexts/src/DeviceContext.ts +++ b/packages/ui-contexts/src/DeviceContext.ts @@ -10,7 +10,8 @@ export interface IExperimentalHTMLAudioElement extends HTMLAudioElement { setSinkId: (sinkId: string) => void; } -type DeviceContextValue = { +type EnabledDeviceContextValue = { + enabled: true; availableAudioOutputDevices: Device[]; availableAudioInputDevices: Device[]; // availableVideoInputDevices: Device[] @@ -22,22 +23,15 @@ type DeviceContextValue = { // setVideoInputDevice: (device: Device) => void; }; +type DisabledDeviceContextValue = { + enabled: false; +}; + +export type DeviceContextValue = EnabledDeviceContextValue | DisabledDeviceContextValue; + +export const isDeviceContextEnabled = (context: DeviceContextValue): context is EnabledDeviceContextValue => + (context as EnabledDeviceContextValue).enabled; + export const DeviceContext = createContext({ - availableAudioOutputDevices: [], - availableAudioInputDevices: [], - // availableVideoInputDevices: [], - selectedAudioOutputDevice: { - id: 'default', - label: '', - type: 'audio', - }, - selectedAudioInputDevice: { - id: 'default', - label: '', - type: 'audio', - }, - // selectedVideoInputDevice: undefined, - setAudioOutputDevice: () => undefined, - setAudioInputDevice: () => undefined, - // setVideoInputDevice: () => undefined, + enabled: false, }); diff --git a/packages/ui-contexts/src/hooks/useAvailableDevices.ts b/packages/ui-contexts/src/hooks/useAvailableDevices.ts index 5824c02e6374..59f43db533fc 100644 --- a/packages/ui-contexts/src/hooks/useAvailableDevices.ts +++ b/packages/ui-contexts/src/hooks/useAvailableDevices.ts @@ -1,13 +1,24 @@ import { useContext } from 'react'; -import { DeviceContext, Device } from '../DeviceContext'; +import { DeviceContext, Device, isDeviceContextEnabled } from '../DeviceContext'; type AvailableDevices = { audioInput?: Device[]; audioOutput?: Device[]; }; -export const useAvailableDevices = (): AvailableDevices => ({ - audioInput: useContext(DeviceContext).availableAudioInputDevices, - audioOutput: useContext(DeviceContext).availableAudioOutputDevices, -}); +export const useAvailableDevices = (): AvailableDevices | null => { + const context = useContext(DeviceContext); + + if (!isDeviceContextEnabled(context)) { + console.warn( + 'Device Management is disabled on unsecure contexts, see https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts', + ); + return null; + } + + return { + audioInput: context.availableAudioInputDevices, + audioOutput: context.availableAudioOutputDevices, + }; +}; diff --git a/packages/ui-contexts/src/hooks/useDeviceConstraints.ts b/packages/ui-contexts/src/hooks/useDeviceConstraints.ts index e21e318204b1..c7320159e426 100644 --- a/packages/ui-contexts/src/hooks/useDeviceConstraints.ts +++ b/packages/ui-contexts/src/hooks/useDeviceConstraints.ts @@ -1,8 +1,17 @@ import { useContext } from 'react'; -import { DeviceContext } from '../DeviceContext'; +import { DeviceContext, isDeviceContextEnabled } from '../DeviceContext'; -export const useDeviceConstraints = (): MediaStreamConstraints => { - const selectedAudioInputDeviceId = useContext(DeviceContext).selectedAudioInputDevice?.id; +export const useDeviceConstraints = (): MediaStreamConstraints | null => { + const context = useContext(DeviceContext); + + if (!isDeviceContextEnabled(context)) { + console.warn( + 'Device Management is disabled on unsecure contexts, see https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts', + ); + return null; + } + + const selectedAudioInputDeviceId = context.selectedAudioInputDevice?.id; return { audio: selectedAudioInputDeviceId === 'default' ? true : { deviceId: { exact: selectedAudioInputDeviceId } } }; }; diff --git a/packages/ui-contexts/src/hooks/useIsDeviceManagementEnabled.ts b/packages/ui-contexts/src/hooks/useIsDeviceManagementEnabled.ts new file mode 100644 index 000000000000..a453b5169542 --- /dev/null +++ b/packages/ui-contexts/src/hooks/useIsDeviceManagementEnabled.ts @@ -0,0 +1,5 @@ +import { useContext } from 'react'; + +import { DeviceContext } from '../DeviceContext'; + +export const useIsDeviceManagementEnabled = (): boolean => useContext(DeviceContext).enabled; diff --git a/packages/ui-contexts/src/hooks/useSelectedDevices.ts b/packages/ui-contexts/src/hooks/useSelectedDevices.ts index ce26b658b1c1..2a682d4ce8d2 100644 --- a/packages/ui-contexts/src/hooks/useSelectedDevices.ts +++ b/packages/ui-contexts/src/hooks/useSelectedDevices.ts @@ -1,13 +1,24 @@ import { useContext } from 'react'; -import { DeviceContext, Device } from '../DeviceContext'; +import { DeviceContext, Device, isDeviceContextEnabled } from '../DeviceContext'; type SelectedDevices = { audioInput?: Device; audioOutput?: Device; }; -export const useSelectedDevices = (): SelectedDevices => ({ - audioInput: useContext(DeviceContext).selectedAudioInputDevice, - audioOutput: useContext(DeviceContext).selectedAudioOutputDevice, -}); +export const useSelectedDevices = (): SelectedDevices | null => { + const context = useContext(DeviceContext); + + if (!isDeviceContextEnabled(context)) { + console.warn( + 'Device Management is disabled on unsecure contexts, see https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts', + ); + return null; + } + + return { + audioInput: context.selectedAudioInputDevice, + audioOutput: context.selectedAudioOutputDevice, + }; +}; diff --git a/packages/ui-contexts/src/hooks/useSetInputMediaDevice.ts b/packages/ui-contexts/src/hooks/useSetInputMediaDevice.ts index 801cf014802e..619c1899054b 100644 --- a/packages/ui-contexts/src/hooks/useSetInputMediaDevice.ts +++ b/packages/ui-contexts/src/hooks/useSetInputMediaDevice.ts @@ -1,9 +1,17 @@ import { useContext } from 'react'; -import { DeviceContext, Device } from '../DeviceContext'; +import { DeviceContext, Device, isDeviceContextEnabled } from '../DeviceContext'; type setInputMediaDevice = (inputDevice: Device) => void; export const useSetInputMediaDevice = (): setInputMediaDevice => { - return useContext(DeviceContext).setAudioInputDevice; + const context = useContext(DeviceContext); + + if (!isDeviceContextEnabled(context)) { + console.warn( + 'Device Management is disabled on unsecure contexts, see https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts', + ); + return () => undefined; + } + return context.setAudioInputDevice; }; diff --git a/packages/ui-contexts/src/hooks/useSetOutputMediaDevice.ts b/packages/ui-contexts/src/hooks/useSetOutputMediaDevice.ts index 531ad1607a00..a7832b52bc55 100644 --- a/packages/ui-contexts/src/hooks/useSetOutputMediaDevice.ts +++ b/packages/ui-contexts/src/hooks/useSetOutputMediaDevice.ts @@ -1,6 +1,6 @@ import { useContext } from 'react'; -import { DeviceContext, Device, IExperimentalHTMLAudioElement } from '../DeviceContext'; +import { DeviceContext, Device, IExperimentalHTMLAudioElement, isDeviceContextEnabled } from '../DeviceContext'; // This allows different places to set the output device by providing a HTMLAudioElement @@ -13,5 +13,14 @@ type setOutputMediaDevice = ({ }) => void; export const useSetOutputMediaDevice = (): setOutputMediaDevice => { - return useContext(DeviceContext).setAudioOutputDevice; + const context = useContext(DeviceContext); + + if (!isDeviceContextEnabled(context)) { + console.warn( + 'Device Management is disabled on unsecure contexts, see https://developer.mozilla.org/en-US/docs/Web/Security/Secure_Contexts/features_restricted_to_secure_contexts', + ); + return () => undefined; + } + + return context.setAudioOutputDevice; }; diff --git a/packages/ui-contexts/src/index.ts b/packages/ui-contexts/src/index.ts index bb57ddd38bb9..0e456969d1c8 100644 --- a/packages/ui-contexts/src/index.ts +++ b/packages/ui-contexts/src/index.ts @@ -13,7 +13,7 @@ export { ToastMessagesContext, ToastMessagesContextValue } from './ToastMessages export { TooltipContext, TooltipContextValue } from './TooltipContext'; export { TranslationContext, TranslationContextValue } from './TranslationContext'; export { UserContext, UserContextValue } from './UserContext'; -export { DeviceContext, Device, IExperimentalHTMLAudioElement } from './DeviceContext'; +export { DeviceContext, Device, IExperimentalHTMLAudioElement, DeviceContextValue } from './DeviceContext'; export { useAbsoluteUrl } from './hooks/useAbsoluteUrl'; export { useAllPermissions } from './hooks/useAllPermissions'; @@ -76,6 +76,7 @@ export { useUserSubscriptions } from './hooks/useUserSubscriptions'; export { useSelectedDevices } from './hooks/useSelectedDevices'; export { useDeviceConstraints } from './hooks/useDeviceConstraints'; export { useAvailableDevices } from './hooks/useAvailableDevices'; +export { useIsDeviceManagementEnabled } from './hooks/useIsDeviceManagementEnabled'; export { useSetOutputMediaDevice } from './hooks/useSetOutputMediaDevice'; export { useSetInputMediaDevice } from './hooks/useSetInputMediaDevice';