tambahan
This commit is contained in:
@@ -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 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>
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user