Skip to content

Commit

Permalink
feat(settings): A user can edit the root key name
Browse files Browse the repository at this point in the history
  • Loading branch information
mcstepp committed Sep 12, 2024
1 parent 4b04836 commit 2fc8a3a
Show file tree
Hide file tree
Showing 5 changed files with 191 additions and 1 deletion.
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,6 @@ export default async function Layout({ children, params: { keyId } }: Props) {
</CardHeader>
<CardContent className="flex flex-wrap justify-between divide-x [&>div:first-child]:pl-0">
<Metric label="ID" value={<span className="font-mono">{key.id}</span>} />
<Metric label="Name" value={key.name ?? "-"} />
<Metric label="Created At" value={key.createdAt.toDateString()} />
<Metric
label={key.expires && key.expires.getTime() < Date.now() ? "Expired" : "Expires in"}
Expand Down
3 changes: 3 additions & 0 deletions apps/dashboard/app/(app)/settings/root-keys/[keyId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { Api } from "./permissions/api";
import { Legacy } from "./permissions/legacy";
import { apiPermissions } from "./permissions/permissions";
import { Workspace } from "./permissions/workspace";
import { UpdateRootKeyName } from "./update-root-key-name";

export const dynamic = "force-dynamic";
export const runtime = "edge";
Expand Down Expand Up @@ -125,6 +126,8 @@ export default async function RootKeyPage(props: {
<Legacy keyId={key.id} permissions={permissions} />
) : null}

<UpdateRootKeyName apiKey={key} />

<Workspace keyId={key.id} permissions={permissions} />

{apisWithActivePermissions.map((api) => (
Expand Down
Original file line number Diff line number Diff line change
@@ -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<Props> = ({ apiKey }) => {
const router = useRouter();

const form = useForm<z.infer<typeof formSchema>>({
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<typeof formSchema>) {
updateName.mutateAsync(values);
}
return (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)}>
<Card>
<CardHeader>
<CardTitle>Name</CardTitle>
<CardDescription>
Give your root key a name. This is optional and not customer facing.
</CardDescription>
</CardHeader>
<CardContent className="flex justify-between item-center">
<div className={cn("flex flex-col space-y-2 w-full ")}>
<input type="hidden" name="keyId" value={apiKey.id} />
<Label htmlFor="remaining">Name</Label>
<FormField
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormControl>
<Input {...field} type="string" className="h-8 max-w-sm" autoComplete="off" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
<CardFooter className="justify-end">
<Button disabled={updateName.isLoading || !form.formState.isValid} type="submit">
{updateName.isLoading ? <Loading /> : "Save"}
</Button>
</CardFooter>
</Card>
</form>
</Form>
);
};
4 changes: 4 additions & 0 deletions apps/dashboard/lib/trpc/routers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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,
Expand Down
80 changes: 80 additions & 0 deletions apps/dashboard/lib/trpc/routers/key/updateRootKeyName.ts
Original file line number Diff line number Diff line change
@@ -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;
});

0 comments on commit 2fc8a3a

Please sign in to comment.