159 lines
6.7 KiB
TypeScript
159 lines
6.7 KiB
TypeScript
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
|
|
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
|
|
import { Button } from "@/components/ui/button";
|
|
import { Ban, Key, Calendar, User, Zap } from "lucide-react";
|
|
import { securityService, type ApiKey } from "@/services";
|
|
import { toast } from "sonner";
|
|
import { format } from "date-fns";
|
|
import type { ApiError } from "@/types/error.types";
|
|
import { cn } from "@/lib/utils";
|
|
|
|
export default function ApiKeysPage() {
|
|
const queryClient = useQueryClient();
|
|
|
|
const { data: apiKeys, isLoading } = useQuery({
|
|
queryKey: ["admin", "security", "api-keys"],
|
|
queryFn: () => securityService.getAllApiKeys(),
|
|
});
|
|
|
|
const revokeMutation = useMutation({
|
|
mutationFn: (id: string) => securityService.revokeApiKey(id),
|
|
onSuccess: () => {
|
|
queryClient.invalidateQueries({
|
|
queryKey: ["admin", "security", "api-keys"],
|
|
});
|
|
toast.success("API access credential revoked");
|
|
},
|
|
onError: (error) => {
|
|
const apiError = error as ApiError;
|
|
toast.error(
|
|
apiError.response?.data?.message || "Failed to revoke access",
|
|
);
|
|
},
|
|
});
|
|
|
|
return (
|
|
<div className="space-y-8 max-w-7xl mx-auto bg-white p-4 min-h-screen">
|
|
<div className="flex flex-col md:flex-row md:items-center justify-between gap-4">
|
|
<div>
|
|
<h1 className="text-3xl font-bold text-gray-900 tracking-tight">
|
|
API Gateway
|
|
</h1>
|
|
<p className="text-gray-500 mt-1">
|
|
Management of system access credentials and authentication tokens.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
|
|
<Card className="border shadow-none rounded-none">
|
|
<CardHeader className="border-b pb-4 flex flex-row items-center justify-between bg-gray-50/30">
|
|
<CardTitle className="text-xs font-bold uppercase tracking-widest text-gray-400">
|
|
Credential Registry
|
|
</CardTitle>
|
|
<Zap className="w-4 h-4 text-gray-300" />
|
|
</CardHeader>
|
|
<CardContent className="p-0">
|
|
<div className="overflow-x-auto">
|
|
<table className="w-full text-left">
|
|
<thead className="bg-gray-50 border-b">
|
|
<tr>
|
|
<th className="px-6 py-4 text-[10px] font-bold text-gray-400 uppercase tracking-widest">
|
|
Key Identifier
|
|
</th>
|
|
<th className="px-6 py-4 text-[10px] font-bold text-gray-400 uppercase tracking-widest">
|
|
Operator
|
|
</th>
|
|
<th className="px-6 py-4 text-[10px] font-bold text-gray-400 uppercase tracking-widest">
|
|
Last Activity
|
|
</th>
|
|
<th className="px-6 py-4 text-[10px] font-bold text-gray-400 uppercase tracking-widest">
|
|
Access Status
|
|
</th>
|
|
<th className="px-6 py-4 text-[10px] font-bold text-gray-400 uppercase tracking-widest text-right">
|
|
Actions
|
|
</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody className="divide-y text-gray-600">
|
|
{isLoading ? (
|
|
<tr>
|
|
<td
|
|
colSpan={5}
|
|
className="px-6 py-20 text-center text-gray-400 animate-pulse font-medium uppercase tracking-widest text-[10px]"
|
|
>
|
|
Retrieving secure credentials...
|
|
</td>
|
|
</tr>
|
|
) : apiKeys && apiKeys.length > 0 ? (
|
|
apiKeys.map((key: ApiKey) => (
|
|
<tr
|
|
key={key.id}
|
|
className="hover:bg-gray-50 transition-colors group"
|
|
>
|
|
<td className="px-6 py-4">
|
|
<div className="flex items-center gap-2">
|
|
<Key className="w-3 h-3 text-gray-300" />
|
|
<span className="text-sm font-bold text-gray-900 tracking-tighter">
|
|
{key.name}
|
|
</span>
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<div className="flex items-center gap-2 text-xs font-medium">
|
|
<User className="w-3 h-3 text-gray-300" />
|
|
{key.userId || "N/A"}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<div className="flex items-center gap-2 text-xs font-medium">
|
|
<Calendar className="w-3 h-3 text-gray-300" />
|
|
{key.lastUsed
|
|
? format(new Date(key.lastUsed), "MMM dd, yyyy")
|
|
: "Inactive"}
|
|
</div>
|
|
</td>
|
|
<td className="px-6 py-4">
|
|
<span
|
|
className={cn(
|
|
"px-2 py-0.5 text-[9px] font-bold uppercase tracking-widest border rounded-none",
|
|
key.isActive
|
|
? "bg-emerald-50 text-emerald-600 border-emerald-100"
|
|
: "bg-rose-50 text-rose-600 border-rose-100",
|
|
)}
|
|
>
|
|
{key.isActive ? "Authorized" : "Deactivated"}
|
|
</span>
|
|
</td>
|
|
<td className="px-6 py-4 text-right">
|
|
{key.isActive && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-8 rounded-none border border-transparent hover:border-rose-100 hover:bg-rose-50 hover:text-rose-600 transition-all font-bold uppercase tracking-widest text-[9px]"
|
|
onClick={() => revokeMutation.mutate(key.id)}
|
|
>
|
|
<Ban className="w-3 h-3 mr-2" /> Revoke Access
|
|
</Button>
|
|
)}
|
|
</td>
|
|
</tr>
|
|
))
|
|
) : (
|
|
<tr>
|
|
<td
|
|
colSpan={5}
|
|
className="px-6 py-20 text-center text-gray-400 italic font-medium uppercase tracking-widest text-[10px]"
|
|
>
|
|
No API access credentials defined.
|
|
</td>
|
|
</tr>
|
|
)}
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|