From 0817b0c7a5741fd7ce8b3460ca9122dac05942fd Mon Sep 17 00:00:00 2001 From: Andreas Enemyr Date: Wed, 10 Sep 2025 13:30:37 +0200 Subject: [PATCH] feat: add domain-based access control for API keys (#198) Co-authored-by: Claude --- .../migration.sql | 5 ++ apps/web/prisma/schema.prisma | 3 + .../dev-settings/api-keys/add-api-key.tsx | 40 +++++++++++++ .../dev-settings/api-keys/api-list.tsx | 12 +++- apps/web/src/server/api/routers/api.ts | 17 +++++- apps/web/src/server/public-api/api-utils.ts | 23 +++++++ .../public-api/api/domains/get-domains.ts | 10 +++- .../public-api/api/domains/verify-domain.ts | 60 ++++++++++++++++++- .../public-api/api/emails/cancel-email.ts | 4 +- .../server/public-api/api/emails/get-email.ts | 15 +++-- .../public-api/api/emails/list-emails.ts | 4 +- .../public-api/api/emails/update-email.ts | 4 +- apps/web/src/server/public-api/auth.ts | 2 +- apps/web/src/server/public-api/hono.ts | 4 +- apps/web/src/server/service/api-service.ts | 23 +++++++ apps/web/src/server/service/domain-service.ts | 27 ++++++++- apps/web/src/server/service/email-service.ts | 24 +++++++- 17 files changed, 250 insertions(+), 27 deletions(-) create mode 100644 apps/web/prisma/migrations/20250822125136_add_domain_access_to_api_keys/migration.sql diff --git a/apps/web/prisma/migrations/20250822125136_add_domain_access_to_api_keys/migration.sql b/apps/web/prisma/migrations/20250822125136_add_domain_access_to_api_keys/migration.sql new file mode 100644 index 0000000..318681a --- /dev/null +++ b/apps/web/prisma/migrations/20250822125136_add_domain_access_to_api_keys/migration.sql @@ -0,0 +1,5 @@ +-- AlterTable +ALTER TABLE "ApiKey" ADD COLUMN "domainId" INTEGER; + +-- AddForeignKey +ALTER TABLE "ApiKey" ADD CONSTRAINT "ApiKey_domainId_fkey" FOREIGN KEY ("domainId") REFERENCES "Domain"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/apps/web/prisma/schema.prisma b/apps/web/prisma/schema.prisma index a2ae7ed..7c4925f 100644 --- a/apps/web/prisma/schema.prisma +++ b/apps/web/prisma/schema.prisma @@ -195,6 +195,7 @@ model Domain { createdAt DateTime @default(now()) updatedAt DateTime @updatedAt team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + apiKeys ApiKey[] } enum ApiPermission { @@ -209,11 +210,13 @@ model ApiKey { partialToken String name String permission ApiPermission @default(SENDING) + domainId Int? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt lastUsed DateTime? teamId Int team Team @relation(fields: [teamId], references: [id], onDelete: Cascade) + domain Domain? @relation(fields: [domainId], references: [id], onDelete: Cascade) } enum EmailStatus { diff --git a/apps/web/src/app/(dashboard)/dev-settings/api-keys/add-api-key.tsx b/apps/web/src/app/(dashboard)/dev-settings/api-keys/add-api-key.tsx index b6e674b..098e10d 100644 --- a/apps/web/src/app/(dashboard)/dev-settings/api-keys/add-api-key.tsx +++ b/apps/web/src/app/(dashboard)/dev-settings/api-keys/add-api-key.tsx @@ -27,11 +27,20 @@ import { FormLabel, FormMessage, } from "@usesend/ui/src/form"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@unsend/ui/src/select"; + const apiKeySchema = z.object({ name: z.string({ required_error: "Name is required" }).min(1, { message: "Name is required", }), + domainId: z.string().optional(), }); export default function AddApiKey() { @@ -40,6 +49,8 @@ export default function AddApiKey() { const createApiKeyMutation = api.apiKey.createToken.useMutation(); const [isCopied, setIsCopied] = useState(false); const [showApiKey, setShowApiKey] = useState(false); + + const domainsQuery = api.domain.domains.useQuery(); const utils = api.useUtils(); @@ -47,6 +58,7 @@ export default function AddApiKey() { resolver: zodResolver(apiKeySchema), defaultValues: { name: "", + domainId: "all", }, }); @@ -55,6 +67,7 @@ export default function AddApiKey() { { name: values.name, permission: "FULL", + domainId: values.domainId === "all" ? undefined : Number(values.domainId), }, { onSuccess: (data) => { @@ -180,6 +193,33 @@ export default function AddApiKey() { )} /> + ( + + Domain access + + + Choose which domain this API key can send emails from. + + + )} + />