This commit is contained in:
bipproduction
2025-10-13 17:27:10 +08:00
parent dd6ca462a9
commit 8b9abcdd03
4 changed files with 721 additions and 203 deletions

View File

@@ -7,16 +7,39 @@ import {
Table,
Text,
TextInput,
Title,
ScrollArea,
ActionIcon,
Tooltip,
Divider,
Loader,
Badge,
useMantineTheme,
} from "@mantine/core";
import { useEffect, useState } from "react";
import apiFetch from "@/lib/apiFetch";
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="md" w={"100%"}>
<Stack>
<Text>API Key</Text>
<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>
@@ -24,6 +47,7 @@ export default function ApiKeyPage() {
}
function CreateApiKey() {
const theme = useMantineTheme();
const [name, setName] = useState("");
const [description, setDescription] = useState("");
const [expiredAt, setExpiredAt] = useState("");
@@ -31,6 +55,14 @@ function CreateApiKey() {
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,
@@ -42,52 +74,123 @@ function CreateApiKey() {
setDescription("");
setExpiredAt("");
showNotification({
title: "Success",
message: "API key created successfully",
color: "green",
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>
<Stack>
<Text>API Create</Text>
<TextInput
label="Name"
placeholder="Name"
value={name}
onChange={(e) => setName(e.target.value)}
/>
<TextInput
label="Description"
placeholder="Description"
value={description}
onChange={(e) => setDescription(e.target.value)}
/>
<TextInput
label="Expired At"
placeholder="Expired At"
type="date"
value={expiredAt}
onChange={(e) => setExpiredAt(e.target.value)}
/>
<Group>
<Button
variant="outline"
onClick={() => {
setName("");
setDescription("");
setExpiredAt("");
}}
>
Cancel
</Button>
<Button onClick={handleSubmit} type="submit" loading={loading}>
Save
</Button>
</Group>
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 keys 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>
@@ -95,69 +198,169 @@ function CreateApiKey() {
}
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>
<Stack>
<Text>API List</Text>
<Table>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Expired At</th>
<th>Created At</th>
<th>Updated At</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{apiKeys.map((apiKey: any, index: number) => (
<tr key={index}>
<td>{apiKey.name}</td>
<td>{apiKey.description}</td>
<td>{apiKey.expiredAt.toISOString().split("T")[0]}</td>
<td>{apiKey.createdAt.toISOString().split("T")[0]}</td>
<td>{apiKey.updatedAt.toISOString().split("T")[0]}</td>
<td>
<Button
variant="outline"
onClick={() => {
apiFetch.api.apikey.delete.delete({ id: apiKey.id });
setApiKeys(
apiKeys.filter((api: any) => api.id !== apiKey.id),
);
<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",
}}
>
<thead>
<tr>
<th>Name</th>
<th>Description</th>
<th>Expires</th>
<th>Created</th>
<th>Updated</th>
<th style={{ textAlign: "center" }}>Actions</th>
</tr>
</thead>
<tbody>
{apiKeys.map((apiKey: any, index: number) => (
<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)")
}
>
Delete
</Button>
<Button
variant="outline"
onClick={() => {
navigator.clipboard.writeText(apiKey.key);
showNotification({
title: "Success",
message: "API key copied to clipboard",
color: "green",
});
}}
>
Copy
</Button>
</td>
</tr>
))}
</tbody>
</Table>
<td>{apiKey.name}</td>
<td>{apiKey.description || "-"}</td>
<td>
<Badge
color={
new Date(apiKey.expiredAt) < new Date()
? "red"
: "grape"
}
variant="light"
>
{new Date(apiKey.expiredAt).toLocaleDateString()}
</Badge>
</td>
<td>{new Date(apiKey.createdAt).toLocaleDateString()}</td>
<td>{new Date(apiKey.updatedAt).toLocaleDateString()}</td>
<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>
</td>
</tr>
))}
</tbody>
</Table>
</ScrollArea>
)}
</Stack>
</Card>
);