diff --git a/apps/web/src/app/(dashboard)/dev-settings/api-keys/api-list.tsx b/apps/web/src/app/(dashboard)/dev-settings/api-keys/api-list.tsx index 491fc744..10427046 100644 --- a/apps/web/src/app/(dashboard)/dev-settings/api-keys/api-list.tsx +++ b/apps/web/src/app/(dashboard)/dev-settings/api-keys/api-list.tsx @@ -11,10 +11,15 @@ import { import { formatDistanceToNow } from "date-fns"; import { api } from "~/trpc/react"; import DeleteApiKey from "./delete-api-key"; +import { EditApiKeyDialog } from "./edit-api-key"; import Spinner from "@usesend/ui/src/spinner"; +import { useState } from "react"; +import { Edit3 } from "lucide-react"; +import { Button } from "@usesend/ui/src/button"; export default function ApiList() { const apiKeysQuery = api.apiKey.getApiKeys.useQuery(); + const [editingId, setEditingId] = useState(null); return (
@@ -60,14 +65,34 @@ export default function ApiList() { {apiKey.lastUsed - ? formatDistanceToNow(apiKey.lastUsed, { addSuffix: true }) + ? formatDistanceToNow(apiKey.lastUsed, { + addSuffix: true, + }) : "Never"} - {formatDistanceToNow(apiKey.createdAt, { addSuffix: true })} + {formatDistanceToNow(apiKey.createdAt, { + addSuffix: true, + })} - +
+ + + { + if (!open) setEditingId(null); + }} + /> +
)) diff --git a/apps/web/src/app/(dashboard)/dev-settings/api-keys/edit-api-key.tsx b/apps/web/src/app/(dashboard)/dev-settings/api-keys/edit-api-key.tsx new file mode 100644 index 00000000..9d2cc4f4 --- /dev/null +++ b/apps/web/src/app/(dashboard)/dev-settings/api-keys/edit-api-key.tsx @@ -0,0 +1,184 @@ +"use client"; + +import { useEffect } from "react"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useForm } from "react-hook-form"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "@usesend/ui/src/dialog"; +import { + Form, + FormControl, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@usesend/ui/src/form"; +import { Input } from "@usesend/ui/src/input"; +import { Button } from "@usesend/ui/src/button"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@usesend/ui/src/select"; +import { api } from "~/trpc/react"; +import { toast } from "@usesend/ui/src/toaster"; + +const editApiKeySchema = z.object({ + name: z + .string({ required_error: "Name is required" }) + .min(1, { message: "Name is required" }), + domainId: z.string().optional(), +}); + +type EditApiKeyFormValues = z.infer; + +interface ApiKeyData { + id: number; + name: string; + domainId: number | null; + domain?: { name: string } | null; +} + +export function EditApiKeyDialog({ + apiKey, + open, + onOpenChange, +}: { + apiKey: ApiKeyData; + open: boolean; + onOpenChange: (open: boolean) => void; +}) { + const updateApiKey = api.apiKey.updateApiKey.useMutation(); + const domainsQuery = api.domain.domains.useQuery(); + const utils = api.useUtils(); + + const form = useForm({ + resolver: zodResolver(editApiKeySchema), + defaultValues: { + name: apiKey.name, + domainId: apiKey.domainId ? apiKey.domainId.toString() : "all", + }, + }); + + useEffect(() => { + if (open) { + form.reset({ + name: apiKey.name, + domainId: apiKey.domainId ? apiKey.domainId.toString() : "all", + }); + } + }, [open, apiKey, form]); + + function handleSubmit(values: EditApiKeyFormValues) { + const domainId = + values.domainId === "all" ? null : Number(values.domainId); + + updateApiKey.mutate( + { + id: apiKey.id, + name: values.name, + domainId, + }, + { + onSuccess: () => { + utils.apiKey.invalidate(); + toast.success("API key updated"); + onOpenChange(false); + }, + onError: (error) => { + toast.error(error.message); + }, + }, + ); + } + + return ( + + + + Edit API key + +
+
+ + ( + + API key name + + + + {formState.errors.name ? ( + + ) : ( + + Use a name to easily identify this API key. + + )} + + )} + /> + ( + + Domain access + + + Choose which domain this API key can send emails from. + + + )} + /> +
+ +
+ + +
+
+
+ ); +} diff --git a/apps/web/src/server/api/routers/api.ts b/apps/web/src/server/api/routers/api.ts index 198fddad..5e431817 100644 --- a/apps/web/src/server/api/routers/api.ts +++ b/apps/web/src/server/api/routers/api.ts @@ -1,13 +1,16 @@ import { z } from "zod"; import { ApiPermission } from "@prisma/client"; -import { TRPCError } from "@trpc/server"; import { apiKeyProcedure, createTRPCRouter, teamProcedure, } from "~/server/api/trpc"; -import { addApiKey, deleteApiKey } from "~/server/service/api-service"; +import { + addApiKey, + deleteApiKey, + updateApiKey, +} from "~/server/service/api-service"; export const apiRouter = createTRPCRouter({ createToken: teamProcedure @@ -45,12 +48,31 @@ export const apiRouter = createTRPCRouter({ name: true, }, }, + }, + orderBy: { + createdAt: "desc", }, }); return keys; }), + updateApiKey: apiKeyProcedure + .input( + z.object({ + name: z.string().min(1).optional(), + domainId: z.number().int().positive().nullable().optional(), + }) + ) + .mutation(async ({ ctx, input }) => { + return await updateApiKey({ + id: input.id, + teamId: ctx.team.id, + name: input.name, + domainId: input.domainId, + }); + }), + deleteApiKey: apiKeyProcedure.mutation(async ({ input }) => { return deleteApiKey(input.id); }), diff --git a/apps/web/src/server/service/api-service.ts b/apps/web/src/server/service/api-service.ts index 5f92de40..b489792f 100644 --- a/apps/web/src/server/service/api-service.ts +++ b/apps/web/src/server/service/api-service.ts @@ -93,6 +93,45 @@ export async function getTeamAndApiKey(apiKey: string) { } } +export async function updateApiKey({ + id, + teamId, + name, + domainId, +}: { + id: number; + teamId: number; + name?: string; + domainId?: number | null; +}) { + try { + if (domainId !== undefined && domainId !== null) { + const domain = await db.domain.findUnique({ + where: { + id: domainId, + teamId: teamId, + }, + select: { id: true }, + }); + + if (!domain) { + throw new Error("DOMAIN_NOT_FOUND"); + } + } + + return await db.apiKey.update({ + where: { id, teamId }, + data: { + ...(name !== undefined && { name }), + ...(domainId !== undefined && { domainId }), + }, + }); + } catch (error) { + logger.error({ err: error }, "Error updating API key"); + throw error; + } +} + export async function deleteApiKey(id: number) { try { await db.apiKey.delete({