Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/i18n.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ concurrency:
jobs:
i18n:
name: Run i18n
if: ${{ secrets.CI_LINGO_DOT_DEV_API_KEY != '' }}
runs-on: blacksmith-2vcpu-ubuntu-2404
permissions:
actions: write
Expand Down
40 changes: 24 additions & 16 deletions packages/app-store/salesforce/lib/CrmService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ class SalesforceCRMService implements CRM {
private credentialId: number;
private describeCache = new Map<string, Set<string>>();

/**
* Escapes a string value for safe interpolation into SOQL queries.
* Prevents SOQL injection by escaping backslashes and single quotes.
*/
private sanitizeSoqlValue(value: string): string {
return value.replace(/\\/g, "\\\\").replace(/'/g, "\\'");
}

constructor(credential: CredentialPayload, appOptions: z.infer<typeof appDataSchema>, testMode = false) {
this.integrationName = "salesforce_other_calendar";
this.credentialId = credential.id;
Expand Down Expand Up @@ -289,7 +297,7 @@ class SalesforceCRMService implements CRM {
private getSalesforceUserIdFromEmail = async (email: string) => {
const conn = await this.conn;
const query = await conn.query(
`SELECT Id, Email FROM User WHERE Email = '${email}' AND IsActive = true LIMIT 1`
`SELECT Id, Email FROM User WHERE Email = '${this.sanitizeSoqlValue(email)}' AND IsActive = true LIMIT 1`
);
if (query.records.length > 0) {
return (query.records[0] as { Email: string; Id: string }).Id;
Expand All @@ -299,7 +307,7 @@ class SalesforceCRMService implements CRM {
private getSalesforceUserFromUserId = async (userId: string) => {
const conn = await this.conn;

return await conn.query(`SELECT Id, Email, Name FROM User WHERE Id = '${userId}' AND IsActive = true`);
return await conn.query(`SELECT Id, Email, Name FROM User WHERE Id = '${this.sanitizeSoqlValue(userId)}' AND IsActive = true`);
};

private getSalesforceEventBody = (event: CalendarEvent): string => {
Expand Down Expand Up @@ -661,10 +669,10 @@ class SalesforceCRMService implements CRM {
// For an account let's assume that the first email is the one we should be querying against
const attendeeEmail = emailArray[0];
log.info("[recordToSearch=ACCOUNT] Searching contact for email", safeStringify({ attendeeEmail }));
soql = `SELECT Id, Email, OwnerId, AccountId, Account.OwnerId, Account.Owner.Email, Account.Website${extraFields} FROM ${SalesforceRecordEnum.CONTACT} WHERE Email = '${attendeeEmail}' AND AccountId != null`;
soql = `SELECT Id, Email, OwnerId, AccountId, Account.OwnerId, Account.Owner.Email, Account.Website${extraFields} FROM ${SalesforceRecordEnum.CONTACT} WHERE Email = '${this.sanitizeSoqlValue(attendeeEmail)}' AND AccountId != null`;
} else {
// Handle Contact/Lead record types
soql = `SELECT Id, Email, OwnerId, Owner.Email${extraFields} FROM ${recordToSearch} WHERE Email IN ('${emailArray.join(
soql = `SELECT Id, Email, OwnerId, Owner.Email${extraFields} FROM ${recordToSearch} WHERE Email IN ('${emailArray.map((e) => this.sanitizeSoqlValue(e)).join(
"','"
)}')`;
}
Expand Down Expand Up @@ -898,7 +906,7 @@ class SalesforceCRMService implements CRM {
if (!accountId && appOptions.createLeadIfAccountNull && !contactCreated) {
// Check to see if the lead exists already
const leadQuery = await conn.query(
`SELECT Id, Email FROM Lead WHERE Email = '${attendee.email}' LIMIT 1`
`SELECT Id, Email FROM Lead WHERE Email = '${this.sanitizeSoqlValue(attendee.email)}' LIMIT 1`
);
if (leadQuery.records.length > 0) {
const contact = leadQuery.records[0] as { Id: string; Email: string };
Expand Down Expand Up @@ -973,17 +981,17 @@ class SalesforceCRMService implements CRM {
}

for (const event of salesforceEvents) {
const salesforceEvent = (await conn.query(`SELECT WhoId FROM Event WHERE Id = '${event.uid}'`)) as {
const salesforceEvent = (await conn.query(`SELECT WhoId FROM Event WHERE Id = '${this.sanitizeSoqlValue(event.uid)}'`)) as {
records: { WhoId: string }[];
};

let salesforceAttendeeEmail: string | undefined = undefined;
// Figure out if the attendee is a contact or lead
const contactQuery = (await conn.query(
`SELECT Email FROM Contact WHERE Id = '${salesforceEvent.records[0].WhoId}'`
`SELECT Email FROM Contact WHERE Id = '${this.sanitizeSoqlValue(salesforceEvent.records[0].WhoId)}'`
)) as { records: { Email: string }[] };
const leadQuery = (await conn.query(
`SELECT Email FROM Lead WHERE Id = '${salesforceEvent.records[0].WhoId}'`
`SELECT Email FROM Lead WHERE Id = '${this.sanitizeSoqlValue(salesforceEvent.records[0].WhoId)}'`
)) as { records: { Email: string }[] };

// Prioritize contacts over leads
Expand Down Expand Up @@ -1266,7 +1274,7 @@ class SalesforceCRMService implements CRM {

// Get the associated record that the event was created on
const recordQuery = (await conn.query(
`SELECT OwnerId, Owner.Name FROM ${recordType} WHERE Id = '${id}'`
`SELECT OwnerId, Owner.Name FROM ${recordType} WHERE Id = '${this.sanitizeSoqlValue(id)}'`
)) as { records: { OwnerId: string; Owner: { Name: string } }[] };

if (!recordQuery || !recordQuery.records.length) {
Expand Down Expand Up @@ -1300,7 +1308,7 @@ class SalesforceCRMService implements CRM {
public getAllPossibleAccountWebsiteFromEmailDomain(emailDomain: string) {
const websites = getAllPossibleWebsiteValuesFromEmailDomain(emailDomain);
// Format for SOQL query
return websites.map((website) => `'${website}'`).join(", ");
return websites.map((website) => `'${this.sanitizeSoqlValue(website)}'`).join(", ");
}

private async getAccountIdBasedOnEmailDomainOfContacts(email: string) {
Expand Down Expand Up @@ -1334,7 +1342,7 @@ class SalesforceCRMService implements CRM {

// Fallback to querying which account the majority of contacts are under
const response = await conn.query(
`SELECT Id, Email, AccountId FROM Contact WHERE Email LIKE '%@${emailDomain}' AND AccountId != null`
`SELECT Id, Email, AccountId FROM Contact WHERE Email LIKE '%@${this.sanitizeSoqlValue(emailDomain)}' AND AccountId != null`
);

SalesforceRoutingTraceService.searchingByContactEmailDomain({
Expand Down Expand Up @@ -1836,7 +1844,7 @@ class SalesforceCRMService implements CRM {
const existingFieldNames = existingFields.map((field) => field.name);

const query = await conn.query(
`SELECT Id, ${existingFieldNames.join(", ")} FROM ${personRecordType} WHERE Id = '${contactId}'`
`SELECT Id, ${existingFieldNames.join(", ")} FROM ${personRecordType} WHERE Id = '${this.sanitizeSoqlValue(contactId)}'`
);

if (!query.records.length) {
Expand All @@ -1863,7 +1871,7 @@ class SalesforceCRMService implements CRM {

// First see if the contact already exists and connect it to the account
const userQuery = await conn.query(
`SELECT Id, Email FROM Contact WHERE Email = '${attendee.email}' LIMIT 1`
`SELECT Id, Email FROM Contact WHERE Email = '${this.sanitizeSoqlValue(attendee.email)}' LIMIT 1`
);
if (userQuery.records.length > 0) {
const contact = userQuery.records[0] as { Id: string; Email: string };
Expand Down Expand Up @@ -1909,7 +1917,7 @@ class SalesforceCRMService implements CRM {
if (!accountId) return;

const accountQuery = (await conn.query(
`SELECT ${lookupField.name} FROM ${SalesforceRecordEnum.ACCOUNT} WHERE Id = '${accountId}'`
`SELECT ${lookupField.name} FROM ${SalesforceRecordEnum.ACCOUNT} WHERE Id = '${this.sanitizeSoqlValue(accountId)}'`
)) as {
// We do not know what fields are included in the account since it's unqiue to each instance
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Expand Down Expand Up @@ -2043,7 +2051,7 @@ class SalesforceCRMService implements CRM {
private async findContactByEmail(email: string) {
const conn = await this.conn;
const contactsQuery = await conn.query(
`SELECT Id, Email FROM ${SalesforceRecordEnum.CONTACT} WHERE Email = '${email}' LIMIT 1`
`SELECT Id, Email FROM ${SalesforceRecordEnum.CONTACT} WHERE Email = '${this.sanitizeSoqlValue(email)}' LIMIT 1`
);

if (contactsQuery.records.length > 0) {
Expand All @@ -2054,7 +2062,7 @@ class SalesforceCRMService implements CRM {
private async findLeadByEmail(email: string) {
const conn = await this.conn;
const leadsQuery = await conn.query(
`SELECT Id, Email FROM ${SalesforceRecordEnum.LEAD} WHERE Email = '${email}' LIMIT 1`
`SELECT Id, Email FROM ${SalesforceRecordEnum.LEAD} WHERE Email = '${this.sanitizeSoqlValue(email)}' LIMIT 1`
);

if (leadsQuery.records.length > 0) {
Expand Down
34 changes: 25 additions & 9 deletions packages/lib/server/service/lingoDotDev.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import type { LocaleCode } from "@lingo.dev/_spec";
import { LingoDotDevEngine } from "lingo.dev/sdk";

import { LINGO_DOT_DEV_API_KEY } from "@calcom/lib/constants";
import logger from "@calcom/lib/logger";
import type { LocaleCode } from "@lingo.dev/_spec";
import { LingoDotDevEngine } from "lingo.dev/sdk";

export class LingoDotDevService {
private static engine = new LingoDotDevEngine({
apiKey: LINGO_DOT_DEV_API_KEY,
});
private static engine = LINGO_DOT_DEV_API_KEY
? new LingoDotDevEngine({
apiKey: LINGO_DOT_DEV_API_KEY,
})
: null;

/**
* Localizes text from one language to another
Expand All @@ -25,8 +26,13 @@ export class LingoDotDevService {
return null;
}

if (!LingoDotDevService.engine) {
logger.warn("LingoDotDevService.localizeText() skipped: LINGO_DOT_DEV_API_KEY is not set");
return null;
}

try {
const result = await this.engine.localizeText(text, {
const result = await LingoDotDevService.engine.localizeText(text, {
sourceLocale,
targetLocale,
});
Expand All @@ -50,8 +56,13 @@ export class LingoDotDevService {
sourceLocale: string,
targetLocales: string[]
): Promise<string[]> {
if (!LingoDotDevService.engine) {
logger.warn("LingoDotDevService.batchLocalizeText() skipped: LINGO_DOT_DEV_API_KEY is not set");
return [];
}

try {
const result = await this.engine.batchLocalizeText(text, {
const result = await LingoDotDevService.engine.batchLocalizeText(text, {
// TODO: LocaleCode is hacky, use our locale mapping instead.
sourceLocale: sourceLocale as LocaleCode,
targetLocales: targetLocales as LocaleCode[],
Expand All @@ -76,8 +87,13 @@ export class LingoDotDevService {
return texts;
}

if (!LingoDotDevService.engine) {
logger.warn("LingoDotDevService.localizeTexts() skipped: LINGO_DOT_DEV_API_KEY is not set");
return texts;
}

try {
const result = await this.engine.localizeChat(
const result = await LingoDotDevService.engine.localizeChat(
texts.map((text) => ({ name: "NO_NAME", text: text.trim() })),
{
sourceLocale,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,10 @@ export const createHandler = async ({ ctx, input }: CreateOptions) => {
});
}

const { webhookId: _webhookId, ...inputWithoutWebhookId } = input;
const webhookData: Prisma.WebhookCreateInput = {
id: v4(),
...input,
...inputWithoutWebhookId,
};
if (input.platform && user.role !== "ADMIN") {
throw new TRPCError({ code: "UNAUTHORIZED" });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ type EditOptions = {
};

export const editHandler = async ({ input, ctx }: EditOptions) => {
const { id, ...data } = input;
const { id, webhookId: _webhookId, ...data } = input;

const webhook = await prisma.webhook.findUnique({
where: {
Expand Down
2 changes: 1 addition & 1 deletion packages/trpc/server/routers/viewer/webhook/get.handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,5 @@ type GetOptions = {

export const getHandler = async ({ ctx: _ctx, input }: GetOptions) => {
const { repository: webhookRepository } = getWebhookFeature();
return await webhookRepository.findByWebhookId(input.webhookId);
return await webhookRepository.findByWebhookId(input.id || input.webhookId);
};
4 changes: 1 addition & 3 deletions packages/trpc/server/routers/viewer/webhook/get.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ import { z } from "zod";

import { webhookIdAndEventTypeIdSchema } from "./types";

export const ZGetInputSchema = webhookIdAndEventTypeIdSchema.extend({
webhookId: z.string().optional(),
});
export const ZGetInputSchema = webhookIdAndEventTypeIdSchema;

export type TGetInputSchema = z.infer<typeof ZGetInputSchema>;
1 change: 1 addition & 0 deletions packages/trpc/server/routers/viewer/webhook/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { z } from "zod";
export const webhookIdAndEventTypeIdSchema = z.object({
// Webhook ID
id: z.string().optional(),
webhookId: z.string().optional(),
eventTypeId: z.number().optional(),
teamId: z.number().optional(),
});
7 changes: 4 additions & 3 deletions packages/trpc/server/routers/viewer/webhook/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,14 @@ export const createWebhookPbacProcedure = (
// Endpoints that just read the logged in user's data - like 'list' don't necessarily have any input
if (!input) return next();

const { id, teamId, eventTypeId } = input;
const { id, webhookId, teamId, eventTypeId } = input;
const lookupId = id || webhookId;
const permissionCheckService = new PermissionCheckService();

if (id) {
if (lookupId) {
// Check if user is authorized to edit webhook
const webhook = await prisma.webhook.findUnique({
where: { id },
where: { id: lookupId },
select: {
id: true,
userId: true,
Expand Down
Loading