diff --git a/apps/web/src/app/(dashboard)/admin/delete-ses-configuration.tsx b/apps/web/src/app/(dashboard)/admin/delete-ses-configuration.tsx new file mode 100644 index 00000000..aa6f6b9b --- /dev/null +++ b/apps/web/src/app/(dashboard)/admin/delete-ses-configuration.tsx @@ -0,0 +1,93 @@ +"use client"; + +import { useState } from "react"; +import { SesSetting } from "@prisma/client"; +import { Trash } from "lucide-react"; + +import { Button } from "@usesend/ui/src/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@usesend/ui/src/dialog"; +import { toast } from "@usesend/ui/src/toaster"; + +import { api } from "~/trpc/react"; + +export default function DeleteSesConfiguration({ + setting, +}: { + setting: SesSetting; +}) { + const [open, setOpen] = useState(false); + + const deleteSesSettings = api.admin.deleteSesSettings.useMutation(); + const utils = api.useUtils(); + + const handleDelete = () => { + deleteSesSettings.mutate( + { settingsId: setting.id }, + { + onSuccess: () => { + utils.admin.invalidate(); + toast.success("SES configuration deleted", { + description: `${setting.region} has been removed`, + }); + setOpen(false); + }, + onError: (error) => { + toast.error("Failed to delete SES configuration", { + description: error.message, + }); + }, + } + ); + }; + + return ( + setOpen(next)}> + + + + + + Delete SES configuration + + This will delete the callback URL, SNS topic, and queues for the{" "} + {setting.region} region. + This action cannot be undone. + + + + + + + + + ); +} diff --git a/apps/web/src/app/(dashboard)/admin/ses-configurations.tsx b/apps/web/src/app/(dashboard)/admin/ses-configurations.tsx index 6d785b64..69b1761c 100644 --- a/apps/web/src/app/(dashboard)/admin/ses-configurations.tsx +++ b/apps/web/src/app/(dashboard)/admin/ses-configurations.tsx @@ -13,6 +13,7 @@ import { api } from "~/trpc/react"; import Spinner from "@usesend/ui/src/spinner"; import EditSesConfiguration from "./edit-ses-configuration"; import { TextWithCopyButton } from "@usesend/ui/src/text-with-copy"; +import DeleteSesConfiguration from "./delete-ses-configuration"; export default function SesConfigurations() { const sesSettingsQuery = api.admin.getSesSettings.useQuery(); @@ -71,7 +72,10 @@ export default function SesConfigurations() { {sesSetting.sesEmailRateLimit} {sesSetting.transactionalQuota}% - +
+ + +
)) diff --git a/apps/web/src/server/api/routers/admin.ts b/apps/web/src/server/api/routers/admin.ts index f6ec36ab..98db8491 100644 --- a/apps/web/src/server/api/routers/admin.ts +++ b/apps/web/src/server/api/routers/admin.ts @@ -122,6 +122,17 @@ export const adminRouter = createTRPCRouter({ }); }), + deleteSesSettings: adminProcedure + .input( + z.object({ + settingsId: z.string(), + }) + ) + .mutation(async ({ input }) => { + await SesSettingsService.deleteSesSetting(input.settingsId); + return true; + }), + getSetting: adminProcedure .input( z.object({ diff --git a/apps/web/src/server/aws/ses.ts b/apps/web/src/server/aws/ses.ts index 1b3296b8..b1a807c9 100644 --- a/apps/web/src/server/aws/ses.ts +++ b/apps/web/src/server/aws/ses.ts @@ -7,6 +7,7 @@ import { SendEmailCommand, CreateConfigurationSetEventDestinationCommand, CreateConfigurationSetCommand, + DeleteConfigurationSetCommand, EventType, GetAccountCommand, CreateTenantResourceAssociationCommand, @@ -310,3 +311,26 @@ export async function addWebhookConfiguration( const response = await sesClient.send(command); return response.$metadata.httpStatusCode === 200; } + +export async function deleteConfigurationSet( + configName: string, + region: string +) { + const sesClient = getSesClient(region); + const command = new DeleteConfigurationSetCommand({ + ConfigurationSetName: configName, + }); + + try { + await sesClient.send(command); + } catch (error: any) { + if (error?.name === "NotFoundException") { + logger.warn( + { configName, region }, + "Configuration set not found while deleting" + ); + return; + } + throw error; + } +} diff --git a/apps/web/src/server/service/email-queue-service.ts b/apps/web/src/server/service/email-queue-service.ts index 730eedbb..e1831eda 100644 --- a/apps/web/src/server/service/email-queue-service.ts +++ b/apps/web/src/server/service/email-queue-service.ts @@ -108,6 +108,27 @@ export class EmailQueueService { } } + public static async removeRegion(region: string) { + logger.info({ region }, `[EmailQueueService]: Removing queues for region`); + + const transactionalQueue = this.transactionalQueue.get(region); + const transactionalWorker = this.transactionalWorker.get(region); + const marketingQueue = this.marketingQueue.get(region); + const marketingWorker = this.marketingWorker.get(region); + + await Promise.all([ + transactionalWorker?.close(), + transactionalQueue?.close(), + marketingWorker?.close(), + marketingQueue?.close(), + ]); + + this.transactionalQueue.delete(region); + this.transactionalWorker.delete(region); + this.marketingQueue.delete(region); + this.marketingWorker.delete(region); + } + public static async queueEmail( emailId: string, teamId: number, diff --git a/apps/web/src/server/service/ses-settings-service.ts b/apps/web/src/server/service/ses-settings-service.ts index 7402d713..d576fba6 100644 --- a/apps/web/src/server/service/ses-settings-service.ts +++ b/apps/web/src/server/service/ses-settings-service.ts @@ -202,6 +202,43 @@ export class SesSettingsService { await this.invalidateCache(); } + public static async deleteSesSetting(id: string) { + await this.checkInitialized(); + + const setting = await db.sesSetting.findUnique({ + where: { id }, + }); + + if (!setting) { + throw new Error("SES setting not found"); + } + + const configNames = [ + setting.configGeneral, + setting.configClick, + setting.configOpen, + setting.configFull, + ].filter(Boolean) as string[]; + + await Promise.all( + configNames.map((configName) => + ses.deleteConfigurationSet(configName, setting.region) + ) + ); + + if (setting.topicArn) { + await sns.deleteTopic(setting.topicArn, setting.region); + } + + await EmailQueueService.removeRegion(setting.region); + + await db.sesSetting.delete({ + where: { id }, + }); + + await this.invalidateCache(); + } + public static async checkInitialized() { if (!this.initialized) { await this.invalidateCache();