850 lines
35 KiB
TypeScript
850 lines
35 KiB
TypeScript
"use client";
|
|
import { CopyButton } from "@/components/dashboard/copy-button";
|
|
import { Loading } from "@/components/dashboard/loading";
|
|
import { VisibleButton } from "@/components/dashboard/visible-button";
|
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
|
import { Badge } from "@/components/ui/badge";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent } from "@/components/ui/card";
|
|
import { Code } from "@/components/ui/code";
|
|
import {
|
|
Form,
|
|
FormControl,
|
|
FormDescription,
|
|
FormField,
|
|
FormItem,
|
|
FormLabel,
|
|
FormMessage,
|
|
} from "@/components/ui/form";
|
|
import { Input } from "@/components/ui/input";
|
|
import {
|
|
Select,
|
|
SelectContent,
|
|
SelectItem,
|
|
SelectTrigger,
|
|
SelectValue,
|
|
} from "@/components/ui/select";
|
|
import { Separator } from "@/components/ui/separator";
|
|
import { Switch } from "@/components/ui/switch";
|
|
import { Textarea } from "@/components/ui/textarea";
|
|
import { toast } from "@/components/ui/toaster";
|
|
// import { trpc } from "@/lib/trpc/client";
|
|
import { zodResolver } from "@hookform/resolvers/zod";
|
|
import { AlertCircle } from "lucide-react";
|
|
import Link from "next/link";
|
|
import { useRouter } from "next/navigation";
|
|
import { useEffect, useState } from "react";
|
|
import { useForm } from "react-hook-form";
|
|
import { z } from "zod";
|
|
|
|
const getDatePlusTwoMinutes = () => {
|
|
const now = new Date();
|
|
const futureDate = new Date(now.getTime() + 2 * 60000);
|
|
return futureDate.toISOString().slice(0, -8);
|
|
};
|
|
const formSchema = z.object({
|
|
bytes: z.coerce
|
|
.number({
|
|
errorMap: (issue, { defaultError }) => ({
|
|
message:
|
|
issue.code === "invalid_type"
|
|
? "Amount must be a number and greater than 0"
|
|
: defaultError,
|
|
}),
|
|
})
|
|
.default(16),
|
|
prefix: z
|
|
.string()
|
|
.max(8, { message: "Please limit the prefix to under 8 characters." })
|
|
.optional(),
|
|
ownerId: z.string().optional(),
|
|
name: z.string().optional(),
|
|
metaEnabled: z.boolean().default(false),
|
|
meta: z
|
|
.string()
|
|
.refine(
|
|
(s) => {
|
|
try {
|
|
JSON.parse(s);
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
},
|
|
{
|
|
message: "Must be valid json",
|
|
},
|
|
)
|
|
.optional(),
|
|
limitEnabled: z.boolean().default(false),
|
|
limit: z
|
|
.object({
|
|
remaining: z.coerce
|
|
.number({
|
|
errorMap: (issue, { defaultError }) => ({
|
|
message:
|
|
issue.code === "invalid_type"
|
|
? "Remaining amount must be greater than 0"
|
|
: defaultError,
|
|
}),
|
|
})
|
|
.int()
|
|
.positive({ message: "Please enter a positive number" })
|
|
.optional(),
|
|
refill: z
|
|
.object({
|
|
interval: z.enum(["none", "daily", "monthly"]).default("none"),
|
|
amount: z.coerce
|
|
.number({
|
|
errorMap: (issue, { defaultError }) => ({
|
|
message:
|
|
issue.code === "invalid_type"
|
|
? "Refill amount must be greater than 0 and a integer"
|
|
: defaultError,
|
|
}),
|
|
})
|
|
.int()
|
|
.min(1)
|
|
.positive()
|
|
.optional(),
|
|
})
|
|
.optional(),
|
|
})
|
|
.optional(),
|
|
expireEnabled: z.boolean().default(false),
|
|
expires: z.coerce
|
|
.date()
|
|
.min(new Date(new Date().getTime() + 2 * 60000))
|
|
.optional(),
|
|
ratelimitEnabled: z.boolean().default(false),
|
|
ratelimit: z
|
|
.object({
|
|
type: z.enum(["consistent", "fast"]).default("fast"),
|
|
refillInterval: z.coerce
|
|
.number({
|
|
errorMap: (issue, { defaultError }) => ({
|
|
message:
|
|
issue.code === "invalid_type"
|
|
? "Refill interval must be greater than 0"
|
|
: defaultError,
|
|
}),
|
|
})
|
|
.positive({ message: "Refill interval must be greater than 0" }),
|
|
refillRate: z.coerce
|
|
.number({
|
|
errorMap: (issue, { defaultError }) => ({
|
|
message:
|
|
issue.code === "invalid_type" ? "Refill rate must be greater than 0" : defaultError,
|
|
}),
|
|
})
|
|
.positive({ message: "Refill rate must be greater than 0" }),
|
|
limit: z.coerce
|
|
.number({
|
|
errorMap: (issue, { defaultError }) => ({
|
|
message:
|
|
issue.code === "invalid_type" ? "Refill limit must be greater than 0" : defaultError,
|
|
}),
|
|
})
|
|
.positive({ message: "Limit must be greater than 0" }),
|
|
})
|
|
.optional(),
|
|
environment: z.string().optional(),
|
|
});
|
|
|
|
type Props = {
|
|
keyAuthId: string;
|
|
};
|
|
|
|
export const CreateKey: React.FC<Props> = ({ keyAuthId }) => {
|
|
const router = useRouter();
|
|
const form = useForm<z.infer<typeof formSchema>>({
|
|
resolver: async (data, context, options) => {
|
|
return zodResolver(formSchema)(data, context, options);
|
|
},
|
|
mode: "all",
|
|
shouldFocusError: true,
|
|
delayError: 100,
|
|
defaultValues: {
|
|
bytes: 16,
|
|
expireEnabled: false,
|
|
limitEnabled: false,
|
|
metaEnabled: false,
|
|
ratelimitEnabled: false,
|
|
},
|
|
});
|
|
|
|
// const key = trpc.key.create.useMutation({
|
|
// onSuccess() {
|
|
// toast("Key Created", {
|
|
// description: "Your Key has been created",
|
|
// });
|
|
// },
|
|
// onError(_err) {
|
|
// toast.error("An error occured, please try again");
|
|
// },
|
|
// });
|
|
|
|
async function onSubmit(values: z.infer<typeof formSchema>) {
|
|
// make sure they aren't sent to the server if they are disabled.
|
|
if (!values.expireEnabled) {
|
|
delete values.expires;
|
|
}
|
|
if (!values.metaEnabled) {
|
|
delete values.meta;
|
|
}
|
|
if (!values.limitEnabled) {
|
|
delete values.limit;
|
|
}
|
|
if (!values.ratelimitEnabled) {
|
|
delete values.ratelimit;
|
|
}
|
|
|
|
// await key.mutateAsync({
|
|
// keyAuthId,
|
|
// ...values,
|
|
// meta: values.meta ? JSON.parse(values.meta) : undefined,
|
|
// expires: values.expires?.getTime() ?? undefined,
|
|
// ownerId: values.ownerId ?? undefined,
|
|
// remaining: values.limit?.remaining ?? undefined,
|
|
// enabled: true,
|
|
// });
|
|
}
|
|
|
|
const key = {
|
|
data: {
|
|
key: "xsadsds"
|
|
}
|
|
}
|
|
|
|
const snippet = `curl -XPOST '${process.env.NEXT_PUBLIC_UNKEY_API_URL ?? "https://api.unkey.dev"}/v1/keys.verifyKey' \\
|
|
-H 'Content-Type: application/json' \\
|
|
-d '{
|
|
"key": "${key.data?.key}"
|
|
}'`;
|
|
|
|
const split = key.data?.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 [showKeyInSnippet, setShowKeyInSnippet] = useState(false);
|
|
|
|
const resetRateLimit = () => {
|
|
// set them to undefined so the form resets properly.
|
|
form.resetField("ratelimit.refillRate", undefined);
|
|
form.resetField("ratelimit.refillInterval", undefined);
|
|
form.resetField("ratelimit.limit", undefined);
|
|
form.resetField("ratelimit", undefined);
|
|
};
|
|
|
|
const resetLimited = () => {
|
|
form.resetField("limit.refill.amount", undefined);
|
|
form.resetField("limit.refill.interval", undefined);
|
|
form.resetField("limit.refill", undefined);
|
|
form.resetField("limit.remaining", undefined);
|
|
form.resetField("limit", undefined);
|
|
};
|
|
|
|
useEffect(() => {
|
|
// React hook form + zod doesn't play nice with nested objects, so we need to reset them on load.
|
|
resetRateLimit();
|
|
resetLimited();
|
|
}, []);
|
|
|
|
return (
|
|
<>
|
|
{key.data ? (
|
|
<div className="w-full max-sm:p-4">
|
|
<div>
|
|
<p className="mb-4 text-xl font-bold">Your API Key</p>
|
|
<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 ? key.data.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={key.data.key} />
|
|
</div>
|
|
</Code>
|
|
</div>
|
|
|
|
<p className="my-2 font-medium text-center text-gray-700 ">Try verifying it:</p>
|
|
<Code className="flex items-start justify-between w-full gap-4 my-8 overflow-hidden max-sm:text-xs ">
|
|
<div className="max-sm:mt-10">
|
|
<pre className="ph-no-capture">
|
|
{showKeyInSnippet ? snippet : snippet.replace(key.data.key, maskedKey)}
|
|
</pre>
|
|
</div>
|
|
<div className="flex items-start justify-between gap-4 max-ms:top-2 max-sm:absolute max-sm:right-11 ">
|
|
<VisibleButton isVisible={showKeyInSnippet} setIsVisible={setShowKeyInSnippet} />
|
|
<CopyButton value={snippet} />
|
|
</div>
|
|
</Code>
|
|
<div className="flex justify-end my-4 space-x-4">
|
|
<Link href={`/app/keys/${keyAuthId}`}>
|
|
<Button variant="secondary">Back</Button>
|
|
</Link>
|
|
<Button
|
|
onClick={() => {
|
|
// key.reset();
|
|
form.setValue("expireEnabled", false);
|
|
form.setValue("ratelimitEnabled", false);
|
|
form.setValue("metaEnabled", false);
|
|
form.setValue("limitEnabled", false);
|
|
router.refresh();
|
|
}}
|
|
>
|
|
Create another key
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
) : (
|
|
<>
|
|
<div>
|
|
<div className="w-full">
|
|
<h2 className="text-2xl font-semibold tracking-tight">Create a new key</h2>
|
|
<Form {...form}>
|
|
<form
|
|
className="flex flex-col h-full gap-8 mt-4 md:flex-row"
|
|
onSubmit={form.handleSubmit(onSubmit)}
|
|
>
|
|
<div className="z-0 flex flex-col w-full h-full gap-4 md:sticky top-24 md:w-1/2">
|
|
<FormField
|
|
control={form.control}
|
|
name="prefix"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>
|
|
Prefix{" "}
|
|
<Badge variant="secondary">
|
|
Optional
|
|
</Badge>
|
|
</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
{...field}
|
|
onBlur={(e) => {
|
|
if (e.target.value === "") {
|
|
return;
|
|
}
|
|
}}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
Using a prefix can make it easier for your users to distinguish between
|
|
apis. Don't add a trailing underscore, we'll do that automatically.
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="bytes"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>
|
|
Bytes{" "}
|
|
<Badge variant="secondary" >
|
|
Optional
|
|
</Badge>
|
|
</FormLabel>
|
|
<FormControl>
|
|
<Input type="number" {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
How long the key will be. Longer keys are harder to guess and more
|
|
secure.
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="ownerId"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>
|
|
Owner{" "}
|
|
<Badge variant="secondary" >
|
|
Optional
|
|
</Badge>
|
|
</FormLabel>
|
|
<FormControl>
|
|
<Input {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
This is the id of the user or workspace in your system, so you can
|
|
identify users from an API key.
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="name"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>
|
|
Name{" "}
|
|
<Badge variant="secondary">
|
|
Optional
|
|
</Badge>
|
|
</FormLabel>
|
|
<FormControl>
|
|
<Input {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
To make it easier to identify a particular key, you can provide a name.
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="environment"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>
|
|
Environment{" "}
|
|
<Badge variant="secondary">
|
|
Optional
|
|
</Badge>
|
|
</FormLabel>
|
|
<FormControl>
|
|
<Input {...field} />
|
|
</FormControl>
|
|
<FormDescription>
|
|
Separate keys into different environments, for example{" "}
|
|
<strong>test</strong> and <strong>live</strong>.
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
<Separator orientation="vertical" className="" />
|
|
|
|
<div className="flex flex-col w-full gap-4 md:w-1/2">
|
|
<Card>
|
|
<CardContent className="justify-between w-full p-4 item-center">
|
|
<div className="flex items-center justify-between w-full">
|
|
<span>Ratelimit</span>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="ratelimitEnabled"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel className="sr-only">Ratelimit</FormLabel>
|
|
<FormControl>
|
|
<Switch
|
|
onCheckedChange={(e) => {
|
|
field.onChange(e);
|
|
if (field.value === false) {
|
|
resetRateLimit();
|
|
}
|
|
}}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
{form.watch("ratelimitEnabled") ? (
|
|
<>
|
|
<div className="flex flex-col gap-4 mt-4">
|
|
<FormField
|
|
control={form.control}
|
|
name="ratelimit.limit"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Limit</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
placeholder="10"
|
|
{...field}
|
|
value={
|
|
form.getValues("ratelimitEnabled")
|
|
? field.value
|
|
: undefined
|
|
}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
The maximum number of requests possible during a burst.
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="ratelimit.refillRate"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Refill Rate</FormLabel>
|
|
<FormControl>
|
|
<Input placeholder="5" type="number" {...field} />
|
|
</FormControl>
|
|
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="ratelimit.refillInterval"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Refill Interval (milliseconds)</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
placeholder="1000"
|
|
type="number"
|
|
{...field}
|
|
value={
|
|
form.getValues("ratelimitEnabled")
|
|
? field.value
|
|
: undefined
|
|
}
|
|
/>
|
|
</FormControl>
|
|
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormDescription>
|
|
How many requests may be performed in a given interval
|
|
</FormDescription>
|
|
</div>
|
|
{form.formState.errors.ratelimit && (
|
|
<p className="text-xs text-center text-content-alert">
|
|
{form.formState.errors.ratelimit.message}
|
|
</p>
|
|
)}
|
|
</>
|
|
) : null}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="justify-between w-full p-4 item-center">
|
|
<div className="flex items-center justify-between w-full">
|
|
<span>Limited Use</span>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="limitEnabled"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel className="sr-only">Limited Use</FormLabel>
|
|
<FormControl>
|
|
<Switch
|
|
onCheckedChange={(e) => {
|
|
field.onChange(e);
|
|
if (field.value === false) {
|
|
resetLimited();
|
|
}
|
|
}}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
{form.watch("limitEnabled") ? (
|
|
<>
|
|
<p className="text-xs text-content-subtle">
|
|
How many times this key can be used before it gets disabled
|
|
automatically.
|
|
</p>
|
|
<div className="flex flex-col gap-4 mt-4">
|
|
<FormField
|
|
control={form.control}
|
|
name="limit.remaining"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Number of uses</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
placeholder="100"
|
|
className="w-full"
|
|
type="number"
|
|
{...field}
|
|
value={
|
|
form.getValues("limitEnabled") ? field.value : undefined
|
|
}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
Enter the remaining amount of uses for this key.
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
name="limit.refill.interval"
|
|
render={({ field }) => (
|
|
<FormItem className="">
|
|
<FormLabel>Refill Rate</FormLabel>
|
|
<Select
|
|
onValueChange={field.onChange}
|
|
defaultValue="none"
|
|
value={field.value}
|
|
>
|
|
<SelectTrigger>
|
|
<SelectValue />
|
|
</SelectTrigger>
|
|
<SelectContent>
|
|
<SelectItem value="none">None</SelectItem>
|
|
<SelectItem value="daily">Daily</SelectItem>
|
|
<SelectItem value="monthly">Monthly</SelectItem>
|
|
</SelectContent>
|
|
</Select>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormField
|
|
control={form.control}
|
|
disabled={
|
|
form.watch("limit.refill.interval") === "none" ||
|
|
form.watch("limit.refill.interval") === undefined
|
|
}
|
|
name="limit.refill.amount"
|
|
render={({ field }) => (
|
|
<FormItem className="mt-4">
|
|
<FormLabel>Number of uses per interval</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
placeholder="100"
|
|
className="w-full"
|
|
type="number"
|
|
{...field}
|
|
value={
|
|
form.getValues("limitEnabled") ? field.value : undefined
|
|
}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
Enter the number of uses to refill per interval.
|
|
</FormDescription>
|
|
<FormMessage defaultValue="Please enter a value if interval is selected" />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormDescription>
|
|
How many requests may be performed in a given interval
|
|
</FormDescription>
|
|
</div>
|
|
{form.formState.errors.ratelimit && (
|
|
<p className="text-xs text-center text-content-alert">
|
|
{form.formState.errors.ratelimit.message}
|
|
</p>
|
|
)}
|
|
</>
|
|
) : null}
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="justify-between w-full p-4 item-center">
|
|
<div className="flex items-center justify-between w-full">
|
|
<span>Expiration</span>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="expireEnabled"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel className="sr-only">Expiration</FormLabel>
|
|
<FormControl>
|
|
<Switch
|
|
onCheckedChange={(e) => {
|
|
field.onChange(e);
|
|
if (field.value === false) {
|
|
resetLimited();
|
|
}
|
|
}}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
{form.watch("expireEnabled") ? (
|
|
<>
|
|
<p className="text-xs text-content-subtle">
|
|
{" "}
|
|
Automatically revoke this key after a certain date.
|
|
</p>
|
|
<div className="flex flex-col gap-4 mt-4">
|
|
<FormField
|
|
control={form.control}
|
|
name="expires"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel>Expiry Date</FormLabel>
|
|
<FormControl>
|
|
<Input
|
|
type="datetime-local"
|
|
{...field}
|
|
defaultValue={getDatePlusTwoMinutes()}
|
|
value={
|
|
form.getValues("expireEnabled")
|
|
? field.value?.toLocaleString()
|
|
: undefined
|
|
}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
This api key will automatically be revoked after the given
|
|
date.
|
|
</FormDescription>
|
|
<FormMessage />
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
<FormDescription>
|
|
How many requests may be performed in a given interval
|
|
</FormDescription>
|
|
</div>
|
|
{form.formState.errors.ratelimit && (
|
|
<p className="text-xs text-center text-content-alert">
|
|
{form.formState.errors.ratelimit.message}
|
|
</p>
|
|
)}
|
|
</>
|
|
) : null}
|
|
</CardContent>
|
|
</Card>
|
|
<Card>
|
|
<CardContent className="justify-between w-full p-4 item-center">
|
|
<div className="flex items-center justify-between w-full">
|
|
<span>Metadata</span>
|
|
|
|
<FormField
|
|
control={form.control}
|
|
name="metaEnabled"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormLabel className="sr-only">Metadata</FormLabel>
|
|
<FormControl>
|
|
<Switch
|
|
onCheckedChange={(e) => {
|
|
field.onChange(e);
|
|
if (field.value === false) {
|
|
resetLimited();
|
|
}
|
|
}}
|
|
/>
|
|
</FormControl>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
|
|
{form.watch("metaEnabled") ? (
|
|
<>
|
|
<p className="text-xs text-content-subtle">
|
|
Store json, or any other data you want to associate with this key.
|
|
Whenever you verify this key, we'll return the metadata to you. Enter
|
|
custom metadata as a JSON object.Format Json
|
|
</p>
|
|
|
|
<div className="flex flex-col gap-4 mt-4">
|
|
<FormField
|
|
control={form.control}
|
|
name="meta"
|
|
render={({ field }) => (
|
|
<FormItem>
|
|
<FormControl>
|
|
<Textarea
|
|
disabled={!form.watch("metaEnabled")}
|
|
className="m-4 mx-auto border rounded-md shadow-sm"
|
|
rows={7}
|
|
placeholder={`{"stripeCustomerId" : "cus_9s6XKzkNRiz8i3"}`}
|
|
{...field}
|
|
value={
|
|
form.getValues("metaEnabled") ? field.value : undefined
|
|
}
|
|
/>
|
|
</FormControl>
|
|
<FormDescription>
|
|
Enter custom metadata as a JSON object.
|
|
</FormDescription>
|
|
<FormMessage />
|
|
<Button
|
|
variant="secondary"
|
|
type="button"
|
|
onClick={(_e) => {
|
|
try {
|
|
if (field.value) {
|
|
const parsed = JSON.parse(field.value);
|
|
field.onChange(JSON.stringify(parsed, null, 2));
|
|
form.clearErrors("meta");
|
|
}
|
|
} catch (_e) {
|
|
form.setError("meta", {
|
|
type: "manual",
|
|
message: "Invalid JSON",
|
|
});
|
|
}
|
|
}}
|
|
value={field.value}
|
|
>
|
|
Format Json
|
|
</Button>
|
|
</FormItem>
|
|
)}
|
|
/>
|
|
</div>
|
|
{form.formState.errors.ratelimit && (
|
|
<p className="text-xs text-center text-content-alert">
|
|
{form.formState.errors.ratelimit.message}
|
|
</p>
|
|
)}
|
|
</>
|
|
) : null}
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<div className="w-full">
|
|
<Button
|
|
className="w-full"
|
|
// disabled={key.isLoading}
|
|
type="submit"
|
|
// variant={key.isLoading || !form.formState.isValid ? "disabled" : "primary"}
|
|
>
|
|
{/* {key.isLoading ? <Loading /> : "Create"} */}
|
|
Create
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
</Form>
|
|
</div>
|
|
</div>
|
|
</>
|
|
)}
|
|
</>
|
|
);
|
|
};
|