263 lines
11 KiB
TypeScript
263 lines
11 KiB
TypeScript
"use client";
|
|
|
|
import { useState } from "react";
|
|
import { ColumnDef } from "@tanstack/react-table";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import {
|
|
Dialog,
|
|
DialogContent,
|
|
DialogDescription,
|
|
DialogHeader,
|
|
DialogTitle,
|
|
} from "@/components/ui/dialog";
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog";
|
|
import { DataTable } from "@/components/data-table/data-table";
|
|
import { useApiTokens, useCreateApiKey, useDeleteApiKey, useTestSecure } from "@/modules/api-management/hooks";
|
|
import { ApiToken } from "@/modules/api-management/schemas";
|
|
import { useToast } from "@/hooks/use-toast";
|
|
import { Key, Trash, TestTube, MoreHorizontal, Plus, Eye, Copy } from "lucide-react";
|
|
import { formatDateTime } from "@/lib/utils";
|
|
import {
|
|
DropdownMenu,
|
|
DropdownMenuContent,
|
|
DropdownMenuItem,
|
|
DropdownMenuTrigger,
|
|
} from "@/components/ui/dropdown-menu";
|
|
|
|
export default function ApiManagementPage() {
|
|
const { toast } = useToast();
|
|
const { data, isLoading } = useApiTokens();
|
|
const createMutation = useCreateApiKey();
|
|
const deleteMutation = useDeleteApiKey();
|
|
const { refetch: testSecure } = useTestSecure();
|
|
const [apiKey, setApiKey] = useState<string | null>(null);
|
|
const [selectedToken, setSelectedToken] = useState<ApiToken | null>(null);
|
|
const [isDetailOpen, setIsDetailOpen] = useState(false);
|
|
const [deleteToken, setDeleteToken] = useState<string | null>(null);
|
|
|
|
const handleCreate = async () => {
|
|
try {
|
|
const response = await createMutation.mutateAsync();
|
|
setApiKey(response.apiKey || response.token);
|
|
toast({ title: "API Key created", description: "New API key generated successfully" });
|
|
} catch (error) {
|
|
toast({ title: "Create failed", variant: "destructive" });
|
|
}
|
|
};
|
|
|
|
const handleDelete = async () => {
|
|
if (!deleteToken) return;
|
|
|
|
try {
|
|
await deleteMutation.mutateAsync(deleteToken);
|
|
toast({ title: "API Key deleted", description: "API token has been deleted successfully" });
|
|
setDeleteToken(null);
|
|
} catch (error) {
|
|
toast({ title: "Delete failed", variant: "destructive" });
|
|
setDeleteToken(null);
|
|
}
|
|
};
|
|
|
|
const handleCopy = async (text: string) => {
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
toast({ title: "Copied to clipboard", description: "API key has been copied successfully" });
|
|
} catch (error) {
|
|
toast({ title: "Copy failed", variant: "destructive" });
|
|
}
|
|
};
|
|
|
|
const handleOpenDetail = (token: ApiToken) => {
|
|
setSelectedToken(token);
|
|
setIsDetailOpen(true);
|
|
};
|
|
|
|
const columns: ColumnDef<ApiToken>[] = [
|
|
{
|
|
accessorKey: "TokenCredential_AC",
|
|
header: "Token",
|
|
cell: ({ row }) => (
|
|
<div className="font-mono text-sm max-w-[300px] truncate">
|
|
{row.original.TokenCredential_AC}
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
accessorKey: "CreatedAt_AC",
|
|
header: "Created",
|
|
cell: ({ row }) => formatDateTime(row.original.CreatedAt_AC),
|
|
},
|
|
{
|
|
accessorKey: "UpdatedAt_AC",
|
|
header: "Updated",
|
|
cell: ({ row }) => formatDateTime(row.original.UpdatedAt_AC),
|
|
},
|
|
{
|
|
id: "actions",
|
|
cell: ({ row }) => (
|
|
<DropdownMenu>
|
|
<DropdownMenuTrigger asChild>
|
|
<Button variant="ghost" size="icon">
|
|
<MoreHorizontal className="h-4 w-4" />
|
|
</Button>
|
|
</DropdownMenuTrigger>
|
|
<DropdownMenuContent align="end">
|
|
<DropdownMenuItem onClick={() => handleOpenDetail(row.original)}>
|
|
<Eye className="mr-2 h-4 w-4" />
|
|
View Details
|
|
</DropdownMenuItem>
|
|
<DropdownMenuItem
|
|
onClick={() => setDeleteToken(row.original.TokenCredential_AC)}
|
|
className="text-destructive"
|
|
>
|
|
<Trash className="mr-2 h-4 w-4" />
|
|
Delete
|
|
</DropdownMenuItem>
|
|
</DropdownMenuContent>
|
|
</DropdownMenu>
|
|
),
|
|
},
|
|
];
|
|
|
|
if (isLoading) {
|
|
return <div>Loading...</div>;
|
|
}
|
|
|
|
const tokens = data?.data?.tokens || [];
|
|
|
|
const handleTest = async () => {
|
|
try {
|
|
await testSecure();
|
|
toast({ title: "Test successful", description: "API connection is working" });
|
|
} catch (error) {
|
|
toast({ title: "Test failed", variant: "destructive" });
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="space-y-6">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<h1 className="text-3xl font-bold">API Management</h1>
|
|
<p className="text-muted-foreground">Manage API keys and tokens</p>
|
|
</div>
|
|
<Button onClick={handleCreate} disabled={createMutation.isPending}>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
{createMutation.isPending ? "Creating..." : "Create Token"}
|
|
</Button>
|
|
</div>
|
|
|
|
{apiKey && (
|
|
<Card className="bg-green-50 dark:bg-green-950 border-green-200 dark:border-green-800">
|
|
<CardHeader>
|
|
<CardTitle className="text-green-900 dark:text-green-100">New API Token Created</CardTitle>
|
|
<CardDescription className="text-green-700 dark:text-green-300">
|
|
Copy and save this token securely. You won't be able to see it again.
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="rounded-lg bg-white dark:bg-slate-900 p-4 border">
|
|
<p className="text-sm font-mono break-all">{apiKey}</p>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
)}
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>API Tokens</CardTitle>
|
|
<CardDescription>
|
|
Total: {data?.data?.total || 0} tokens
|
|
</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<DataTable columns={columns} data={tokens} searchKey="TokenCredential_AC" />
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle>API Testing</CardTitle>
|
|
<CardDescription>Test API connectivity</CardDescription>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<Button onClick={handleTest}>
|
|
<TestTube className="mr-2 h-4 w-4" />
|
|
Test Secure Endpoint
|
|
</Button>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Dialog open={isDetailOpen} onOpenChange={setIsDetailOpen}>
|
|
<DialogContent className="sm:max-w-2xl">
|
|
<DialogHeader>
|
|
<DialogTitle>API Token Details</DialogTitle>
|
|
<DialogDescription>
|
|
View complete API token information
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
{selectedToken && (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<label className="text-sm font-medium text-muted-foreground">Token Credential</label>
|
|
<div className="mt-2 flex items-center gap-2">
|
|
<div className="flex-1 rounded-lg bg-muted p-3 font-mono text-sm break-all">
|
|
{selectedToken.TokenCredential_AC}
|
|
</div>
|
|
<Button
|
|
size="icon"
|
|
variant="outline"
|
|
onClick={() => handleCopy(selectedToken.TokenCredential_AC)}
|
|
>
|
|
<Copy className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
<div className="grid grid-cols-2 gap-4">
|
|
<div>
|
|
<label className="text-sm font-medium text-muted-foreground">Created At</label>
|
|
<p className="mt-1 text-sm">{formatDateTime(selectedToken.CreatedAt_AC)}</p>
|
|
</div>
|
|
<div>
|
|
<label className="text-sm font-medium text-muted-foreground">Updated At</label>
|
|
<p className="mt-1 text-sm">{formatDateTime(selectedToken.UpdatedAt_AC)}</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
|
|
<AlertDialog open={!!deleteToken} onOpenChange={(open) => !open && setDeleteToken(null)}>
|
|
<AlertDialogContent>
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Delete API Key</AlertDialogTitle>
|
|
<AlertDialogDescription>
|
|
Are you sure you want to delete this API key? This action cannot be undone and will immediately revoke access for this token.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
onClick={handleDelete}
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
>
|
|
Delete
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</div>
|
|
);
|
|
}
|