Skip to content

Commit

Permalink
Merge branch 'develop' into fix/forward-livechat-room
Browse files Browse the repository at this point in the history
  • Loading branch information
sampaiodiego committed Sep 27, 2024
2 parents 56d3cbf + 5965a1d commit a0b1672
Show file tree
Hide file tree
Showing 25 changed files with 154 additions and 195 deletions.
22 changes: 0 additions & 22 deletions .github/workflows/vulnerabilities-jira-integration.yml

This file was deleted.

1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ yarn-error.log*
.env.test.local
.env.production.local

storybook-static
# turbo
.turbo

Expand Down
27 changes: 14 additions & 13 deletions apps/meteor/app/e2e/client/rocketchat.e2e.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { isE2EEMessage } from '@rocket.chat/core-typings';
import { Emitter } from '@rocket.chat/emitter';
import EJSON from 'ejson';
import _ from 'lodash';
import { Accounts } from 'meteor/accounts-base';
import { Meteor } from 'meteor/meteor';
import { Tracker } from 'meteor/tracker';

Expand Down Expand Up @@ -308,8 +309,8 @@ class E2E extends Emitter {

getKeysFromLocalStorage(): KeyPair {
return {
public_key: Meteor._localStorage.getItem('public_key'),
private_key: Meteor._localStorage.getItem('private_key'),
public_key: Accounts.storageLocation.getItem('public_key'),
private_key: Accounts.storageLocation.getItem('private_key'),
};
}

Expand All @@ -332,7 +333,7 @@ class E2E extends Emitter {
imperativeModal.close();
},
onConfirm: () => {
Meteor._localStorage.removeItem('e2e.randomPassword');
Accounts.storageLocation.removeItem('e2e.randomPassword');
this.setState(E2EEState.READY);
dispatchToastMessage({ type: 'success', message: t('End_To_End_Encryption_Enabled') });
this.closeAlert();
Expand Down Expand Up @@ -394,7 +395,7 @@ class E2E extends Emitter {
await this.persistKeys(this.getKeysFromLocalStorage(), await this.createRandomPassword());
}

const randomPassword = Meteor._localStorage.getItem('e2e.randomPassword');
const randomPassword = Accounts.storageLocation.getItem('e2e.randomPassword');
if (randomPassword) {
this.setState(E2EEState.SAVE_PASSWORD);
this.openAlert({
Expand All @@ -412,8 +413,8 @@ class E2E extends Emitter {
this.log('-> Stop Client');
this.closeAlert();

Meteor._localStorage.removeItem('public_key');
Meteor._localStorage.removeItem('private_key');
Accounts.storageLocation.removeItem('public_key');
Accounts.storageLocation.removeItem('private_key');
this.instancesByRoomId = {};
this.privateKey = undefined;
this.started = false;
Expand All @@ -425,8 +426,8 @@ class E2E extends Emitter {
async changePassword(newPassword: string): Promise<void> {
await this.persistKeys(this.getKeysFromLocalStorage(), newPassword, { force: true });

if (Meteor._localStorage.getItem('e2e.randomPassword')) {
Meteor._localStorage.setItem('e2e.randomPassword', newPassword);
if (Accounts.storageLocation.getItem('e2e.randomPassword')) {
Accounts.storageLocation.setItem('e2e.randomPassword', newPassword);
}
}

Expand All @@ -447,12 +448,12 @@ class E2E extends Emitter {
}

async loadKeys({ public_key, private_key }: { public_key: string; private_key: string }): Promise<void> {
Meteor._localStorage.setItem('public_key', public_key);
Accounts.storageLocation.setItem('public_key', public_key);

try {
this.privateKey = await importRSAKey(EJSON.parse(private_key), ['decrypt']);

Meteor._localStorage.setItem('private_key', private_key);
Accounts.storageLocation.setItem('private_key', private_key);
} catch (error) {
this.setState(E2EEState.ERROR);
return this.error('Error importing private key: ', error);
Expand All @@ -474,7 +475,7 @@ class E2E extends Emitter {
try {
const publicKey = await exportJWKKey(key.publicKey);

Meteor._localStorage.setItem('public_key', JSON.stringify(publicKey));
Accounts.storageLocation.setItem('public_key', JSON.stringify(publicKey));
} catch (error) {
this.setState(E2EEState.ERROR);
return this.error('Error exporting public key: ', error);
Expand All @@ -483,7 +484,7 @@ class E2E extends Emitter {
try {
const privateKey = await exportJWKKey(key.privateKey);

Meteor._localStorage.setItem('private_key', JSON.stringify(privateKey));
Accounts.storageLocation.setItem('private_key', JSON.stringify(privateKey));
} catch (error) {
this.setState(E2EEState.ERROR);
return this.error('Error exporting private key: ', error);
Expand All @@ -498,7 +499,7 @@ class E2E extends Emitter {

async createRandomPassword(): Promise<string> {
const randomPassword = await generateMnemonicPhrase(5);
Meteor._localStorage.setItem('e2e.randomPassword', randomPassword);
Accounts.storageLocation.setItem('e2e.randomPassword', randomPassword);
return randomPassword;
}

Expand Down
14 changes: 7 additions & 7 deletions apps/meteor/app/livechat/server/api/v1/contact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { Match, check } from 'meteor/check';
import { Meteor } from 'meteor/meteor';

import { API } from '../../../../api/server';
import { Contacts, createContact, updateContact } from '../../lib/Contacts';
import { Contacts, createContact, updateContact, isSingleContactEnabled } from '../../lib/Contacts';

API.v1.addRoute(
'omnichannel/contact',
Expand Down Expand Up @@ -96,8 +96,8 @@ API.v1.addRoute(
{ authRequired: true, permissionsRequired: ['create-livechat-contact'], validateParams: isPOSTOmnichannelContactsProps },
{
async post() {
if (process.env.TEST_MODE?.toUpperCase() !== 'TRUE') {
throw new Meteor.Error('error-not-allowed', 'This endpoint is only allowed in test mode');
if (!isSingleContactEnabled()) {
return API.v1.unauthorized();
}
const contactId = await createContact({ ...this.bodyParams, unknown: false });

Expand All @@ -111,8 +111,8 @@ API.v1.addRoute(
{ authRequired: true, permissionsRequired: ['update-livechat-contact'], validateParams: isPOSTUpdateOmnichannelContactsProps },
{
async post() {
if (process.env.TEST_MODE?.toUpperCase() !== 'TRUE') {
throw new Meteor.Error('error-not-allowed', 'This endpoint is only allowed in test mode');
if (!isSingleContactEnabled()) {
return API.v1.unauthorized();
}

const contact = await updateContact({ ...this.bodyParams });
Expand All @@ -127,8 +127,8 @@ API.v1.addRoute(
{ authRequired: true, permissionsRequired: ['view-livechat-contact'], validateParams: isGETOmnichannelContactsProps },
{
async get() {
if (process.env.TEST_MODE?.toUpperCase() !== 'TRUE') {
throw new Meteor.Error('error-not-allowed', 'This endpoint is only allowed in test mode');
if (!isSingleContactEnabled()) {
return API.v1.unauthorized();
}
const contact = await LivechatContacts.findOneById(this.queryParams.contactId);

Expand Down
87 changes: 38 additions & 49 deletions apps/meteor/app/livechat/server/lib/Contacts.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type {
AtLeast,
ILivechatContact,
ILivechatContactChannel,
ILivechatCustomField,
Expand Down Expand Up @@ -113,41 +114,8 @@ export const Contacts = {
}
}

const allowedCF = LivechatCustomField.findByScope<Pick<ILivechatCustomField, '_id' | 'label' | 'regexp' | 'required' | 'visibility'>>(
'visitor',
{
projection: { _id: 1, label: 1, regexp: 1, required: 1 },
},
false,
);

const livechatData: Record<string, string> = {};

for await (const cf of allowedCF) {
if (!customFields.hasOwnProperty(cf._id)) {
if (cf.required) {
throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label }));
}
continue;
}
const cfValue: string = trim(customFields[cf._id]);

if (!cfValue || typeof cfValue !== 'string') {
if (cf.required) {
throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label }));
}
continue;
}

if (cf.regexp) {
const regex = new RegExp(cf.regexp);
if (!regex.test(cfValue)) {
throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label }));
}
}

livechatData[cf._id] = cfValue;
}
const allowedCF = await getAllowedCustomFields();
const livechatData: Record<string, string> = validateCustomFields(allowedCF, customFields, { ignoreAdditionalFields: true });

const fieldsToRemove = {
// if field is explicitely set to empty string, remove
Expand Down Expand Up @@ -202,15 +170,20 @@ export const Contacts = {
},
};

export function isSingleContactEnabled(): boolean {
// The Single Contact feature is not yet available in production, but can already be partially used in test environments.
return process.env.TEST_MODE?.toUpperCase() === 'TRUE';
}

export async function createContact(params: CreateContactParams): Promise<string> {
const { name, emails, phones, customFields = {}, contactManager, channels, unknown } = params;
const { name, emails, phones, customFields: receivedCustomFields = {}, contactManager, channels, unknown } = params;

if (contactManager) {
await validateContactManager(contactManager);
}

const allowedCustomFields = await getAllowedCustomFields();
validateCustomFields(allowedCustomFields, customFields);
const customFields = validateCustomFields(allowedCustomFields, receivedCustomFields);

const { insertedId } = await LivechatContacts.insertOne({
name,
Expand All @@ -226,7 +199,7 @@ export async function createContact(params: CreateContactParams): Promise<string
}

export async function updateContact(params: UpdateContactParams): Promise<ILivechatContact> {
const { contactId, name, emails, phones, customFields, contactManager, channels } = params;
const { contactId, name, emails, phones, customFields: receivedCustomFields, contactManager, channels } = params;

const contact = await LivechatContacts.findOneById<Pick<ILivechatContact, '_id'>>(contactId, { projection: { _id: 1 } });

Expand All @@ -238,17 +211,21 @@ export async function updateContact(params: UpdateContactParams): Promise<ILivec
await validateContactManager(contactManager);
}

if (customFields) {
const allowedCustomFields = await getAllowedCustomFields();
validateCustomFields(allowedCustomFields, customFields);
}
const customFields = receivedCustomFields && validateCustomFields(await getAllowedCustomFields(), receivedCustomFields);

const updatedContact = await LivechatContacts.updateContact(contactId, { name, emails, phones, contactManager, channels, customFields });
const updatedContact = await LivechatContacts.updateContact(contactId, {
name,
emails,
phones,
contactManager,
channels,
customFields,
});

return updatedContact;
}

async function getAllowedCustomFields(): Promise<ILivechatCustomField[]> {
async function getAllowedCustomFields(): Promise<Pick<ILivechatCustomField, '_id' | 'label' | 'regexp' | 'required'>[]> {
return LivechatCustomField.findByScope(
'visitor',
{
Expand All @@ -258,7 +235,13 @@ async function getAllowedCustomFields(): Promise<ILivechatCustomField[]> {
).toArray();
}

export function validateCustomFields(allowedCustomFields: ILivechatCustomField[], customFields: Record<string, string | unknown>) {
export function validateCustomFields(
allowedCustomFields: AtLeast<ILivechatCustomField, '_id' | 'label' | 'regexp' | 'required'>[],
customFields: Record<string, string | unknown>,
options?: { ignoreAdditionalFields?: boolean },
): Record<string, string> {
const validValues: Record<string, string> = {};

for (const cf of allowedCustomFields) {
if (!customFields.hasOwnProperty(cf._id)) {
if (cf.required) {
Expand All @@ -281,14 +264,20 @@ export function validateCustomFields(allowedCustomFields: ILivechatCustomField[]
throw new Error(i18n.t('error-invalid-custom-field-value', { field: cf.label }));
}
}

validValues[cf._id] = cfValue;
}

const allowedCustomFieldIds = new Set(allowedCustomFields.map((cf) => cf._id));
for (const key in customFields) {
if (!allowedCustomFieldIds.has(key)) {
throw new Error(i18n.t('error-custom-field-not-allowed', { key }));
if (!options?.ignoreAdditionalFields) {
const allowedCustomFieldIds = new Set(allowedCustomFields.map((cf) => cf._id));
for (const key in customFields) {
if (!allowedCustomFieldIds.has(key)) {
throw new Error(i18n.t('error-custom-field-not-allowed', { key }));
}
}
}

return validValues;
}

export async function validateContactManager(contactManagerUserId: string) {
Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/app/livechat/server/lib/LivechatTyped.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ import * as Mailer from '../../../mailer/server/api';
import { metrics } from '../../../metrics/server';
import { settings } from '../../../settings/server';
import { businessHourManager } from '../business-hour';
import { createContact } from './Contacts';
import { createContact, isSingleContactEnabled } from './Contacts';
import { parseAgentCustomFields, updateDepartmentAgents, validateEmail, normalizeTransferredByData } from './Helper';
import { QueueManager } from './QueueManager';
import { RoutingManager } from './RoutingManager';
Expand Down Expand Up @@ -669,7 +669,7 @@ class LivechatClass {
}
}

if (process.env.TEST_MODE?.toUpperCase() === 'TRUE') {
if (isSingleContactEnabled()) {
const contactId = await createContact({
name: name ?? (visitorDataToUpdate.username as string),
emails: email ? [email] : [],
Expand Down
1 change: 1 addition & 0 deletions apps/meteor/app/ui-master/server/scripts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ window.addEventListener('load', function() {
});
window.localStorage.clear();
Meteor._localStorage = window.sessionStorage;
Accounts.config({ clientStorage: 'session' });
}
});
`
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { IMessage } from '@rocket.chat/core-typings';
import { Emitter } from '@rocket.chat/emitter';
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';

import type { ComposerAPI } from '../../../../client/lib/chats/ChatAPI';
import { withDebouncing } from '../../../../lib/utils/highOrderFunctions';
Expand Down Expand Up @@ -31,11 +31,11 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string)

const persist = withDebouncing({ wait: 300 })(() => {
if (input.value) {
Meteor._localStorage.setItem(storageID, input.value);
Accounts.storageLocation.setItem(storageID, input.value);
return;
}

Meteor._localStorage.removeItem(storageID);
Accounts.storageLocation.removeItem(storageID);
});

const notifyQuotedMessagesUpdate = (): void => {
Expand Down Expand Up @@ -262,7 +262,7 @@ export const createComposerAPI = (input: HTMLTextAreaElement, storageID: string)

const insertNewLine = (): void => insertText('\n');

setText(Meteor._localStorage.getItem(storageID) ?? '', {
setText(Accounts.storageLocation.getItem(storageID) ?? '', {
skipFocus: true,
});

Expand Down
4 changes: 2 additions & 2 deletions apps/meteor/app/ui-utils/server/Message.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { IMessage } from '@rocket.chat/core-typings';
import { escapeHTML } from '@rocket.chat/string-helpers';
import { Meteor } from 'meteor/meteor';
import { Accounts } from 'meteor/accounts-base';

import { trim } from '../../../lib/utils/stringUtils';
import { i18n } from '../../../server/lib/i18n';
Expand All @@ -17,7 +17,7 @@ export const Message = {
}
if (messageType.message) {
if (!language) {
language = Meteor._localStorage.getItem('userLanguage') || 'en';
language = Accounts.storageLocation.getItem('userLanguage') || 'en';
}
const data = (typeof messageType.data === 'function' && messageType.data(msg)) || {};
return i18n.t(messageType.message, { ...data, lng: language });
Expand Down
Loading

0 comments on commit a0b1672

Please sign in to comment.