diff --git a/AdminDashboard/src/Pages/Settings/CallSMS/Index.tsx b/AdminDashboard/src/Pages/Settings/CallSMS/Index.tsx index 4743ff6c3a..861c6b7e45 100644 --- a/AdminDashboard/src/Pages/Settings/CallSMS/Index.tsx +++ b/AdminDashboard/src/Pages/Settings/CallSMS/Index.tsx @@ -72,9 +72,9 @@ const Settings: FunctionComponent = (): ReactElement => { }, { field: { - twilioPhoneNumber: true, + twilioPrimaryPhoneNumber: true, }, - title: "Twilio Phone Number", + title: "Primary Twilio Phone Number", fieldType: FormFieldSchemaType.Phone, required: true, description: "You can find this in your Twilio console.", @@ -83,6 +83,19 @@ const Settings: FunctionComponent = (): ReactElement => { minLength: 2, }, }, + { + field: { + twilioSecondaryPhoneNumbers: true, + }, + title: "Secondary Twilio Phone Number", + fieldType: FormFieldSchemaType.LongText, + required: true, + description: "If you have bought more phone numbers from Twilio for specific countries, you can add them here.", + placeholder: "+1234567890, +4444444444", + validation: { + minLength: 2, + }, + }, ]} modelDetailProps={{ modelType: GlobalConfig, @@ -97,12 +110,20 @@ const Settings: FunctionComponent = (): ReactElement => { }, { field: { - twilioPhoneNumber: true, + twilioPrimaryPhoneNumber: true, }, - title: "Twilio Phone Number", + title: "Primary Twilio Phone Number", fieldType: FieldType.Phone, placeholder: "None", }, + { + field: { + twilioSecondaryPhoneNumbers: true, + }, + title: "Secondary Twilio Phone Numbers", + fieldType: FieldType.LongText, + placeholder: "None", + }, ], modelId: ObjectID.getZeroObjectID(), }} diff --git a/App/FeatureSet/Notification/API/Call.ts b/App/FeatureSet/Notification/API/Call.ts index af7a513da6..3a06784646 100644 --- a/App/FeatureSet/Notification/API/Call.ts +++ b/App/FeatureSet/Notification/API/Call.ts @@ -54,7 +54,8 @@ router.post("/test", async (req: ExpressRequest, res: ExpressResponse) => { _id: true, twilioAccountSID: true, twilioAuthToken: true, - twilioPhoneNumber: true, + twilioPrimaryPhoneNumber: true, + twilioSecondaryPhoneNumbers: true, projectId: true, }, }); @@ -97,11 +98,11 @@ router.post("/test", async (req: ExpressRequest, res: ExpressResponse) => { ); } - if (!config.twilioPhoneNumber) { + if (!config.twilioPrimaryPhoneNumber) { return Response.sendErrorResponse( req, res, - new BadDataException("twilioPhoneNumber is required"), + new BadDataException("twilioPrimaryPhoneNumber is required"), ); } diff --git a/App/FeatureSet/Notification/API/SMS.ts b/App/FeatureSet/Notification/API/SMS.ts index e889b9068b..4c3da74139 100644 --- a/App/FeatureSet/Notification/API/SMS.ts +++ b/App/FeatureSet/Notification/API/SMS.ts @@ -53,7 +53,8 @@ router.post("/test", async (req: ExpressRequest, res: ExpressResponse) => { _id: true, twilioAccountSID: true, twilioAuthToken: true, - twilioPhoneNumber: true, + twilioPrimaryPhoneNumber: true, + twilioSecondaryPhoneNumbers: true, projectId: true, }, }); @@ -96,11 +97,11 @@ router.post("/test", async (req: ExpressRequest, res: ExpressResponse) => { ); } - if (!config.twilioPhoneNumber) { + if (!config.twilioPrimaryPhoneNumber) { return Response.sendErrorResponse( req, res, - new BadDataException("twilioPhoneNumber is required"), + new BadDataException("twilioPrimaryPhoneNumber is required"), ); } diff --git a/App/FeatureSet/Notification/Config.ts b/App/FeatureSet/Notification/Config.ts index d88a1b4a69..ad602cceea 100644 --- a/App/FeatureSet/Notification/Config.ts +++ b/App/FeatureSet/Notification/Config.ts @@ -10,6 +10,7 @@ import GlobalConfigService from "Common/Server/Services/GlobalConfigService"; import GlobalConfig, { EmailServerType, } from "Common/Models/DatabaseModels/GlobalConfig"; +import Phone from "Common/Types/Phone"; export const InternalSmtpPassword: string = process.env["INTERNAL_SMTP_PASSWORD"] || ""; @@ -196,7 +197,8 @@ export const getTwilioConfig: GetTwilioConfigFunction = select: { twilioAccountSID: true, twilioAuthToken: true, - twilioPhoneNumber: true, + twilioPrimaryPhoneNumber: true, + twilioSecondaryPhoneNumbers: true, }, }); @@ -207,7 +209,7 @@ export const getTwilioConfig: GetTwilioConfigFunction = if ( !globalConfig.twilioAccountSID || !globalConfig.twilioAuthToken || - !globalConfig.twilioPhoneNumber + !globalConfig.twilioPrimaryPhoneNumber ) { return null; } @@ -215,7 +217,10 @@ export const getTwilioConfig: GetTwilioConfigFunction = return { accountSid: globalConfig.twilioAccountSID, authToken: globalConfig.twilioAuthToken, - phoneNumber: globalConfig.twilioPhoneNumber, + primaryPhoneNumber: globalConfig.twilioPrimaryPhoneNumber, + secondaryPhoneNumbers: globalConfig.twilioSecondaryPhoneNumbers && globalConfig.twilioSecondaryPhoneNumbers.length > 0 ? globalConfig.twilioSecondaryPhoneNumbers.split(",").map((phoneNumber: string)=>{ + return new Phone(phoneNumber.trim()); + }) : [], }; }; diff --git a/Common/Models/DatabaseModels/GlobalConfig.ts b/Common/Models/DatabaseModels/GlobalConfig.ts index 33657988ce..cc46655547 100644 --- a/Common/Models/DatabaseModels/GlobalConfig.ts +++ b/Common/Models/DatabaseModels/GlobalConfig.ts @@ -232,8 +232,8 @@ export default class GlobalConfig extends GlobalConfigModel { }) @TableColumn({ type: TableColumnType.Phone, - title: "Twilio Phone Number", - description: "Phone Number for your Twilio account", + title: "Twilio Primary Phone Number", + description: "Secondary Phone Number for your Twilio account", }) @Column({ type: ColumnType.Phone, @@ -242,7 +242,26 @@ export default class GlobalConfig extends GlobalConfigModel { unique: true, transformer: Phone.getDatabaseTransformer(), }) - public twilioPhoneNumber?: Phone = undefined; + public twilioPrimaryPhoneNumber?: Phone = undefined; + + + @ColumnAccessControl({ + create: [], + read: [], + update: [], + }) + @TableColumn({ + type: TableColumnType.LongText, + title: "Twilio Secondary Phone Numbers", + description: "Secondary Phone Number for your Twilio account", + }) + @Column({ + type: ColumnType.LongText, + length: ColumnLength.LongText, + nullable: true, + unique: false, + }) + public twilioSecondaryPhoneNumbers?: string = undefined; // phone numbers seperated by comma @ColumnAccessControl({ create: [], diff --git a/Common/Models/DatabaseModels/ProjectCallSMSConfig.ts b/Common/Models/DatabaseModels/ProjectCallSMSConfig.ts index 70162ee5a1..d00b0e98aa 100644 --- a/Common/Models/DatabaseModels/ProjectCallSMSConfig.ts +++ b/Common/Models/DatabaseModels/ProjectCallSMSConfig.ts @@ -426,5 +426,37 @@ export default class ProjectCallSMSConfig extends BaseModel { unique: true, transformer: Phone.getDatabaseTransformer(), }) - public twilioPhoneNumber?: Phone = undefined; + public twilioPrimaryPhoneNumber?: Phone = undefined; + + + @ColumnAccessControl({ + create: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.CreateProjectCallSMSConfig, + ], + read: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.ProjectMember, + Permission.ReadProjectCallSMSConfig, + ], + update: [ + Permission.ProjectOwner, + Permission.ProjectAdmin, + Permission.EditProjectCallSMSConfig, + ], + }) + @TableColumn({ + type: TableColumnType.LongText, + title: "Twilio Secondary Phone Numbers", + description: "Secondary Phone Number for your Twilio account", + }) + @Column({ + type: ColumnType.LongText, + length: ColumnLength.LongText, + nullable: true, + unique: false, + }) + public twilioSecondaryPhoneNumbers?: string = undefined; // phone numbers seperated by comma } diff --git a/Common/Server/Services/ProjectCallSMSConfigService.ts b/Common/Server/Services/ProjectCallSMSConfigService.ts index 86c74c83e2..f36e7a3cd4 100644 --- a/Common/Server/Services/ProjectCallSMSConfigService.ts +++ b/Common/Server/Services/ProjectCallSMSConfigService.ts @@ -2,6 +2,7 @@ import DatabaseService from "./DatabaseService"; import TwilioConfig from "../../Types/CallAndSMS/TwilioConfig"; import BadDataException from "../../Types/Exception/BadDataException"; import Model from "Common/Models/DatabaseModels/ProjectCallSMSConfig"; +import Phone from "../../Types/Phone"; export class Service extends DatabaseService { public constructor() { @@ -25,7 +26,7 @@ export class Service extends DatabaseService { ); } - if (!projectCallSmsConfig.twilioPhoneNumber) { + if (!projectCallSmsConfig.twilioPrimaryPhoneNumber) { throw new BadDataException( "Project Call and SMS Config twilio phone number is not set", ); @@ -40,7 +41,10 @@ export class Service extends DatabaseService { return { accountSid: projectCallSmsConfig.twilioAccountSID.toString(), authToken: projectCallSmsConfig.twilioAuthToken.toString(), - phoneNumber: projectCallSmsConfig.twilioPhoneNumber, + primaryPhoneNumber: projectCallSmsConfig.twilioPrimaryPhoneNumber, + secondaryPhoneNumbers: projectCallSmsConfig.twilioSecondaryPhoneNumbers && projectCallSmsConfig.twilioSecondaryPhoneNumbers.length > 0 ? projectCallSmsConfig.twilioSecondaryPhoneNumbers.split(",").map((phone: string)=>{ + return new Phone(phone); + }) : [], }; } } diff --git a/Common/Server/Services/StatusPageSubscriberService.ts b/Common/Server/Services/StatusPageSubscriberService.ts index ad8a5681a4..b2d13da06b 100644 --- a/Common/Server/Services/StatusPageSubscriberService.ts +++ b/Common/Server/Services/StatusPageSubscriberService.ts @@ -262,7 +262,9 @@ export class Service extends DatabaseService { _id: true, twilioAccountSID: true, twilioAuthToken: true, - twilioPhoneNumber: true, + twilioPrimaryPhoneNumber: true, + twilioSecondaryPhoneNumbers: true, + }, }, props: { @@ -795,7 +797,8 @@ export class Service extends DatabaseService { _id: true, twilioAccountSID: true, twilioAuthToken: true, - twilioPhoneNumber: true, + twilioPrimaryPhoneNumber: true, + twilioSecondaryPhoneNumbers: true, }, subscriberTimezones: true, reportDataInDays: true, diff --git a/Common/Types/CallAndSMS/TwilioConfig.ts b/Common/Types/CallAndSMS/TwilioConfig.ts index fdcfc3534f..8728dec838 100644 --- a/Common/Types/CallAndSMS/TwilioConfig.ts +++ b/Common/Types/CallAndSMS/TwilioConfig.ts @@ -3,5 +3,6 @@ import Phone from "../Phone"; export default interface TwilioConfig { accountSid: string; authToken: string; - phoneNumber: Phone; + primaryPhoneNumber: Phone; + secondaryPhoneNumbers: Phone[]; } diff --git a/Dashboard/src/Components/CallSMS/CallSMSConfigTable.tsx b/Dashboard/src/Components/CallSMS/CallSMSConfigTable.tsx index c380a1c85c..a2743917e1 100644 --- a/Dashboard/src/Components/CallSMS/CallSMSConfigTable.tsx +++ b/Dashboard/src/Components/CallSMS/CallSMSConfigTable.tsx @@ -170,9 +170,9 @@ const CustomCallSMSTable: FunctionComponent = (): ReactElement => { }, { field: { - twilioPhoneNumber: true, + twilioPrimaryPhoneNumber: true, }, - title: "Twilio Phone Number", + title: "Twilio Primary Phone Number", stepId: "twilio-info", fieldType: FormFieldSchemaType.Phone, required: true, @@ -182,6 +182,23 @@ const CustomCallSMSTable: FunctionComponent = (): ReactElement => { minLength: 2, }, }, + + // add twilioSecondaryPhoneNumbers + { + field: { + twilioSecondaryPhoneNumbers: true, + }, + title: "Twilio Secondary Phone Numbers", + stepId: "twilio-info", + fieldType: FormFieldSchemaType.LongText, + required: false, + description: + "If you have multiple phone numbers, add them here. These numbers will be used to send SMS and make calls if the country code matches instead of primary phone number. If the country code does not match, then primary phone number will be used.", + placeholder: "+441234567890, +461234567890", + validation: { + minLength: 2, + }, + }, ]} showRefreshButton={true} viewPageRoute={Navigation.getCurrentRoute()} @@ -208,10 +225,17 @@ const CustomCallSMSTable: FunctionComponent = (): ReactElement => { }, }, { - title: "Twilio Phone Number", + title: "Twilio Primary Phone Number", type: FieldType.Phone, field: { - twilioPhoneNumber: true, + twilioPrimaryPhoneNumber: true, + }, + }, + { + title: "Twilio Secondary Primary Phone Numbers", + type: FieldType.LongText, + field: { + twilioSecondaryPhoneNumbers: true, }, }, ]} @@ -240,11 +264,18 @@ const CustomCallSMSTable: FunctionComponent = (): ReactElement => { }, { field: { - twilioPhoneNumber: true, + twilioPrimaryPhoneNumber: true, }, title: "Twilio Phone Number", type: FieldType.Phone, }, + { + field: { + twilioSecondaryPhoneNumbers: true, + }, + title: "Twilio Phone Number", + type: FieldType.LongText, + }, ]} /> @@ -284,19 +315,19 @@ const CustomCallSMSTable: FunctionComponent = (): ReactElement => { const response: | HTTPResponse | HTTPErrorResponse = await API.post( - URL.fromString(NOTIFICATION_URL.toString()).addRoute( - `/sms/test`, - ), + URL.fromString(NOTIFICATION_URL.toString()).addRoute( + `/sms/test`, + ), - { - toPhone: values["toPhone"], - callSMSConfigId: new ObjectID( - currentCallSMSTestConfig["_id"] - ? currentCallSMSTestConfig["_id"].toString() - : "", - ).toString(), - }, - ); + { + toPhone: values["toPhone"], + callSMSConfigId: new ObjectID( + currentCallSMSTestConfig["_id"] + ? currentCallSMSTestConfig["_id"].toString() + : "", + ).toString(), + }, + ); if (response.isSuccess()) { setIsCallSMSTestLoading(false); setShowSMSTestModal(false); @@ -321,7 +352,7 @@ const CustomCallSMSTable: FunctionComponent = (): ReactElement => { title={`SMS Sent`} error={ error === - "Error connecting to server. Please try again in few minutes." + "Error connecting to server. Please try again in few minutes." ? "Request timed out. Please check your twilio credentials and make sure they are correct." : error } @@ -373,19 +404,19 @@ const CustomCallSMSTable: FunctionComponent = (): ReactElement => { const response: | HTTPResponse | HTTPErrorResponse = await API.post( - URL.fromString(NOTIFICATION_URL.toString()).addRoute( - `/call/test`, - ), + URL.fromString(NOTIFICATION_URL.toString()).addRoute( + `/call/test`, + ), - { - toPhone: values["toPhone"], - callSMSConfigId: new ObjectID( - currentCallSMSTestConfig["_id"] - ? currentCallSMSTestConfig["_id"].toString() - : "", - ).toString(), - }, - ); + { + toPhone: values["toPhone"], + callSMSConfigId: new ObjectID( + currentCallSMSTestConfig["_id"] + ? currentCallSMSTestConfig["_id"].toString() + : "", + ).toString(), + }, + ); if (response.isSuccess()) { setIsCallSMSTestLoading(false); setShowCallTestModal(false); @@ -410,7 +441,7 @@ const CustomCallSMSTable: FunctionComponent = (): ReactElement => { title={`Call Sent`} error={ error === - "Error connecting to server. Please try again in few minutes." + "Error connecting to server. Please try again in few minutes." ? "Request timed out. Please check your twilio credentials and make sure they are correct." : error } diff --git a/Worker/DataMigrations/UpdateGlobalCongfigFromEnv.ts b/Worker/DataMigrations/UpdateGlobalCongfigFromEnv.ts index 16fa97f051..5d52eab2cd 100644 --- a/Worker/DataMigrations/UpdateGlobalCongfigFromEnv.ts +++ b/Worker/DataMigrations/UpdateGlobalCongfigFromEnv.ts @@ -30,7 +30,7 @@ export default class UpdateGlobalConfigFromEnv extends DataMigrationBase { twilioAccountSID: process.env["TWILIO_ACCOUNT_SID"] || "", twilioAuthToken: process.env["TWILIO_AUTH_TOKEN"] || "", - twilioPhoneNumber: process.env["TWILIO_PHONE_NUMBER"] || "", + twilioPrimaryPhoneNumber: process.env["TWILIO_PHONE_NUMBER"] || "", // Update SMTP smtpUsername: process.env["SMTP_USERNAME"] || "",