Skip to content

Commit

Permalink
Add Confirmation Dialog and New Key Dialog
Browse files Browse the repository at this point in the history
  • Loading branch information
hiroasano committed Sep 6, 2024
1 parent 16513bc commit 1438410
Show file tree
Hide file tree
Showing 4 changed files with 204 additions and 84 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,19 @@ 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";
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: {
Expand Down Expand Up @@ -72,7 +72,7 @@ export default async function SettingsPage(props: Props) {
</Code>
</CardContent>
</Card>
<RerollKey apiId={props.params.apiId} apiKey={key as unknown as Key & { roles : []}} />
<RerollKey apiId={props.params.apiId} apiKey={key as unknown as Key & { roles: [] }} />
<DeleteKey apiKey={key} keyAuthId={key.keyAuthId} />
</div>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -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 (
<Dialog open={open} onOpenChange={(o: boolean) => setOpen(o)}>
<DialogContent className="border-alert">
<DialogHeader>
<DialogTitle>Reroll Key</DialogTitle>
<DialogDescription>
Make sure to replace it in your system before it expires. This action cannot be undone.
</DialogDescription>
</DialogHeader>

<Alert variant="alert">
<AlertTitle>Warning</AlertTitle>
<AlertDescription>This action is not reversible. Please be certain.</AlertDescription>
</Alert>

<DialogFooter className="justify-end">
<Button type="button" onClick={() => setOpen(!open)} variant="secondary">
Cancel
</Button>
<Button type="submit" variant="alert" onClick={onClick}>
Reroll Key
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
);
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"use client";

import { Loading } from "@/components/dashboard/loading";
import { Button } from "@/components/ui/button";
import {
Card,
Expand All @@ -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,
Expand All @@ -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: [];
};
};

Expand All @@ -57,9 +52,6 @@ const formSchema = z.object({
});

export const RerollKey: React.FC<Props> = ({ apiKey, apiId }: Props) => {
const router = useRouter();
const [newKeyId, setNewKeyId] = useState<string | null>();

const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema),
reValidateMode: "onBlur",
Expand All @@ -72,9 +64,6 @@ export const RerollKey: React.FC<Props> = ({ apiKey, apiId }: Props) => {
onMutate() {
toast.success("Rerolling Key");
},
onSuccess({ keyId }) {
setNewKeyId(keyId);
},
onError(err) {
console.error(err);
const message = parseTrpcError(err);
Expand All @@ -93,7 +82,6 @@ export const RerollKey: React.FC<Props> = ({ 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);
Expand All @@ -103,17 +91,21 @@ export const RerollKey: React.FC<Props> = ({ apiKey, apiId }: Props) => {
});

async function onSubmit(values: z.infer<typeof formSchema>) {
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,
Expand All @@ -127,70 +119,80 @@ export const RerollKey: React.FC<Props> = ({ 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 (
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-8">
<Card className="relative border-alert">
<CardHeader>
<CardTitle>Reroll Key</CardTitle>
<CardDescription>
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.
</CardDescription>
</CardHeader>
<CardContent className="flex justify-between item-center">
<div className="flex flex-col w-full space-y-2">
<Label htmlFor="expiresIn">Expire old key in:</Label>
<FormField
control={form.control}
name="expiresIn"
render={({ field }) => (
<FormItem className="max-w-sm">
<Select onValueChange={field.onChange} defaultValue="1h" value={field.value}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{EXPIRATION_OPTIONS.map((item) => (
<SelectItem key={item.key} value={item.key}>
{item.value}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
Choose an optional delay period before the old key expires.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
<CardFooter className="items-center justify-end gap-4">
<Button type="submit" variant="alert">
{createKey.isLoading || updateDeletedAt.isLoading ? (
<Loading className="w-4 h-4" />
) : (
"Reroll Key"
)}
</Button>
</CardFooter>
</Card>
</form>
</Form>
<>
<RerollConfirmationDialog
open={confirmatioDialogOpen}
setOpen={setConfirmationDialogOpen}
onClick={confirmationSubmit}
/>
<RerollNewKeyDialog newKey={createKey.data} apiId={apiId} keyAuthId={apiKey.keyAuthId} />
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col gap-8">
<Card className="relative border-alert">
<CardHeader>
<CardTitle>Reroll Key</CardTitle>
<CardDescription>
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.
</CardDescription>
</CardHeader>
<CardContent className="flex justify-between item-center">
<div className="flex flex-col w-full space-y-2">
<Label htmlFor="expiresIn">Expire old key in:</Label>
<FormField
control={form.control}
name="expiresIn"
render={({ field }) => (
<FormItem className="max-w-sm">
<Select onValueChange={field.onChange} defaultValue="1h" value={field.value}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
{EXPIRATION_OPTIONS.map((item) => (
<SelectItem key={item.key} value={item.key}>
{item.value}
</SelectItem>
))}
</SelectContent>
</Select>
<FormDescription>
Choose an optional delay period before the old key expires.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</div>
</CardContent>
<CardFooter className="items-center justify-end gap-4">
<Button type="button" variant="alert" onClick={() => setConfirmationDialogOpen(true)}>
Reroll Key
</Button>
</CardFooter>
</Card>
</form>
</Form>
</>
);
};
Original file line number Diff line number Diff line change
@@ -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 (
<Dialog open={open} onOpenChange={(o: boolean) => setOpen(o)}>
<DialogContent>
<DialogHeader>
<DialogTitle>Your New API Key</DialogTitle>
</DialogHeader>
<Alert>
<AlertCircle className="w-4 h-4" />
<AlertTitle>This key is only shown once and can not be recovered </AlertTitle>
<AlertDescription>
Please pass it on to your user or store it somewhere safe.
</AlertDescription>
</Alert>
<Code className="flex items-center justify-between w-full gap-4 my-8 ph-no-capture max-sm:text-xs sm:overflow-hidden">
<pre>{showKey ? newKey.key : maskedKey}</pre>
<div className="flex items-start justify-between gap-4 max-sm:absolute max-sm:right-11">
<VisibleButton isVisible={showKey} setIsVisible={setShowKey} />
<CopyButton value={newKey.key} />
</div>
</Code>
<DialogFooter className="justify-end">
<Link href={`/keys/${keyAuthId}`}>
<Button variant="secondary">Back</Button>
</Link>
<Link href={`/apis/${apiId}/keys/${keyAuthId}/${newKey.keyId}`}>
<Button variant="secondary">View key details</Button>
</Link>
</DialogFooter>
</DialogContent>
</Dialog>
);
}

0 comments on commit 1438410

Please sign in to comment.