From 1a36a9e119d855756f3638763a26e22dfa5af45d Mon Sep 17 00:00:00 2001 From: hiroasano Date: Sun, 1 Sep 2024 15:40:22 +0200 Subject: [PATCH 01/16] feat: Reroll keys --- .../[apiId]/keys/[keyAuthId]/[keyId]/page.tsx | 23 ++- .../keys/[keyAuthId]/[keyId]/reroll-key.tsx | 170 ++++++++++++++++++ apps/dashboard/lib/trpc/routers/index.ts | 2 + .../lib/trpc/routers/key/updateDeletedAt.ts | 67 +++++++ 4 files changed, 255 insertions(+), 7 deletions(-) create mode 100644 apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx create mode 100644 apps/dashboard/lib/trpc/routers/key/updateDeletedAt.ts diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx index f079901c7..6a0e49df8 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx @@ -25,6 +25,7 @@ import ms from "ms"; import { notFound } from "next/navigation"; import { Chart } from "./chart"; import { VerificationTable } from "./verification-table"; +import { RerollKey } from "./reroll-key"; export default async function APIKeyDetailPage(props: { params: { @@ -164,13 +165,21 @@ export default async function APIKeyDetailPage(props: { > Back to API Keys listing - - - Key settings - + +
+ Reroll Key} + currentKey={key} + apiId={api.id} + /> + + + Key settings + +
diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx new file mode 100644 index 000000000..c877d2b15 --- /dev/null +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx @@ -0,0 +1,170 @@ +"use client"; + +import { Loading } from "@/components/dashboard/loading"; +import { Button } from "@/components/ui/button"; +import { Dialog, DialogTrigger, DialogContent, DialogTitle, DialogDescription, DialogFooter, DialogHeader } from "@/components/ui/dialog"; +import { Form, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; +import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { z } from "zod"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { trpc } from "@/lib/trpc/client"; +import { parseTrpcError } from "@/lib/utils"; +import { toast } from "sonner"; +import type { Key } from "@unkey/db"; + +type Props = { + trigger: React.ReactNode; + currentKey: Key; + apiId: string; +} + +const EXPIRATION_OPTIONS = [ + { key: 'now', value: 'Now' }, + { key: '5m', value: '5 minutes' }, + { key: '30m', value: '30 minutes' }, + { key: '1h', value: '1 hour' }, + { key: '6h', value: '6 hours' }, + { key: '24h', value: '24 hours' }, + { key: '7d', value: '7 days' } +]; + +const formSchema = z.object({ + expiresIn: z.coerce + .string(), +}); + +export const RerollKey: React.FC = ({ trigger, currentKey, apiId }: Props) => { + const router = useRouter(); + const [open, setOpen] = useState(false); + const { keyAuthId } = currentKey; + const [newKeyId, setNewKeyId] = useState(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + reValidateMode: "onBlur", + defaultValues: { + expiresIn: '1h', + }, + }); + + const createKey = trpc.key.create.useMutation({ + onSuccess({keyId}) { + toast.success("Rerolling in progress.", { + description: "Step 1/2", + }); + setNewKeyId(keyId); + }, + onError(err) { + console.error(err); + const message = parseTrpcError(err); + toast.error(message); + }, + }); + + const updateDeletedAt = trpc.key.update.deletedAt.useMutation({ + onSuccess() { + toast.success("Rerolling completed.", { + description: "Step 2/2", + }); + router.push(`/apis/${apiId}/keys/${keyAuthId}/${newKeyId}`); + }, + onError(err) { + console.error(err); + const message = parseTrpcError(err); + toast.error(message); + }, + }); + + async function onSubmit(values: z.infer) { + toast.success("Rerolling in progress.", { + description: "Step 0/2", + }); + + await createKey.mutateAsync({ + ...currentKey, + keyAuthId: currentKey.keyAuthId, + name: currentKey.name || undefined, + environment: currentKey.environment || undefined, + meta: currentKey.meta ? JSON.parse(currentKey.meta) : undefined, + expires: currentKey.expires?.getTime() ?? undefined, + remaining: currentKey.remaining ?? undefined, + }); + + await updateDeletedAt.mutate({ + keyId: currentKey.id, + deletedAt: getDateFromExpirationOption(values.expiresIn), + enabled: values.expiresIn === 'now' ? false: true, + }); + } + + return ( + + {trigger} + + + Regenerate API Key + Rerolling creates a new identical key with the same configuration and automatically expires the current one. Make sure to replace it in your system before it expires. + + +
+ + ( + + Expire previous key in: + + + Choose an optional delay period before the old key expires. + + + + )} + /> + + + + + +
+
+ ); +} + +function getDateFromExpirationOption(option: string) { + switch (option) { + case "5m": + return new Date(new Date().getTime() + 5 * 60 * 1000); + case "30m": + return new Date(new Date().getTime() + 30 * 60 * 1000); + case "1h": + return new Date(new Date().getTime() + 1 * 60 * 60 * 1000); + case "6h": + return new Date(new Date().getTime() + 6 * 60 * 60 * 1000); + case "24h": + return new Date(new Date().getTime() + 1 * 24 * 60 * 60 * 1000); + case "7d": + return new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000); + default: + return new Date(); + } +} \ No newline at end of file diff --git a/apps/dashboard/lib/trpc/routers/index.ts b/apps/dashboard/lib/trpc/routers/index.ts index 81c9efe2f..48d0f876c 100644 --- a/apps/dashboard/lib/trpc/routers/index.ts +++ b/apps/dashboard/lib/trpc/routers/index.ts @@ -9,6 +9,7 @@ import { createKey } from "./key/create"; import { createRootKey } from "./key/createRootKey"; import { deleteKeys } from "./key/delete"; import { deleteRootKeys } from "./key/deleteRootKey"; +import { updateKeyDeletedAt } from "./key/updateDeletedAt"; import { updateKeyEnabled } from "./key/updateEnabled"; import { updateKeyExpiration } from "./key/updateExpiration"; import { updateKeyMetadata } from "./key/updateMetadata"; @@ -65,6 +66,7 @@ export const router = t.router({ create: createKey, delete: deleteKeys, update: t.router({ + deletedAt: updateKeyDeletedAt, enabled: updateKeyEnabled, expiration: updateKeyExpiration, metadata: updateKeyMetadata, diff --git a/apps/dashboard/lib/trpc/routers/key/updateDeletedAt.ts b/apps/dashboard/lib/trpc/routers/key/updateDeletedAt.ts new file mode 100644 index 000000000..c2c9dc86c --- /dev/null +++ b/apps/dashboard/lib/trpc/routers/key/updateDeletedAt.ts @@ -0,0 +1,67 @@ +import { db, eq, schema } from "@/lib/db"; +import { ingestAuditLogs } from "@/lib/tinybird"; +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { auth, t } from "../../trpc"; + +export const updateKeyDeletedAt = t.procedure + .use(auth) + .input( + z.object({ + keyId: z.string(), + deletedAt: z.date(), + enabled: z.boolean(), + }), + ) + .mutation(async ({ input, ctx }) => { + const key = await db.query.keys.findFirst({ + where: (table, { eq, and, isNull }) => + and(eq(table.id, input.keyId), isNull(table.deletedAt)), + with: { + workspace: true, + }, + }); + if (!key || key.workspace.tenantId !== ctx.tenant.id) { + throw new TRPCError({ + message: + "We are unable to find the the correct key. Please contact support using support@unkey.dev.", + code: "NOT_FOUND", + }); + } + + await db + .update(schema.keys) + .set({ + deletedAt: input.deletedAt, + enabled: input.enabled, + }) + .where(eq(schema.keys.id, key.id)) + .catch((_err) => { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "We were unable to update 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 the deletion date of ${key.id} to ${input.deletedAt.toUTCString()}`, + resources: [ + { + type: "key", + id: key.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + return true; + }); From 27241a5f500912f3bc6d210814a433bf45919428 Mon Sep 17 00:00:00 2001 From: hiroasano Date: Sun, 1 Sep 2024 15:42:24 +0200 Subject: [PATCH 02/16] SF --- .../[apiId]/keys/[keyAuthId]/[keyId]/page.tsx | 2 +- .../keys/[keyAuthId]/[keyId]/reroll-key.tsx | 87 ++++++++++++------- 2 files changed, 57 insertions(+), 32 deletions(-) diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx index 6a0e49df8..97d382f8c 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx @@ -165,7 +165,7 @@ export default async function APIKeyDetailPage(props: { > Back to API Keys listing - +
Reroll Key} diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx index c877d2b15..fe9666fa6 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx @@ -2,38 +2,58 @@ import { Loading } from "@/components/dashboard/loading"; import { Button } from "@/components/ui/button"; -import { Dialog, DialogTrigger, DialogContent, DialogTitle, DialogDescription, DialogFooter, DialogHeader } from "@/components/ui/dialog"; -import { Form, FormDescription, FormField, FormItem, FormLabel, FormMessage } from "@/components/ui/form"; -import { Select, SelectTrigger, SelectValue, SelectContent, SelectItem } from "@/components/ui/select"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, + DialogTrigger, +} from "@/components/ui/dialog"; +import { + Form, + FormDescription, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { trpc } from "@/lib/trpc/client"; +import { parseTrpcError } from "@/lib/utils"; +import { zodResolver } from "@hookform/resolvers/zod"; +import type { Key } from "@unkey/db"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { useForm } from "react-hook-form"; -import { z } from "zod"; -import { zodResolver } from "@hookform/resolvers/zod"; -import { trpc } from "@/lib/trpc/client"; -import { parseTrpcError } from "@/lib/utils"; import { toast } from "sonner"; -import type { Key } from "@unkey/db"; +import { z } from "zod"; type Props = { trigger: React.ReactNode; currentKey: Key; apiId: string; -} +}; const EXPIRATION_OPTIONS = [ - { key: 'now', value: 'Now' }, - { key: '5m', value: '5 minutes' }, - { key: '30m', value: '30 minutes' }, - { key: '1h', value: '1 hour' }, - { key: '6h', value: '6 hours' }, - { key: '24h', value: '24 hours' }, - { key: '7d', value: '7 days' } + { key: "now", value: "Now" }, + { key: "5m", value: "5 minutes" }, + { key: "30m", value: "30 minutes" }, + { key: "1h", value: "1 hour" }, + { key: "6h", value: "6 hours" }, + { key: "24h", value: "24 hours" }, + { key: "7d", value: "7 days" }, ]; const formSchema = z.object({ - expiresIn: z.coerce - .string(), + expiresIn: z.coerce.string(), }); export const RerollKey: React.FC = ({ trigger, currentKey, apiId }: Props) => { @@ -46,12 +66,12 @@ export const RerollKey: React.FC = ({ trigger, currentKey, apiId }: Props resolver: zodResolver(formSchema), reValidateMode: "onBlur", defaultValues: { - expiresIn: '1h', + expiresIn: "1h", }, }); const createKey = trpc.key.create.useMutation({ - onSuccess({keyId}) { + onSuccess({ keyId }) { toast.success("Rerolling in progress.", { description: "Step 1/2", }); @@ -96,7 +116,7 @@ export const RerollKey: React.FC = ({ trigger, currentKey, apiId }: Props await updateDeletedAt.mutate({ keyId: currentKey.id, deletedAt: getDateFromExpirationOption(values.expiresIn), - enabled: values.expiresIn === 'now' ? false: true, + enabled: values.expiresIn === "now" ? false : true, }); } @@ -106,7 +126,10 @@ export const RerollKey: React.FC = ({ trigger, currentKey, apiId }: Props Regenerate API Key - Rerolling creates a new identical key with the same configuration and automatically expires the current one. Make sure to replace it in your system before it expires. + + Rerolling creates a new identical key with the same configuration and automatically + expires the current one. Make sure to replace it in your system before it expires. +
@@ -117,17 +140,15 @@ export const RerollKey: React.FC = ({ trigger, currentKey, apiId }: Props render={({ field }) => ( Expire previous key in: - {EXPIRATION_OPTIONS.map((item) => ( - {item.value} + + {item.value} + ))} @@ -140,7 +161,11 @@ export const RerollKey: React.FC = ({ trigger, currentKey, apiId }: Props /> @@ -148,7 +173,7 @@ export const RerollKey: React.FC = ({ trigger, currentKey, apiId }: Props
); -} +}; function getDateFromExpirationOption(option: string) { switch (option) { @@ -167,4 +192,4 @@ function getDateFromExpirationOption(option: string) { default: return new Date(); } -} \ No newline at end of file +} From e1dfb0a3d8cb5a8fef9f703600ed1a7f167fe7a3 Mon Sep 17 00:00:00 2001 From: hiroasano Date: Sun, 1 Sep 2024 16:47:37 +0200 Subject: [PATCH 03/16] Improve error handling and use a transaction --- .../lib/trpc/routers/key/updateDeletedAt.ts | 71 ++++++++++--------- 1 file changed, 39 insertions(+), 32 deletions(-) diff --git a/apps/dashboard/lib/trpc/routers/key/updateDeletedAt.ts b/apps/dashboard/lib/trpc/routers/key/updateDeletedAt.ts index c2c9dc86c..3ddc6b894 100644 --- a/apps/dashboard/lib/trpc/routers/key/updateDeletedAt.ts +++ b/apps/dashboard/lib/trpc/routers/key/updateDeletedAt.ts @@ -29,39 +29,46 @@ export const updateKeyDeletedAt = t.procedure }); } - await db - .update(schema.keys) - .set({ - deletedAt: input.deletedAt, - enabled: input.enabled, - }) - .where(eq(schema.keys.id, key.id)) - .catch((_err) => { - throw new TRPCError({ - code: "INTERNAL_SERVER_ERROR", - message: - "We were unable to update this key. Please contact support using support@unkey.dev", - }); - }); + try { + await db.transaction(async (tx) => { + await tx + .update(schema.keys) + .set({ + deletedAt: input.deletedAt, + enabled: input.enabled, + }) + .where(eq(schema.keys.id, key.id)); - await ingestAuditLogs({ - workspaceId: key.workspace.id, - actor: { - type: "user", - id: ctx.user.id, - }, - event: "key.update", - description: `Changed the deletion date of ${key.id} to ${input.deletedAt.toUTCString()}`, - resources: [ - { - type: "key", - id: key.id, - }, - ], - context: { - location: ctx.audit.location, - userAgent: ctx.audit.userAgent, - }, + await ingestAuditLogs({ + workspaceId: key.workspace.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "key.update", + description: `Changed the deletion date of ${key.id} to ${input.deletedAt.toUTCString()}`, + resources: [ + { + type: "key", + id: key.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }) + .catch((err) => { + tx.rollback(); + throw err; + });; }); + } catch (_err) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "We were unable to update this key. Please contact support using support@unkey.dev", + }); + } return true; }); From 5baa9d617c87c02f4fdb25a28b2383709ebfead2 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Tue, 3 Sep 2024 13:48:58 +0000 Subject: [PATCH 04/16] [autofix.ci] apply automated fixes --- apps/dashboard/lib/trpc/routers/key/updateDeletedAt.ts | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/apps/dashboard/lib/trpc/routers/key/updateDeletedAt.ts b/apps/dashboard/lib/trpc/routers/key/updateDeletedAt.ts index 3ddc6b894..4bdbfa2cc 100644 --- a/apps/dashboard/lib/trpc/routers/key/updateDeletedAt.ts +++ b/apps/dashboard/lib/trpc/routers/key/updateDeletedAt.ts @@ -37,7 +37,7 @@ export const updateKeyDeletedAt = t.procedure deletedAt: input.deletedAt, enabled: input.enabled, }) - .where(eq(schema.keys.id, key.id)); + .where(eq(schema.keys.id, key.id)); await ingestAuditLogs({ workspaceId: key.workspace.id, @@ -57,12 +57,11 @@ export const updateKeyDeletedAt = t.procedure location: ctx.audit.location, userAgent: ctx.audit.userAgent, }, - }) - .catch((err) => { + }).catch((err) => { tx.rollback(); throw err; - });; - }); + }); + }); } catch (_err) { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", From 9ee9a18298698fd6adde248fe4ba9110ac5646f5 Mon Sep 17 00:00:00 2001 From: hiroasano Date: Wed, 4 Sep 2024 11:35:12 +0200 Subject: [PATCH 05/16] Remove code that updates the enabled field --- .../(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx | 1 - apps/dashboard/lib/trpc/routers/key/updateDeletedAt.ts | 2 -- 2 files changed, 3 deletions(-) diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx index fe9666fa6..c4b993625 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx @@ -116,7 +116,6 @@ export const RerollKey: React.FC = ({ trigger, currentKey, apiId }: Props await updateDeletedAt.mutate({ keyId: currentKey.id, deletedAt: getDateFromExpirationOption(values.expiresIn), - enabled: values.expiresIn === "now" ? false : true, }); } diff --git a/apps/dashboard/lib/trpc/routers/key/updateDeletedAt.ts b/apps/dashboard/lib/trpc/routers/key/updateDeletedAt.ts index 4bdbfa2cc..5a988c8d3 100644 --- a/apps/dashboard/lib/trpc/routers/key/updateDeletedAt.ts +++ b/apps/dashboard/lib/trpc/routers/key/updateDeletedAt.ts @@ -10,7 +10,6 @@ export const updateKeyDeletedAt = t.procedure z.object({ keyId: z.string(), deletedAt: z.date(), - enabled: z.boolean(), }), ) .mutation(async ({ input, ctx }) => { @@ -35,7 +34,6 @@ export const updateKeyDeletedAt = t.procedure .update(schema.keys) .set({ deletedAt: input.deletedAt, - enabled: input.enabled, }) .where(eq(schema.keys.id, key.id)); From 32f2a51c2889fb6bbab350b5a9f3cf009184a224 Mon Sep 17 00:00:00 2001 From: hiroasano Date: Wed, 4 Sep 2024 11:37:25 +0200 Subject: [PATCH 06/16] Remove "Step X/X" message from toast --- .../[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx index c4b993625..fbfb26d9d 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx @@ -72,9 +72,6 @@ export const RerollKey: React.FC = ({ trigger, currentKey, apiId }: Props const createKey = trpc.key.create.useMutation({ onSuccess({ keyId }) { - toast.success("Rerolling in progress.", { - description: "Step 1/2", - }); setNewKeyId(keyId); }, onError(err) { @@ -86,9 +83,7 @@ export const RerollKey: React.FC = ({ trigger, currentKey, apiId }: Props const updateDeletedAt = trpc.key.update.deletedAt.useMutation({ onSuccess() { - toast.success("Rerolling completed.", { - description: "Step 2/2", - }); + toast.success("Rerolling completed."); router.push(`/apis/${apiId}/keys/${keyAuthId}/${newKeyId}`); }, onError(err) { @@ -99,9 +94,7 @@ export const RerollKey: React.FC = ({ trigger, currentKey, apiId }: Props }); async function onSubmit(values: z.infer) { - toast.success("Rerolling in progress.", { - description: "Step 0/2", - }); + toast.success("Rerolling in progress."); await createKey.mutateAsync({ ...currentKey, From d1ef3e68fa44949b30fdc9b650c4872e38932275 Mon Sep 17 00:00:00 2001 From: hiroasano Date: Wed, 4 Sep 2024 11:44:07 +0200 Subject: [PATCH 07/16] Remove getDateFromExpirationOption function and use ms lib --- .../keys/[keyAuthId]/[keyId]/reroll-key.tsx | 25 ++++--------------- 1 file changed, 5 insertions(+), 20 deletions(-) diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx index fbfb26d9d..2a415f03a 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx @@ -30,6 +30,7 @@ import { trpc } from "@/lib/trpc/client"; import { parseTrpcError } from "@/lib/utils"; import { zodResolver } from "@hookform/resolvers/zod"; import type { Key } from "@unkey/db"; +import ms from "ms"; import { useRouter } from "next/navigation"; import { useState } from "react"; import { useForm } from "react-hook-form"; @@ -106,9 +107,12 @@ export const RerollKey: React.FC = ({ trigger, currentKey, apiId }: Props remaining: currentKey.remaining ?? undefined, }); + const miliseconds = ms(values.expiresIn); + const deletedAt = new Date(Date.now() + miliseconds); + await updateDeletedAt.mutate({ keyId: currentKey.id, - deletedAt: getDateFromExpirationOption(values.expiresIn), + deletedAt, }); } @@ -166,22 +170,3 @@ export const RerollKey: React.FC = ({ trigger, currentKey, apiId }: Props ); }; - -function getDateFromExpirationOption(option: string) { - switch (option) { - case "5m": - return new Date(new Date().getTime() + 5 * 60 * 1000); - case "30m": - return new Date(new Date().getTime() + 30 * 60 * 1000); - case "1h": - return new Date(new Date().getTime() + 1 * 60 * 60 * 1000); - case "6h": - return new Date(new Date().getTime() + 6 * 60 * 60 * 1000); - case "24h": - return new Date(new Date().getTime() + 1 * 24 * 60 * 60 * 1000); - case "7d": - return new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000); - default: - return new Date(); - } -} From 21c96139c0ba7234e352bb3aeda24ea1ead068d9 Mon Sep 17 00:00:00 2001 From: hiroasano Date: Wed, 4 Sep 2024 12:39:39 +0200 Subject: [PATCH 08/16] Move Reroll Keys to key settings page as a Card --- .../[apiId]/keys/[keyAuthId]/[keyId]/page.tsx | 22 +-- .../keys/[keyAuthId]/[keyId]/reroll-key.tsx | 172 ------------------ .../[keyAuthId]/[keyId]/settings/page.tsx | 4 + .../[keyId]/settings/reroll-key.tsx | 167 +++++++++++++++++ 4 files changed, 178 insertions(+), 187 deletions(-) delete mode 100644 apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx create mode 100644 apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx index 97d382f8c..711ef5bc9 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/page.tsx @@ -25,7 +25,6 @@ import ms from "ms"; import { notFound } from "next/navigation"; import { Chart } from "./chart"; import { VerificationTable } from "./verification-table"; -import { RerollKey } from "./reroll-key"; export default async function APIKeyDetailPage(props: { params: { @@ -166,20 +165,13 @@ export default async function APIKeyDetailPage(props: { Back to API Keys listing -
- Reroll Key} - currentKey={key} - apiId={api.id} - /> - - - Key settings - -
+ + + Key settings +
diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx deleted file mode 100644 index 2a415f03a..000000000 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/reroll-key.tsx +++ /dev/null @@ -1,172 +0,0 @@ -"use client"; - -import { Loading } from "@/components/dashboard/loading"; -import { Button } from "@/components/ui/button"; -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog"; -import { - Form, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select"; -import { trpc } from "@/lib/trpc/client"; -import { parseTrpcError } from "@/lib/utils"; -import { zodResolver } from "@hookform/resolvers/zod"; -import type { Key } from "@unkey/db"; -import ms from "ms"; -import { useRouter } from "next/navigation"; -import { useState } from "react"; -import { useForm } from "react-hook-form"; -import { toast } from "sonner"; -import { z } from "zod"; - -type Props = { - trigger: React.ReactNode; - currentKey: Key; - apiId: string; -}; - -const EXPIRATION_OPTIONS = [ - { key: "now", value: "Now" }, - { key: "5m", value: "5 minutes" }, - { key: "30m", value: "30 minutes" }, - { key: "1h", value: "1 hour" }, - { key: "6h", value: "6 hours" }, - { key: "24h", value: "24 hours" }, - { key: "7d", value: "7 days" }, -]; - -const formSchema = z.object({ - expiresIn: z.coerce.string(), -}); - -export const RerollKey: React.FC = ({ trigger, currentKey, apiId }: Props) => { - const router = useRouter(); - const [open, setOpen] = useState(false); - const { keyAuthId } = currentKey; - const [newKeyId, setNewKeyId] = useState(); - - const form = useForm>({ - resolver: zodResolver(formSchema), - reValidateMode: "onBlur", - defaultValues: { - expiresIn: "1h", - }, - }); - - const createKey = trpc.key.create.useMutation({ - onSuccess({ keyId }) { - setNewKeyId(keyId); - }, - onError(err) { - console.error(err); - const message = parseTrpcError(err); - toast.error(message); - }, - }); - - const updateDeletedAt = trpc.key.update.deletedAt.useMutation({ - onSuccess() { - toast.success("Rerolling completed."); - router.push(`/apis/${apiId}/keys/${keyAuthId}/${newKeyId}`); - }, - onError(err) { - console.error(err); - const message = parseTrpcError(err); - toast.error(message); - }, - }); - - async function onSubmit(values: z.infer) { - toast.success("Rerolling in progress."); - - await createKey.mutateAsync({ - ...currentKey, - keyAuthId: currentKey.keyAuthId, - name: currentKey.name || undefined, - environment: currentKey.environment || undefined, - meta: currentKey.meta ? JSON.parse(currentKey.meta) : undefined, - expires: currentKey.expires?.getTime() ?? undefined, - remaining: currentKey.remaining ?? undefined, - }); - - const miliseconds = ms(values.expiresIn); - const deletedAt = new Date(Date.now() + miliseconds); - - await updateDeletedAt.mutate({ - keyId: currentKey.id, - deletedAt, - }); - } - - return ( - - {trigger} - - - Regenerate API Key - - Rerolling creates a new identical key with the same configuration and automatically - expires the current one. Make sure to replace it in your system before it expires. - - - -
- - ( - - Expire previous key in: - - - Choose an optional delay period before the old key expires. - - - - )} - /> - - - - - -
-
- ); -}; diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx index 44dfb57fa..44c15afce 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx @@ -14,6 +14,7 @@ import { UpdateKeyName } from "./update-key-name"; import { UpdateKeyOwnerId } from "./update-key-owner-id"; import { UpdateKeyRatelimit } from "./update-key-ratelimit"; import { UpdateKeyRemaining } from "./update-key-remaining"; +import { RerollKey } from "./reroll-key"; type Props = { params: { @@ -31,6 +32,8 @@ export default async function SettingsPage(props: Props) { with: { workspace: true, + encrypted: true, + identity: true, }, }); if (!key || key.workspace.tenantId !== tenantId) { @@ -67,6 +70,7 @@ export default async function SettingsPage(props: Props) { +
); diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx new file mode 100644 index 000000000..969a7797d --- /dev/null +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx @@ -0,0 +1,167 @@ +"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, + FormDescription, + FormField, + FormItem, FormMessage +} from "@/components/ui/form"; +import { Label } from "@/components/ui/label"; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select"; +import { trpc } from "@/lib/trpc/client"; +import { parseTrpcError } from "@/lib/utils"; +import { zodResolver } from "@hookform/resolvers/zod"; +import type { Key } from "@unkey/db"; +import ms from "ms"; +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { toast } from "sonner"; +import { z } from "zod"; + +type Props = { + apiId: string; + apiKey: Key; +}; + +const EXPIRATION_OPTIONS = [ + { key: "now", value: "Now" }, + { key: "5m", value: "5 minutes" }, + { key: "30m", value: "30 minutes" }, + { key: "1h", value: "1 hour" }, + { key: "6h", value: "6 hours" }, + { key: "24h", value: "24 hours" }, + { key: "7d", value: "7 days" }, +]; + +const formSchema = z.object({ + expiresIn: z.coerce.string(), +}); + +export const RerollKey: React.FC = ({ apiKey, apiId }: Props) => { + const router = useRouter(); + const [newKeyId, setNewKeyId] = useState(); + + const form = useForm>({ + resolver: zodResolver(formSchema), + reValidateMode: "onBlur", + defaultValues: { + expiresIn: "1h", + }, + }); + + const createKey = trpc.key.create.useMutation({ + onSuccess({ keyId }) { + setNewKeyId(keyId); + }, + onError(err) { + console.error(err); + const message = parseTrpcError(err); + toast.error(message); + }, + }); + + const updateDeletedAt = trpc.key.update.deletedAt.useMutation({ + onSuccess() { + toast.success("Rerolling completed."); + router.push(`/apis/${apiId}/keys/${apiKey.keyAuthId}/${newKeyId}/settings`); + }, + onError(err) { + console.error(err); + const message = parseTrpcError(err); + toast.error(message); + }, + }); + + async function onSubmit(values: z.infer) { + toast.success("Rerolling in progress."); + + await createKey.mutateAsync({ + ...apiKey, + keyAuthId: apiKey.keyAuthId, + name: apiKey.name || undefined, + environment: apiKey.environment || undefined, + meta: apiKey.meta ? JSON.parse(apiKey.meta) : undefined, + expires: apiKey.expires?.getTime() ?? undefined, + remaining: apiKey.remaining ?? undefined, + }); + + const miliseconds = ms(values.expiresIn); + const deletedAt = new Date(Date.now() + miliseconds); + + await updateDeletedAt.mutate({ + keyId: apiKey.id, + deletedAt, + }); + } + + return ( +
+ + + + Reroll Key + + Rerolling creates a new identical key with the same configuration and automatically + expires the current one. Make sure to replace it in your system before it expires. + + + +
+ + ( + + + + Choose an optional delay period before the old key expires. + + + + )} + /> +
+
+ + + +
+
+ + ); +}; From 7f15853b968d9912ffe2448b497c4dc90c9aac6c Mon Sep 17 00:00:00 2001 From: hiroasano Date: Thu, 5 Sep 2024 21:53:33 +0200 Subject: [PATCH 09/16] Include copy of ratelimit, refill, identityId and roles --- .../[keyAuthId]/[keyId]/settings/page.tsx | 6 ++- .../[keyId]/settings/reroll-key.tsx | 37 +++++++++++++++++-- apps/dashboard/lib/trpc/routers/key/create.ts | 2 + 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx index 44c15afce..617918150 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx @@ -2,7 +2,7 @@ import { CopyButton } from "@/components/dashboard/copy-button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Code } from "@/components/ui/code"; import { getTenantId } from "@/lib/auth"; -import { and, db, eq, isNull, schema } from "@/lib/db"; +import { and, db, eq, isNull, Key, schema } from "@/lib/db"; import { ArrowLeft } from "lucide-react"; import Link from "next/link"; import { notFound } from "next/navigation"; @@ -34,6 +34,8 @@ export default async function SettingsPage(props: Props) { workspace: true, encrypted: true, identity: true, + roles: true, + permissions: true, }, }); if (!key || key.workspace.tenantId !== tenantId) { @@ -70,7 +72,7 @@ export default async function SettingsPage(props: Props) { - +
); diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx index 969a7797d..57e687a42 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx @@ -37,7 +37,9 @@ import { z } from "zod"; type Props = { apiId: string; - apiKey: Key; + apiKey: Key & { + roles: [] + }; }; const EXPIRATION_OPTIONS = [ @@ -67,6 +69,9 @@ export const RerollKey: React.FC = ({ apiKey, apiId }: Props) => { }); const createKey = trpc.key.create.useMutation({ + onMutate() { + toast.success("Rerolling Key"); + }, onSuccess({ keyId }) { setNewKeyId(keyId); }, @@ -77,9 +82,17 @@ export const RerollKey: React.FC = ({ apiKey, apiId }: Props) => { }, }); + const updateNewKey = trpc.rbac.connectRoleToKey.useMutation({ + onError(err) { + console.error(err); + const message = parseTrpcError(err); + toast.error(message); + }, + }); + const updateDeletedAt = trpc.key.update.deletedAt.useMutation({ onSuccess() { - toast.success("Rerolling completed."); + toast.success("Key Rerolled."); router.push(`/apis/${apiId}/keys/${apiKey.keyAuthId}/${newKeyId}/settings`); }, onError(err) { @@ -90,9 +103,18 @@ export const RerollKey: React.FC = ({ apiKey, apiId }: Props) => { }); async function onSubmit(values: z.infer) { - toast.success("Rerolling in progress."); + const ratelimit = apiKey.ratelimitLimit ? { + async: apiKey.ratelimitAsync ?? false, + duration: apiKey.ratelimitDuration ?? 0, + limit: apiKey.ratelimitLimit ?? 0, + } : undefined; - await createKey.mutateAsync({ + const refill = apiKey.refillInterval ? { + interval: apiKey.refillInterval ?? "daily", + amount: apiKey.refillAmount ?? 0, + } : undefined; + + const newKey = await createKey.mutateAsync({ ...apiKey, keyAuthId: apiKey.keyAuthId, name: apiKey.name || undefined, @@ -100,6 +122,13 @@ export const RerollKey: React.FC = ({ apiKey, apiId }: Props) => { meta: apiKey.meta ? JSON.parse(apiKey.meta) : undefined, expires: apiKey.expires?.getTime() ?? undefined, remaining: apiKey.remaining ?? undefined, + identityId: apiKey.identityId ?? undefined, + ratelimit, + refill, + }); + + apiKey.roles.forEach(async (role: { roleId : string }) => { + await updateNewKey.mutateAsync({ roleId: role.roleId, keyId: newKey.keyId}) }); const miliseconds = ms(values.expiresIn); diff --git a/apps/dashboard/lib/trpc/routers/key/create.ts b/apps/dashboard/lib/trpc/routers/key/create.ts index fd1b4b0be..2ff5dd5a9 100644 --- a/apps/dashboard/lib/trpc/routers/key/create.ts +++ b/apps/dashboard/lib/trpc/routers/key/create.ts @@ -33,6 +33,7 @@ export const createKey = t.procedure .optional(), enabled: z.boolean().default(true), environment: z.string().optional(), + identityId: z.string().optional(), }), ) .mutation(async ({ input, ctx }) => { @@ -92,6 +93,7 @@ export const createKey = t.procedure deletedAt: null, enabled: input.enabled, environment: input.environment, + identityId: input.identityId, }) .catch((_err) => { throw new TRPCError({ From 7939ede00ce879bd7227a5b62a95ff5385a17b38 Mon Sep 17 00:00:00 2001 From: hiroasano Date: Fri, 6 Sep 2024 12:30:13 +0200 Subject: [PATCH 10/16] Add Confirmation Dialog and New Key Dialog --- .../[keyAuthId]/[keyId]/settings/page.tsx | 6 +- .../settings/reroll-confirmation-dialog.tsx | 45 +++++ .../[keyId]/settings/reroll-key.tsx | 164 +++++++++--------- .../settings/reroll-new-key-dialog.tsx | 73 ++++++++ 4 files changed, 204 insertions(+), 84 deletions(-) create mode 100644 apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-confirmation-dialog.tsx create mode 100644 apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-new-key-dialog.tsx diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx index 617918150..087c96f2c 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx @@ -2,11 +2,12 @@ import { CopyButton } from "@/components/dashboard/copy-button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Code } from "@/components/ui/code"; import { getTenantId } from "@/lib/auth"; -import { and, db, eq, isNull, Key, schema } from "@/lib/db"; +import { type Key, and, db, eq, isNull, schema } from "@/lib/db"; import { ArrowLeft } from "lucide-react"; import Link from "next/link"; import { notFound } from "next/navigation"; import { DeleteKey } from "./delete-key"; +import { RerollKey } from "./reroll-key"; import { UpdateKeyEnabled } from "./update-key-enabled"; import { UpdateKeyExpiration } from "./update-key-expiration"; import { UpdateKeyMetadata } from "./update-key-metadata"; @@ -14,7 +15,6 @@ import { UpdateKeyName } from "./update-key-name"; import { UpdateKeyOwnerId } from "./update-key-owner-id"; import { UpdateKeyRatelimit } from "./update-key-ratelimit"; import { UpdateKeyRemaining } from "./update-key-remaining"; -import { RerollKey } from "./reroll-key"; type Props = { params: { @@ -72,7 +72,7 @@ export default async function SettingsPage(props: Props) { - + ); diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-confirmation-dialog.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-confirmation-dialog.tsx new file mode 100644 index 000000000..aa44429dd --- /dev/null +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-confirmation-dialog.tsx @@ -0,0 +1,45 @@ +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; + +type Props = { + open: boolean; + setOpen: (open: boolean) => void; + onClick: () => void; +}; + +export function RerollConfirmationDialog({ open, setOpen, onClick }: Props) { + return ( + setOpen(o)}> + + + Reroll Key + + Make sure to replace it in your system before it expires. This action cannot be undone. + + + + + Warning + This action is not reversible. Please be certain. + + + + + + + + + ); +} diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx index 57e687a42..57006532e 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx @@ -1,6 +1,5 @@ "use client"; -import { Loading } from "@/components/dashboard/loading"; import { Button } from "@/components/ui/button"; import { Card, @@ -10,12 +9,7 @@ import { CardHeader, CardTitle, } from "@/components/ui/card"; -import { - Form, - FormDescription, - FormField, - FormItem, FormMessage -} from "@/components/ui/form"; +import { Form, FormDescription, FormField, FormItem, FormMessage } from "@/components/ui/form"; import { Label } from "@/components/ui/label"; import { Select, @@ -29,16 +23,17 @@ import { parseTrpcError } from "@/lib/utils"; import { zodResolver } from "@hookform/resolvers/zod"; import type { Key } from "@unkey/db"; import ms from "ms"; -import { useRouter } from "next/navigation"; import { useState } from "react"; import { useForm } from "react-hook-form"; import { toast } from "sonner"; import { z } from "zod"; +import { RerollConfirmationDialog } from "./reroll-confirmation-dialog"; +import { RerollNewKeyDialog } from "./reroll-new-key-dialog"; type Props = { apiId: string; apiKey: Key & { - roles: [] + roles: []; }; }; @@ -57,9 +52,6 @@ const formSchema = z.object({ }); export const RerollKey: React.FC = ({ apiKey, apiId }: Props) => { - const router = useRouter(); - const [newKeyId, setNewKeyId] = useState(); - const form = useForm>({ resolver: zodResolver(formSchema), reValidateMode: "onBlur", @@ -72,9 +64,6 @@ export const RerollKey: React.FC = ({ apiKey, apiId }: Props) => { onMutate() { toast.success("Rerolling Key"); }, - onSuccess({ keyId }) { - setNewKeyId(keyId); - }, onError(err) { console.error(err); const message = parseTrpcError(err); @@ -93,7 +82,6 @@ export const RerollKey: React.FC = ({ apiKey, apiId }: Props) => { const updateDeletedAt = trpc.key.update.deletedAt.useMutation({ onSuccess() { toast.success("Key Rerolled."); - router.push(`/apis/${apiId}/keys/${apiKey.keyAuthId}/${newKeyId}/settings`); }, onError(err) { console.error(err); @@ -103,17 +91,21 @@ export const RerollKey: React.FC = ({ apiKey, apiId }: Props) => { }); async function onSubmit(values: z.infer) { - const ratelimit = apiKey.ratelimitLimit ? { - async: apiKey.ratelimitAsync ?? false, - duration: apiKey.ratelimitDuration ?? 0, - limit: apiKey.ratelimitLimit ?? 0, - } : undefined; - - const refill = apiKey.refillInterval ? { - interval: apiKey.refillInterval ?? "daily", - amount: apiKey.refillAmount ?? 0, - } : undefined; - + const ratelimit = apiKey.ratelimitLimit + ? { + async: apiKey.ratelimitAsync ?? false, + duration: apiKey.ratelimitDuration ?? 0, + limit: apiKey.ratelimitLimit ?? 0, + } + : undefined; + + const refill = apiKey.refillInterval + ? { + interval: apiKey.refillInterval ?? "daily", + amount: apiKey.refillAmount ?? 0, + } + : undefined; + const newKey = await createKey.mutateAsync({ ...apiKey, keyAuthId: apiKey.keyAuthId, @@ -127,70 +119,80 @@ export const RerollKey: React.FC = ({ apiKey, apiId }: Props) => { refill, }); - apiKey.roles.forEach(async (role: { roleId : string }) => { - await updateNewKey.mutateAsync({ roleId: role.roleId, keyId: newKey.keyId}) + apiKey.roles?.forEach(async (role: { roleId: string }) => { + await updateNewKey.mutateAsync({ roleId: role.roleId, keyId: newKey.keyId }); }); const miliseconds = ms(values.expiresIn); const deletedAt = new Date(Date.now() + miliseconds); - + await updateDeletedAt.mutate({ keyId: apiKey.id, deletedAt, }); } + const [confirmatioDialogOpen, setConfirmationDialogOpen] = useState(false); + const confirmationSubmit = () => { + setConfirmationDialogOpen(false); + onSubmit(form.getValues()); + }; + return ( -
- - - - Reroll Key - - Rerolling creates a new identical key with the same configuration and automatically - expires the current one. Make sure to replace it in your system before it expires. - - - -
- - ( - - - - Choose an optional delay period before the old key expires. - - - - )} - /> -
-
- - - -
-
- + <> + + +
+ + + + Reroll Key + + Rerolling creates a new identical key with the same configuration and automatically + expires the current one. Make sure to replace it in your system before it expires. + + + +
+ + ( + + + + Choose an optional delay period before the old key expires. + + + + )} + /> +
+
+ + + +
+
+ + ); }; diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-new-key-dialog.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-new-key-dialog.tsx new file mode 100644 index 000000000..3a45b4223 --- /dev/null +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-new-key-dialog.tsx @@ -0,0 +1,73 @@ +import { CopyButton } from "@/components/dashboard/copy-button"; +import { VisibleButton } from "@/components/dashboard/visible-button"; +import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; +import { Button } from "@/components/ui/button"; +import { Code } from "@/components/ui/code"; +import { + Dialog, + DialogContent, + DialogFooter, + DialogHeader, + DialogTitle, +} from "@/components/ui/dialog"; +import { AlertCircle } from "lucide-react"; +import Link from "next/link"; +import { useState } from "react"; + +type Props = { + newKey: + | { + keyId: `key_${string}`; + key: string; + } + | undefined; + apiId: string; + keyAuthId: string; +}; + +export function RerollNewKeyDialog({ newKey, apiId, keyAuthId }: Props) { + if (!newKey) { + return null; + } + + const split = newKey.key.split("_") ?? []; + const maskedKey = + split.length >= 2 + ? `${split.at(0)}_${"*".repeat(split.at(1)?.length ?? 0)}` + : "*".repeat(split.at(0)?.length ?? 0); + const [showKey, setShowKey] = useState(false); + + const [open, setOpen] = useState(Boolean(newKey)); + + return ( + setOpen(o)}> + + + Your New API Key + + + + This key is only shown once and can not be recovered + + Please pass it on to your user or store it somewhere safe. + + + +
{showKey ? newKey.key : maskedKey}
+
+ + +
+
+ + + + + + + + +
+
+ ); +} From 0cd363e9ee433f56d89dab94cdc7985fa2a3179b Mon Sep 17 00:00:00 2001 From: hiroasano Date: Fri, 6 Sep 2024 13:09:57 +0200 Subject: [PATCH 11/16] coderabbitai: Improve readability --- .../settings/reroll-confirmation-dialog.tsx | 9 +++++++-- .../[keyId]/settings/reroll-new-key-dialog.tsx | 14 ++++++++------ 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-confirmation-dialog.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-confirmation-dialog.tsx index aa44429dd..415837f3a 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-confirmation-dialog.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-confirmation-dialog.tsx @@ -32,10 +32,15 @@ export function RerollConfirmationDialog({ open, setOpen, onClick }: Props) { - - diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-new-key-dialog.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-new-key-dialog.tsx index 3a45b4223..77a21176c 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-new-key-dialog.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-new-key-dialog.tsx @@ -30,11 +30,9 @@ export function RerollNewKeyDialog({ newKey, apiId, keyAuthId }: Props) { return null; } - const split = newKey.key.split("_") ?? []; + const split = newKey.key.split("_"); const maskedKey = - split.length >= 2 - ? `${split.at(0)}_${"*".repeat(split.at(1)?.length ?? 0)}` - : "*".repeat(split.at(0)?.length ?? 0); + split.length >= 2 ? `${split[0]}_${"*".repeat(split[1].length)}` : "*".repeat(split[0].length); const [showKey, setShowKey] = useState(false); const [open, setOpen] = useState(Boolean(newKey)); @@ -61,10 +59,14 @@ export function RerollNewKeyDialog({ newKey, apiId, keyAuthId }: Props) { - + - + From ca74f0f7296180b2adc6ef7feb8ffd43b0810d1b Mon Sep 17 00:00:00 2001 From: hiroasano Date: Fri, 6 Sep 2024 14:34:27 +0200 Subject: [PATCH 12/16] Add last used info to Reroll Confirmation Dialog --- .../keys/[keyAuthId]/[keyId]/settings/page.tsx | 11 ++++++++++- .../[keyId]/settings/reroll-confirmation-dialog.tsx | 10 +++++++++- .../keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx | 5 +++-- .../[keyId]/settings/reroll-new-key-dialog.tsx | 1 + 4 files changed, 23 insertions(+), 4 deletions(-) diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx index 087c96f2c..aed7ebf2e 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx @@ -3,6 +3,7 @@ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/com import { Code } from "@/components/ui/code"; import { getTenantId } from "@/lib/auth"; import { type Key, and, db, eq, isNull, schema } from "@/lib/db"; +import { getLastUsed } from "@/lib/tinybird"; import { ArrowLeft } from "lucide-react"; import Link from "next/link"; import { notFound } from "next/navigation"; @@ -42,6 +43,10 @@ export default async function SettingsPage(props: Props) { return notFound(); } + const lastUsed = await getLastUsed({ keyId: key.id }).then( + (res) => res.data.at(0)?.lastUsed ?? 0, + ); + return (
- +
); diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-confirmation-dialog.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-confirmation-dialog.tsx index 415837f3a..56b8c93ce 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-confirmation-dialog.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-confirmation-dialog.tsx @@ -1,3 +1,4 @@ +"use client"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; import { Button } from "@/components/ui/button"; import { @@ -13,9 +14,10 @@ type Props = { open: boolean; setOpen: (open: boolean) => void; onClick: () => void; + lastUsed: number; }; -export function RerollConfirmationDialog({ open, setOpen, onClick }: Props) { +export function RerollConfirmationDialog({ open, setOpen, onClick, lastUsed }: Props) { return ( setOpen(o)}> @@ -26,6 +28,12 @@ export function RerollConfirmationDialog({ open, setOpen, onClick }: Props) { +

+ {lastUsed + ? `This key was used for the last time on ${new Date(lastUsed).toString()}` + : "This key was never used."} +

+ Warning This action is not reversible. Please be certain. diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx index 57006532e..af29508de 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx @@ -1,5 +1,4 @@ "use client"; - import { Button } from "@/components/ui/button"; import { Card, @@ -35,6 +34,7 @@ type Props = { apiKey: Key & { roles: []; }; + lastUsed: number; }; const EXPIRATION_OPTIONS = [ @@ -51,7 +51,7 @@ const formSchema = z.object({ expiresIn: z.coerce.string(), }); -export const RerollKey: React.FC = ({ apiKey, apiId }: Props) => { +export const RerollKey: React.FC = ({ apiKey, apiId, lastUsed }: Props) => { const form = useForm>({ resolver: zodResolver(formSchema), reValidateMode: "onBlur", @@ -144,6 +144,7 @@ export const RerollKey: React.FC = ({ apiKey, apiId }: Props) => { open={confirmatioDialogOpen} setOpen={setConfirmationDialogOpen} onClick={confirmationSubmit} + lastUsed={lastUsed} />
diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-new-key-dialog.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-new-key-dialog.tsx index 77a21176c..8bb3521b9 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-new-key-dialog.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-new-key-dialog.tsx @@ -1,3 +1,4 @@ +"use client"; import { CopyButton } from "@/components/dashboard/copy-button"; import { VisibleButton } from "@/components/dashboard/visible-button"; import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; From 34497937c53881e08afb89ba4f31bdba28737a9d Mon Sep 17 00:00:00 2001 From: hiroasano Date: Wed, 11 Sep 2024 11:35:29 +0200 Subject: [PATCH 13/16] Fix reroll key when selecting "now" as expiration time --- .../[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx index af29508de..fa34c5986 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx @@ -123,7 +123,7 @@ export const RerollKey: React.FC = ({ apiKey, apiId, lastUsed }: Props) = await updateNewKey.mutateAsync({ roleId: role.roleId, keyId: newKey.keyId }); }); - const miliseconds = ms(values.expiresIn); + const miliseconds = values.expiresIn === "now" ? 0 : ms(values.expiresIn); const deletedAt = new Date(Date.now() + miliseconds); await updateDeletedAt.mutate({ From f4ef0e99d2a24d25700945074eaaa8f630c3a6e6 Mon Sep 17 00:00:00 2001 From: hiroasano Date: Wed, 11 Sep 2024 17:49:27 +0200 Subject: [PATCH 14/16] Copy permissions and encrypted to new key --- .../[keyAuthId]/[keyId]/settings/page.tsx | 5 +- .../[keyId]/settings/reroll-key.tsx | 36 ++++++++-- .../[keyId]/permissions/permission_toggle.tsx | 8 +-- apps/dashboard/lib/trpc/routers/index.ts | 6 +- .../lib/trpc/routers/key/updateEncrypted.ts | 70 +++++++++++++++++++ ...sionToRootKey.ts => addPermissionToKey.ts} | 18 ++--- 6 files changed, 119 insertions(+), 24 deletions(-) create mode 100644 apps/dashboard/lib/trpc/routers/key/updateEncrypted.ts rename apps/dashboard/lib/trpc/routers/rbac/{addPermissionToRootKey.ts => addPermissionToKey.ts} (89%) diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx index aed7ebf2e..abf8f2874 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx @@ -2,7 +2,7 @@ import { CopyButton } from "@/components/dashboard/copy-button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Code } from "@/components/ui/code"; import { getTenantId } from "@/lib/auth"; -import { type Key, and, db, eq, isNull, schema } from "@/lib/db"; +import { type EncryptedKey, Key, Permission, Role, and, db, eq, isNull, schema } from "@/lib/db"; import { getLastUsed } from "@/lib/tinybird"; import { ArrowLeft } from "lucide-react"; import Link from "next/link"; @@ -34,7 +34,6 @@ export default async function SettingsPage(props: Props) { with: { workspace: true, encrypted: true, - identity: true, roles: true, permissions: true, }, @@ -79,7 +78,7 @@ export default async function SettingsPage(props: Props) { diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx index fa34c5986..fb66abed4 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx @@ -20,7 +20,7 @@ import { import { trpc } from "@/lib/trpc/client"; import { parseTrpcError } from "@/lib/utils"; import { zodResolver } from "@hookform/resolvers/zod"; -import type { Key } from "@unkey/db"; +import type { EncryptedKey, Key, Permission, Role } from "@unkey/db"; import ms from "ms"; import { useState } from "react"; import { useForm } from "react-hook-form"; @@ -32,7 +32,9 @@ import { RerollNewKeyDialog } from "./reroll-new-key-dialog"; type Props = { apiId: string; apiKey: Key & { - roles: []; + roles: Role[]; + permissions: Permission[]; + encrypted: EncryptedKey; }; lastUsed: number; }; @@ -71,7 +73,23 @@ export const RerollKey: React.FC = ({ apiKey, apiId, lastUsed }: Props) = }, }); - const updateNewKey = trpc.rbac.connectRoleToKey.useMutation({ + const copyRolesToNewKey = trpc.rbac.connectRoleToKey.useMutation({ + onError(err) { + console.error(err); + const message = parseTrpcError(err); + toast.error(message); + }, + }); + + const copyPermissionsToNewKey = trpc.rbac.addPermissionToKey.useMutation({ + onError(err) { + console.error(err); + const message = parseTrpcError(err); + toast.error(message); + }, + }); + + const copyEncryptedToNewKey = trpc.key.update.encrypted.useMutation({ onError(err) { console.error(err); const message = parseTrpcError(err); @@ -119,10 +137,18 @@ export const RerollKey: React.FC = ({ apiKey, apiId, lastUsed }: Props) = refill, }); - apiKey.roles?.forEach(async (role: { roleId: string }) => { - await updateNewKey.mutateAsync({ roleId: role.roleId, keyId: newKey.keyId }); + apiKey.roles?.forEach(async (role) => { + await copyRolesToNewKey.mutateAsync({ roleId: role.id, keyId: newKey.keyId }); }); + apiKey.permissions?.forEach(async (permission) => { + await copyPermissionsToNewKey.mutateAsync({ permission: permission.name, keyId: newKey.keyId, }); + }) + + if (apiKey.encrypted) { + await copyEncryptedToNewKey.mutateAsync({ encrypted: apiKey.encrypted.encrypted, encryptiodKeyId: apiKey.encrypted.encryptionKeyId, keyId: newKey.keyId }) + } + const miliseconds = values.expiresIn === "now" ? 0 : ms(values.expiresIn); const deletedAt = new Date(Date.now() + miliseconds); diff --git a/apps/dashboard/app/(app)/settings/root-keys/[keyId]/permissions/permission_toggle.tsx b/apps/dashboard/app/(app)/settings/root-keys/[keyId]/permissions/permission_toggle.tsx index f463f1305..fa35e44dc 100644 --- a/apps/dashboard/app/(app)/settings/root-keys/[keyId]/permissions/permission_toggle.tsx +++ b/apps/dashboard/app/(app)/settings/root-keys/[keyId]/permissions/permission_toggle.tsx @@ -1,10 +1,8 @@ "use client"; -import { CopyButton } from "@/components/dashboard/copy-button"; import { Checkbox } from "@/components/ui/checkbox"; import { Label } from "@/components/ui/label"; import { toast } from "@/components/ui/toaster"; -import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip"; import { trpc } from "@/lib/trpc/client"; import { parseTrpcError } from "@/lib/utils"; import { Loader2 } from "lucide-react"; @@ -32,7 +30,7 @@ export const PermissionToggle: React.FC = ({ const router = useRouter(); const [optimisticChecked, setOptimisticChecked] = useState(checked); - const addPermission = trpc.rbac.addPermissionToRootKey.useMutation({ + const addPermission = trpc.rbac.addPermissionToKey.useMutation({ onMutate: () => { setOptimisticChecked(true); }, @@ -60,7 +58,7 @@ export const PermissionToggle: React.FC = ({ cancel: { label: "Undo", onClick: () => { - addPermission.mutate({ rootKeyId, permission: permissionName }); + addPermission.mutate({ keyId: rootKeyId, permission: permissionName }); }, }, }); @@ -98,7 +96,7 @@ export const PermissionToggle: React.FC = ({ } } else { if (!preventEnabling) { - addPermission.mutate({ rootKeyId, permission: permissionName }); + addPermission.mutate({ keyId: rootKeyId, permission: permissionName }); } } }} diff --git a/apps/dashboard/lib/trpc/routers/index.ts b/apps/dashboard/lib/trpc/routers/index.ts index 48d0f876c..70d438c66 100644 --- a/apps/dashboard/lib/trpc/routers/index.ts +++ b/apps/dashboard/lib/trpc/routers/index.ts @@ -11,6 +11,7 @@ import { deleteKeys } from "./key/delete"; import { deleteRootKeys } from "./key/deleteRootKey"; import { updateKeyDeletedAt } from "./key/updateDeletedAt"; import { updateKeyEnabled } from "./key/updateEnabled"; +import { updateKeyEncrypted } from "./key/updateEncrypted"; import { updateKeyExpiration } from "./key/updateExpiration"; import { updateKeyMetadata } from "./key/updateMetadata"; import { updateKeyName } from "./key/updateName"; @@ -27,7 +28,7 @@ import { deleteNamespace } from "./ratelimit/deleteNamespace"; import { deleteOverride } from "./ratelimit/deleteOverride"; import { updateNamespaceName } from "./ratelimit/updateNamespaceName"; import { updateOverride } from "./ratelimit/updateOverride"; -import { addPermissionToRootKey } from "./rbac/addPermissionToRootKey"; +import { addPermissionToKey } from "./rbac/addPermissionToKey"; import { connectPermissionToRole } from "./rbac/connectPermissionToRole"; import { connectRoleToKey } from "./rbac/connectRoleToKey"; import { createPermission } from "./rbac/createPermission"; @@ -68,6 +69,7 @@ export const router = t.router({ update: t.router({ deletedAt: updateKeyDeletedAt, enabled: updateKeyEnabled, + encrypted: updateKeyEncrypted, expiration: updateKeyExpiration, metadata: updateKeyMetadata, name: updateKeyName, @@ -110,7 +112,7 @@ export const router = t.router({ createIssue: createPlainIssue, }), rbac: t.router({ - addPermissionToRootKey: addPermissionToRootKey, + addPermissionToKey: addPermissionToKey, connectPermissionToRole: connectPermissionToRole, connectRoleToKey: connectRoleToKey, createPermission: createPermission, diff --git a/apps/dashboard/lib/trpc/routers/key/updateEncrypted.ts b/apps/dashboard/lib/trpc/routers/key/updateEncrypted.ts new file mode 100644 index 000000000..ae19d18ab --- /dev/null +++ b/apps/dashboard/lib/trpc/routers/key/updateEncrypted.ts @@ -0,0 +1,70 @@ +import { db, schema } from "@/lib/db"; +import { ingestAuditLogs } from "@/lib/tinybird"; +import { TRPCError } from "@trpc/server"; +import { z } from "zod"; +import { auth, t } from "../../trpc"; + +export const updateKeyEncrypted = t.procedure + .use(auth) + .input( + z.object({ + keyId: z.string(), + encrypted: z.string(), + encryptiodKeyId: z.string(), + }), + ) + .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, + }, + }); + if (!key || key.workspace.tenantId !== ctx.tenant.id) { + throw new TRPCError({ + message: + "We are unable to find the correct key. Please contact support using support@unkey.dev.", + code: "NOT_FOUND", + }); + } + + const tuple = { + keyId: input.keyId, + encrypted: input.encrypted, + encryptionKeyId: input.encryptiodKeyId, + workspaceId: ctx.tenant.id, + } + await db + .insert(schema.encryptedKeys) + .values({ ...tuple }) + .onDuplicateKeyUpdate({ set: { ...tuple }}) + .catch((_err) => { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR", + message: + "We are unable to update key encrypted. Please contact support using support@unkey.dev", + }); + }); + + await ingestAuditLogs({ + workspaceId: key.workspace.id, + actor: { + type: "user", + id: ctx.user.id, + }, + event: "key.update", + description: `Created a encrypted relation to ${key.id}`, + resources: [ + { + type: "key", + id: key.id, + }, + ], + context: { + location: ctx.audit.location, + userAgent: ctx.audit.userAgent, + }, + }); + return true; + }); diff --git a/apps/dashboard/lib/trpc/routers/rbac/addPermissionToRootKey.ts b/apps/dashboard/lib/trpc/routers/rbac/addPermissionToKey.ts similarity index 89% rename from apps/dashboard/lib/trpc/routers/rbac/addPermissionToRootKey.ts rename to apps/dashboard/lib/trpc/routers/rbac/addPermissionToKey.ts index 2ff9505a4..18d838beb 100644 --- a/apps/dashboard/lib/trpc/routers/rbac/addPermissionToRootKey.ts +++ b/apps/dashboard/lib/trpc/routers/rbac/addPermissionToKey.ts @@ -6,11 +6,11 @@ import { z } from "zod"; import { auth, t } from "../../trpc"; import { upsertPermissions } from "../rbac"; -export const addPermissionToRootKey = t.procedure +export const addPermissionToKey = t.procedure .use(auth) .input( z.object({ - rootKeyId: z.string(), + keyId: z.string(), permission: z.string(), }), ) @@ -35,9 +35,9 @@ export const addPermissionToRootKey = t.procedure }); } - const rootKey = await db.query.keys.findFirst({ + const key = await db.query.keys.findFirst({ where: (table, { eq, and }) => - and(eq(table.forWorkspaceId, workspace.id), eq(table.id, input.rootKeyId)), + and(eq(table.forWorkspaceId, workspace.id), eq(table.id, input.keyId)), with: { permissions: { with: { @@ -46,7 +46,7 @@ export const addPermissionToRootKey = t.procedure }, }, }); - if (!rootKey) { + if (!key) { throw new TRPCError({ code: "NOT_FOUND", message: @@ -54,14 +54,14 @@ export const addPermissionToRootKey = t.procedure }); } - const { permissions, auditLogs } = await upsertPermissions(ctx, rootKey.workspaceId, [ + const { permissions, auditLogs } = await upsertPermissions(ctx, key.workspaceId, [ permission.data, ]); const p = permissions[0]; await db .insert(schema.keysPermissions) .values({ - keyId: rootKey.id, + keyId: key.id, permissionId: p.id, workspaceId: p.workspaceId, }) @@ -80,11 +80,11 @@ export const addPermissionToRootKey = t.procedure workspaceId: workspace.id, actor: { type: "user", id: ctx.user.id }, event: "authorization.connect_permission_and_key", - description: `Attached ${p.id} to ${rootKey.id}`, + description: `Attached ${p.id} to ${key.id}`, resources: [ { type: "key", - id: rootKey.id, + id: key.id, }, { type: "permission", From 2103aa87aa39a16d3069a357b6eae1f47798c498 Mon Sep 17 00:00:00 2001 From: "autofix-ci[bot]" <114827586+autofix-ci[bot]@users.noreply.github.com> Date: Thu, 12 Sep 2024 12:01:47 +0000 Subject: [PATCH 15/16] [autofix.ci] apply automated fixes --- .../[keyAuthId]/[keyId]/settings/page.tsx | 20 +++++++++++++++++-- .../[keyId]/settings/reroll-key.tsx | 13 +++++++++--- .../lib/trpc/routers/key/updateEncrypted.ts | 4 ++-- 3 files changed, 30 insertions(+), 7 deletions(-) diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx index abf8f2874..2e6afb382 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/page.tsx @@ -2,7 +2,17 @@ import { CopyButton } from "@/components/dashboard/copy-button"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Code } from "@/components/ui/code"; import { getTenantId } from "@/lib/auth"; -import { type EncryptedKey, Key, Permission, Role, and, db, eq, isNull, schema } from "@/lib/db"; +import { + type EncryptedKey, + type Key, + type Permission, + type Role, + and, + db, + eq, + isNull, + schema, +} from "@/lib/db"; import { getLastUsed } from "@/lib/tinybird"; import { ArrowLeft } from "lucide-react"; import Link from "next/link"; @@ -78,7 +88,13 @@ export default async function SettingsPage(props: Props) { diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx index fb66abed4..a350f241a 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx @@ -142,11 +142,18 @@ export const RerollKey: React.FC = ({ apiKey, apiId, lastUsed }: Props) = }); apiKey.permissions?.forEach(async (permission) => { - await copyPermissionsToNewKey.mutateAsync({ permission: permission.name, keyId: newKey.keyId, }); - }) + await copyPermissionsToNewKey.mutateAsync({ + permission: permission.name, + keyId: newKey.keyId, + }); + }); if (apiKey.encrypted) { - await copyEncryptedToNewKey.mutateAsync({ encrypted: apiKey.encrypted.encrypted, encryptiodKeyId: apiKey.encrypted.encryptionKeyId, keyId: newKey.keyId }) + await copyEncryptedToNewKey.mutateAsync({ + encrypted: apiKey.encrypted.encrypted, + encryptiodKeyId: apiKey.encrypted.encryptionKeyId, + keyId: newKey.keyId, + }); } const miliseconds = values.expiresIn === "now" ? 0 : ms(values.expiresIn); diff --git a/apps/dashboard/lib/trpc/routers/key/updateEncrypted.ts b/apps/dashboard/lib/trpc/routers/key/updateEncrypted.ts index ae19d18ab..555967950 100644 --- a/apps/dashboard/lib/trpc/routers/key/updateEncrypted.ts +++ b/apps/dashboard/lib/trpc/routers/key/updateEncrypted.ts @@ -34,11 +34,11 @@ export const updateKeyEncrypted = t.procedure encrypted: input.encrypted, encryptionKeyId: input.encryptiodKeyId, workspaceId: ctx.tenant.id, - } + }; await db .insert(schema.encryptedKeys) .values({ ...tuple }) - .onDuplicateKeyUpdate({ set: { ...tuple }}) + .onDuplicateKeyUpdate({ set: { ...tuple } }) .catch((_err) => { throw new TRPCError({ code: "INTERNAL_SERVER_ERROR", From f01308e127e667cab7f97309bc75f02f56e14b58 Mon Sep 17 00:00:00 2001 From: hiroasano Date: Fri, 13 Sep 2024 10:48:16 +0200 Subject: [PATCH 16/16] Update delete/expiration logic --- .../[keyId]/settings/reroll-key.tsx | 33 +++++++++++++++---- 1 file changed, 26 insertions(+), 7 deletions(-) diff --git a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx index a350f241a..329a093ee 100644 --- a/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx +++ b/apps/dashboard/app/(app)/apis/[apiId]/keys/[keyAuthId]/[keyId]/settings/reroll-key.tsx @@ -108,6 +108,17 @@ export const RerollKey: React.FC = ({ apiKey, apiId, lastUsed }: Props) = }, }); + const updateExpiration = trpc.key.update.expiration.useMutation({ + onSuccess() { + toast.success("Key Rerolled."); + }, + onError(err) { + console.error(err); + const message = parseTrpcError(err); + toast.error(message); + }, + }); + async function onSubmit(values: z.infer) { const ratelimit = apiKey.ratelimitLimit ? { @@ -156,13 +167,21 @@ export const RerollKey: React.FC = ({ apiKey, apiId, lastUsed }: Props) = }); } - const miliseconds = values.expiresIn === "now" ? 0 : ms(values.expiresIn); - const deletedAt = new Date(Date.now() + miliseconds); - - await updateDeletedAt.mutate({ - keyId: apiKey.id, - deletedAt, - }); + if (values.expiresIn === "now") { + await updateDeletedAt.mutateAsync({ + keyId: apiKey.id, + deletedAt: new Date(Date.now()), + }); + } else { + const miliseconds = ms(values.expiresIn); + const expiration = new Date(Date.now() + miliseconds); + + await updateExpiration.mutateAsync({ + keyId: apiKey.id, + expiration, + enableExpiration: true, + }) + } } const [confirmatioDialogOpen, setConfirmationDialogOpen] = useState(false);