Skip to content

Commit e4a2a26

Browse files
committed
feat: api keys integration
1 parent 64b3a5f commit e4a2a26

File tree

11 files changed

+404
-17
lines changed

11 files changed

+404
-17
lines changed
Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,31 @@
1+
"use client";
2+
3+
import Keys from "@/components/keys/keys";
4+
import KeyActions from "@/components/keys/keys-actions";
5+
import { BoxLoader } from "@/components/loader";
16
import PageHeader from "@/components/page-header"
7+
import { Separator } from "@/components/ui/separator"
8+
import { useGetKeys } from "@/hooks/keys/keys.hooks";
9+
10+
export default function APIKeys() {
11+
const { data, isLoading } = useGetKeys();
12+
13+
console.log(data)
214

3-
export default async function APIKeys() {
415
return (
5-
<div className="mt-4 px-4">
16+
<div className="mt-4 px-4 mb-24 flex flex-col h-full">
617
<PageHeader
718
title="API Keys"
819
description="Manage your API keys from here!"
20+
actions={<KeyActions />}
921
showBackButton={false}
1022
/>
23+
<Separator />
24+
{isLoading ? (
25+
<BoxLoader height="h-[24vh]" />
26+
) : (
27+
<Keys keysData={data} />
28+
)}
1129
</div>
1230
)
1331
}

src/app/api/keys/keys-schema.ts

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import { z } from "zod";
22

33
export const addKeySchema = z.object({
4-
name: z
5-
.string()
6-
.min(1, "Incoming address is required")
4+
name: z.string().min(1, "Name is required!"),
75
});
8-
export type AddKeyValues = z.infer<typeof addKeySchema>
6+
export type AddKeyValues = z.infer<typeof addKeySchema>;
97

108
export const deleteKeySchema = z.object({
11-
key: z
12-
.string()
13-
.min(1, "Incoming address is required")
9+
key: z.string().min(1, "Key is required!"),
1410
});
15-
export type DeleteKeyValues = z.infer<typeof deleteKeySchema>
11+
export type DeleteKeyValues = z.infer<typeof deleteKeySchema>;
12+
13+
export type GetKeysResponse = {
14+
name: string;
15+
key: string;
16+
createdAt: string;
17+
};
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
'use client'
2+
3+
import { useForm } from 'react-hook-form'
4+
import { zodResolver } from '@hookform/resolvers/zod'
5+
import { Button } from '@/components/ui/button'
6+
import {
7+
Dialog,
8+
DialogContent,
9+
DialogDescription,
10+
DialogHeader,
11+
DialogTitle,
12+
} from '@/components/ui/dialog'
13+
import { Form, FormControl, FormField, FormItem, FormLabel, FormMessage } from '../ui/form'
14+
import { Input } from '../ui/input'
15+
import { useAddKey } from '@/hooks/keys/keys.hooks'
16+
import { addKeySchema, AddKeyValues } from '@/app/api/keys/keys-schema'
17+
18+
interface Props {
19+
open: boolean
20+
onClose: VoidFunction
21+
}
22+
23+
export function CreateKeyDialog({ open, onClose }: Props) {
24+
const addKeyMutation = useAddKey()
25+
26+
const form = useForm<AddKeyValues>({
27+
resolver: zodResolver(addKeySchema),
28+
defaultValues: {
29+
name: ''
30+
}
31+
})
32+
33+
const onSubmit = async (values: AddKeyValues) => {
34+
await addKeyMutation.mutateAsync(values);
35+
form.reset()
36+
onClose()
37+
}
38+
39+
return (
40+
<Dialog
41+
open={open}
42+
onOpenChange={onClose}
43+
>
44+
<DialogContent className='sm:max-w-lg'>
45+
<DialogHeader className='text-left'>
46+
<DialogTitle>{'Create API Key'}</DialogTitle>
47+
<DialogDescription>
48+
{'Enter the details below.'}{' '}
49+
Click save when you&apos;re done.
50+
</DialogDescription>
51+
</DialogHeader>
52+
<div>
53+
<Form {...form}>
54+
<form onSubmit={form.handleSubmit(onSubmit)}>
55+
<div className='grid gap-6'>
56+
<FormField
57+
control={form.control}
58+
name='name'
59+
render={({ field }) => (
60+
<FormItem className='space-y-1'>
61+
<FormLabel>Name</FormLabel>
62+
<FormControl>
63+
<Input placeholder="Enter identifying name of the key" {...field} />
64+
</FormControl>
65+
<FormMessage />
66+
</FormItem>
67+
)}
68+
/>
69+
<Button loading={addKeyMutation.isPending} type='submit' className='mt-2 cursor-pointer' disabled={false}>
70+
Save
71+
</Button>
72+
</div>
73+
</form>
74+
</Form>
75+
</div>
76+
</DialogContent>
77+
</Dialog>
78+
)
79+
}
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { useDeleteDomain } from '@/hooks/domains/domain.hooks';
2+
import { IconAlertTriangle } from '@tabler/icons-react';
3+
import { FC } from 'react'
4+
import { ConfirmDialog } from '../confirm-dialog';
5+
import { GetKeysResponse } from '@/app/api/keys/keys-schema';
6+
import { useDeleteKey } from '@/hooks/keys/keys.hooks';
7+
8+
type Props = {
9+
open: boolean;
10+
onCancel: VoidFunction;
11+
selectedKey: GetKeysResponse | null;
12+
}
13+
14+
const KeyDeleteConfirm: FC<Props> = ({
15+
open,
16+
onCancel,
17+
selectedKey,
18+
}) => {
19+
if (!selectedKey) return null;
20+
21+
const deleteKeyMutation = useDeleteKey()
22+
const handleConfirmDelete = async () => {
23+
await deleteKeyMutation.mutateAsync({
24+
key: selectedKey.key
25+
})
26+
onCancel()
27+
}
28+
29+
return (
30+
<ConfirmDialog
31+
open={open}
32+
onOpenChange={onCancel}
33+
handleConfirm={handleConfirmDelete}
34+
isLoading={deleteKeyMutation.isPending}
35+
title={
36+
<span className='text-destructive'>
37+
<IconAlertTriangle
38+
className='mr-1 inline-block stroke-destructive'
39+
size={18}
40+
/>{' '}
41+
Delete Key
42+
</span>
43+
}
44+
desc={
45+
<div className='space-y-4'>
46+
<p className='mb-2'>
47+
Are you sure you want to delete{' '}
48+
<span className='font-bold'>{selectedKey.name}</span>?
49+
<br />
50+
This cannot be undone.
51+
</p>
52+
</div>
53+
}
54+
confirmText='Delete'
55+
destructive
56+
/>
57+
)
58+
}
59+
60+
export default KeyDeleteConfirm

src/components/keys/keys-actions.tsx

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
import React, { useState } from 'react'
2+
import { Button } from '../ui/button'
3+
import { IconPlus, IconRefresh } from '@tabler/icons-react'
4+
import { useQueryClient, useIsFetching } from '@tanstack/react-query'
5+
import { CreateKeyDialog } from './create-key-dialog'
6+
7+
const KeyActions = () => {
8+
const [addDialogOpen, setAddDialogOpen] = useState(false);
9+
const queryClient = useQueryClient();
10+
const isFetchingKeys = useIsFetching({ queryKey: ["api-keys"] });
11+
12+
const refreshKeys = async () => {
13+
queryClient.invalidateQueries({
14+
queryKey: ["api-keys"]
15+
})
16+
}
17+
18+
const handleAddProxy = () => {
19+
setAddDialogOpen(true)
20+
}
21+
22+
return (
23+
<>
24+
<div className='flex items-center justify-end gap-4'>
25+
<Button onClick={refreshKeys} className='cursor-pointer' variant={'outline'}>
26+
<span>
27+
<IconRefresh className={isFetchingKeys ? 'animate-spin' : ''} />
28+
</span>
29+
Refresh
30+
</Button>
31+
<Button onClick={handleAddProxy} className='cursor-pointer' variant={'default'}>
32+
<span>
33+
<IconPlus />
34+
</span>
35+
Create API Key
36+
</Button>
37+
</div>
38+
<CreateKeyDialog
39+
open={addDialogOpen}
40+
onClose={() => setAddDialogOpen(false)}
41+
/>
42+
</>
43+
)
44+
}
45+
46+
export default KeyActions

src/components/keys/keys.tsx

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
import { GetKeysResponse } from "@/app/api/keys/keys-schema";
2+
import { useState } from "react";
3+
import { Button } from "../ui/button";
4+
import { Trash, Clipboard, Eye, EyeOff } from "lucide-react";
5+
import KeyDeleteConfirm from "./key-delete-confirm";
6+
import { toast } from "sonner";
7+
8+
type Props = {
9+
keysData: {
10+
data: GetKeysResponse[];
11+
total: number;
12+
} | undefined;
13+
};
14+
15+
const Keys = ({ keysData }: Props) => {
16+
const [openDelete, setOpenDelete] = useState(false);
17+
const [selectedKey, setSelectedKey] = useState<GetKeysResponse | null>(null);
18+
const [visibleKeys, setVisibleKeys] = useState<{ [key: number]: boolean }>({});
19+
20+
const handleDeleteCancel = () => {
21+
setOpenDelete(false);
22+
setSelectedKey(null);
23+
};
24+
25+
const handleDeleteClick = (selectedKey: GetKeysResponse) => {
26+
setSelectedKey(selectedKey);
27+
setOpenDelete(true);
28+
};
29+
30+
const handleCopy = (key: string) => {
31+
navigator.clipboard.writeText(key);
32+
toast("Copied to clipboard!");
33+
};
34+
35+
const toggleVisibility = (index: number) => {
36+
setVisibleKeys((prev) => ({ ...prev, [index]: !prev[index] }));
37+
};
38+
39+
return (
40+
<>
41+
<div className="space-y-4 mt-2 overflow-y-auto max-h-[400px] p-2">
42+
<div>
43+
Found <span className="font-bold">{keysData?.total}</span> record
44+
{keysData && keysData?.total > 1 ? "s." : "."}
45+
</div>
46+
<div className="space-y-3 pr-2">
47+
{keysData?.data.map((record, index) => (
48+
<div key={index} className="border-l-4 border-gray-600 pl-4 pr-2 py-1 flex items-center justify-between">
49+
<div className="flex flex-col">
50+
<span className="font-medium text-gray-700">{record.name}</span>
51+
<div className="flex items-center gap-2">
52+
<span className="text-sm text-gray-500 truncate max-w-[250px]">
53+
{visibleKeys[index] ? record.key : `${record.key.substring(0,4)} * * * * * * * * *`}
54+
</span>
55+
<Button
56+
size="icon"
57+
variant="ghost"
58+
onClick={() => toggleVisibility(index)}
59+
className="cursor-pointer hover:bg-gray-200 text-gray-600 hover:text-gray-800"
60+
>
61+
{visibleKeys[index] ? <EyeOff size={16} /> : <Eye size={16} />}
62+
</Button>
63+
</div>
64+
</div>
65+
<div className="flex items-center gap-2">
66+
<Button
67+
size="icon"
68+
variant="ghost"
69+
onClick={() => handleCopy(record.key)}
70+
className="cursor-pointer hover:bg-blue-100 text-blue-500 hover:text-blue-600"
71+
>
72+
<Clipboard size={16} />
73+
</Button>
74+
<Button
75+
size="icon"
76+
variant="ghost"
77+
onClick={() => handleDeleteClick(record)}
78+
className="cursor-pointer hover:bg-red-100 text-red-400 hover:text-red-500"
79+
>
80+
<Trash size={16} />
81+
</Button>
82+
</div>
83+
</div>
84+
))}
85+
</div>
86+
</div>
87+
<KeyDeleteConfirm
88+
open={openDelete}
89+
onCancel={handleDeleteCancel}
90+
selectedKey={selectedKey}
91+
/>
92+
</>
93+
);
94+
};
95+
96+
export default Keys;

src/components/proxies/add-proxy-dialog.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ export function AddProxyDialog({ open, onClose }: Props) {
111111
)}
112112
/>
113113

114-
<Button loading={false} type='submit' className='mt-2 cursor-pointer' disabled={false}>
114+
<Button loading={addDomainMutation.isPending} type='submit' className='mt-2 cursor-pointer' disabled={false}>
115115
Save
116116
</Button>
117117
</div>

src/components/proxies/proxies.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ interface ProxyCheckResults extends ProxyRecordProps {
2222
}
2323

2424
const ProxyRecord = ({ record }: ProxyRecordProps) => {
25-
console.log(record)
2625
return (
2726
<div className="flex flex-col items-start gap-1">
2827
<div className="font-semibold flex items-center justify-start gap-2">

src/components/proxies/proxy-delete-confirm.tsx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@ const ProxyDeleteConfirm: FC<Props> = ({
2828

2929
return (
3030
<ConfirmDialog
31-
key='delete-role-confirm'
3231
open={open}
3332
onOpenChange={onCancel}
3433
handleConfirm={handleConfirmDelete}

0 commit comments

Comments
 (0)