diff --git a/apps/dashboard/app/(app)/settings/root-keys/[keyId]/layout.tsx b/apps/dashboard/app/(app)/settings/root-keys/[keyId]/layout.tsx index 3d64889ffd..f47a085ccf 100644 --- a/apps/dashboard/app/(app)/settings/root-keys/[keyId]/layout.tsx +++ b/apps/dashboard/app/(app)/settings/root-keys/[keyId]/layout.tsx @@ -48,7 +48,6 @@ export default async function Layout({ children, params: { keyId } }: Props) { {key.id}} /> - ) : null} + + {apisWithActivePermissions.map((api) => ( diff --git a/apps/dashboard/app/(app)/settings/root-keys/[keyId]/update-root-key-name.tsx b/apps/dashboard/app/(app)/settings/root-keys/[keyId]/update-root-key-name.tsx new file mode 100644 index 0000000000..24c755b370 --- /dev/null +++ b/apps/dashboard/app/(app)/settings/root-keys/[keyId]/update-root-key-name.tsx @@ -0,0 +1,104 @@ +"use client"; +import { Loading } from "@/components/dashboard/loading"; +import { Button } from "@/components/ui/button"; +import { + Card, + CardContent, + CardDescription, + CardFooter, + CardHeader, + CardTitle, +} from "@/components/ui/card"; +import { Form, FormControl, FormField, FormItem, FormMessage } from "@/components/ui/form"; +import { Input } from "@/components/ui/input"; +import { Label } from "@/components/ui/label"; +import { toast } from "@/components/ui/toaster"; +import { trpc } from "@/lib/trpc/client"; +import { cn, parseTrpcError } from "@/lib/utils"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { useRouter } from "next/navigation"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; + +const formSchema = z.object({ + keyId: z.string(), + name: z + .string() + .transform((e) => (e === "" ? undefined : e)) + .optional(), +}); +type Props = { + apiKey: { + id: string; + workspaceId: string; + name: string | null; + }; +}; + +export const UpdateRootKeyName: React.FC = ({ apiKey }) => { + const router = useRouter(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + mode: "all", + shouldFocusError: true, + delayError: 100, + defaultValues: { + keyId: apiKey.id, + name: apiKey.name ?? "", + }, + }); + + const updateName = trpc.rootKey.update.name.useMutation({ + onSuccess() { + toast.success("Your root key name has been updated!"); + router.refresh(); + }, + onError(err) { + console.error(err); + const message = parseTrpcError(err); + toast.error(message); + }, + }); + + async function onSubmit(values: z.infer) { + updateName.mutateAsync(values); + } + return ( +
+ + + + Name + + Give your root key a name. This is optional and not customer facing. + + + +
+ + + ( + + + + + + + )} + /> +
+
+ + + +
+
+ + ); +}; diff --git a/apps/dashboard/lib/trpc/routers/index.ts b/apps/dashboard/lib/trpc/routers/index.ts index 81c9efe2f3..f5873f80a3 100644 --- a/apps/dashboard/lib/trpc/routers/index.ts +++ b/apps/dashboard/lib/trpc/routers/index.ts @@ -16,6 +16,7 @@ import { updateKeyName } from "./key/updateName"; import { updateKeyOwnerId } from "./key/updateOwnerId"; import { updateKeyRatelimit } from "./key/updateRatelimit"; import { updateKeyRemaining } from "./key/updateRemaining"; +import { updateRootKeyName } from "./key/updateRootKeyName"; import { createLlmGateway } from "./llmGateway/create"; import { deleteLlmGateway } from "./llmGateway/delete"; import { createVerificationMonitor } from "./monitor/verification/create"; @@ -89,6 +90,9 @@ export const router = t.router({ rootKey: t.router({ create: createRootKey, delete: deleteRootKeys, + update: t.router({ + name: updateRootKeyName, + }), }), api: t.router({ create: createApi, diff --git a/apps/dashboard/lib/trpc/routers/key/updateRootKeyName.ts b/apps/dashboard/lib/trpc/routers/key/updateRootKeyName.ts new file mode 100644 index 0000000000..b9fbd2fbdc --- /dev/null +++ b/apps/dashboard/lib/trpc/routers/key/updateRootKeyName.ts @@ -0,0 +1,80 @@ +import { db, eq, schema } from "@/lib/db"; +import { env } from "@/lib/env"; +import { ingestAuditLogs } from "@/lib/tinybird"; +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { auth, t } from "../../trpc"; + +export const updateRootKeyName = t.procedure + .use(auth) + .input( + z.object({ + keyId: z.string(), + name: z.string().nullish(), + }), + ) + .mutation(async ({ input, ctx }) => { + const key = await db.query.keys.findFirst({ + where: (table, { eq, isNull, and }) => + and(eq(table.id, input.keyId), isNull(table.deletedAt)), + with: { + workspace: true, + }, + }); + + const workspace = await db.query.workspaces.findFirst({ + where: (table, { and, eq, isNull }) => + and(eq(table.tenantId, ctx.tenant.id), isNull(table.deletedAt)), + }); + + if (!workspace) { + throw new TRPCError({ + code: "NOT_FOUND", + message: + "We are unable to find the correct workspace. Please contact support using support@unkey.dev.", + }); + } + + if (!key || key.forWorkspaceId !== workspace.id) { + throw new TRPCError({ + message: + "We are unable to find the correct key. Please contact support using support@unkey.dev.", + code: "NOT_FOUND", + }); + } + + await db + .update(schema.keys) + .set({ + name: input.name ?? null, + }) + .where(eq(schema.keys.id, key.id)) + .catch((_err) => { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "We are unable to update name on this key. Please contact support using support@unkey.dev", + }); + }); + + await ingestAuditLogs({ + workspaceId: key.workspace.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "key.update", + description: `Changed name of ${key.id} to ${input.name}`, + resources: [ + { + type: "key", + id: key.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + return true; + });