368 lines
11 KiB
TypeScript
368 lines
11 KiB
TypeScript
import {
|
||
Button,
|
||
Card,
|
||
Container,
|
||
Group,
|
||
Stack,
|
||
Table,
|
||
Text,
|
||
TextInput,
|
||
Title,
|
||
ScrollArea,
|
||
ActionIcon,
|
||
Tooltip,
|
||
Divider,
|
||
Loader,
|
||
Badge,
|
||
useMantineTheme,
|
||
} from "@mantine/core";
|
||
import { useEffect, useState } from "react";
|
||
import {
|
||
IconPlus,
|
||
IconCopy,
|
||
IconTrash,
|
||
IconKey,
|
||
IconDatabase,
|
||
IconLock,
|
||
} from "@tabler/icons-react";
|
||
import { showNotification } from "@mantine/notifications";
|
||
import apiFetch from "@/lib/apiFetch";
|
||
|
||
export default function ApiKeyPage() {
|
||
return (
|
||
<Container size="lg" py="xl" w="100%">
|
||
<Stack gap="xl">
|
||
<Group justify="space-between" align="center">
|
||
<Group gap="xs">
|
||
<IconKey size={28} color="var(--mantine-color-cyan-5)" />
|
||
<Title order={2} fw={700} c="gray.0">
|
||
API Key Management
|
||
</Title>
|
||
</Group>
|
||
</Group>
|
||
<CreateApiKey />
|
||
</Stack>
|
||
</Container>
|
||
);
|
||
}
|
||
|
||
function CreateApiKey() {
|
||
const theme = useMantineTheme();
|
||
const [name, setName] = useState("");
|
||
const [description, setDescription] = useState("");
|
||
const [expiredAt, setExpiredAt] = useState("");
|
||
const [loading, setLoading] = useState(false);
|
||
|
||
const handleSubmit = async (e: React.FormEvent) => {
|
||
e.preventDefault();
|
||
if (!name || !expiredAt) {
|
||
showNotification({
|
||
color: "red",
|
||
title: "Missing Information",
|
||
message: "Please provide a name and expiration date.",
|
||
});
|
||
return;
|
||
}
|
||
setLoading(true);
|
||
const res = await apiFetch.api.apikey.create.post({
|
||
name,
|
||
description,
|
||
expiredAt,
|
||
});
|
||
if (res.status === 200) {
|
||
setName("");
|
||
setDescription("");
|
||
setExpiredAt("");
|
||
showNotification({
|
||
title: "API Key Created",
|
||
message: "Your new API key is now active and ready to use.",
|
||
color: "teal",
|
||
});
|
||
} else {
|
||
showNotification({
|
||
title: "Error",
|
||
message: "Failed to create API key. Please try again.",
|
||
color: "red",
|
||
});
|
||
}
|
||
setLoading(false);
|
||
};
|
||
|
||
return (
|
||
<Card
|
||
radius="lg"
|
||
p="xl"
|
||
withBorder
|
||
style={{
|
||
background:
|
||
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||
borderColor: "rgba(100,100,100,0.2)",
|
||
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||
}}
|
||
>
|
||
<Stack gap="md">
|
||
<Group justify="space-between" align="center">
|
||
<Title order={3} c="gray.0">
|
||
Create New API Key
|
||
</Title>
|
||
<Tooltip label="Generate a new API key for your app">
|
||
<ActionIcon
|
||
variant="light"
|
||
radius="md"
|
||
size="lg"
|
||
style={{ boxShadow: "0 0 10px rgba(0,255,200,0.2)" }}
|
||
>
|
||
<IconPlus size={20} />
|
||
</ActionIcon>
|
||
</Tooltip>
|
||
</Group>
|
||
<Divider my="sm" />
|
||
<form onSubmit={handleSubmit}>
|
||
<Stack gap="md">
|
||
<TextInput
|
||
label="Key Name"
|
||
placeholder="Enter a name for this key"
|
||
required
|
||
radius="md"
|
||
value={name}
|
||
onChange={(e) => setName(e.target.value)}
|
||
styles={{
|
||
input: {
|
||
backgroundColor: "rgba(255,255,255,0.05)",
|
||
borderColor: "rgba(255,255,255,0.1)",
|
||
},
|
||
}}
|
||
/>
|
||
<TextInput
|
||
label="Description"
|
||
placeholder="Describe this key’s purpose (optional)"
|
||
radius="md"
|
||
value={description}
|
||
onChange={(e) => setDescription(e.target.value)}
|
||
styles={{
|
||
input: {
|
||
backgroundColor: "rgba(255,255,255,0.05)",
|
||
borderColor: "rgba(255,255,255,0.1)",
|
||
},
|
||
}}
|
||
/>
|
||
<TextInput
|
||
label="Expiration Date"
|
||
type="date"
|
||
required
|
||
radius="md"
|
||
value={expiredAt}
|
||
onChange={(e) => setExpiredAt(e.target.value)}
|
||
styles={{
|
||
input: {
|
||
backgroundColor: "rgba(255,255,255,0.05)",
|
||
borderColor: "rgba(255,255,255,0.1)",
|
||
},
|
||
}}
|
||
/>
|
||
<Group justify="flex-end" mt="md">
|
||
<Button
|
||
variant="light"
|
||
color="gray"
|
||
radius="md"
|
||
onClick={() => {
|
||
setName("");
|
||
setDescription("");
|
||
setExpiredAt("");
|
||
}}
|
||
>
|
||
Clear
|
||
</Button>
|
||
<Button
|
||
type="submit"
|
||
loading={loading}
|
||
radius="md"
|
||
size="md"
|
||
variant="gradient"
|
||
gradient={{ from: "teal", to: "cyan", deg: 45 }}
|
||
style={{
|
||
boxShadow: "0 0 12px rgba(0,255,200,0.3)",
|
||
transition: "all 0.2s ease",
|
||
}}
|
||
>
|
||
Create Key
|
||
</Button>
|
||
</Group>
|
||
</Stack>
|
||
</form>
|
||
<Divider my="lg" />
|
||
<ListApiKey />
|
||
</Stack>
|
||
</Card>
|
||
);
|
||
}
|
||
|
||
function ListApiKey() {
|
||
const theme = useMantineTheme();
|
||
const [apiKeys, setApiKeys] = useState<any[]>([]);
|
||
const [loading, setLoading] = useState(true);
|
||
|
||
useEffect(() => {
|
||
const fetchApiKeys = async () => {
|
||
const res = await apiFetch.api.apikey.list.get();
|
||
if (res.status === 200) {
|
||
setApiKeys(res.data?.apiKeys || []);
|
||
}
|
||
setLoading(false);
|
||
};
|
||
fetchApiKeys();
|
||
}, []);
|
||
|
||
const handleDelete = async (id: string) => {
|
||
const res = await apiFetch.api.apikey.delete.delete({ id });
|
||
if (res.status === 200) {
|
||
setApiKeys(apiKeys.filter((api: any) => api.id !== id));
|
||
showNotification({
|
||
title: "API Key Deleted",
|
||
message: "The API key has been successfully removed.",
|
||
color: "teal",
|
||
});
|
||
}
|
||
};
|
||
|
||
const handleCopy = (key: string) => {
|
||
navigator.clipboard.writeText(key);
|
||
showNotification({
|
||
title: "Copied",
|
||
message: "API key copied to clipboard.",
|
||
color: "cyan",
|
||
});
|
||
};
|
||
|
||
return (
|
||
<Card
|
||
radius="lg"
|
||
withBorder
|
||
p="xl"
|
||
style={{
|
||
background:
|
||
"linear-gradient(145deg, rgba(25,25,25,0.95), rgba(45,45,45,0.85))",
|
||
borderColor: "rgba(100,100,100,0.2)",
|
||
boxShadow: "0 0 20px rgba(0,255,200,0.08)",
|
||
}}
|
||
>
|
||
<Stack gap="sm">
|
||
<Group justify="space-between" align="center">
|
||
<Group gap="xs">
|
||
<IconDatabase size={22} color="var(--mantine-color-cyan-5)" />
|
||
<Title order={3} c="gray.0">
|
||
Active API Keys
|
||
</Title>
|
||
</Group>
|
||
<Badge
|
||
size="lg"
|
||
radius="sm"
|
||
color="teal"
|
||
variant="light"
|
||
style={{ textTransform: "none" }}
|
||
>
|
||
{apiKeys.length} Active
|
||
</Badge>
|
||
</Group>
|
||
<Divider my="sm" />
|
||
{loading ? (
|
||
<Group justify="center" py="xl">
|
||
<Loader color="teal" size="lg" />
|
||
</Group>
|
||
) : apiKeys.length === 0 ? (
|
||
<Stack align="center" justify="center" py="xl" gap={4}>
|
||
<IconLock size={32} color="gray" />
|
||
<Text c="dimmed" size="sm">
|
||
No API keys created yet.
|
||
</Text>
|
||
</Stack>
|
||
) : (
|
||
<ScrollArea>
|
||
<Table
|
||
highlightOnHover
|
||
withTableBorder
|
||
withColumnBorders
|
||
style={{
|
||
borderColor: "rgba(255,255,255,0.1)",
|
||
color: "var(--mantine-color-gray-0)",
|
||
fontSize: "0.9rem",
|
||
}}
|
||
>
|
||
<Table.Thead>
|
||
<Table.Tr>
|
||
<Table.Th>Name</Table.Th>
|
||
<Table.Th>Description</Table.Th>
|
||
<Table.Th>Expires</Table.Th>
|
||
<Table.Th>Created</Table.Th>
|
||
<Table.Th>Updated</Table.Th>
|
||
<Table.Th style={{ textAlign: "center" }}>Actions</Table.Th>
|
||
</Table.Tr>
|
||
</Table.Thead>
|
||
<Table.Tbody>
|
||
{apiKeys.map((apiKey: any, index: number) => (
|
||
<Table.Tr
|
||
key={index}
|
||
style={{
|
||
backgroundColor: "rgba(255,255,255,0.03)",
|
||
transition: "background 0.2s ease",
|
||
}}
|
||
onMouseEnter={(e) =>
|
||
(e.currentTarget.style.backgroundColor =
|
||
"rgba(0,255,200,0.05)")
|
||
}
|
||
onMouseLeave={(e) =>
|
||
(e.currentTarget.style.backgroundColor =
|
||
"rgba(255,255,255,0.03)")
|
||
}
|
||
>
|
||
<Table.Td>{apiKey.name}</Table.Td>
|
||
<Table.Td>{apiKey.description || "-"}</Table.Td>
|
||
<Table.Td>
|
||
<Badge
|
||
color={
|
||
new Date(apiKey.expiredAt) < new Date()
|
||
? "red"
|
||
: "grape"
|
||
}
|
||
variant="light"
|
||
>
|
||
{new Date(apiKey.expiredAt).toLocaleDateString()}
|
||
</Badge>
|
||
</Table.Td>
|
||
<Table.Td>{new Date(apiKey.createdAt).toLocaleDateString()}</Table.Td>
|
||
<Table.Td>{new Date(apiKey.updatedAt).toLocaleDateString()}</Table.Td>
|
||
<Table.Td>
|
||
<Group gap="xs" justify="center">
|
||
<Tooltip label="Copy API Key">
|
||
<ActionIcon
|
||
variant="light"
|
||
color="teal"
|
||
onClick={() => handleCopy(apiKey.key)}
|
||
radius="md"
|
||
>
|
||
<IconCopy size={18} />
|
||
</ActionIcon>
|
||
</Tooltip>
|
||
<Tooltip label="Delete API Key">
|
||
<ActionIcon
|
||
variant="light"
|
||
color="red"
|
||
onClick={() => handleDelete(apiKey.id)}
|
||
radius="md"
|
||
>
|
||
<IconTrash size={18} />
|
||
</ActionIcon>
|
||
</Tooltip>
|
||
</Group>
|
||
</Table.Td>
|
||
</Table.Tr>
|
||
))}
|
||
</Table.Tbody>
|
||
</Table>
|
||
</ScrollArea>
|
||
)}
|
||
</Stack>
|
||
</Card>
|
||
);
|
||
}
|