253 lines
13 KiB
JavaScript
253 lines
13 KiB
JavaScript
"use client";
|
|
|
|
import { useState, useEffect, useCallback } from 'react';
|
|
import { useAuth } from '@/contexts/AuthContext';
|
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card';
|
|
import { Button } from '@/components/ui/button';
|
|
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
|
import { Skeleton } from '@/components/ui/skeleton';
|
|
import { Trash2, PlusCircle, Copy, KeyRound } from 'lucide-react';
|
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
|
|
import { Input } from '@/components/ui/input';
|
|
import { Label } from '@/components/ui/label';
|
|
import { format } from 'date-fns';
|
|
import {
|
|
AlertDialog,
|
|
AlertDialogAction,
|
|
AlertDialogCancel,
|
|
AlertDialogContent,
|
|
AlertDialogDescription,
|
|
AlertDialogFooter,
|
|
AlertDialogHeader,
|
|
AlertDialogTitle,
|
|
} from "@/components/ui/alert-dialog"
|
|
import { Badge } from "@/components/ui/badge"
|
|
|
|
export default function ApiKeys() {
|
|
const { token } = useAuth();
|
|
const [keys, setKeys] = useState([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const [newKey, setNewKey] = useState(null);
|
|
const [expiresInDays, setExpiresInDays] = useState(90);
|
|
const [filterType, setFilterType] = useState('all');
|
|
const [searchTerm, setSearchTerm] = useState('');
|
|
const [keyToDelete, setKeyToDelete] = useState(null);
|
|
const [isCreateDialogOpen, setIsCreateDialogOpen] = useState(false);
|
|
const [isCreating, setIsCreating] = useState(false);
|
|
|
|
const fetchKeys = useCallback(async () => {
|
|
if (!token) return;
|
|
setLoading(true);
|
|
try {
|
|
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/keys`, {
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
const data = await res.json();
|
|
setKeys(data);
|
|
} catch (error) {
|
|
console.error("Failed to fetch API keys", error);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, [token]);
|
|
|
|
useEffect(() => {
|
|
fetchKeys();
|
|
}, [fetchKeys]);
|
|
|
|
const handleCreateKey = async () => {
|
|
if (!token) return;
|
|
setIsCreating(true);
|
|
try {
|
|
const res = await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/keys`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Authorization': `Bearer ${token}`,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ expiresInDays })
|
|
});
|
|
const data = await res.json();
|
|
setNewKey(data.key);
|
|
fetchKeys();
|
|
} catch (error) {
|
|
console.error("Failed to create key", error);
|
|
} finally {
|
|
setIsCreating(false);
|
|
}
|
|
};
|
|
|
|
const handleDeleteKey = async (keyToDelete) => {
|
|
if (!token) return;
|
|
try {
|
|
await fetch(`${process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001'}/keys/${keyToDelete}`, {
|
|
method: 'DELETE',
|
|
headers: { 'Authorization': `Bearer ${token}` }
|
|
});
|
|
fetchKeys();
|
|
} catch (error) {
|
|
console.error("Failed to delete key", error);
|
|
}
|
|
};
|
|
|
|
const copyToClipboard = (text) => {
|
|
navigator.clipboard.writeText(text);
|
|
alert('Key copied to clipboard!');
|
|
};
|
|
|
|
const filteredKeys = keys
|
|
.filter(key => filterType === 'all' || key.type === filterType)
|
|
.filter(key => key.key.toLowerCase().includes(searchTerm.toLowerCase()));
|
|
|
|
return (
|
|
<Card className="mt-6 bg-gray-950/50 border-gray-800">
|
|
<CardHeader>
|
|
<div className="flex justify-between items-center">
|
|
<div>
|
|
<CardTitle>API Keys</CardTitle>
|
|
<CardDescription>Manage your API keys for programmatic access.</CardDescription>
|
|
</div>
|
|
<Dialog open={isCreateDialogOpen} onOpenChange={(open) => {
|
|
if (isCreating) return;
|
|
setIsCreateDialogOpen(open);
|
|
if (!open) setNewKey(null);
|
|
}}>
|
|
<DialogTrigger asChild>
|
|
<Button><PlusCircle className="h-4 w-4 mr-2" /> Create New Key</Button>
|
|
</DialogTrigger>
|
|
<DialogContent>
|
|
<DialogHeader>
|
|
<DialogTitle>{newKey ? 'API Key Created' : 'Create New API Key'}</DialogTitle>
|
|
<DialogDescription>
|
|
{newKey ? 'Your new API key is shown below. Please save it now. You will not be able to see it again.' : 'Set an expiration for your new key.'}
|
|
</DialogDescription>
|
|
</DialogHeader>
|
|
{newKey ? (
|
|
<div className="flex items-center space-x-2 bg-gray-900 p-3 rounded-md">
|
|
<code className="text-sm font-mono flex-grow break-all">{newKey}</code>
|
|
<Button variant="ghost" size="icon" onClick={() => copyToClipboard(newKey)}>
|
|
<Copy className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
) : (
|
|
<div className="space-y-4">
|
|
<div>
|
|
<Label htmlFor="expiresIn">Expires in (days)</Label>
|
|
<Input id="expiresIn" type="number" value={expiresInDays} onChange={(e) => setExpiresInDays(parseInt(e.target.value))} />
|
|
</div>
|
|
<Button onClick={handleCreateKey} className="w-full" disabled={isCreating}>
|
|
{isCreating ? 'Generating...' : 'Generate Key'}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
</DialogContent>
|
|
</Dialog>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="flex flex-col sm:flex-row gap-4 justify-between mb-4">
|
|
<Input
|
|
placeholder="Search keys..."
|
|
value={searchTerm}
|
|
onChange={(e) => setSearchTerm(e.target.value)}
|
|
className="max-w-xs bg-gray-900 border-gray-700"
|
|
/>
|
|
<div className="flex items-center gap-1 p-1 bg-gray-900 rounded-lg">
|
|
<Button size="sm" variant={filterType === 'all' ? 'secondary' : 'ghost'} className="w-full sm:w-auto" onClick={() => setFilterType('all')}>All</Button>
|
|
<Button size="sm" variant={filterType === 'api' ? 'secondary' : 'ghost'} className="w-full sm:w-auto" onClick={() => setFilterType('api')}>API</Button>
|
|
<Button size="sm" variant={filterType === 'session' ? 'secondary' : 'ghost'} className="w-full sm:w-auto" onClick={() => setFilterType('session')}>Session</Button>
|
|
</div>
|
|
</div>
|
|
{loading ? (
|
|
<div className="space-y-2">
|
|
<Skeleton className="h-10 w-full" />
|
|
<Skeleton className="h-10 w-full" />
|
|
<Skeleton className="h-10 w-full" />
|
|
</div>
|
|
) : filteredKeys.length > 0 ? (
|
|
<Table>
|
|
<TableHeader>
|
|
<TableRow>
|
|
<TableHead>Key</TableHead>
|
|
<TableHead>Type</TableHead>
|
|
<TableHead>IP Address</TableHead>
|
|
<TableHead>Created</TableHead>
|
|
<TableHead>Expires</TableHead>
|
|
<TableHead className="text-right">Actions</TableHead>
|
|
</TableRow>
|
|
</TableHeader>
|
|
<TableBody>
|
|
{filteredKeys.map(key => {
|
|
const isCurrentSession = key.type === 'session' && token?.startsWith(key.key);
|
|
return (
|
|
<TableRow key={key.key} className="hover:bg-gray-900">
|
|
<TableCell className="font-mono">
|
|
{key.type === 'api' ? `${key.key.substring(0, 12)}...` : key.key}
|
|
{isCurrentSession && <span className="text-xs text-gray-400 ml-2">(this session)</span>}
|
|
</TableCell>
|
|
<TableCell>
|
|
<Badge variant="outline" className={key.type === 'session' ? 'border-gray-600/50 bg-gray-900/20 text-gray-300' : 'border-blue-600/50 bg-blue-900/20 text-blue-300'}>
|
|
{key.type}
|
|
</Badge>
|
|
</TableCell>
|
|
<TableCell>{key.ipAddress || 'N/A'}</TableCell>
|
|
<TableCell>{format(new Date(key.createdAt), 'PP')}</TableCell>
|
|
<TableCell>{key.expiresAt ? format(new Date(key.expiresAt), 'PP') : 'Never'}</TableCell>
|
|
<TableCell className="text-right">
|
|
{!isCurrentSession && (
|
|
<Button variant="ghost" size="icon" onClick={() => setKeyToDelete(key)}>
|
|
<Trash2 className="h-4 w-4 text-red-500" />
|
|
</Button>
|
|
)}
|
|
</TableCell>
|
|
</TableRow>
|
|
);
|
|
})}
|
|
</TableBody>
|
|
</Table>
|
|
) : (
|
|
<div className="text-center py-10 text-gray-400">
|
|
<KeyRound className="mx-auto h-12 w-12 mb-4" />
|
|
<h3 className="text-lg font-semibold text-white">
|
|
{searchTerm || filterType !== 'all' ? 'No Matching Keys' : 'No API Keys'}
|
|
</h3>
|
|
<p>
|
|
{searchTerm || filterType !== 'all'
|
|
? 'Try adjusting your search or filter.'
|
|
: 'Create your first API key to get started with integrations.'}
|
|
</p>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
<AlertDialog open={!!keyToDelete} onOpenChange={setKeyToDelete}>
|
|
<AlertDialogContent className="bg-gray-950 border-gray-800 text-white">
|
|
<AlertDialogHeader>
|
|
<AlertDialogTitle>Are you absolutely sure?</AlertDialogTitle>
|
|
<AlertDialogDescription className="text-gray-400">
|
|
This action cannot be undone. This will permanently delete the key
|
|
<code className="text-sm font-mono bg-gray-800 rounded p-1 mx-1">
|
|
{keyToDelete?.type === 'api' ? `${keyToDelete?.key.substring(0, 12)}...` : keyToDelete?.key}
|
|
</code>
|
|
and revoke its access.
|
|
</AlertDialogDescription>
|
|
</AlertDialogHeader>
|
|
<AlertDialogFooter>
|
|
<AlertDialogCancel className="border-gray-700 hover:bg-gray-800">Cancel</AlertDialogCancel>
|
|
<AlertDialogAction
|
|
className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
|
|
onClick={() => {
|
|
if (keyToDelete) {
|
|
handleDeleteKey(keyToDelete.key);
|
|
}
|
|
setKeyToDelete(null);
|
|
}}
|
|
>
|
|
Delete
|
|
</AlertDialogAction>
|
|
</AlertDialogFooter>
|
|
</AlertDialogContent>
|
|
</AlertDialog>
|
|
</Card>
|
|
);
|
|
}
|