From d9414f9d313ff71f54ef598fc45ecc1184cacec6 Mon Sep 17 00:00:00 2001 From: KM Koushik Date: Mon, 13 Jul 2026 09:26:23 +1000 Subject: [PATCH 1/3] fix: reject invalid SES regions Entire-Checkpoint: 26b0394dad5f --- .../components/settings/AddSesSettings.tsx | 54 ++++++++++++++++--- apps/web/src/env.js | 3 +- apps/web/src/lib/zod/ses-setting-schema.ts | 7 +++ .../lib/zod/ses-setting-schema.unit.test.ts | 24 +++++++++ apps/web/src/server/api/routers/admin.ts | 7 ++- apps/web/src/server/api/routers/domain.ts | 8 ++- .../server/service/ses-settings-service.ts | 3 ++ 7 files changed, 94 insertions(+), 12 deletions(-) create mode 100644 apps/web/src/lib/zod/ses-setting-schema.ts create mode 100644 apps/web/src/lib/zod/ses-setting-schema.unit.test.ts diff --git a/apps/web/src/components/settings/AddSesSettings.tsx b/apps/web/src/components/settings/AddSesSettings.tsx index f90cdfd3..b1a70f9a 100644 --- a/apps/web/src/components/settings/AddSesSettings.tsx +++ b/apps/web/src/components/settings/AddSesSettings.tsx @@ -18,9 +18,10 @@ import { Button } from "@usesend/ui/src/button"; import Spinner from "@usesend/ui/src/spinner"; import { toast } from "@usesend/ui/src/toaster"; import { isLocalhost } from "~/utils/client"; +import { sesRegionSchema } from "~/lib/zod/ses-setting-schema"; const FormSchema = z.object({ - region: z.string(), + region: sesRegionSchema, usesendUrl: z.string().url(), sendRate: z.coerce.number(), transactionalQuota: z.coerce.number().min(0).max(100), @@ -48,14 +49,51 @@ export const AddSesSettings: React.FC = ({ onSuccess }) => { export const AddSesSettingsForm: React.FC = ({ onSuccess, }) => { - const addSesSettings = api.admin.addSesSettings.useMutation(); + const defaultRegion = api.admin.getDefaultSesRegion.useQuery(); + + if (defaultRegion.isLoading) { + return ( +
+ +
+ ); + } + + if (!defaultRegion.data) { + return ( +
+

+ Failed to load the default AWS region. +

+ +
+ ); + } + return ( + + ); +}; + +const SesSettingsForm: React.FC< + SesSettingsProps & { defaultRegion: string } +> = ({ defaultRegion, onSuccess }) => { + const addSesSettings = api.admin.addSesSettings.useMutation(); const utils = api.useUtils(); const form = useForm>({ resolver: zodResolver(FormSchema), defaultValues: { - region: "", + region: defaultRegion, usesendUrl: "", sendRate: 1, transactionalQuota: 50, @@ -101,10 +139,12 @@ export const AddSesSettingsForm: React.FC = ({ } const onRegionInputOutOfFocus = async () => { - const region = form.getValues("region"); + const region = sesRegionSchema.safeParse(form.getValues("region")); - if (region) { - const quota = await utils.admin.getQuotaForRegion.fetch({ region }); + if (region.success) { + const quota = await utils.admin.getQuotaForRegion.fetch({ + region: region.data, + }); form.setValue("sendRate", quota ?? 1); } }; @@ -113,7 +153,7 @@ export const AddSesSettingsForm: React.FC = ({
region.trim()).filter(Boolean))]; +} diff --git a/apps/web/src/lib/zod/ses-setting-schema.unit.test.ts b/apps/web/src/lib/zod/ses-setting-schema.unit.test.ts new file mode 100644 index 00000000..8bcef77d --- /dev/null +++ b/apps/web/src/lib/zod/ses-setting-schema.unit.test.ts @@ -0,0 +1,24 @@ +import { describe, expect, it } from "vitest"; +import { + getValidSesRegions, + sesRegionSchema, +} from "~/lib/zod/ses-setting-schema"; + +describe("sesRegionSchema", () => { + it("rejects empty regions", () => { + expect(sesRegionSchema.safeParse("").success).toBe(false); + expect(sesRegionSchema.safeParse(" ").success).toBe(false); + }); + + it("normalizes valid regions", () => { + expect(sesRegionSchema.parse(" us-east-1 ")).toBe("us-east-1"); + }); +}); + +describe("getValidSesRegions", () => { + it("filters legacy empty regions and removes duplicates", () => { + expect( + getValidSesRegions(["", " ", "us-east-1", " us-east-1 ", "eu-west-1"]), + ).toEqual(["us-east-1", "eu-west-1"]); + }); +}); diff --git a/apps/web/src/server/api/routers/admin.ts b/apps/web/src/server/api/routers/admin.ts index 4ca82b2a..33b3ab08 100644 --- a/apps/web/src/server/api/routers/admin.ts +++ b/apps/web/src/server/api/routers/admin.ts @@ -11,6 +11,7 @@ import { logger } from "~/server/logger/log"; import { UseSend } from "usesend-js"; import { isCloud } from "~/utils/common"; import { toPlainHtml } from "~/server/utils/email-content"; +import { sesRegionSchema } from "~/lib/zod/ses-setting-schema"; const waitlistUserSelection = { id: true, @@ -67,10 +68,12 @@ export const adminRouter = createTRPCRouter({ return SesSettingsService.getAllSettings(); }), + getDefaultSesRegion: adminProcedure.query(() => env.AWS_DEFAULT_REGION), + getQuotaForRegion: adminProcedure .input( z.object({ - region: z.string(), + region: sesRegionSchema, }), ) .query(async ({ input }) => { @@ -81,7 +84,7 @@ export const adminRouter = createTRPCRouter({ addSesSettings: adminProcedure .input( z.object({ - region: z.string(), + region: sesRegionSchema, usesendUrl: z.string().url(), sendRate: z.number(), transactionalQuota: z.number(), diff --git a/apps/web/src/server/api/routers/domain.ts b/apps/web/src/server/api/routers/domain.ts index 848d24aa..33c8fb9b 100644 --- a/apps/web/src/server/api/routers/domain.ts +++ b/apps/web/src/server/api/routers/domain.ts @@ -16,15 +16,19 @@ import { } from "~/server/service/domain-service"; import { sendEmail } from "~/server/service/email-service"; import { SesSettingsService } from "~/server/service/ses-settings-service"; +import { + getValidSesRegions, + sesRegionSchema, +} from "~/lib/zod/ses-setting-schema"; export const domainRouter = createTRPCRouter({ getAvailableRegions: protectedProcedure.query(async () => { const settings = await SesSettingsService.getAllSettings(); - return settings.map((setting) => setting.region); + return getValidSesRegions(settings.map((setting) => setting.region)); }), createDomain: teamProcedure - .input(z.object({ name: z.string(), region: z.string() })) + .input(z.object({ name: z.string(), region: sesRegionSchema })) .mutation(async ({ ctx, input }) => { return createDomain( ctx.team.id, diff --git a/apps/web/src/server/service/ses-settings-service.ts b/apps/web/src/server/service/ses-settings-service.ts index 7402d713..49aa473a 100644 --- a/apps/web/src/server/service/ses-settings-service.ts +++ b/apps/web/src/server/service/ses-settings-service.ts @@ -7,6 +7,7 @@ import { EventType } from "@aws-sdk/client-sesv2"; import { EmailQueueService } from "./email-queue-service"; import { smallNanoid } from "../nanoid"; import { logger } from "../logger/log"; +import { sesRegionSchema } from "~/lib/zod/ses-setting-schema"; const GENERAL_EVENTS: EventType[] = [ "BOUNCE", @@ -62,6 +63,8 @@ export class SesSettingsService { sendingRateLimit: number; transactionalQuota: number; }) { + region = sesRegionSchema.parse(region); + await this.checkInitialized(); if (this.cache[region]) { throw new Error(`SesSetting for region ${region} already exists`); From 7e0346916868ab83e5debd5b32adc8e690dc2a5b Mon Sep 17 00:00:00 2001 From: KM Koushik Date: Mon, 13 Jul 2026 09:58:51 +1000 Subject: [PATCH 2/3] fix: handle SES region lookup errors --- .../src/components/settings/AddSesSettings.tsx | 17 +++++++++++++---- .../src/server/service/ses-settings-service.ts | 6 ++++-- 2 files changed, 17 insertions(+), 6 deletions(-) diff --git a/apps/web/src/components/settings/AddSesSettings.tsx b/apps/web/src/components/settings/AddSesSettings.tsx index b1a70f9a..7e199d58 100644 --- a/apps/web/src/components/settings/AddSesSettings.tsx +++ b/apps/web/src/components/settings/AddSesSettings.tsx @@ -142,10 +142,19 @@ const SesSettingsForm: React.FC< const region = sesRegionSchema.safeParse(form.getValues("region")); if (region.success) { - const quota = await utils.admin.getQuotaForRegion.fetch({ - region: region.data, - }); - form.setValue("sendRate", quota ?? 1); + form.clearErrors("region"); + + try { + const quota = await utils.admin.getQuotaForRegion.fetch({ + region: region.data, + }); + form.setValue("sendRate", quota ?? 1); + } catch { + form.setValue("sendRate", 1); + form.setError("region", { + message: "Unable to load the SES quota for this region", + }); + } } }; diff --git a/apps/web/src/server/service/ses-settings-service.ts b/apps/web/src/server/service/ses-settings-service.ts index 49aa473a..ba90a7a6 100644 --- a/apps/web/src/server/service/ses-settings-service.ts +++ b/apps/web/src/server/service/ses-settings-service.ts @@ -28,10 +28,12 @@ export class SesSettingsService { public static async getSetting( region = env.AWS_DEFAULT_REGION ): Promise { + const normalizedRegion = sesRegionSchema.parse(region); + await this.checkInitialized(); - if (this.cache[region]) { - return this.cache[region] as SesSetting; + if (this.cache[normalizedRegion]) { + return this.cache[normalizedRegion] as SesSetting; } return null; } From c9432f7f4feb8170c1401405e9bb9da0bc0a685c Mon Sep 17 00:00:00 2001 From: KM Koushik Date: Mon, 13 Jul 2026 10:04:31 +1000 Subject: [PATCH 3/3] fix: keep env validation node-resolvable --- apps/web/src/env.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/apps/web/src/env.js b/apps/web/src/env.js index 2d868372..140852f4 100644 --- a/apps/web/src/env.js +++ b/apps/web/src/env.js @@ -1,7 +1,6 @@ import { EmailStatus } from "@prisma/client"; import { createEnv } from "@t3-oss/env-nextjs"; import { z } from "zod"; -import { sesRegionSchema } from "~/lib/zod/ses-setting-schema"; export const env = createEnv({ /** @@ -40,7 +39,11 @@ export const env = createEnv({ GOOGLE_CLIENT_SECRET: z.string().optional(), AWS_SES_ENDPOINT: z.string().optional(), AWS_SNS_ENDPOINT: z.string().optional(), - AWS_DEFAULT_REGION: sesRegionSchema.default("us-east-1"), + AWS_DEFAULT_REGION: z + .string() + .trim() + .min(1, "Region is required") + .default("us-east-1"), API_RATE_LIMIT: z .string() .default("1")