diff --git a/infrastructure/terraform/jumphost.tf b/infrastructure/terraform/jumphost.tf index 95fc95f6..2ed1e342 100644 --- a/infrastructure/terraform/jumphost.tf +++ b/infrastructure/terraform/jumphost.tf @@ -3,7 +3,7 @@ data "aws_ami" "ubuntu" { filter { name = "name" - values = ["ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server-*"] + values = ["ubuntu/images/hvm-ssd/ubuntu-jammy-22.04-amd64-server-*"] } filter { @@ -38,7 +38,7 @@ resource "aws_instance" "jumphost" { # Public IP resource "aws_eip" "jumphost" { - vpc = true + domain = "vpc" instance = aws_instance.jumphost.id depends_on = [aws_internet_gateway.gw] } diff --git a/tests/general/twilio.test.ts b/tests/general/twilio.test.ts index d9ee2ad8..a52d3cb4 100644 --- a/tests/general/twilio.test.ts +++ b/tests/general/twilio.test.ts @@ -3,6 +3,12 @@ jest.mock("../../twilio/client", () => ({ twilioClient: jest.fn(), })); +// Mock Prisma to prevent database initialization issues +jest.mock("../../services/prisma", () => ({ + __esModule: true, + default: {}, +})); + import notifs from "../../twilio/notifs"; import express from "express"; import macros from "../../utils/macros"; @@ -132,17 +138,17 @@ describe("TwilioNotifyer", () => { create: jest.fn(async (args) => { const err = new Error(); switch (args.to) { - case "1": + case "+1 (1)": return 200; - case "2": + case "+1 (2)": // @ts-expect-error -- wrong error type err.code = notifs.TWILIO_ERRORS.SMS_NOT_FOR_LANDLINE; throw err; - case "3": + case "+1 (3)": // @ts-expect-error -- wrong error type err.code = notifs.TWILIO_ERRORS.INVALID_PHONE_NUMBER; throw err; - case "4": + case "+1 (4)": // @ts-expect-error -- wrong error type err.code = notifs.TWILIO_ERRORS.MAX_SEND_ATTEMPTS_REACHED; throw err; @@ -158,31 +164,41 @@ describe("TwilioNotifyer", () => { }); it("non-error", async () => { - const resp = await notifs.sendVerificationCode("1"); + const resp = await notifs.sendVerificationCode("+1 (1)"); expect(resp.statusCode).toBe(200); expect(resp.message).toMatch(/code sent/i); }); it("landline error", async () => { - const resp = await notifs.sendVerificationCode("2"); + const resp = await notifs.sendVerificationCode("+1 (2)"); expect(resp.statusCode).toBe(400); expect(resp.message).toMatch(/not supported by landline/i); }); + it("blocks international numbers", async () => { + const resp = await notifs.sendVerificationCode("+441234567890"); + expect(resp.statusCode).toBe(400); + expect(resp.message).toBe( + "Invalid phone number format. Please use a US or Canadian number.", + ); + }); + it("invalid number error", async () => { - const resp = await notifs.sendVerificationCode("3"); + const resp = await notifs.sendVerificationCode("+1 (3)"); expect(resp.statusCode).toBe(400); expect(resp.message).toMatch(/invalid phone number/i); }); it("max send attempts error", async () => { - const resp = await notifs.sendVerificationCode("4"); + const resp = await notifs.sendVerificationCode("+1 (4)"); expect(resp.statusCode).toBe(400); expect(resp.message).toMatch(/attempted to send.*too many times/i); }); it("default error", async () => { - await expect(notifs.sendVerificationCode("123123142")).rejects.toThrow(); + await expect( + notifs.sendVerificationCode("+1 (123)-123-1420"), + ).rejects.toThrow(); }); }); @@ -193,9 +209,9 @@ describe("TwilioNotifyer", () => { create: jest.fn(async (args) => { const err = new Error(); switch (args.to) { - case "1": + case "+1 (1)": return; - case "2": + case "+1 (2)": // @ts-expect-error -- wrong error type err.code = notifs.TWILIO_ERRORS.USER_UNSUBSCRIBED; throw err; @@ -208,7 +224,7 @@ describe("TwilioNotifyer", () => { it("Successfully sends a message", async () => { jest.spyOn(macros, "log"); - await notifs.sendNotificationText("1", "message"); + await notifs.sendNotificationText("+1 (1)", "message"); expect(macros.log).toHaveBeenCalledWith( expect.stringMatching(/sent.*text/i), ); @@ -222,13 +238,22 @@ describe("TwilioNotifyer", () => { // don't do anytthing }); - await notifs.sendNotificationText("2", "message"); + await notifs.sendNotificationText("+1 (2)", "message"); expect(macros.warn).toHaveBeenCalledWith( expect.stringMatching(/has unsubscribed/i), ); expect( notificationsManager.deleteAllUserSubscriptions, - ).toHaveBeenCalledWith("2"); + ).toHaveBeenCalledWith("+1 (2)"); + }); + + it("blocks international numbers", async () => { + jest.spyOn(macros, "warn"); + + await notifs.sendNotificationText("+441234567890", "test message"); + expect(macros.warn).toHaveBeenCalledWith( + "Invalid phone number format for +441234567890. Please use a US or Canadian number.", + ); }); it("Default error", async () => { @@ -236,7 +261,7 @@ describe("TwilioNotifyer", () => { // don't do anytthing }); - await notifs.sendNotificationText("3", "message"); + await notifs.sendNotificationText("+15555555555", "message"); expect(macros.error).toHaveBeenCalledWith( expect.stringMatching(/error trying to send/i), expect.any(Error), diff --git a/twilio/notifs.ts b/twilio/notifs.ts index 1837ec28..8cc24269 100644 --- a/twilio/notifs.ts +++ b/twilio/notifs.ts @@ -37,10 +37,25 @@ class TwilioNotifyer { this.twilioClient = twilioClient; } + /** + * Validates that the phone number is US or Canadian (+1 country code) + */ + private isValidDomesticNumber(phoneNumber: string): boolean { + // Check if number starts with +1 (US/Canada country code) + return phoneNumber.startsWith("+1"); + } + async sendNotificationText( recipientNumber: string, message: string, ): Promise { + if (!this.isValidDomesticNumber(recipientNumber)) { + macros.warn( + `Invalid phone number format for ${recipientNumber}. Please use a US or Canadian number.`, + ); + return; + } + return this.twilioClient.messages .create({ body: message, from: this.TWILIO_NUMBER, to: recipientNumber }) .then(() => { @@ -67,6 +82,17 @@ class TwilioNotifyer { } async sendVerificationCode(recipientNumber: string): Promise { + if (!this.isValidDomesticNumber(recipientNumber)) { + macros.warn( + `Invalid phone number format for ${recipientNumber}. Please use a US or Canadian number.`, + ); + return { + statusCode: 400, + message: + "Invalid phone number format. Please use a US or Canadian number.", + }; + } + return this.twilioClient.verify.v2 .services(this.TWILIO_VERIFY_SERVICE_SID) .verifications.create({ to: recipientNumber, channel: "sms" })