From d5777fcc1ac41381307626894253624982a77d54 Mon Sep 17 00:00:00 2001 From: Bluestart83 Date: Tue, 28 Apr 2026 13:11:33 +0200 Subject: [PATCH 1/3] feat(domains): delete button on domain list page Reuse DeleteDomain component from detail page; same typed confirmation dialog. Adds delete affordance to /domains list rows. --- apps/web/src/app/(dashboard)/domains/domain-list.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/apps/web/src/app/(dashboard)/domains/domain-list.tsx b/apps/web/src/app/(dashboard)/domains/domain-list.tsx index a2ef5f4e..58254cc7 100644 --- a/apps/web/src/app/(dashboard)/domains/domain-list.tsx +++ b/apps/web/src/app/(dashboard)/domains/domain-list.tsx @@ -9,6 +9,7 @@ import React from "react"; import { StatusIndicator } from "./status-indicator"; import { DomainStatusBadge } from "./domain-badge"; import Spinner from "@usesend/ui/src/spinner"; +import { DeleteDomain } from "./[domainId]/delete-domain"; export default function DomainsList() { const domainsQuery = api.domain.domains.useQuery(); @@ -116,6 +117,9 @@ const DomainItem: React.FC<{ domain: Domain }> = ({ domain }) => { /> +
+ +
From 3521883c486b7abe2f072e924b33d76d04206961 Mon Sep 17 00:00:00 2001 From: Bluestart83 Date: Tue, 28 Apr 2026 13:11:33 +0200 Subject: [PATCH 2/3] fix(domains): make SES deleteDomain idempotent + surface errors - aws/ses.ts: catch NotFoundException on DeleteTenantResourceAssociation and DeleteEmailIdentity, log warn and continue (record can still be removed from DB when the domain was already deleted on SES). Other SES errors are now logged with full context before rethrow. - delete-domain.tsx: add onError toast so failures surface in the UI instead of silently leaving the dialog open. --- .../domains/[domainId]/delete-domain.tsx | 3 + apps/web/src/server/aws/ses.ts | 68 +++++++++++++------ 2 files changed, 52 insertions(+), 19 deletions(-) diff --git a/apps/web/src/app/(dashboard)/domains/[domainId]/delete-domain.tsx b/apps/web/src/app/(dashboard)/domains/[domainId]/delete-domain.tsx index b5e8fab9..ae9dacc7 100644 --- a/apps/web/src/app/(dashboard)/domains/[domainId]/delete-domain.tsx +++ b/apps/web/src/app/(dashboard)/domains/[domainId]/delete-domain.tsx @@ -33,6 +33,9 @@ export const DeleteDomain: React.FC<{ domain: Domain }> = ({ domain }) => { toast.success(`Domain ${domain.name} deleted`); router.replace("/domains"); }, + onError: (error) => { + toast.error(`Failed to delete domain: ${error.message}`); + }, }, ); } diff --git a/apps/web/src/server/aws/ses.ts b/apps/web/src/server/aws/ses.ts index 0fda94c8..eaebad72 100644 --- a/apps/web/src/server/aws/ses.ts +++ b/apps/web/src/server/aws/ses.ts @@ -150,30 +150,60 @@ export async function deleteDomain( const sesClient = getSesClient(region); if (sesTenantId) { - const tenantResourceAssociationCommand = - new DeleteTenantResourceAssociationCommand({ - TenantName: sesTenantId, - ResourceArn: await getIdentityArn(domain, region), - }); + try { + const tenantResourceAssociationCommand = + new DeleteTenantResourceAssociationCommand({ + TenantName: sesTenantId, + ResourceArn: await getIdentityArn(domain, region), + }); + + const tenantResourceAssociationResponse = await sesClient.send( + tenantResourceAssociationCommand + ); - const tenantResourceAssociationResponse = await sesClient.send( - tenantResourceAssociationCommand - ); + if (tenantResourceAssociationResponse.$metadata.httpStatusCode !== 200) { + logger.error( + { domain, region, sesTenantId, tenantResourceAssociationResponse }, + "[ses.deleteDomain] non-200 from DeleteTenantResourceAssociation" + ); + throw new Error("Failed to delete tenant resource association"); + } + } catch (error: any) { + if (error?.name === "NotFoundException") { + logger.warn( + { domain, region, sesTenantId, errorName: error.name }, + "[ses.deleteDomain] tenant association already gone, continuing" + ); + } else { + logger.error( + { err: error, domain, region, sesTenantId }, + "[ses.deleteDomain] DeleteTenantResourceAssociation failed" + ); + throw error; + } + } + } - if (tenantResourceAssociationResponse.$metadata.httpStatusCode !== 200) { - logger.error( - { tenantResourceAssociationResponse }, - "Failed to delete tenant resource association" + try { + const command = new DeleteEmailIdentityCommand({ + EmailIdentity: domain, + }); + const response = await sesClient.send(command); + return response.$metadata.httpStatusCode === 200; + } catch (error: any) { + if (error?.name === "NotFoundException") { + logger.warn( + { domain, region, errorName: error.name }, + "[ses.deleteDomain] identity already gone on SES, continuing" ); - throw new Error("Failed to delete tenant resource association"); + return true; } + logger.error( + { err: error, domain, region }, + "[ses.deleteDomain] DeleteEmailIdentity failed" + ); + throw error; } - - const command = new DeleteEmailIdentityCommand({ - EmailIdentity: domain, - }); - const response = await sesClient.send(command); - return response.$metadata.httpStatusCode === 200; } export async function getDomainIdentity(domain: string, region: string) { From c9116046454d5c4070ffd8ac64db92133eb46dc3 Mon Sep 17 00:00:00 2001 From: Bluestart83 Date: Tue, 28 Apr 2026 14:03:12 +0200 Subject: [PATCH 3/3] fix(domains): log non-200 DeleteEmailIdentity responses Address CodeRabbit review: when DeleteEmailIdentity returns a non-200 status without throwing, the function silently returned false, leaving callers with no SES context. Now logs domain/region/response before returning false, consistent with the rest of this PR's no-silent-failures philosophy. --- apps/web/src/server/aws/ses.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/apps/web/src/server/aws/ses.ts b/apps/web/src/server/aws/ses.ts index eaebad72..62d4d0d6 100644 --- a/apps/web/src/server/aws/ses.ts +++ b/apps/web/src/server/aws/ses.ts @@ -189,7 +189,14 @@ export async function deleteDomain( EmailIdentity: domain, }); const response = await sesClient.send(command); - return response.$metadata.httpStatusCode === 200; + if (response.$metadata.httpStatusCode !== 200) { + logger.error( + { domain, region, response }, + "[ses.deleteDomain] non-200 from DeleteEmailIdentity" + ); + return false; + } + return true; } catch (error: any) { if (error?.name === "NotFoundException") { logger.warn(